Snap for 7803083 from e4e6d8a85251197b520e9f1dac121e4f252291af to mainline-tzdata2-release
Change-Id: I112495773b041d82d1619d8379b498870b2ff5e3
diff --git a/Android.bp b/Android.bp
index e132854..1b6ffe4 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12,6 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["packages_apps_Launcher3_license"],
+}
+
+min_launcher3_sdk_version = "26"
+
+// Added automatically by a large-scale-change
+// See: http://go/android-license-faq
+license {
+ name: "packages_apps_Launcher3_license",
+ visibility: [":__subpackages__"],
+ license_kinds: [
+ "SPDX-license-identifier-Apache-2.0",
+ ],
+ license_text: [
+ "NOTICE",
+ ],
+}
+
android_library {
name: "launcher-aosp-tapl",
static_libs: [
@@ -19,14 +38,15 @@
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.uiautomator_uiautomator",
+ "androidx.preference_preference",
"SystemUISharedLib",
],
srcs: [
"tests/tapl/**/*.java",
- "src/com/android/launcher3/util/SecureSettingsObserver.java",
"src/com/android/launcher3/ResourceUtils.java",
"src/com/android/launcher3/testing/TestProtocol.java",
],
+ resource_dirs: [ ],
manifest: "tests/tapl/AndroidManifest.xml",
platform_apis: true,
}
@@ -35,19 +55,37 @@
name: "launcher_log_protos_lite",
srcs: [
"protos/*.proto",
- "proto_overrides/*.proto",
+ "protos_overrides/*.proto",
],
sdk_version: "current",
proto: {
type: "lite",
local_include_dirs:[
"protos",
- "proto_overrides",
+ "protos_overrides",
],
},
static_libs: ["libprotobuf-java-lite"],
}
+java_library_static {
+ name: "launcher_quickstep_log_protos_lite",
+ srcs: [
+ "quickstep/protos_overrides/*.proto",
+ ],
+ sdk_version: "current",
+ proto: {
+ type: "lite",
+ local_include_dirs:[
+ "quickstep/protos_overrides",
+ ],
+ },
+ static_libs: [
+ "libprotobuf-java-lite",
+ "launcher_log_protos_lite"
+ ],
+}
+
java_library {
name: "LauncherPluginLib",
@@ -56,5 +94,161 @@
srcs: ["src_plugins/**/*.java"],
sdk_version: "current",
- min_sdk_version: "28",
+ min_sdk_version: min_launcher3_sdk_version,
}
+
+// Library with all the dependencies for building Launcher3
+android_library {
+ name: "Launcher3ResLib",
+ srcs: [ ],
+ resource_dirs: ["res"],
+ static_libs: [
+ "LauncherPluginLib",
+ "launcher_quickstep_log_protos_lite",
+ "androidx-constraintlayout_constraintlayout",
+ "androidx.recyclerview_recyclerview",
+ "androidx.dynamicanimation_dynamicanimation",
+ "androidx.fragment_fragment",
+ "androidx.preference_preference",
+ "androidx.slice_slice-view",
+ "androidx.cardview_cardview",
+ "iconloader_base",
+ ],
+ manifest: "AndroidManifest-common.xml",
+ sdk_version: "current",
+ min_sdk_version: min_launcher3_sdk_version,
+ lint: {
+ baseline_filename: "lint-baseline-res-lib.xml",
+ },
+}
+
+//
+// Build rule for Launcher3 dependencies lib.
+//
+android_library {
+ name: "Launcher3CommonDepsLib",
+ srcs: ["src_build_config/**/*.java"],
+ static_libs: ["Launcher3ResLib"],
+ sdk_version: "current",
+ min_sdk_version: min_launcher3_sdk_version,
+ manifest: "AndroidManifest-common.xml",
+ lint: {
+ baseline_filename: "lint-baseline-common-deps-lib.xml",
+ },
+}
+
+//
+// Build rule for Launcher3 app.
+//
+android_app {
+ name: "Launcher3",
+
+ static_libs: [
+ "Launcher3CommonDepsLib",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src_shortcuts_overrides/**/*.java",
+ "src_ui_overrides/**/*.java",
+ "ext_tests/src/**/*.java",
+ ],
+ resource_dirs: [
+ "ext_tests/res",
+ ],
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ // Proguard is disable for testing. Derivarive prjects to keep proguard enabled
+ enabled: false,
+ },
+
+ sdk_version: "current",
+ min_sdk_version: min_launcher3_sdk_version,
+ target_sdk_version: "current",
+ privileged: true,
+ system_ext_specific: true,
+
+ overrides: [
+ "Home",
+ "Launcher2",
+ ],
+ required: ["privapp_whitelist_com.android.launcher3"],
+
+ jacoco: {
+ include_filter: ["com.android.launcher3.**"],
+ },
+ additional_manifests: [
+ "AndroidManifest-common.xml",
+ ],
+ lint: {
+ baseline_filename: "lint-baseline-launcher3.xml",
+ },
+}
+
+// Library with all the dependencies for building quickstep
+android_library {
+ name: "QuickstepResLib",
+ srcs: [ ],
+ resource_dirs: [
+ "quickstep/res",
+ ],
+ static_libs: [
+ "Launcher3ResLib",
+ "SystemUISharedLib",
+ "SystemUI-statsd",
+ ],
+ manifest: "quickstep/AndroidManifest.xml",
+ min_sdk_version: "current",
+}
+
+
+// Source code used for test helpers
+filegroup {
+ name: "launcher-src-ext-tests",
+ srcs: ["ext_tests/src/**/*.java"],
+}
+
+// Common source files used to build launcher
+filegroup {
+ name: "launcher-src-no-build-config",
+ srcs: [
+ "src/**/*.java",
+ "src_shortcuts_overrides/**/*.java",
+ "quickstep/src/**/*.java",
+ ],
+}
+
+// Proguard files for Launcher3
+filegroup {
+ name: "launcher-proguard-rules",
+ srcs: ["proguard.flags"],
+}
+
+
+// Library with all the dependencies for building Launcher Go
+android_library {
+ name: "LauncherGoResLib",
+ srcs: [
+ "src/**/*.java",
+ "quickstep/src/**/*.java",
+ "go/src/**/*.java",
+ "go/quickstep/src/**/*.java",
+ ],
+ resource_dirs: [
+ "go/res",
+ "go/quickstep/res",
+ ],
+ static_libs: [
+ "Launcher3CommonDepsLib",
+ "QuickstepResLib",
+ ],
+ manifest: "quickstep/AndroidManifest-launcher.xml",
+ additional_manifests: [
+ "go/AndroidManifest.xml",
+ "AndroidManifest-common.xml",
+ ],
+ min_sdk_version: "current",
+ lint: {
+ baseline_filename: "lint-baseline-go-res-lib.xml",
+ },
+}
+
diff --git a/Android.mk b/Android.mk
index 7805b32..c222f24 100644
--- a/Android.mk
+++ b/Android.mk
@@ -17,80 +17,6 @@
LOCAL_PATH := $(call my-dir)
#
-# Build rule for Launcher3 dependencies lib.
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT2_ONLY := true
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- androidx.recyclerview_recyclerview \
- androidx.dynamicanimation_dynamicanimation \
- androidx.preference_preference \
- iconloader_base
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- LauncherPluginLib \
- launcher_log_protos_lite
-
-LOCAL_SRC_FILES := \
- $(call all-proto-files-under, protos) \
- $(call all-proto-files-under, proto_overrides) \
- $(call all-java-files-under, src_build_config) \
-
-LOCAL_RESOURCE_DIR := $(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_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 21
-LOCAL_MODULE := Launcher3CommonDepsLib
-LOCAL_PRIVILEGED_MODULE := true
-LOCAL_MANIFEST_FILE := AndroidManifest-common.xml
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#
-# Build rule for Launcher3 app.
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
-
-LOCAL_SRC_FILES := \
- $(call all-java-files-under, src) \
- $(call all-java-files-under, src_shortcuts_overrides) \
- $(call all-java-files-under, src_ui_overrides) \
- $(call all-java-files-under, ext_tests/src)
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/ext_tests/res
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-# Proguard is disable for testing. Derivarive prjects to keep proguard enabled
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 21
-LOCAL_PACKAGE_NAME := Launcher3
-LOCAL_PRIVILEGED_MODULE := true
-LOCAL_SYSTEM_EXT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2
-LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
-
-LOCAL_FULL_LIBS_MANIFEST_FILES := $(LOCAL_PATH)/AndroidManifest-common.xml
-
-LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
-
-include $(BUILD_PACKAGE)
-
-#
# Build rule for Launcher3 Go app for Android Go devices.
#
include $(CLEAR_VARS)
@@ -108,7 +34,7 @@
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 21
+LOCAL_MIN_SDK_VERSION := 26
LOCAL_PACKAGE_NAME := Launcher3Go
LOCAL_PRIVILEGED_MODULE := true
LOCAL_SYSTEM_EXT_MODULE := true
@@ -121,6 +47,9 @@
LOCAL_MANIFEST_FILE := go/AndroidManifest.xml
LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE
include $(BUILD_PACKAGE)
#
@@ -133,9 +62,7 @@
LOCAL_STATIC_JAVA_LIBRARIES := \
SystemUI-statsd \
- SystemUISharedLib \
- launcherprotosnano \
- launcher_log_protos_lite
+ SystemUISharedLib
ifneq (,$(wildcard frameworks/base))
LOCAL_PRIVATE_PLATFORM_APIS := true
else
@@ -143,18 +70,18 @@
LOCAL_MIN_SDK_VERSION := 26
endif
LOCAL_MODULE := Launcher3QuickStepLib
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE
LOCAL_PRIVILEGED_MODULE := true
LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
LOCAL_SRC_FILES := \
$(call all-java-files-under, src) \
$(call all-java-files-under, quickstep/src) \
- $(call all-java-files-under, quickstep/recents_ui_overrides/src) \
$(call all-java-files-under, src_shortcuts_overrides)
-LOCAL_RESOURCE_DIR := \
- $(LOCAL_PATH)/quickstep/res \
- $(LOCAL_PATH)/quickstep/recents_ui_overrides/res
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res
LOCAL_PROGUARD_ENABLED := disabled
@@ -183,9 +110,7 @@
LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
-LOCAL_RESOURCE_DIR := \
- $(LOCAL_PATH)/quickstep/res \
- $(LOCAL_PATH)/quickstep/recents_ui_overrides/res
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res
LOCAL_FULL_LIBS_MANIFEST_FILES := \
$(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
@@ -194,6 +119,9 @@
LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE
include $(BUILD_PACKAGE)
@@ -206,9 +134,7 @@
LOCAL_STATIC_JAVA_LIBRARIES := \
SystemUI-statsd \
- SystemUISharedLib \
- launcherprotosnano \
- launcher_log_protos_lite
+ SystemUISharedLib
ifneq (,$(wildcard frameworks/base))
LOCAL_PRIVATE_PLATFORM_APIS := true
else
@@ -220,13 +146,13 @@
LOCAL_SRC_FILES := \
$(call all-java-files-under, src) \
$(call all-java-files-under, quickstep/src) \
- $(call all-java-files-under, quickstep/recents_ui_overrides/src) \
- $(call all-java-files-under, go/src)
+ $(call all-java-files-under, go/src) \
+ $(call all-java-files-under, go/quickstep/src)
LOCAL_RESOURCE_DIR := \
- $(LOCAL_PATH)/quickstep/res \
- $(LOCAL_PATH)/quickstep/recents_ui_overrides/res \
- $(LOCAL_PATH)/go/res
+ $(LOCAL_PATH)/go/quickstep/res \
+ $(LOCAL_PATH)/go/res \
+ $(LOCAL_PATH)/quickstep/res
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
LOCAL_PROGUARD_ENABLED := full
@@ -239,11 +165,14 @@
LOCAL_FULL_LIBS_MANIFEST_FILES := \
$(LOCAL_PATH)/go/AndroidManifest.xml \
- $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
+ $(LOCAL_PATH)/go/AndroidManifest-launcher.xml \
$(LOCAL_PATH)/AndroidManifest-common.xml
LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE
include $(BUILD_PACKAGE)
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index dd0fc21..d725a16 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -29,13 +29,8 @@
at compile time. Note that the components defined in AndroidManifest.xml are also required,
with some minor changed based on the derivative app.
-->
- <permission
- android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
- android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
- android:protectionLevel="dangerous"
- android:label="@string/permlab_install_shortcut"
- android:description="@string/permdesc_install_shortcut" />
+ <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
@@ -45,9 +40,8 @@
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-
- <!-- TODO(b/150802536): Enabled only for ENABLE_FIXED_ROTATION_TRANSFORM feature flag -->
- <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+ <!-- for rotating surface by arbitrary degree -->
+ <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
<!--
Permissions required for read/write access to the workspace data. These permission name
@@ -82,41 +76,27 @@
android:restoreAnyVersion="true"
android:supportsRtl="true" >
- <!-- Intent received used to install shortcuts from other applications -->
- <receiver
- android:name="com.android.launcher3.InstallShortcutReceiver"
- android:permission="com.android.launcher.permission.INSTALL_SHORTCUT"
- android:enabled="@bool/enable_install_shortcut_api" >
- <intent-filter>
- <action android:name="com.android.launcher.action.INSTALL_SHORTCUT" />
- </intent-filter>
- </receiver>
-
<!-- Intent received when a session is committed -->
<receiver
- android:name="com.android.launcher3.SessionCommitReceiver" >
+ android:name="com.android.launcher3.SessionCommitReceiver"
+ android:exported="true">
<intent-filter>
<action android:name="android.content.pm.action.SESSION_COMMITTED" />
</intent-filter>
</receiver>
<!-- Intent received used to initialize a restored widget -->
- <receiver android:name="com.android.launcher3.AppWidgetsRestoredReceiver" >
+ <receiver android:name="com.android.launcher3.AppWidgetsRestoredReceiver"
+ android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_HOST_RESTORED"/>
</intent-filter>
</receiver>
<service
- android:name="com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompatVL$ColorExtractionService"
- android:exported="false"
- android:process=":wallpaper_chooser"
- android:permission="android.permission.BIND_JOB_SERVICE" />
-
- <service
android:name="com.android.launcher3.notification.NotificationListener"
android:label="@string/notification_dots_service_title"
- android:enabled="@bool/notification_dots_enabled"
+ android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
@@ -127,10 +107,10 @@
android:value="true" />
<activity android:name="com.android.launcher3.dragndrop.AddItemActivity"
- android:theme="@style/AppItemActivityTheme"
+ android:theme="@style/AddItemActivityTheme"
android:excludeFromRecents="true"
android:autoRemoveFromRecents="true"
- android:label="@string/action_add_to_workspace" >
+ android:exported="true">
<intent-filter>
<action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
<action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET" />
@@ -154,10 +134,9 @@
TODO: Add proper permissions
-->
<provider
- android:name="com.android.launcher3.graphics.GridOptionsProvider"
+ android:name="com.android.launcher3.graphics.GridCustomizationsProvider"
android:authorities="${packageName}.grid_control"
- android:exported="true"
- android:enabled="false" />
+ android:exported="true" />
<!--
The settings activity. To extend point settings_fragment_name to appropriate fragment class
@@ -166,6 +145,7 @@
android:name="com.android.launcher3.settings.SettingsActivity"
android:label="@string/settings_button_text"
android:theme="@style/HomeSettingsTheme"
+ android:exported="true"
android:autoRemoveFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
@@ -188,6 +168,7 @@
android:name="com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher"
android:theme="@style/AppTheme"
android:launchMode="singleTop"
+ android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b031ffb..8066816 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,7 +20,7 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.launcher3">
- <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
+ <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="26"/>
<!--
Manifest entries specific to Launcher3. This is merged with AndroidManifest-common.xml.
Refer comments around specific entries on how to extend individual components.
@@ -49,10 +49,11 @@
android:stateNotNeeded="true"
android:windowSoftInputMode="adjustPan"
android:screenOrientation="unspecified"
- android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|density"
android:resizeableActivity="true"
android:resumeWhilePausing="true"
android:taskAffinity=""
+ android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
deleted file mode 100644
index e69de29..0000000
--- a/MODULE_LICENSE_APACHE2
+++ /dev/null
diff --git a/OWNERS b/OWNERS
index 3069afa..05fa502 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,6 +4,16 @@
# People who can approve changes for submission
#
+alexchau@google.com
+andraskloczl@google.com
+patmanning@google.com
+petrcermak@google.com
+pbdr@google.com
+kideckel@google.com
+stevenckng@google.com
+ydixit@google.com
+boadway@google.com
+alinazaidi@google.com
adamcohen@google.com
hyunyoungs@google.com
mrcasey@google.com
@@ -28,6 +38,7 @@
peanutbutter@google.com
xuqiu@google.com
sreyasr@google.com
+thiruram@google.com
per-file FeatureFlags.java, globs = set noparent
per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, zakcohen@google.com, mrcasey@google.com, adamcohen@google.com, hyunyoungs@google.com
diff --git a/SharedLibWrapper/build.gradle b/SharedLibWrapper/build.gradle
deleted file mode 100644
index 674e38a..0000000
--- a/SharedLibWrapper/build.gradle
+++ /dev/null
@@ -1,17 +0,0 @@
-apply plugin: 'java'
-
-final String ANDROID_TOP = "${rootDir}/../../.."
-final String FRAMEWORK_PREBUILTS_DIR = "${ANDROID_TOP}/prebuilts/framework_intermediates/"
-
-sourceSets {
- main {
- java.srcDirs = ["${ANDROID_TOP}/frameworks/lib/systemui/SharedLibWrapper/src"]
- }
-}
-
-sourceCompatibility = 1.8
-
-dependencies {
- implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
- compileOnly fileTree(dir: "$ANDROID_TOP/prebuilts/fullsdk-${org.gradle.internal.os.OperatingSystem.current().isMacOsX() ? "darwin" : "linux"}/platforms/${COMPILE_SDK}", include: 'android.jar')
-}
diff --git a/buglist.txt b/buglist.txt
new file mode 100644
index 0000000..53dcc35
--- /dev/null
+++ b/buglist.txt
@@ -0,0 +1,42 @@
+171450807
+170675311
+170338029
+170338170
+160544577
+171171594
+170488559
+171131394
+171131394
+171026321
+170648272
+170752716
+170611866
+170702596
+170487752
+170665892
+168608912
+170636685
+169771796
+141126144
+166614700
+168805872
+170263425
+169221288
+143965596
+169221287
+167259591
+156044202
+169438169
+164926736
+168653219
+169963211
+170121063
+169988381
+169980192
+169221288
+169385783
+168167693
+169796517
+169330678
+168818961
+168608912
diff --git a/buglist_unique.txt b/buglist_unique.txt
new file mode 100644
index 0000000..93dbefb
--- /dev/null
+++ b/buglist_unique.txt
@@ -0,0 +1,39 @@
+141126144
+143965596
+156044202
+160544577
+164926736
+166614700
+167259591
+168167693
+168608912
+168653219
+168805872
+168818961
+169221287
+169221288
+169330678
+169385783
+169438169
+169771796
+169796517
+169963211
+169980192
+169988381
+170121063
+170263425
+170338029
+170338170
+170487752
+170488559
+170611866
+170636685
+170648272
+170665892
+170675311
+170702596
+170752716
+171026321
+171131394
+171171594
+171450807
diff --git a/buglist_with_title.txt b/buglist_with_title.txt
new file mode 100644
index 0000000..aa8b413
--- /dev/null
+++ b/buglist_with_title.txt
@@ -0,0 +1,24 @@
+144170434 twickham P1 FIXED Improve Overview -> Home transition ----
+149934536 twickham P2 FIXED Update gesture nav pullback logic ----
+154951045 peanutbutter P1 FIXED Odd animation occuring at times when swiping to home ----
+154964045 awickham P2 FIXED "Clear all" text is not in the middle of app's window vertically ----
+158701272 twickham P4 FIXED Discontinuities when long-swiping to home ----
+160361464 tracyzhou P2 FIXED Place launcher above the target app in live tile mode ----
+160568387 twickham P2 FIXED Can't get to app switcher by swiping up (motion pause not detected) ----
+160718310 xuqiu P1 FIXED With "Select" overview action selected, App icon is missing in other overview apps after orientation change ----
+160748731 sunnygoyal P2 ASSIGNED Unify prediction model with Launcher model ----
+160759508 twickham P2 FIXED Swipe up cannot back to home screen in overview. ----
+161273376 xuqiu P2 FIXED [Overview Actions] Add logging and helpful messages ----
+161536946 twickham P2 FIXED Haptics don't indicate snap-to in overview, ----
+161685099 winsonc P2 FIXED Screen still stay at the quick settings/notification when I swipe up with 3 finger to check the all apps. ----
+161801331 hyunyoungs P2 FIXED Change AllAppsSearch plugin to support only data fetch ----
+161901771 xuqiu P1 FIXED Overlapping layer of highlights with app layout getting darker when keep rotating the device from "Feedback" viewpoint in split screen ----
+161939759 sunnygoyal P2 FIXED RD1A: Going to overview in landscape mode clips the screen content ----
+162012217 perumaal P2 ASSIGNED Leaked Activity Caused by Gleams ----
+162454040 bookatz P2 ASSIGNED Create multiuser test that checks that opening an app works properly ----
+162480567 sfufa P4 FIXED Enable Item Decorations for search items ----
+162564471 tracyzhou P2 FIXED [Live tile] Handle tapping overview actions in live tile mode ----
+162623012 zakcohen P1 ASSIGNED Enable chips flag ----
+162812884 winsonc P2 ASSIGNED [R]The color have not changed in some page after turning on the dark theme. ----
+162861289 hyunyoungs P2 FIXED Add FocusIndicator support to DEVICE_SEARCH feature in S ----
+162871508 sfufa P2 ASSIGNED Introduce support for Hero app section ----
diff --git a/build.gradle b/build.gradle
index 534ca65..a7eef13 100644
--- a/build.gradle
+++ b/build.gradle
@@ -81,8 +81,7 @@
java.srcDirs = ['src', 'src_plugins']
manifest.srcFile 'AndroidManifest-common.xml'
proto {
- srcDir 'protos/'
- srcDir 'proto_overrides/'
+ srcDirs = ['protos/', 'protos_overrides/']
}
}
@@ -150,7 +149,6 @@
implementation "androidx.preference:preference:${ANDROID_X_VERSION}"
implementation project(':IconLoader')
withQuickstepImplementation project(':SharedLibWrapper')
- implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'launcher_protos.jar')
// Recents lib dependency
withQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
@@ -171,18 +169,14 @@
protobuf {
// Configure the protoc executable
protoc {
- artifact = 'com.google.protobuf:protoc:3.0.0'
-
- generateProtoTasks {
- all().each { task ->
- task.builtins {
- remove java
- javanano {
- option "java_package=launcher_log_extension.proto|com.android.launcher3.userevent.nano"
- option "java_package=launcher_log.proto|com.android.launcher3.userevent.nano"
- option "java_package=launcher_dump.proto|com.android.launcher3.model.nano"
- option "enum_style=java"
- }
+ artifact = "com.google.protobuf:protoc:${protocVersion}"
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ remove java
+ java {
+ option "lite"
}
}
}
diff --git a/commitlist.txt b/commitlist.txt
new file mode 100644
index 0000000..27b8bac
--- /dev/null
+++ b/commitlist.txt
@@ -0,0 +1,934 @@
+[34mCOMMAND>> git log f99351888c3e5a128559678304fefd647472bc7f..4c3952dc60fc78d3816012a86d7e71747ef34c74[m
+commit 4c3952dc60fc78d3816012a86d7e71747ef34c74
+Merge: cb403d9e5 70e8b1572
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Fri Oct 23 00:27:39 2020 +0000
+
+ Merge "Minor stylistic changes in Workspace.java." into ub-launcher3-master
+
+commit cb403d9e5235c7323bc2fdffe6a264d17bb6d0a6
+Author: Pinyao Ting <pinyaoting@google.com>
+Date: Thu Oct 22 16:07:08 2020 -0700
+
+ flip default value of minimal device feature flag
+
+ Test: manual
+ Change-Id: Iaf46dffb935bdf4b46e7c57d547bdc697250ec56
+
+commit a97557a15eb111616d868120a9f4659f1b451fa2
+Merge: f5ce80b8a 932a327eb
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Thu Oct 22 18:22:56 2020 +0000
+
+ Merge "Consider overscroll adjustment of RecentsView for live tile" into ub-launcher3-master
+
+commit 932a327ebf0587b8324b9fea7d31328b2f6719a8
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Wed Oct 21 23:29:00 2020 -0700
+
+ Consider overscroll adjustment of RecentsView for live tile
+
+ Fixes: 171450807
+ Test: manual
+ Change-Id: I83eebf1f6b61c67f289db51aabe5a971815d0df1
+
+commit f5ce80b8a0a1636fc8159475177a07b281492c88
+Author: Hilary Huo <hhuo@google.com>
+Date: Wed Oct 14 16:35:55 2020 -0700
+
+ [pixel-search] Latency analysis, add logging statement in launcher
+
+ Bug: b/170675311
+ Change-Id: I229ace399085bea1c3f9535eb713edd329dff8bd
+
+commit 31b03941ef3aa17edc08c1b509d4fa23766f2d2c
+Merge: e0a50c9e3 0731273d5
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Wed Oct 21 20:03:57 2020 +0000
+
+ Merge "Track live tile better by considering resistance animation" into ub-launcher3-master
+
+commit 0731273d5409149fca32dfb2ad76eab45f6ea79a
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Wed Oct 21 12:03:40 2020 -0700
+
+ Track live tile better by considering resistance animation
+
+ Fixes: 170338029
+ Test: Manual
+ Change-Id: I66536bae567aa94385d5e0352cec9d46d512927a
+
+commit e0a50c9e3f1d4b9f113d6afae01ff2c4ed452fba
+Merge: d2c27a595 acfac6187
+Author: Alex Chau <alexchau@google.com>
+Date: Wed Oct 21 17:02:57 2020 +0000
+
+ Merge "Use Diplay.getMetrics in DisplayController" into ub-launcher3-master
+
+commit d2c27a595065d43bbea37dd2a512d37080f5233e
+Merge: ff8febabb 8b488ccc2
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Wed Oct 21 07:19:48 2020 +0000
+
+ Merge "[Live Tile] Support launching running task animation" into ub-launcher3-master
+
+commit 8b488ccc2e433a708c8b06f0b6866f2a305e4b0a
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Wed Oct 14 12:13:04 2020 -0700
+
+ [Live Tile] Support launching running task animation
+
+ Fixes: 170338170
+ Test: manual
+ Change-Id: I2526b7cfbacaea7899b8e2ed233f913630071d36
+
+commit 70e8b157219e9090ba5e47fdfa51b2b92e98449d
+Author: Andy Wickham <awickham@google.com>
+Date: Wed Oct 7 23:00:06 2020 -0700
+
+ Minor stylistic changes in Workspace.java.
+
+ Change-Id: Ib07611f27cbc427d11abccd8b74ea144485752f7
+
+commit acfac6187dd9d13d55b566a77a5da867a1813573
+Author: Alex Chau <alexchau@google.com>
+Date: Mon Oct 19 18:00:39 2020 +0100
+
+ Use Diplay.getMetrics in DisplayController
+
+ - This is a workaround of b/163815566, where DisplayMetrics is stale
+ when onDisplayChanged is called.
+ - Instead of relying on stale DisplayConext, get the DisplayMetrics
+ from the Display directly.
+ - Also optimized how DisplayController.Info is created by passing in
+ Display only
+ - Use mDisplayContext.getDisplay directly if availalbe
+
+ Bug: 163815566, 160544577
+ Test: DPI looks correct on device boot
+ Change-Id: I2a7454bb8cf2073ce592e8662781b87fc998444f
+ (cherry picked from commit 177c38243dc3bf245d1f7db3c265dfb56522f441)
+
+commit ff8febabb039a3c27ee068f85119860a048b917c
+Merge: b03d2b416 102746823
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Tue Oct 20 17:46:09 2020 +0000
+
+ Merge "Makes Plugin Settings gear adjust to dark mode." into ub-launcher3-master
+
+commit b03d2b41616d479ba360fa4f97e57722c7f57b8e
+Merge: fb79f5541 caa1e9c39
+Author: Hyunyoung Song <hyunyoungs@google.com>
+Date: Tue Oct 20 15:25:56 2020 +0000
+
+ Merge "Search query method should support multiple consumers" into ub-launcher3-master
+
+commit fb79f5541dcbe587002756bb40a3c632d38cc25a
+Merge: f6b05068d cf0b275a4
+Author: Schneider Victor-tulias <victortulias@google.com>
+Date: Tue Oct 20 13:51:06 2020 +0000
+
+ Merge "Add the ability to specify a list of tutorial steps in the gesture sandbox tutorial intent." into ub-launcher3-master
+
+commit f6b05068d901d4e989b2e107c06f9c7a6e7b113f
+Author: Hyunyoung Song <hyunyoungs@google.com>
+Date: Tue Oct 20 00:19:29 2020 -0700
+
+ Invert the badging
+
+ Bug: 171171594
+ Change-Id: If84fdc03254105c843e16f39f479505b16e1cd5f
+
+commit caa1e9c39978cb3b467b5ac441eb39b5e883fa2e
+Author: Hyunyoung Song <hyunyoungs@google.com>
+Date: Mon Oct 12 13:56:02 2020 -0700
+
+ Search query method should support multiple consumers
+
+ Bug: 170488559
+ Change-Id: I64bef9523d3c3950c4ca3a4b9ce1d506d1672200
+
+commit 10274682339bb60cb24c50536b4f48f921970f3c
+Author: Andy Wickham <awickham@google.com>
+Date: Mon Oct 19 19:06:52 2020 -0700
+
+ Makes Plugin Settings gear adjust to dark mode.
+
+ It wasn't visible in dark mode before because it was
+ black on black. This makes it adjust automatically.
+
+ Change-Id: I5176cffc01842509ddafc4f30ff5029a0c4b8050
+
+commit 744a0fbeae8efaa942d21c61e25012d86f5ff81e
+Merge: 29c79947e 71f24588c
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 19 22:17:26 2020 +0000
+
+ Merge "Call click event on IME quick select for SearchResultIcon" into ub-launcher3-master
+
+commit 29c79947ecf82f662d02004ba9a7289017fc0783
+Merge: 13a2a010d a68ac3e5d
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 19 18:42:05 2020 +0000
+
+ Merge "Removing condition for CUJ tracing/metrics" into ub-launcher3-master
+
+commit 71f24588c0a66449a0c68bcb360a8c671914ce75
+Author: Samuel Fufa <sfufa@google.com>
+Date: Mon Oct 19 10:19:31 2020 -0700
+
+ Call click event on IME quick select for SearchResultIcon
+
+ Bug: 171131394
+ Change-Id: I8a703e8d0ca10570e3f774510610d3fb4c0eaab8
+
+commit 13a2a010decd87eeaf8932430c692f587d2de165
+Author: Samuel Fufa <sfufa@google.com>
+Date: Sun Oct 18 21:19:57 2020 -0700
+
+ Handle IME event for SearchResultIcon
+
+ Bug: 171131394
+ Test: Manual
+ Change-Id: I2ed1c61053c78aaecc3324418229d69634a72ae4
+
+commit 1f79eeda76246534697e92740defc7f73c3c8d14
+Author: Samuel Fufa <sfufa@google.com>
+Date: Fri Oct 16 02:01:31 2020 -0700
+
+ Remove hardcoded itemTypes from SearchTarget
+
+ - Introduces componentName and userHandle members to SearchTarget
+ - SearchTargetEvent now has searchTarget member
+ - Builder pattern for SearchTarget and SearchTargetEvent
+ - Search backend should add headers manually instead of launcher inferring sections
+
+ Bug: 171026321
+ Test: Manual
+ Change-Id: I28e0455e82b925277a17703b9aa061c8f9f15262
+
+commit a68ac3e5dd23095cea7c872c0ff1c5042d1695ba
+Author: vadimt <vadimt@google.com>
+Date: Fri Oct 16 10:48:28 2020 -0700
+
+ Removing condition for CUJ tracing/metrics
+
+ Is doesn't reflect whether jank monitors is collecting metrics,
+ which will eventually be always true anyways.
+
+ Change-Id: Iaebdc838ed2b2cebd32c8c48d7e45bdd93f76fb4
+
+commit 9228ff53c2fb26850b7bd92d86214a6aaebb11d3
+Author: Sunny Goyal <sunnygoyal@google.com>
+Date: Mon Oct 12 13:43:51 2020 -0700
+
+ Trimming activity and task label
+
+ Bug: 170648272
+ Change-Id: Icd099acee65305e0aa0f98a2a301a0df8a27cf07
+
+commit 7a09177e500a53205f9969bb6cbd4251d54e8fde
+Merge: 37ed5ead3 314761a80
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Thu Oct 15 22:36:14 2020 +0000
+
+ Merge "Setup SearchResultIcon for single cell results" into ub-launcher3-master
+
+commit 37ed5ead391df5747003b2d3a345be0347362f19
+Merge: d5bbe6809 702ed2788
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Thu Oct 15 22:06:12 2020 +0000
+
+ Merge "Fix the issue where shortcuts are removed in minimal device mode" into ub-launcher3-master
+
+commit 314761a80819a6e64a136161f51eebb0f0528c4d
+Author: Samuel Fufa <sfufa@google.com>
+Date: Wed Oct 14 10:15:07 2020 -0700
+
+ Setup SearchResultIcon for single cell results
+
+ SearchResultIcon will be able to render apps, shortcuts and remote actions. It can also handle its own focused state drawing.
+
+ Screenshot: https://screenshot.googleplex.com/C3KgjJtLQTBPgaf
+
+ Bug: 170752716
+ Test: Manual
+ Change-Id: I460a9c128ea3f5814784e342c5d5fa5b7e310882
+
+commit 702ed2788678ac744c768aad6a6302e7cf91a26b
+Author: Pinyao Ting <pinyaoting@google.com>
+Date: Wed Oct 14 11:17:04 2020 -0700
+
+ Fix the issue where shortcuts are removed in minimal device mode
+
+ When loading the workspace, Launcher pins/unpins shortcuts in comply
+ with the loaded workspace. Since minimal device mode creates a mostly
+ empty workspace, existing shortcuts are getting unpinned as a result.
+
+ To mitigate the issue this CL compares the db name and only invoke
+ sanitizeData when it matches the one defined in InvariantDeviceProfile.
+
+ Bug: 170611866
+ Test: manual
+ 1. add some deep shortcut in workspace (e.g. long tap on chrome, drag
+ "incognito tab" to workspace)
+ 2. opt-in to sunshine fishfood (g/sunshine-teamfood)
+ 3. enable bedtime mode with minimal device in Settings -> Digital
+ Wellbeing -> Show Your Data -> Bedtime mode -> Customize -> minimal
+ device
+ 4. toggle bedtime mode, wait for apps in minimal device to show, then
+ toggle off bedtime mode
+ 5. verify the deep shortcut still exist
+
+ Change-Id: Ie18216ecb288e7481aa2404c4cb3ea418aee85cb
+
+commit cf0b275a48d3c9f91a346f7fc24b9604f6dde25a
+Author: Schneider Victor-tulias <victortulias@google.com>
+Date: Tue Oct 6 09:33:40 2020 -0400
+
+ Add the ability to specify a list of tutorial steps in the gesture sandbox tutorial intent.
+
+ Added tutorial_steps string array in the intent to allow specifying an ordered list of tutorial steps.
+
+ Change-Id: Ic42a65598a74a64f8441a22f58c6cd988a5762e3
+
+commit d5bbe6809dcc056fbfc307909b171651f0fb3044
+Author: Samuel Fufa <sfufa@google.com>
+Date: Wed Oct 14 15:39:38 2020 -0700
+
+ Rename shrotcut container to deep-shrotcuts
+
+ Change-Id: If94f0dfa447235f3b1a652f7b6c749695b42d97c
+
+commit 26c1105fa04c2bcc156051e51df90a6a253349bb
+Author: Samuel Fufa <sfufa@google.com>
+Date: Tue Oct 13 01:12:03 2020 -0700
+
+ [search api part 1] Setup centralized SearchEventTracker
+
+ - Rename AdapterItemWIthPayload to SearchAdapterItem, PayloadResultHandler to SearchTargetHandler
+ - Setup SliceViewWrapper for self contained slices
+
+ Bug: 170702596
+ Change-Id: I0baf984ec8123c95011abcc17372f8d055e98ad7
+
+commit 057f2d0d7df67e3680e479ac76b48b30d8bcf884
+Merge: 4bb65ff51 9a6145efb
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Tue Oct 13 01:57:47 2020 +0000
+
+ Merge "Introduce shortcut container for hotseat event reporting" into ub-launcher3-master
+
+commit 4bb65ff516c6d9a429971ab7e04780792d5cb751
+Merge: 69740e62b 2afcab804
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Tue Oct 13 00:07:36 2020 +0000
+
+ Merge "Search UI clean up" into ub-launcher3-master
+
+commit 2afcab804b638ff3b9da5bad40c8f70bdcaae78d
+Author: Samuel Fufa <sfufa@google.com>
+Date: Mon Oct 12 15:38:14 2020 -0700
+
+ Search UI clean up
+
+ - Resolve spacing issue when work profile is installed
+ - Cache play icons and use icon shape
+ - Only draw focus indicator for the first result
+
+ Bug: 170487752
+ Bug: 170665892
+ Change-Id: I864d2e796786637132e127ef9b418c0a76c74d6e
+
+commit 69740e62be3800fc918648009645f7a8e52cb73d
+Merge: 2d7bfc878 979da64d8
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 12 20:53:29 2020 +0000
+
+ Merge "Add app start source info of apps launched from launcher" into ub-launcher3-master
+
+commit 2d7bfc8782e9ed01178672aeb09ba2a6a07f4f4c
+Author: Jon Miranda <jonmiranda@google.com>
+Date: Mon Oct 12 12:09:22 2020 -0700
+
+ Fix shadowRadius not being used in swipe up animation.
+
+ Bug: 168608912
+ Change-Id: I08f7bb057237e5061d5f1fc29afb488b204ee385
+
+commit a433fe1fb34715efb38ed094f39da49fce8cd51e
+Merge: 2de606fe7 0471b9836
+Author: Sunny Goyal <sunnygoyal@google.com>
+Date: Mon Oct 12 18:08:35 2020 +0000
+
+ Merge "Using FrameCallbacks instead of windowCallbacks for surface removal" into ub-launcher3-master
+
+commit 9a6145efb85f2bbdaccc07166a55e22c15fe27db
+Author: Samuel Fufa <sfufa@google.com>
+Date: Mon Oct 12 09:33:00 2020 -0700
+
+ Introduce shortcut container for hotseat event reporting
+
+ Bug: 170636685
+ Test: Manual
+ Change-Id: I5abeb17976bbafdc8cc74fb8b9a586d544c682fc
+
+commit 2de606fe731573c081fd2d6ba166e21ea6aa2e9c
+Author: Yogisha Dixit <ydixit@google.com>
+Date: Mon Oct 12 15:36:07 2020 +0100
+
+ Delete the minimal database to force refresh.
+
+ Bug: 169771796
+ Test: manual
+ Change-Id: Ic2188bb162f295c208346861fddc137ace19ddcb
+
+commit 0471b9836c9e382dc14bdc3abdf8502fb2b9f266
+Author: Sunny Goyal <sunnygoyal@google.com>
+Date: Wed Sep 23 13:54:37 2020 -0700
+
+ Using FrameCallbacks instead of windowCallbacks for surface removal
+
+ WindowCallbacks is called during the draw pass, before the frame has
+ been sent to the surfaceFlinger. Frame callback will provide a closer
+ approximation for when the frame is actually rendered on screen.
+
+ Bug: 141126144
+ Change-Id: I62aab526c2ca24b00b5e7b312b36080f26c7b439
+
+commit 2727434c44d06882925369bf4b43687a06be4a3f
+Merge: 59f532fe9 1b9e199b3
+Author: Schneider Victor-tulias <victortulias@google.com>
+Date: Fri Oct 9 20:09:08 2020 +0000
+
+ Merge "Fix hotseat and prediction row to allow updates when empty." into ub-launcher3-master
+
+commit 59f532fe9e2b1817c094641f3c7c517f42e4faf0
+Merge: d2bfce71f b5334e3f0
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Fri Oct 9 19:52:54 2020 +0000
+
+ Merge "Improve search section header" into ub-launcher3-master
+
+commit 979da64d8254599c332d83bf94f3f1fc3fe45fef
+Author: Riddle Hsu <riddlehsu@google.com>
+Date: Tue Sep 22 21:52:40 2020 +0800
+
+ Add app start source info of apps launched from launcher
+
+ Bug: 166614700
+ Test: Enable statsd log: "adb shell cmd stats print-logs"
+ adb logcat | grep statsd | grep "(48)"
+ The line may contain 0x100000->1[I] 0x110000->10[I]
+ that means 1=from launcher and 10=latency 10ms.
+ Change-Id: Iddaff7066b66e241ba58ec87129ddbe2c531dc7e
+ (cherry picked from commit 7bdf3574a3bff06a377b4364877687bfa7619d06)
+
+commit d2bfce71f776fd05633dfd915dfc664309274677
+Merge: ed4530fed 222afb970
+Author: Winson Chung <winsonc@google.com>
+Date: Fri Oct 9 16:39:06 2020 +0000
+
+ Merge "Comply with the ISystemUiProxy.aidl change" into ub-launcher3-master
+
+commit ed4530fedda0bf876f91d0745fc70d0f30d42991
+Merge: 692d2109a 9d4a96ed0
+Author: Winson Chung <winsonc@google.com>
+Date: Fri Oct 9 16:39:06 2020 +0000
+
+ Merge "Add latency metrics for recents gesture" into ub-launcher3-master
+
+commit 1b9e199b3d9c81c793758d96bb03e0c51c1b3fb1
+Author: Schneider Victor-tulias <victortulias@google.com>
+Date: Thu Oct 8 15:50:22 2020 -0400
+
+ Fix hotseat and prediction row to allow updates when empty.
+
+ Rotating the screen in the homescreen empties the hotseat, however it does not get populated while it is visible to the user. The user should not be able to see an empty hotseat or prediction row if predictions are available. It should therefore be possible to populate these when they are empty even if they are visible to the user.
+
+ Change-Id: I8e5252bd29050c2cd9d443aedcb3f3e305c0e2d7
+
+commit b5334e3f07f0561808a2d6e9bba55f1e3a89191e
+Author: Hyunyoung Song <hyunyoungs@google.com>
+Date: Fri Oct 9 00:50:48 2020 -0700
+
+ Improve search section header
+
+ Change-Id: I47cf207f0d0ab792c0e7a47c9d1185eec087ec88
+
+commit 692d2109a6702706d24b3b819d115882f7362509
+Author: Samuel Fufa <sfufa@google.com>
+Date: Thu Oct 8 18:42:48 2020 -0700
+
+ invalidate itemDecoration on predictedRow focus draw
+
+ Change-Id: I66c731f00ae1c1292c51ff281957f05fd2d70dfa
+
+commit 8d5b118060bff7f7518a9a14c0be5d265621f14c
+Author: Samuel Fufa <sfufa@google.com>
+Date: Thu Oct 8 13:11:25 2020 -0700
+
+ Revert PredictionRow shuoldDraw check
+
+ + Show Rounded play result icons
+
+ Bug: 168805872
+ Test: Manual
+ Change-Id: I663c7f7ca1f1ac072e5e9c441deabef7c3fbd97b
+
+commit 86f8df6cf954ac27ab092b9ef8a4db3c9979c4cb
+Merge: 4d19854b2 16045060c
+Author: Hilary Huo <hhuo@google.com>
+Date: Thu Oct 8 18:43:51 2020 +0000
+
+ Merge "[pixel-search] add escape hatch" into ub-launcher3-master
+
+commit 4d19854b25a54599fe9b0ac8be9d60cf6c21d7ba
+Merge: 0827e1e32 ab9ad20be
+Author: Samuel Fufa <sfufa@google.com>
+Date: Thu Oct 8 18:40:56 2020 +0000
+
+ Merge "Search UI cleanup" into ub-launcher3-master
+
+commit 16045060c35639aea85afc572bea768d16e6c9f9
+Author: Hilary Huo <hhuo@google.com>
+Date: Thu Oct 8 10:18:41 2020 -0700
+
+ [pixel-search] add escape hatch
+
+ Change-Id: I33ffea1fc0859564955380d7d1db317293d1a2cb
+
+commit 0827e1e32a5f99fa02418dae37270c6db8c989d2
+Merge: 3463f0a87 68d7a6e5b
+Author: Andy Wickham <awickham@google.com>
+Date: Thu Oct 8 16:53:29 2020 +0000
+
+ Merge "Adds feature flag for BC Smartspace." into ub-launcher3-master
+
+commit ab9ad20be600d1cbdc6b54a491d5fbb4c2cf9c16
+Author: Samuel Fufa <sfufa@google.com>
+Date: Wed Oct 7 15:18:24 2020 -0700
+
+ Search UI cleanup
+
+ - offset all apps header padding with search input margin
+ - avoid check shouldDraw check on HeaderRow. (race condition)
+
+ Bug: 170263425
+ Change-Id: I11a1fbb448aa6afd18ec0984af9bb8b1d7600f69
+
+commit 68d7a6e5b28af8cc55bdae7efc24cc7ebee81257
+Author: Andy Wickham <awickham@google.com>
+Date: Wed Oct 7 14:27:17 2020 -0700
+
+ Adds feature flag for BC Smartspace.
+
+ Change-Id: Iaf9fb7507d0ccd004a4e00188c75dadd6a059246
+
+commit 3463f0a876ff486ce03e160134e0504158271a92
+Merge: 2470d812a 4b7f38b8f
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Wed Oct 7 20:09:04 2020 +0000
+
+ Merge "Align fallback result query with result text" into ub-launcher3-master
+
+commit 2470d812a1ae989e67781e5056b534ad9a960819
+Merge: cae7d74d8 7a6e4c931
+Author: Vadim Tryshev <vadimt@google.com>
+Date: Wed Oct 7 20:04:09 2020 +0000
+
+ Merge "Annotating Quick Switch CUJ for 3-button mode" into ub-launcher3-master
+
+commit cae7d74d898769727105850ea5473c2c0ae25fdb
+Merge: e9bf2bd14 1fddddb4f
+Author: Tony Wickham <twickham@google.com>
+Date: Wed Oct 7 18:32:48 2020 +0000
+
+ Merge "Update launcher_trace.proto for quick switch" into ub-launcher3-master
+
+commit 7a6e4c931f13b369bfa4328196b4632d6d848a19
+Author: vadimt <vadimt@google.com>
+Date: Tue Oct 6 14:09:16 2020 -0700
+
+ Annotating Quick Switch CUJ for 3-button mode
+
+ Bug: 169221288
+ Change-Id: Ief62345fe6004dde699f44aa0c90329b7cd84e8b
+
+commit 4b7f38b8fa004b514244304fcc07ff514a2fa46b
+Author: Samuel Fufa <sfufa@google.com>
+Date: Tue Oct 6 18:37:46 2020 -0700
+
+ Align fallback result query with result text
+
+ screenshot: https://screenshot.googleplex.com/6Daj5vdmz2jmznX
+ bug: 169438169
+ test: Manual
+ Change-Id: Ie621ed3c834aec5e9467607da4f685d05d152183
+
+commit 222afb970434c7972589adfc509bd2c256ca6556
+Author: Hongwei Wang <hwwang@google.com>
+Date: Fri Oct 2 13:51:36 2020 -0700
+
+ Comply with the ISystemUiProxy.aidl change
+
+ Two methods are added to support communications between Launcher and
+ SysUI when user swipes an auto PiP-able Activity to home.
+
+ Bug: 143965596
+ Test: N/A
+ Change-Id: I2c73a287a094e882bde3cd71c27f9f66ae20e64a
+ (cherry picked from commit 88ddae38db924f700082a113670ce5a719116a95)
+
+commit 9d4a96ed029fdad1e369d5eedd082938f0dc9e01
+Author: Riddle Hsu <riddlehsu@google.com>
+Date: Wed Sep 30 00:32:04 2020 +0800
+
+ Add latency metrics for recents gesture
+
+ Pass the touch down time to RecentsAnimation#startRecentsActivity.
+
+ Bug: 169221287
+ Test: Enable statsd log: "adb shell cmd stats print-logs"
+ Touch gesture navigation bar.
+ adb logcat | grep statsd | grep "(48)"
+ The line may contain 0x100000->4[I] 0x110000->20[I]
+ that means 4=by recents and 20=latency 20ms.
+ Change-Id: I81ee804895b7712f4d925736f5b4694c11a12cbe
+ (cherry picked from commit 63623967b83edad56db58173ebb6687c685b9177)
+
+commit e9bf2bd14c9a7a48f8f93687932d41b1418cf4e4
+Merge: 73ae75474 d028937e7
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Wed Oct 7 02:19:04 2020 +0000
+
+ Merge "[Live tile] Finish recents animation when the phone goes to sleep in live tile mode" into ub-launcher3-master
+
+commit 1fddddb4f30505e0fc9bb2e7c0d88b38ad900e54
+Author: Tony Wickham <twickham@google.com>
+Date: Tue Sep 29 17:29:06 2020 -0700
+
+ Update launcher_trace.proto for quick switch
+
+ Sample output from one entry:
+ entry {
+ elapsed_realtime_nanos: 440461382888540
+ launcher {
+ touch_interaction_service {
+ service_connected: true
+ overview_component_obvserver {
+ overview_activity_started: true
+ overview_activity_resumed: false
+ }
+ input_consumer {
+ name: "TYPE_OTHER_ACTIVITY:TYPE_ONE_HANDED"
+ swipe_handler {
+ gesture_state {
+ endTarget: NEW_TASK
+ }
+ is_recents_attached_to_app_window: true
+ scroll_offset: 846
+ app_to_overview_progress: 0
+ }
+ }
+ }
+ }
+ }
+
+ Bug: 167259591
+ Change-Id: I7f199d88f1d736efcea6b9165b8c4b77a5d27c58
+
+commit 73ae75474ec1dd8807d814ea6c22323905d2070c
+Merge: 8a6f3e40d 0ebbc1880
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Tue Oct 6 23:18:44 2020 +0000
+
+ Merge "Removing tracing for a gone flake" into ub-launcher3-master
+
+commit 8a6f3e40d0321217c624055db7929c397e455e0c
+Merge: e29a9f796 565ed4ff6
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Tue Oct 6 22:49:40 2020 +0000
+
+ Merge "Update Search UI" into ub-launcher3-master
+
+commit 0ebbc18803aaf8ef2f6db7d628d7ae1ce322e842
+Author: vadimt <vadimt@google.com>
+Date: Tue Oct 6 14:52:27 2020 -0700
+
+ Removing tracing for a gone flake
+
+ Bug: 156044202
+ Change-Id: Ice142bb941fee7b731f46c2073fab17d83bbc871
+
+commit 565ed4ff69b534812818a2b9aa8789a1aea210eb
+Author: Samuel Fufa <sfufa@google.com>
+Date: Wed Sep 30 10:42:07 2020 -0700
+
+ Update Search UI
+
+ [preview attached to bug]
+
+ Bug: 169438169
+ Test: Manual
+ Change-Id: I085f3dd38ac373c1afab82a637ec08715a6e0cc5
+
+commit e29a9f7961e6db0915bc028ef7e871dcb2c8bde0
+Merge: 2c5ed10ff be17bdcd2
+Author: Jayaprakash Sundararaj <jayaprakashs@google.com>
+Date: Tue Oct 6 21:00:20 2020 +0000
+
+ Merge "[Search] Add logging to People and badding as to icons." into ub-launcher3-master
+
+commit be17bdcd221f501c45876abe2249c1007858d0c0
+Author: jayaprakashs <jayaprakashs@google.com>
+Date: Mon Oct 5 09:01:52 2020 -0700
+
+ [Search] Add logging to People and badding as to icons.
+
+ Change-Id: I65948a2faca436216a94aa46139d425b8eade827
+
+commit 2c5ed10ffa1a870de35f9b3c0c558270aff498dd
+Merge: b2b65a1ef 8ed9707cf
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Tue Oct 6 18:40:57 2020 +0000
+
+ Merge "[Live Tile] Support launching another task (other than the current running task) in Overview" into ub-launcher3-master
+
+commit b2b65a1ef58b020923d112051535b6eb83b582df
+Merge: 3cf264f49 4c14f4b9e
+Author: Samuel Fufa <sfufa@google.com>
+Date: Tue Oct 6 17:45:35 2020 +0000
+
+ Merge "Avoid double search item highlight" into ub-launcher3-master
+
+commit 8ed9707cf3a4300cb61942f08f0752c80eed086b
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Mon Sep 14 23:25:37 2020 -0700
+
+ [Live Tile] Support launching another task (other than the current running task) in Overview
+
+ - Get rid of the defer cancelation logic
+ - Render animation on the task view of the task being launched upon task view appeared callback
+ - Finish the recents animation upon the end of the recents window animation
+
+ Fixes: 164926736
+ Test: manual
+ Change-Id: Ibffb6a9c74c235efc8615a22b0306551532c7b61
+
+commit 3cf264f498e37c482fa4c559bf48ffa791279585
+Author: Schneider Victor-tulias <victortulias@google.com>
+Date: Tue Sep 22 12:58:38 2020 -0700
+
+ Prevent hotseat updates if it is visible to the user.
+
+ Test: manual
+
+ Fixes: 168653219
+
+ Changing app icons under the user's finger could be disruptive. Added a checks for whether the hotseatand all apps predictions are visible and callbacks to update them when they become hidden.
+
+ Change-Id: Ib9e6e904e9f662ecfaeea6a2fe21d1d81ba39b96
+
+commit b6aff1f56d55a36256446ec3970d92e9da39b98c
+Author: Hyunyoung Song <hyunyoungs@google.com>
+Date: Mon Oct 5 16:08:35 2020 -0700
+
+ Fix NPE inside RecentsOrientedState
+
+ Bug: 169963211
+ Change-Id: I86dd337dc1b862f3fa99b91b47fa250076233f96
+
+commit eab40983b9a48b933bde5ca95a82ebd4d83b233d
+Merge: 83ce7c0b5 020e628f2
+Author: Jonathan Miranda <jonmiranda@google.com>
+Date: Mon Oct 5 22:20:27 2020 +0000
+
+ Merge "Add shadow radius to windows during app launch / close animations." into ub-launcher3-master
+
+commit 83ce7c0b5e461386bb92883a8d6cefe8365cd9ae
+Merge: 679d920bf d6b1f3c08
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 5 19:18:39 2020 +0000
+
+ Merge "Action icon should be used as a badge instead of main icon" into ub-launcher3-master
+
+commit 679d920bf5151cffed4e8186c12c25d8d7907af9
+Merge: e108cc609 0c943966d
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 5 19:13:50 2020 +0000
+
+ Merge "Add null check for input receiver before updating batching" into ub-launcher3-master
+
+commit e108cc609d0a7fd58f0c7e16ce45fa79be6dd272
+Merge: 470403eb5 f622e42bf
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 5 18:39:58 2020 +0000
+
+ Merge "Removing unused proto extensions" into ub-launcher3-master
+
+commit 470403eb58879380e2edac2262dc7f40327b2a15
+Merge: a5130482a 1d7ed30db
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 5 18:29:54 2020 +0000
+
+ Merge "Remove widgets that no longer fit the workspace in their current spans." into ub-launcher3-master
+
+commit 4c14f4b9eda8332347c81e0cf51c5de4dbc06399
+Author: Samuel Fufa <sfufa@google.com>
+Date: Mon Oct 5 10:50:00 2020 -0700
+
+ Avoid double search item highlight
+
+ Change-Id: Ic2e28b18f6d5e3ed32cd5646bc3bb4789c378e57
+
+commit 0c943966d373d8ae7eef2b08e88ac44bf57d8a8d
+Author: Winson Chung <winsonc@google.com>
+Date: Mon Oct 5 10:23:27 2020 -0700
+
+ Add null check for input receiver before updating batching
+
+ - A change in the system (ie. sysui crash or nav mode change) could
+ cause the input monitor to be disposed before the swipe animation
+ settles
+
+ Bug: 170121063
+ Test: Kill sysui while swiping up
+
+ Change-Id: I1417b109fecdb98fae6197c7038dbe9307470853
+
+commit a5130482aee1b0592661bc1c6e178a0de7a163da
+Merge: b21819e18 7fcd74abb
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Mon Oct 5 17:14:21 2020 +0000
+
+ Merge "Suggest result should launch Bug: 169980192" into ub-launcher3-master
+
+commit d028937e74a9ea6d36e463de4c87ed37283bbdf6
+Author: Tracy Zhou <tracyzhou@google.com>
+Date: Sat Oct 3 00:36:53 2020 -0700
+
+ [Live tile] Finish recents animation when the phone goes to sleep in live tile mode
+
+ Fixes: 169988381
+ Test: manual
+ Change-Id: Ic71d3e6767eadb6854dbd46581bf9d3242c161a4
+
+commit 7fcd74abb399100ac8243be6ca28c09cc8adc8c8
+Author: Hyunyoung Song <hyunyoungs@google.com>
+Date: Fri Oct 2 19:20:11 2020 -0700
+
+ Suggest result should launch
+ Bug: 169980192
+
+ Change-Id: I762245a5cc4740d093c9cb3b44a508e9e3f2b763
+
+commit b21819e181e99504c22c6ca028261a1f2665c6f9
+Merge: 931bce369 a762b0241
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Fri Oct 2 22:07:12 2020 +0000
+
+ Merge "Annotating Quick Switch CUJ for non-3-button modes" into ub-launcher3-master
+
+commit a762b02418695f5a1ff2f96586660de8c3610280
+Author: vadimt <vadimt@google.com>
+Date: Fri Oct 2 13:56:28 2020 -0700
+
+ Annotating Quick Switch CUJ for non-3-button modes
+
+ Bug: 169221288
+ Change-Id: I7145a9e28a2f0a789d19d2a0e3d15630c6e50f6a
+
+commit 931bce3697595a214023bc72923dad47a61d5711
+Merge: c935ba6b8 733e3c609
+Author: TreeHugger Robot <treehugger-gerrit@google.com>
+Date: Fri Oct 2 19:19:50 2020 +0000
+
+ Merge "Moving some initializations to the background thread" into ub-launcher3-master
+
+commit c935ba6b8a2ec163533c0b19309dacb6199e6552
+Merge: a4111f250 58804ac52
+Author: Sunny Goyal <sunnygoyal@google.com>
+Date: Fri Oct 2 18:26:06 2020 +0000
+
+ Merge "Adding stats log for add item flow" into ub-launcher3-master
+
+commit 733e3c609b7653a36e58747c881458ec00d98df8
+Author: Sunny Goyal <sunnygoyal@google.com>
+Date: Tue Sep 29 10:32:32 2020 -0700
+
+ Moving some initializations to the background thread
+
+ HandlerThread.getLooper blocks until the thread is ready. Instead
+ moving all looper dependency to the new thread itself.
+
+ Change-Id: I240e8c56b855a991433a7fe93875059e6dab146b
+
+commit 58804ac5257f45dddbf7a6db35cf8f369ee1e88e
+Author: Sunny Goyal <sunnygoyal@google.com>
+Date: Wed Sep 16 16:27:40 2020 -0700
+
+ Adding stats log for add item flow
+
+ Bug: 169385783
+ Bug: 168167693
+ Change-Id: I37395f1b118727f67e0f14c02f945b8213b165c8
+
+commit a4111f250003328d1aef8bbaab59512208ec46cb
+Merge: 8d14dbe04 f6b72c4ad
+Author: Hilary Huo <hhuo@google.com>
+Date: Fri Oct 2 17:41:22 2020 +0000
+
+ Merge "[pixel-search] Bug fix: automatically launch screenshot + center&crop remoteaction icon" into ub-launcher3-master
+
+commit f622e42bf6983d3adb95386bfd6375d281f1d4f2
+Author: Sunny Goyal <sunnygoyal@google.com>
+Date: Fri Oct 2 10:35:56 2020 -0700
+
+ Removing unused proto extensions
+
+ Change-Id: I6d0319c99934dad5176b6f70b895a4ca772ec45f
+
+commit d6b1f3c086f9ac097cd03e1ee898b153478ec11a
+Author: Hyunyoung Song <hyunyoungs@google.com>
+Date: Fri Oct 2 00:26:35 2020 -0700
+
+ Action icon should be used as a badge instead of main icon
+
+ Bug: 169796517
+ Change-Id: I3f07fdc2ae6e1af463701f942c26c3ca5d836ee2
+
+commit f6b72c4ad1d2e082441a64c4d6a5a02ee8a251ca
+Author: Hilary Huo <hhuo@google.com>
+Date: Thu Oct 1 12:26:48 2020 -0700
+
+ [pixel-search] Bug fix: automatically launch screenshot + center&crop remoteaction icon
+
+ Bug: b/169330678
+ Change-Id: Id5f8a0ce6d68f7ed9e4d1ff258ee3772229eb63b
+
+commit 1d7ed30dba4b2c71fc7b0981532a872a13e5aedb
+Author: Jon Miranda <jonmiranda@google.com>
+Date: Wed Sep 23 12:15:43 2020 -0700
+
+ Remove widgets that no longer fit the workspace in their current spans.
+
+ This can happen when display size changes.
+ We compare span sizes of widget in the db to the min sizes of the widget
+ in the current display size. If the widget can no longer fit in its existing
+ spans, we remove it.
+
+ Also update test widgets to have minWidth/minHeight of 1dp. This ensures that
+ the spanX, spanY, min* values remain consistent between different test devices.
+
+ Bug: 168818961
+ Change-Id: I723372e4582658f78b2f23ced9073cb77977a6b8
+
+commit 020e628f22cc7975beab439c6da26af2f9ebc15b
+Author: Jon Miranda <jonmiranda@google.com>
+Date: Mon Sep 28 17:01:42 2020 -0700
+
+ Add shadow radius to windows during app launch / close animations.
+
+ Bug: 168608912
+ Change-Id: I2ec50b0b3711c0861659f9c641bbc05fcdeaab45
diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
index e649ce1..72b8d3f 100644
--- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -16,16 +16,11 @@
package com.android.launcher3.testing;
-import static android.graphics.Bitmap.Config.ARGB_8888;
-
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
import android.os.Binder;
import android.os.Bundle;
-import android.os.Debug;
import android.system.Os;
import android.view.View;
@@ -106,40 +101,15 @@
return response;
}
- case TestProtocol.REQUEST_TOTAL_PSS_KB: {
+ case TestProtocol.REQUEST_FORCE_GC: {
runGcAndFinalizersSync();
- Debug.MemoryInfo mem = new Debug.MemoryInfo();
- Debug.getMemoryInfo(mem);
- response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, mem.getTotalPss());
- return response;
- }
-
- case TestProtocol.REQUEST_JAVA_LEAK: {
- if (sLeaks == null) sLeaks = new LinkedList();
-
- // Allocate and dirty the memory.
- final int leakSize = 1024 * 1024;
- final byte[] bytes = new byte[leakSize];
- for (int i = 0; i < leakSize; i += 239) {
- bytes[i] = (byte) (i % 256);
- }
- sLeaks.add(bytes);
- return response;
- }
-
- case TestProtocol.REQUEST_NATIVE_LEAK: {
- if (sLeaks == null) sLeaks = new LinkedList();
-
- // Allocate and dirty a bitmap.
- final Bitmap bitmap = Bitmap.createBitmap(512, 512, ARGB_8888);
- bitmap.eraseColor(Color.RED);
- sLeaks.add(bitmap);
return response;
}
case TestProtocol.REQUEST_VIEW_LEAK: {
if (sLeaks == null) sLeaks = new LinkedList();
sLeaks.add(new View(mContext));
+ sLeaks.add(new View(mContext));
return response;
}
@@ -164,6 +134,12 @@
}
case TestProtocol.REQUEST_GET_TEST_EVENTS: {
+ if (sEvents == null) {
+ // sEvents can be null if Launcher died and restarted after
+ // REQUEST_START_EVENT_LOGGING.
+ return response;
+ }
+
synchronized (sEvents) {
response.putStringArrayList(
TestProtocol.TEST_INFO_RESPONSE_FIELD, new ArrayList<>(sEvents));
diff --git a/go/AndroidManifest-launcher.xml b/go/AndroidManifest-launcher.xml
new file mode 100644
index 0000000..6a8f715
--- /dev/null
+++ b/go/AndroidManifest-launcher.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2020, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.launcher3">
+ <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
+ <!--
+ Manifest entries specific to Launcher3. This is merged with AndroidManifest-common.xml.
+ Refer comments around specific entries on how to extend individual components.
+ -->
+
+ <application
+ android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+ android:fullBackupOnly="true"
+ android:fullBackupContent="@xml/backupscheme"
+ android:hardwareAccelerated="true"
+ android:icon="@drawable/ic_launcher_home"
+ android:label="@string/derived_app_name"
+ android:theme="@style/AppTheme"
+ android:largeHeap="@bool/config_largeHeap"
+ android:restoreAnyVersion="true"
+ android:supportsRtl="true" >
+
+ <!--
+ Main launcher activity. When extending only change the name, and keep all the
+ attributes and intent filters the same
+ -->
+ <activity
+ android:name="com.android.launcher3.Launcher3QuickStepGo"
+ android:launchMode="singleTask"
+ android:clearTaskOnLaunch="true"
+ android:stateNotNeeded="true"
+ android:windowSoftInputMode="adjustPan"
+ android:screenOrientation="unspecified"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|density|uiMode"
+ android:resizeableActivity="true"
+ android:resumeWhilePausing="true"
+ android:taskAffinity=""
+ android:exported="true"
+ android:enabled="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.HOME" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.MONKEY"/>
+ <category android:name="android.intent.category.LAUNCHER_APP" />
+ </intent-filter>
+ <meta-data
+ android:name="com.android.launcher3.grid.control"
+ android:value="${packageName}.grid_control" />
+ </activity>
+
+ </application>
+</manifest>
diff --git a/go/AndroidManifest.xml b/go/AndroidManifest.xml
index f84a82e..2671604 100644
--- a/go/AndroidManifest.xml
+++ b/go/AndroidManifest.xml
@@ -24,6 +24,8 @@
<uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
+ <uses-permission android:name="android.permission.GET_TOP_ACTIVITY_INFO" />
+
<application
android:backupAgent="com.android.launcher3.LauncherBackupAgent"
android:fullBackupOnly="true"
@@ -46,6 +48,12 @@
tools:node="replace" >
</activity>
+ <service
+ android:name="com.android.launcher3.notification.NotificationListener"
+ android:label="@string/notification_dots_service_title"
+ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
+ android:enabled="false"
+ tools:node="replace" />
</application>
</manifest>
diff --git a/go/OWNERS b/go/OWNERS
new file mode 100644
index 0000000..903b3c4
--- /dev/null
+++ b/go/OWNERS
@@ -0,0 +1,2 @@
+rajekumar@google.com
+spivack@google.com
diff --git a/go/quickstep/res/drawable/arrow_toast_rounded_background.xml b/go/quickstep/res/drawable/arrow_toast_rounded_background.xml
new file mode 100644
index 0000000..9c815fd
--- /dev/null
+++ b/go/quickstep/res/drawable/arrow_toast_rounded_background.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/arrow_tip_view_bg" />
+ <corners android:radius="@dimen/tooltip_corner_radius" />
+</shape>
diff --git a/go/quickstep/res/drawable/ic_listen.xml b/go/quickstep/res/drawable/ic_listen.xml
new file mode 100644
index 0000000..16bb597
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_listen.xml
@@ -0,0 +1,24 @@
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M15.08,7.05c0.84,1.18 0.84,2.71 0,3.89l1.68,1.69c2.02,-2.02 2.02,-5.07 0,-7.27l-1.68,1.69zM20.07,2l-1.63,1.63c2.77,3.02 2.77,7.56 0,10.74L20.07,16c3.9,-3.89 3.91,-9.95 0,-14zM9,13c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM9,7c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2zM9,14c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4zM15,19L3,19v-0.99C3.2,17.29 6.3,16 9,16s5.8,1.29 6,2v1z"/>
+</vector>
diff --git a/go/quickstep/res/drawable/ic_search.xml b/go/quickstep/res/drawable/ic_search.xml
new file mode 100644
index 0000000..ce290d9
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_search.xml
@@ -0,0 +1,36 @@
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M16,3h-2v2h2c1.65,0 3,1.35 3,3v2h2V8C21,5.24 18.76,3 16,3z"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M5,16v-2H3v2c0,2.76 2.24,5 5,5h2v-2H8C6.35,19 5,17.65 5,16z"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,8.5c-1.93,0 -3.5,1.57 -3.5,3.5s1.57,3.5 3.5,3.5s3.5,-1.57 3.5,-3.5S13.93,8.5 12,8.5z"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M18,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M5,8c0,-1.65 1.35,-3 3,-3h2V3H8C5.24,3 3,5.24 3,8v2h2V8z"/>
+</vector>
diff --git a/go/quickstep/res/drawable/ic_translate.xml b/go/quickstep/res/drawable/ic_translate.xml
new file mode 100644
index 0000000..e3df56f
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_translate.xml
@@ -0,0 +1,24 @@
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
+</vector>
diff --git a/go/quickstep/res/drawable/round_rect_button.xml b/go/quickstep/res/drawable/round_rect_button.xml
new file mode 100644
index 0000000..b276686
--- /dev/null
+++ b/go/quickstep/res/drawable/round_rect_button.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/go_button_corner_radius" />
+</shape>
diff --git a/go/quickstep/res/drawable/round_rect_dialog.xml b/go/quickstep/res/drawable/round_rect_dialog.xml
new file mode 100644
index 0000000..bbb7c5b
--- /dev/null
+++ b/go/quickstep/res/drawable/round_rect_dialog.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/modal_dialog_corner_radius" />
+</shape>
diff --git a/go/quickstep/res/layout/niu_actions_dialog.xml b/go/quickstep/res/layout/niu_actions_dialog.xml
new file mode 100644
index 0000000..6e944f8
--- /dev/null
+++ b/go/quickstep/res/layout/niu_actions_dialog.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/niu_actions_dialog_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:layout_gravity="center">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1"/>
+
+ <LinearLayout
+ android:layout_width="@dimen/modal_dialog_width"
+ android:layout_height="wrap_content"
+ android:background="@drawable/round_rect_dialog"
+ android:backgroundTint="?attr/modalDialogBackground"
+ android:orientation="vertical"
+ android:layout_gravity="center"
+ android:paddingTop="@dimen/modal_dialog_padding"
+ android:paddingLeft="@dimen/modal_dialog_padding"
+ android:paddingRight="@dimen/modal_dialog_padding"
+ android:paddingBottom="@dimen/modal_dialog_padding_bottom">
+
+ <TextView
+ style="@style/ModalDialogTitle"
+ android:id="@+id/niu_actions_dialog_header"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="@dimen/modal_dialog_vertical_spacer"/>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constrainedHeight="true"
+ app:layout_constraintHeight_max="@dimen/modal_dialog_text_height">
+
+ <TextView
+ style="@style/ModalDialogText"
+ android:id="@+id/niu_actions_dialog_description"/>
+ </ScrollView>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="@dimen/modal_dialog_vertical_spacer"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ <Button
+ style="@style/ModalDialogButton"
+ android:id="@+id/niu_actions_dialog_button_1"
+ android:text="@string/dialog_cancel"/>
+
+ <Button
+ style="@style/ModalDialogButton"
+ android:id="@+id/niu_actions_dialog_button_2"
+ android:text="@string/dialog_acknowledge"/>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/go/quickstep/res/layout/overview_actions_container.xml b/go/quickstep/res/layout/overview_actions_container.xml
new file mode 100644
index 0000000..0e718ca
--- /dev/null
+++ b/go/quickstep/res/layout/overview_actions_container.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- NOTE! don't add dimensions for margins / gravity to root view in this file, they need to be
+ loaded at runtime. -->
+<com.android.quickstep.views.GoOverviewActionsView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/action_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/overview_actions_height"
+ android:layout_gravity="top|center_horizontal"
+ android:orientation="horizontal">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1" />
+
+ <LinearLayout
+ android:id="@+id/layout_translate"
+ style="@style/GoOverviewActionButtonContainer">
+ <ImageButton
+ android:id="@+id/action_translate"
+ style="@style/GoOverviewActionButton"
+ android:src="@drawable/ic_translate"
+ android:contentDescription="@string/action_translate" />
+ <TextView
+ style="@style/GoOverviewActionButtonCaption"
+ android:text="@string/action_translate" />
+ </LinearLayout>
+
+ <Space
+ android:id="@+id/spacer_translate"
+ android:layout_width="@dimen/go_overview_button_container_margin"
+ android:layout_height="1dp" />
+
+ <LinearLayout
+ android:id="@+id/layout_listen"
+ style="@style/GoOverviewActionButtonContainer">
+ <ImageButton
+ android:id="@+id/action_listen"
+ style="@style/GoOverviewActionButton"
+ android:src="@drawable/ic_listen"
+ android:contentDescription="@string/action_listen"
+ android:background="@drawable/round_rect_button" />
+ <TextView
+ style="@style/GoOverviewActionButtonCaption"
+ android:text="@string/action_listen" />
+ </LinearLayout>
+
+ <Space
+ android:id="@+id/spacer_listen"
+ android:layout_width="@dimen/go_overview_button_container_margin"
+ android:layout_height="1dp" />
+
+ <LinearLayout
+ android:id="@+id/layout_screenshot"
+ style="@style/GoOverviewActionButtonContainer">
+ <ImageButton
+ android:id="@+id/action_screenshot"
+ style="@style/GoOverviewActionButton"
+ android:src="@drawable/ic_screenshot"
+ android:contentDescription="@string/action_screenshot"
+ android:background="@drawable/round_rect_button" />
+ <TextView
+ style="@style/GoOverviewActionButtonCaption"
+ android:text="@string/action_screenshot" />
+ </LinearLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1" />
+
+ <!-- Will be enabled in a future version. -->
+ <LinearLayout
+ style="@style/GoOverviewActionButtonContainer"
+ android:visibility="gone" >
+ <ImageButton
+ android:id="@+id/action_search"
+ style="@style/GoOverviewActionButton"
+ android:src="@drawable/ic_search"
+ android:contentDescription="@string/action_search"
+ android:background="@drawable/round_rect_button" />
+ <TextView
+ style="@style/GoOverviewActionButtonCaption"
+ android:text="@string/action_search" />
+ </LinearLayout>
+
+ <!-- Unused. Included only for compatibility with parent class. -->
+ <Button
+ android:id="@+id/action_share"
+ style="@style/GoOverviewActionButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_share"
+ android:text="@string/action_share"
+ android:theme="@style/ThemeControlHighlightWorkspaceColor"
+ android:visibility="gone" />
+ </LinearLayout>
+
+</com.android.quickstep.views.GoOverviewActionsView>
\ No newline at end of file
diff --git a/go/quickstep/res/values-land/dimens.xml b/go/quickstep/res/values-land/dimens.xml
new file mode 100644
index 0000000..6b5d0b4
--- /dev/null
+++ b/go/quickstep/res/values-land/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Modal Dialogs -->
+ <dimen name="modal_dialog_width">360dp</dimen>
+ <dimen name="modal_dialog_text_height">168dp</dimen>
+</resources>
diff --git a/go/quickstep/res/values/attrs.xml b/go/quickstep/res/values/attrs.xml
new file mode 100644
index 0000000..cdbdc2a
--- /dev/null
+++ b/go/quickstep/res/values/attrs.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Attributes used for Overview theming -->
+ <attr name="overviewButtonTextColor" format="color" />
+ <attr name="overviewButtonIconColor" format="color" />
+ <attr name="overviewButtonBackgroundColor" format="color" />
+ <!-- Modal dialog theming -->
+ <attr name="modalDialogBackground" format="color" />
+</resources>
\ No newline at end of file
diff --git a/go/quickstep/res/values/colors.xml b/go/quickstep/res/values/colors.xml
new file mode 100644
index 0000000..4ce7669
--- /dev/null
+++ b/go/quickstep/res/values/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Overview -->
+ <color name="go_overview_text_color">#3C4043</color>
+ <color name="go_overview_text_color_dark">#F8F9FA</color>
+ <color name="go_overview_button_color">#70FFFFFF</color>
+ <color name="go_overview_button_color_dark">#474747</color>
+ <!-- Modal Dialogs -->
+ <color name="go_modal_dialog_background">#FFFFFF</color>
+ <color name="go_modal_dialog_background_dark">#424242</color>
+
+ <!-- Tooltip Color -->
+ <color name="arrow_tip_view_bg">#1A73E8</color>
+ <color name="arrow_tip_view_content">#FFFFFF</color>
+</resources>
diff --git a/go/quickstep/res/values/config.xml b/go/quickstep/res/values/config.xml
new file mode 100644
index 0000000..796d14d
--- /dev/null
+++ b/go/quickstep/res/values/config.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- The component to receive app sharing Intents -->
+ <string name="app_sharing_component" translatable="false"/>
+
+ <!-- Feature Flags -->
+ <bool name="enable_niu_actions">true</bool>
+
+ <string name="task_overlay_factory_class" translatable="false">com.android.quickstep.TaskOverlayFactoryGo</string>
+</resources>
\ No newline at end of file
diff --git a/go/quickstep/res/values/dimens.xml b/go/quickstep/res/values/dimens.xml
new file mode 100644
index 0000000..84d7fe5
--- /dev/null
+++ b/go/quickstep/res/values/dimens.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <dimen name="go_button_corner_radius">20dp</dimen>
+
+ <!-- Overview -->
+ <dimen name="go_overview_button_width">60dp</dimen>
+ <dimen name="go_overview_button_height">60dp</dimen>
+ <dimen name="go_overview_button_container_width">80dp</dimen>
+ <dimen name="go_overview_button_container_margin">16dp</dimen>
+ <dimen name="go_overview_button_caption_margin">8dp</dimen>
+ <dimen name="overview_actions_height">96dp</dimen>
+ <dimen name="overview_proactive_row_height">0dp</dimen>
+ <dimen name="overview_proactive_row_bottom_margin">24dp</dimen>
+ <dimen name="task_corner_radius_override">28dp</dimen>
+
+ <!-- Modal Dialogs -->
+ <dimen name="modal_dialog_width">288dp</dimen>
+ <dimen name="modal_dialog_padding">24dp</dimen>
+ <dimen name="modal_dialog_padding_bottom">8dp</dimen>
+ <dimen name="modal_dialog_vertical_spacer">12dp</dimen>
+ <dimen name="modal_dialog_corner_radius">8dp</dimen>
+ <dimen name="modal_dialog_text_height">216dp</dimen>
+
+ <!-- Tooltip -->
+ <dimen name="tooltip_corner_radius">8dp</dimen>
+ <dimen name="tooltip_top_margin">3dp</dimen>
+</resources>
diff --git a/go/quickstep/res/values/strings.xml b/go/quickstep/res/values/strings.xml
new file mode 100644
index 0000000..8429f6a
--- /dev/null
+++ b/go/quickstep/res/values/strings.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Label for app share drop target. [CHAR_LIMIT=20] -->
+ <string name="app_share_drop_target_label">Share App</string>
+
+ <!-- ******* Overview ******* -->
+ <!-- Label for a button that lets the user listen to the content of the current app. [CHAR_LIMIT=40] -->
+ <string name="action_listen">Listen</string>
+ <!-- Label for a button that translates a screenshot of the current app. [CHAR_LIMIT=40] -->
+ <string name="action_translate">Translate</string>
+ <!-- Label for a button that triggers Search on a screenshot of the current app. [CHAR_LIMIT=40] -->
+ <string name="action_search">Lens</string>
+
+ <!-- ******* Dialogs ******* -->
+ <!-- Label for a button that acknowledges the contents of the dialog. [CHAR_LIMIT=40] -->
+ <string name="dialog_acknowledge">GOT IT</string>
+ <!-- Label for a button that cancels the dialog. [CHAR_LIMIT=40] -->
+ <string name="dialog_cancel">CANCEL</string>
+ <!-- Label for a button that redirects the user to Settings. [CHAR_LIMIT=40] -->
+ <string name="dialog_settings">SETTINGS</string>
+
+ <!-- ******* NIU Actions First-Run Confirmation Dialog ******* -->
+ <!-- Dialog title -->
+ <string name="niu_actions_confirmation_title">Translate or listen to text on screen</string>
+ <!-- Dialog content -->
+ <string name="niu_actions_confirmation_text">Information such as text on your screen, web addresses, and screenshots may be shared with Google.\n\nTo change what information you share, go to <b>Settings > Apps > Default apps > Digital assistant app</b>.</string>
+
+ <!-- ******* NIU Actions Default Assistant Error Dialogs ******* -->
+ <!-- Assistant not selected: Dialog title -->
+ <string name="assistant_not_selected_title">Choose an assistant to use this feature</string>
+ <!-- Assistant not selected: Dialog content -->
+ <string name="assistant_not_selected_text">To listen to or translate text on your screen, choose a digital assistant app in Settings</string>
+ <!-- Assistant not supported: Dialog title -->
+ <string name="assistant_not_supported_title">Change your assistant to use this feature</string>
+ <!-- Assistant not supported: Dialog content -->
+ <string name="assistant_not_supported_text">To listen to or translate text on your screen, change your digital assistant app in Settings</string>
+
+ <!-- ******* NIU Actions Tooltip ******* -->
+ <!-- Tooltip to highlight and explain the Listen button -->
+ <string name="tooltip_listen">Tap here to listen to text on this screen</string>
+ <!-- Tooltip to highlight and explain the Translate button -->
+ <string name="tooltip_translate">Tap here to translate text on this screen</string>
+</resources>
diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml
new file mode 100644
index 0000000..442c413
--- /dev/null
+++ b/go/quickstep/res/values/styles.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- App themes -->
+ <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
+ <item name="overviewButtonTextColor">@color/go_overview_text_color</item>
+ <item name="overviewButtonIconColor">@color/go_overview_text_color</item>
+ <item name="overviewButtonBackgroundColor">@color/go_overview_button_color</item>
+ <item name="modalDialogBackground">@color/go_modal_dialog_background</item>
+ </style>
+
+ <style name="LauncherTheme.Dark" parent="@style/LauncherTheme">
+ <item name="overviewButtonTextColor">@color/go_overview_text_color_dark</item>
+ <item name="overviewButtonIconColor">@color/go_overview_text_color_dark</item>
+ <item name="overviewButtonBackgroundColor">@color/go_overview_button_color_dark</item>
+ <item name="modalDialogBackground">@color/go_modal_dialog_background_dark</item>
+ </style>
+
+ <!-- Overview -->
+ <style name="GoOverviewActionButton">
+ <item name="android:tint">?attr/overviewButtonIconColor</item>
+ <item name="android:backgroundTint">?attr/overviewButtonBackgroundColor</item>
+ <item name="android:background">@drawable/round_rect_button</item>
+ <item name="android:layout_width">@dimen/go_overview_button_width</item>
+ <item name="android:layout_height">@dimen/go_overview_button_height</item>
+ <item name="android:layout_gravity">center_horizontal</item>
+ </style>
+
+ <style name="GoOverviewActionButtonCaption">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ <item name="android:textSize">14dp</item>
+ <item name="android:textColor">?attr/overviewButtonTextColor</item>
+ <item name="android:lineHeight">20dp</item>
+ <item name="android:textAlignment">center</item>
+ <item name="android:importantForAccessibility">no</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginTop">@dimen/go_overview_button_caption_margin</item>
+ <item name="android:layout_gravity">center_horizontal</item>
+ </style>
+
+ <style name="GoOverviewActionButtonContainer">
+ <item name="android:layout_width">@dimen/go_overview_button_container_width</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <!-- Modal Dialogs -->
+ <style name="ModalDialogTitle">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ <item name="android:textSize">20sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:lineHeight">24dp</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center_horizontal</item>
+ </style>
+
+ <style name="ModalDialogText">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:lineHeight">24dp</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center_horizontal</item>
+ </style>
+
+ <style name="ModalDialogButton" parent="@style/Widget.AppCompat.Button.Borderless">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/colorAccent</item>
+ <item name="android:lineHeight">20dp</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/launcher3/AppSharing.java b/go/quickstep/src/com/android/launcher3/AppSharing.java
new file mode 100644
index 0000000..b72e71c
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/AppSharing.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import androidx.core.content.FileProvider;
+
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+
+import java.io.File;
+
+/**
+ * Defines the Share system shortcut and its factory.
+ * This shortcut can be added to the app long-press menu on the home screen.
+ * Clicking the button will initiate peer-to-peer sharing of the app.
+ */
+public final class AppSharing {
+ /**
+ * This flag enables this feature. It is defined here rather than in launcher3's FeatureFlags
+ * because it is unique to Go and not toggleable at runtime.
+ */
+ public static final boolean ENABLE_APP_SHARING = true;
+
+ private static final String TAG = "AppSharing";
+ private static final String FILE_PROVIDER_SUFFIX = ".overview.fileprovider";
+ private static final String APP_EXSTENSION = ".apk";
+ private static final String APP_MIME_TYPE = "application/application";
+
+ private final String mSharingComponent;
+
+ private AppSharing(Launcher launcher) {
+ mSharingComponent = launcher.getText(R.string.app_sharing_component).toString();
+ }
+
+ private boolean canShare(ItemInfo info) {
+ /**
+ * TODO: Implement once b/168831749 has been resolved
+ * The implementation should check the validity of the app.
+ * It should also check whether the app is free or paid, returning false in the latter case.
+ * For now, all checks occur in the sharing app.
+ * So, we simply check whether the sharing app is defined.
+ */
+ return !TextUtils.isEmpty(mSharingComponent);
+ }
+
+ private Uri getShareableUri(Context context, String path, String displayName) {
+ String authority = BuildConfig.APPLICATION_ID + FILE_PROVIDER_SUFFIX;
+ File pathFile = new File(path);
+ return FileProvider.getUriForFile(context, authority, pathFile, displayName);
+ }
+
+ private SystemShortcut<Launcher> getShortcut(Launcher launcher, ItemInfo info) {
+ if (!canShare(info)) {
+ return null;
+ }
+
+ return new Share(launcher, info);
+ }
+
+ /**
+ * The Share App system shortcut, used to initiate p2p sharing of a given app
+ */
+ public final class Share extends SystemShortcut<Launcher> {
+ public Share(Launcher target, ItemInfo itemInfo) {
+ super(R.drawable.ic_share, R.string.app_share_drop_target_label, target, itemInfo);
+ }
+
+ @Override
+ public void onClick(View view) {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+
+ ComponentName targetComponent = mItemInfo.getTargetComponent();
+ if (targetComponent == null) {
+ Log.e(TAG, "Item missing target component");
+ return;
+ }
+ String packageName = targetComponent.getPackageName();
+ PackageManager packageManager = view.getContext().getPackageManager();
+ String sourceDir, appLabel;
+ try {
+ PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
+ sourceDir = packageInfo.applicationInfo.sourceDir;
+ appLabel = packageManager.getApplicationLabel(packageInfo.applicationInfo)
+ .toString() + APP_EXSTENSION;
+ } catch (Exception e) {
+ Log.e(TAG, "Could not find info for package \"" + packageName + "\"");
+ return;
+ }
+ Uri uri = getShareableUri(view.getContext(), sourceDir, appLabel);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, uri);
+ sendIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
+
+ sendIntent.setType(APP_MIME_TYPE);
+ sendIntent.setComponent(ComponentName.unflattenFromString(mSharingComponent));
+
+ mTarget.startActivitySafely(view, sendIntent, mItemInfo);
+
+ AbstractFloatingView.closeAllOpenViews(mTarget);
+ }
+ }
+
+ /**
+ * Shortcut factory for generating the Share App button
+ */
+ public static final SystemShortcut.Factory<Launcher> SHORTCUT_FACTORY = (launcher, itemInfo) ->
+ (new AppSharing(launcher)).getShortcut(launcher, itemInfo);
+}
diff --git a/go/quickstep/src/com/android/launcher3/Launcher3QuickStepGo.java b/go/quickstep/src/com/android/launcher3/Launcher3QuickStepGo.java
new file mode 100644
index 0000000..8bd0fec
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/Launcher3QuickStepGo.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+
+import java.util.stream.Stream;
+
+/**
+ * The Launcher variant used for Android Go Edition
+ */
+public class Launcher3QuickStepGo extends QuickstepLauncher {
+ private static final String TAG = "Launcher3QuickStepGo";
+
+ @Override
+ public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+ Stream<SystemShortcut.Factory> shortcuts = super.getSupportedShortcuts();
+
+ if (AppSharing.ENABLE_APP_SHARING) {
+ shortcuts = Stream.concat(shortcuts, Stream.of(AppSharing.SHORTCUT_FACTORY));
+ }
+
+ return shortcuts;
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
new file mode 100644
index 0000000..91cab3c
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.assist.AssistContent;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.util.AssistContentRequester;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.views.GoOverviewActionsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Go-specific extension of the factory class that adds an overlay to TaskView
+ */
+public final class TaskOverlayFactoryGo extends TaskOverlayFactory {
+ public static final String ACTION_LISTEN = "com.android.quickstep.ACTION_LISTEN";
+ public static final String ACTION_TRANSLATE = "com.android.quickstep.ACTION_TRANSLATE";
+ public static final String ACTION_SEARCH = "com.android.quickstep.ACTION_SEARCH";
+ public static final String ELAPSED_NANOS = "niu_actions_elapsed_realtime_nanos";
+ public static final String ACTIONS_URL = "niu_actions_app_url";
+ public static final String ACTIONS_APP_PACKAGE = "niu_actions_app_package";
+ public static final String ACTIONS_ERROR_CODE = "niu_actions_app_error_code";
+ public static final int ERROR_PERMISSIONS_STRUCTURE = 1;
+ public static final int ERROR_PERMISSIONS_SCREENSHOT = 2;
+ private static final String NIU_ACTIONS_CONFIRMED = "launcher_go.niu_actions_confirmed";
+ private static final String ASSIST_SETTINGS_ARGS_BUNDLE = ":settings:show_fragment_args";
+ private static final String ASSIST_SETTINGS_ARGS_KEY = ":settings:fragment_args_key";
+ private static final String ASSIST_SETTINGS_PREFERENCE_KEY = "default_assist";
+ private static final String TAG = "TaskOverlayFactoryGo";
+
+ public static final String LISTEN_TOOL_TIP_SEEN = "launcher.go_listen_tip_seen";
+ public static final String TRANSLATE_TOOL_TIP_SEEN = "launcher.go_translate_tip_seen";
+
+ @Retention(SOURCE)
+ @IntDef({PRIVACY_CONFIRMATION, ASSISTANT_NOT_SELECTED, ASSISTANT_NOT_SUPPORTED})
+ private @interface DialogType{}
+ private static final int PRIVACY_CONFIRMATION = 0;
+ private static final int ASSISTANT_NOT_SELECTED = 1;
+ private static final int ASSISTANT_NOT_SUPPORTED = 2;
+
+ private AssistContentRequester mContentRequester;
+
+ public TaskOverlayFactoryGo(Context context) {
+ mContentRequester = new AssistContentRequester(context);
+ }
+
+ /**
+ * Create a new overlay instance for the given View
+ */
+ public TaskOverlayGo createOverlay(TaskThumbnailView thumbnailView) {
+ return new TaskOverlayGo(thumbnailView, mContentRequester);
+ }
+
+ /**
+ * Overlay on each task handling Overview Action Buttons.
+ * @param <T> The type of View in which the overlay will be placed
+ */
+ public static final class TaskOverlayGo<T extends GoOverviewActionsView> extends TaskOverlay {
+ private String mNIUPackageName;
+ private String mTaskPackageName;
+ private String mWebUrl;
+ private boolean mAssistStructurePermitted;
+ private boolean mAssistScreenshotPermitted;
+ private AssistContentRequester mFactoryContentRequester;
+ private SharedPreferences mSharedPreferences;
+ private OverlayDialogGo mDialog;
+
+ private TaskOverlayGo(TaskThumbnailView taskThumbnailView,
+ AssistContentRequester assistContentRequester) {
+ super(taskThumbnailView);
+ mFactoryContentRequester = assistContentRequester;
+ mSharedPreferences = Utilities.getPrefs(mApplicationContext);
+ }
+
+ /**
+ * Called when the current task is interactive for the user
+ */
+ @Override
+ public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+ boolean rotated) {
+ if (mDialog != null && mDialog.isShowing()) {
+ // Redraw the dialog in case the layout changed
+ mDialog.dismiss();
+ showDialog(mDialog.getAction(), mDialog.getType());
+ }
+
+ getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
+ if (thumbnail == null) {
+ return;
+ }
+
+ getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+ // Disable Overview Actions for Work Profile apps
+ boolean isManagedProfileTask =
+ UserManager.get(mApplicationContext).isManagedProfile(task.key.userId);
+ boolean isAllowedByPolicy = mThumbnailView.isRealSnapshot() && !isManagedProfileTask;
+ getActionsView().setCallbacks(new OverlayUICallbacksGoImpl(isAllowedByPolicy, task));
+ mTaskPackageName = task.key.getPackageName();
+ mSharedPreferences = Utilities.getPrefs(mApplicationContext);
+ checkSettings();
+
+ if (!mAssistStructurePermitted || !mAssistScreenshotPermitted
+ || TextUtils.isEmpty(mNIUPackageName)) {
+ return;
+ }
+
+ int taskId = task.key.id;
+ mFactoryContentRequester.requestAssistContent(taskId, this::onAssistContentReceived);
+
+ RecentsOrientedState orientedState =
+ mThumbnailView.getTaskView().getRecentsView().getPagedViewOrientedState();
+ boolean isInLandscape = orientedState.getDisplayRotation() != ROTATION_0;
+
+ // show tooltips in portrait mode only
+ // TODO: remove If check once b/183714277 is fixed
+ if (!isInLandscape) {
+ new Handler().post(() -> {
+ showTooltipsIfUnseen();
+ });
+ }
+ }
+
+ /** Provide Assist Content to the overlay. */
+ @VisibleForTesting
+ public void onAssistContentReceived(AssistContent assistContent) {
+ mWebUrl = assistContent.getWebUri() != null
+ ? assistContent.getWebUri().toString() : null;
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ mWebUrl = null;
+ }
+
+ @Override
+ public void updateOrientationState(RecentsOrientedState state) {
+ super.updateOrientationState(state);
+ ((GoOverviewActionsView) getActionsView()).updateOrientationState(state);
+ }
+
+ /**
+ * Creates and sends an Intent corresponding to the button that was clicked
+ */
+ private void sendNIUIntent(String actionType) {
+ if (TextUtils.isEmpty(mNIUPackageName)) {
+ showDialog(actionType, ASSISTANT_NOT_SELECTED);
+ return;
+ }
+
+ if (!mSharedPreferences.getBoolean(NIU_ACTIONS_CONFIRMED, false)) {
+ showDialog(actionType, PRIVACY_CONFIRMATION);
+ return;
+ }
+
+ Intent intent = createNIUIntent(actionType);
+ // Only add and send the image if the appropriate permissions are held
+ if (mAssistStructurePermitted && mAssistScreenshotPermitted) {
+ mImageApi.shareAsDataWithExplicitIntent(/* crop */ null, intent);
+ } else {
+ // If both permissions are disabled, the structure error code takes priority
+ // The user must enable that one before they can enable screenshots
+ int code = mAssistStructurePermitted ? ERROR_PERMISSIONS_SCREENSHOT
+ : ERROR_PERMISSIONS_STRUCTURE;
+ intent.putExtra(ACTIONS_ERROR_CODE, code);
+ try {
+ mApplicationContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found to receive permission error intent");
+ showDialog(actionType, ASSISTANT_NOT_SUPPORTED);
+ }
+ }
+ }
+
+ private Intent createNIUIntent(String actionType) {
+ Intent intent = new Intent(actionType)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+ .setPackage(mNIUPackageName)
+ .putExtra(ACTIONS_APP_PACKAGE, mTaskPackageName)
+ .putExtra(ELAPSED_NANOS, SystemClock.elapsedRealtimeNanos());
+
+ if (mWebUrl != null) {
+ intent.putExtra(ACTIONS_URL, mWebUrl);
+ }
+
+ return intent;
+ }
+
+ /**
+ * Checks whether the Assistant has screen context permissions
+ */
+ @VisibleForTesting
+ public void checkSettings() {
+ ContentResolver contentResolver = mApplicationContext.getContentResolver();
+ mAssistStructurePermitted = Settings.Secure.getInt(contentResolver,
+ Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1) != 0;
+ mAssistScreenshotPermitted = Settings.Secure.getInt(contentResolver,
+ Settings.Secure.ASSIST_SCREENSHOT_ENABLED, 1) != 0;
+
+ String assistantPackage =
+ Settings.Secure.getString(contentResolver, Settings.Secure.ASSISTANT);
+ mNIUPackageName = assistantPackage.split("/", 2)[0];
+ }
+
+ protected class OverlayUICallbacksGoImpl extends OverlayUICallbacksImpl
+ implements OverlayUICallbacksGo {
+ public OverlayUICallbacksGoImpl(boolean isAllowedByPolicy, Task task) {
+ super(isAllowedByPolicy, task);
+ }
+
+ @SuppressLint("NewApi")
+ public void onListen() {
+ if (mIsAllowedByPolicy) {
+ endLiveTileMode(() -> sendNIUIntent(ACTION_LISTEN));
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public void onTranslate() {
+ if (mIsAllowedByPolicy) {
+ endLiveTileMode(() -> sendNIUIntent(ACTION_TRANSLATE));
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public void onSearch() {
+ if (mIsAllowedByPolicy) {
+ endLiveTileMode(() -> sendNIUIntent(ACTION_SEARCH));
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void setImageActionsAPI(ImageActionsApi imageActionsApi) {
+ mImageApi = imageActionsApi;
+ }
+
+ // TODO (b/192406446): Test that these dialogs are shown at the appropriate times
+ private void showDialog(String action, @DialogType int type) {
+ switch (type) {
+ case PRIVACY_CONFIRMATION:
+ showDialog(action, PRIVACY_CONFIRMATION,
+ R.string.niu_actions_confirmation_title,
+ R.string.niu_actions_confirmation_text, R.string.dialog_cancel,
+ this::onDialogClickCancel, R.string.dialog_acknowledge,
+ this::onNiuActionsConfirmationAccept);
+ break;
+ case ASSISTANT_NOT_SELECTED:
+ showDialog(action, ASSISTANT_NOT_SELECTED,
+ R.string.assistant_not_selected_title,
+ R.string.assistant_not_selected_text, R.string.dialog_cancel,
+ this::onDialogClickCancel, R.string.dialog_settings,
+ this::onDialogClickSettings);
+ break;
+ case ASSISTANT_NOT_SUPPORTED:
+ showDialog(action, ASSISTANT_NOT_SUPPORTED,
+ R.string.assistant_not_supported_title,
+ R.string.assistant_not_supported_text, R.string.dialog_cancel,
+ this::onDialogClickCancel, R.string.dialog_settings,
+ this::onDialogClickSettings);
+ break;
+ default:
+ Log.e(TAG, "Unexpected dialog type");
+ }
+ }
+
+ private void showDialog(String action, @DialogType int type, int titleTextID,
+ int bodyTextID, int button1TextID,
+ View.OnClickListener button1Callback, int button2TextID,
+ View.OnClickListener button2Callback) {
+ BaseDraggingActivity activity = BaseActivity.fromContext(getActionsView().getContext());
+ LayoutInflater inflater = LayoutInflater.from(activity);
+ View view = inflater.inflate(R.layout.niu_actions_dialog, /* root */ null);
+
+ TextView dialogTitle = view.findViewById(R.id.niu_actions_dialog_header);
+ dialogTitle.setText(titleTextID);
+
+ TextView dialogBody = view.findViewById(R.id.niu_actions_dialog_description);
+ dialogBody.setText(bodyTextID);
+
+ Button button1 = view.findViewById(R.id.niu_actions_dialog_button_1);
+ button1.setText(button1TextID);
+ button1.setOnClickListener(button1Callback);
+
+ Button button2 = view.findViewById(R.id.niu_actions_dialog_button_2);
+ button2.setText(button2TextID);
+ button2.setOnClickListener(button2Callback);
+
+ mDialog = new OverlayDialogGo(activity, type, action);
+ mDialog.setView(view);
+ mDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ mDialog.show();
+ }
+
+ private void onNiuActionsConfirmationAccept(View v) {
+ mDialog.dismiss();
+ mSharedPreferences.edit().putBoolean(NIU_ACTIONS_CONFIRMED, true).apply();
+ sendNIUIntent(mDialog.getAction());
+ }
+
+ private void onDialogClickCancel(View v) {
+ mDialog.cancel();
+ }
+
+ private void onDialogClickSettings(View v) {
+ mDialog.dismiss();
+
+ Bundle fragmentArgs = new Bundle();
+ fragmentArgs.putString(ASSIST_SETTINGS_ARGS_KEY, ASSIST_SETTINGS_PREFERENCE_KEY);
+ Intent intent = new Intent(Settings.ACTION_VOICE_INPUT_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ .putExtra(ASSIST_SETTINGS_ARGS_BUNDLE, fragmentArgs);
+ try {
+ mApplicationContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found to receive assistant settings intent");
+ }
+ }
+
+ /**
+ * Checks and Shows the tooltip if they are not seen by user
+ * Order of tooltips are translate and then listen
+ */
+ private void showTooltipsIfUnseen() {
+ if (!mSharedPreferences.getBoolean(TRANSLATE_TOOL_TIP_SEEN, false)) {
+ ((GoOverviewActionsView) getActionsView()).showTranslateToolTip();
+ mSharedPreferences.edit().putBoolean(TRANSLATE_TOOL_TIP_SEEN, true).apply();
+ } else if (!mSharedPreferences.getBoolean(LISTEN_TOOL_TIP_SEEN, false)) {
+ ((GoOverviewActionsView) getActionsView()).showListenToolTip();
+ mSharedPreferences.edit().putBoolean(LISTEN_TOOL_TIP_SEEN, true).apply();
+ }
+ }
+ }
+
+ private static final class OverlayDialogGo extends AlertDialog {
+ private final String mAction;
+ private final @DialogType int mType;
+
+ OverlayDialogGo(Context context, @DialogType int type, String action) {
+ super(context);
+ mType = type;
+ mAction = action;
+ }
+
+ public String getAction() {
+ return mAction;
+ }
+ public @DialogType int getType() {
+ return mType;
+ }
+ }
+
+ /**
+ * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
+ * controller.
+ */
+ public interface OverlayUICallbacksGo extends OverlayUICallbacks {
+ /** User has requested to listen to the current content read aloud */
+ void onListen();
+
+ /** User has requested a translation of the current content */
+ void onTranslate();
+
+ /** User has requested a visual search of the current content */
+ void onSearch();
+ }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/GoOverviewActionsView.java b/go/quickstep/src/com/android/quickstep/views/GoOverviewActionsView.java
new file mode 100644
index 0000000..97ba590
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/GoOverviewActionsView.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views;
+
+import static android.view.View.GONE;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+
+import com.android.launcher3.R;
+import com.android.launcher3.views.ArrowTipView;
+import com.android.quickstep.TaskOverlayFactoryGo.OverlayUICallbacksGo;
+import com.android.quickstep.util.RecentsOrientedState;
+
+/**
+ * View for showing Go-specific action buttons in Overview
+ */
+public class GoOverviewActionsView extends OverviewActionsView<OverlayUICallbacksGo> {
+
+ private ArrowTipView mArrowTipView;
+
+ public GoOverviewActionsView(Context context) {
+ this(context, null);
+ }
+
+ public GoOverviewActionsView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GoOverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ if (getResources().getBoolean(R.bool.enable_niu_actions)) {
+ findViewById(R.id.action_listen).setOnClickListener(this);
+ findViewById(R.id.action_translate).setOnClickListener(this);
+ } else {
+ findViewById(R.id.layout_listen).setVisibility(GONE);
+ findViewById(R.id.spacer_listen).setVisibility(GONE);
+ findViewById(R.id.layout_translate).setVisibility(GONE);
+ findViewById(R.id.spacer_translate).setVisibility(GONE);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ super.onClick(view);
+
+ if (mCallbacks == null) {
+ return;
+ }
+ int id = view.getId();
+ if (id == R.id.action_listen) {
+ mCallbacks.onListen();
+ } else if (id == R.id.action_translate) {
+ mCallbacks.onTranslate();
+ } else if (id == R.id.action_search) {
+ mCallbacks.onSearch();
+ }
+ }
+
+ /**
+ * Shows Tooltip for action icons
+ */
+ private void showToolTip(int viewId, int textResourceId) {
+ int[] location = new int[2];
+ @Px int topMargin = getResources().getDimensionPixelSize(R.dimen.tooltip_top_margin);
+ findViewById(viewId).getLocationOnScreen(location);
+ mArrowTipView = new ArrowTipView(getContext(), /* isPointingUp= */ false)
+ .showAtLocation(getResources().getString(textResourceId),
+ /* arrowXCoord= */ location[0] + findViewById(viewId).getWidth() / 2,
+ /* yCoord= */ location[1] - topMargin);
+
+ mArrowTipView.bringToFront();
+ }
+
+ /**
+ * Shows Tooltip for listen action icon
+ */
+ public void showListenToolTip() {
+ showToolTip(/* viewId= */ R.id.action_listen,
+ /* textResourceId= */ R.string.tooltip_listen);
+ }
+
+ /**
+ * Shows Tooltip for translate action icon
+ */
+ public void showTranslateToolTip() {
+ showToolTip(/* viewId= */ R.id.action_translate,
+ /* textResourceId= */ R.string.tooltip_translate);
+ }
+
+ /**
+ * Called when device orientation is changed
+ */
+ public void updateOrientationState(RecentsOrientedState orientedState) {
+ // dismiss tooltip
+ boolean canLauncherRotate = orientedState.canRecentsActivityRotate();
+ if (mArrowTipView != null && !canLauncherRotate) {
+ mArrowTipView.close(/* animate= */ false);
+ }
+ }
+}
diff --git a/go/res/values/dimens.xml b/go/res/values/dimens.xml
deleted file mode 100644
index f1b1053..0000000
--- a/go/res/values/dimens.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<resources>
- <!-- Dynamic Grid -->
- <dimen name="dynamic_grid_hotseat_size">60dp</dimen>
-</resources>
\ No newline at end of file
diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java
index 7130531..5f71061 100644
--- a/go/src/com/android/launcher3/model/LoaderResults.java
+++ b/go/src/com/android/launcher3/model/LoaderResults.java
@@ -20,7 +20,6 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.util.LooperExecutor;
/**
* Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -29,12 +28,7 @@
public LoaderResults(LauncherAppState app, BgDataModel dataModel,
AllAppsList allAppsList, Callbacks[] callbacks) {
- this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
- }
-
- public LoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
- super(app, dataModel, allAppsList, callbacks, executor);
+ super(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
}
@Override
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 3b3dc01..f8448da 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -24,12 +24,14 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.icons.ComponentWithLabelAndIcon;
+import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -41,21 +43,27 @@
// True is the widget support is disabled.
public static final boolean GO_DISABLE_WIDGETS = true;
+ public static final boolean GO_DISABLE_NOTIFICATION_DOTS = true;
- private static final ArrayList<WidgetListRowEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
+ private static final ArrayList<WidgetsListBaseEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
/**
- * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row
- * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s
- * is not sorted. This list is sorted at the UI when using
- * {@link com.android.launcher3.widget.WidgetsDiffReporter}
+ * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row are
+ * sorted (based on label and user), but the overall list of {@link WidgetsListBaseEntry}s is
+ * not sorted. This list is sorted at the UI when using
+ * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter}
*
- * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList)
+ * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
*/
- public synchronized ArrayList<WidgetListRowEntry> getWidgetsList(Context context) {
+ public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
return EMPTY_WIDGET_LIST;
}
+ /** Returns a mapping of packages to their widgets without static shortcuts. */
+ public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
+ return Map.of();
+ }
+
/**
* @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
* only widgets and shortcuts associated with the package/user are.
@@ -74,4 +82,9 @@
ComponentName providerName) {
return null;
}
+
+ /** Returns {@link PackageItemInfo} of a pending widget. */
+ public static PackageItemInfo newPendingItemInfo(ComponentName provider) {
+ return new PackageItemInfo(provider.getPackageName());
+ }
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 7a51375..7f4c609 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,4 +10,4 @@
PROTOBUF_DEPENDENCY=com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7
BUILD_TOOLS_VERSION=28.0.3
-COMPILE_SDK=android-R
+COMPILE_SDK=android-S
diff --git a/lint-baseline-common-deps-lib.xml b/lint-baseline-common-deps-lib.xml
new file mode 100644
index 0000000..e52f8fb
--- /dev/null
+++ b/lint-baseline-common-deps-lib.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+ <issue
+ id="NewApi"
+ message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
+ errorLine1=" android:topLeftRadius="?android:attr/dialogCornerRadius""
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
+ line="6"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
+ errorLine1=" android:topRightRadius="?android:attr/dialogCornerRadius" />"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
+ line="7"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`@android:style/Widget.DeviceDefault.Button.Colored` requires API level 28 (current min is 26)"
+ errorLine1=" <style name="Widget.DeviceDefault.Button.Rounded.Colored" parent="@android:style/Widget.DeviceDefault.Button.Colored">"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/values/styles.xml"
+ line="287"
+ column="63"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`@android:dimen/system_app_widget_background_radius` requires API level 31 (current min is 26)"
+ errorLine1=" <corners android:radius="@android:dimen/system_app_widget_background_radius" />"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/drawable/widget_resize_frame.xml"
+ line="20"
+ column="14"/>
+ </issue>
+
+</issues>
diff --git a/lint-baseline-go-res-lib.xml b/lint-baseline-go-res-lib.xml
new file mode 100644
index 0000000..c5669f2
--- /dev/null
+++ b/lint-baseline-go-res-lib.xml
@@ -0,0 +1,576 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.View#getWindowInsetsController`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
+ line="203"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#ime`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
+ line="203"
+ column="60"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsetsController#hide`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
+ line="203"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.View#getWindowInsetsController`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
+ line="193"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#ime`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
+ line="193"
+ column="68"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsetsController#hide`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
+ line="193"
+ column="45"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.appwidget.AppWidgetHostView#updateAppWidgetSize`"
+ errorLine1=" widgetView.updateAppWidgetSize(new Bundle(), sizes);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/AppWidgetResizeFrame.java"
+ line="399"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
+ errorLine1=" potentialTaskRect.inset(insets.left, insets.top, insets.right, insets.bottom);"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
+ line="248"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
+ errorLine1=" potentialTaskRect.inset("
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
+ line="249"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
+ errorLine1=" outRect.inset(Math.max(insets.left, sideMargin), Math.max(insets.top, topMargin),"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
+ line="291"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
+ errorLine1=" gridRect.inset(0, dp.overviewTaskThumbnailTopMarginPx, 0, 0);"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
+ line="315"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowManager#getCurrentWindowMetrics`"
+ errorLine1=" .getCurrentWindowMetrics().getWindowInsets();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
+ line="236"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowMetrics#getWindowInsets`"
+ errorLine1=" .getCurrentWindowMetrics().getWindowInsets();"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
+ line="236"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.content.Context#getDisplay`"
+ errorLine1=" if (mContext.getDisplay() != null) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java"
+ line="115"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.content.Context#getDisplay`"
+ errorLine1=" mContext.getDisplay().getRealSize(mDisplaySize);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java"
+ line="116"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.content.ContextWrapper#getDisplay`"
+ errorLine1=" Display display = getDisplay();"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java"
+ line="153"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `java.util.List#of`"
+ errorLine1=" List.of(new Rect(0, 0, metrics.widthPixels, metrics.heightPixels)));"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java"
+ line="158"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.appwidget.AppWidgetHostView#resetColorResources`"
+ errorLine1=" resetColorResources();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
+ line="137"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `java.util.List#of`"
+ errorLine1=" mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
+ line="367"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `java.util.List#of`"
+ errorLine1=" mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
+ line="390"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
+ errorLine1=" (ATLEAST_S && maxResizeWidth > 0)"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="91"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
+ errorLine1=" ? getSpanX(widgetPadding, maxResizeWidth, smallestCellWidth)"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="92"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
+ errorLine1=" (ATLEAST_S && maxResizeHeight > 0)"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="95"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
+ errorLine1=" ? getSpanY(widgetPadding, maxResizeHeight, smallestCellHeight)"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="96"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
+ errorLine1=" if (ATLEAST_S && targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="101"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
+ errorLine1=" if (ATLEAST_S && targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="101"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
+ errorLine1=" && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="102"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
+ errorLine1=" && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="102"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
+ errorLine1=" spanX = targetCellWidth;"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="103"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
+ errorLine1=" spanY = targetCellHeight;"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="104"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.content.Context#getDisplay`"
+ errorLine1=" final Display display = mContext.getDisplay();"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java"
+ line="94"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.content.pm.LauncherActivityInfo#getLoadingProgress`"
+ errorLine1=" return (int) (100 * info.getLoadingProgress());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/util/PackageManagerHelper.java"
+ line="338"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `java.util.List#of`"
+ errorLine1=" private List<WidgetsListBaseEntry> mAllWidgets = List.of();"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
+ line="64"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `java.util.List#of`"
+ errorLine1=" private List<ItemInfo> mRecommendedWidgets = List.of();"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
+ line="66"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `new android.view.SurfaceControlViewHost`"
+ errorLine1=" .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="91"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.SurfaceControlViewHost#getSurfacePackage`"
+ errorLine1=" surfacePackage = mSurfaceControlViewHost.getSurfacePackage();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="93"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.SurfaceControlViewHost#setView`"
+ errorLine1=" host.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="127"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `SurfacePackage` to `Parcelable` requires API level 30 (current min is 29)"
+ errorLine1=" result.putParcelable(KEY_SURFACE_PACKAGE, surfacePackage);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="132"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.SurfaceControlViewHost#release`"
+ errorLine1=" mSurfaceControlViewHost.release();"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="149"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.graphics.Outline#setPath`"
+ errorLine1=" outline.setPath(mPath);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/popup/RoundedArrowDrawable.java"
+ line="88"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `new android.widget.EdgeEffect`"
+ errorLine1=" ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
+ line="49"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `new android.widget.EdgeEffect`"
+ errorLine1=" ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
+ line="51"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowManager.LayoutParams#setFitInsetsTypes`"
+ errorLine1=" mWindowLayoutParams.setFitInsetsTypes(0);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java"
+ line="316"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsets#getInsets`"
+ errorLine1=" Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java"
+ line="118"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#systemBars`"
+ errorLine1=" Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java"
+ line="118"
+ column="70"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#loadDescription`"
+ errorLine1=" CharSequence description = mItem.widgetInfo.loadDescription(getContext());"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
+ line="193"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
+ errorLine1=" && item.widgetInfo.previewLayout != Resources.ID_NULL) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
+ line="214"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
+ errorLine1=" launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
+ line="222"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.View#getWindowInsetsController`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+ line="558"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#ime`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+ line="558"
+ column="60"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `android.view.WindowInsetsController#hide`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+ line="558"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 29): `java.util.List#of`"
+ errorLine1=" return new RecommendationTableData(List.of(), previewScale);"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java"
+ line="139"
+ column="53"/>
+ </issue>
+
+</issues>
diff --git a/lint-baseline-launcher3.xml b/lint-baseline-launcher3.xml
new file mode 100644
index 0000000..9a68405
--- /dev/null
+++ b/lint-baseline-launcher3.xml
@@ -0,0 +1,598 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.View#getWindowInsetsController`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
+ line="203"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowInsets.Type#ime`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
+ line="203"
+ column="60"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowInsetsController#hide`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
+ line="203"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.View#getWindowInsetsController`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
+ line="193"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowInsets.Type#ime`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
+ line="193"
+ column="68"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowInsetsController#hide`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
+ line="193"
+ column="45"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#updateAppWidgetSize`"
+ errorLine1=" widgetView.updateAppWidgetSize(new Bundle(), sizes);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/AppWidgetResizeFrame.java"
+ line="399"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowManager#getCurrentWindowMetrics`"
+ errorLine1=" .getCurrentWindowMetrics().getWindowInsets();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
+ line="236"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowMetrics#getWindowInsets`"
+ errorLine1=" .getCurrentWindowMetrics().getWindowInsets();"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
+ line="236"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`"
+ errorLine1=" folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
+ line="256"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`"
+ errorLine1=" return mContext.getResources().getFloat(resId);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/util/DynamicResource.java"
+ line="73"
+ column="40"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#resetColorResources`"
+ errorLine1=" resetColorResources();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
+ line="137"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `java.util.List#of`"
+ errorLine1=" mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
+ line="367"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `java.util.List#of`"
+ errorLine1=" mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
+ line="390"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
+ errorLine1=" (ATLEAST_S && maxResizeWidth > 0)"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="91"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
+ errorLine1=" ? getSpanX(widgetPadding, maxResizeWidth, smallestCellWidth)"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="92"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
+ errorLine1=" (ATLEAST_S && maxResizeHeight > 0)"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="95"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
+ errorLine1=" ? getSpanY(widgetPadding, maxResizeHeight, smallestCellHeight)"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="96"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
+ errorLine1=" if (ATLEAST_S && targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="101"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
+ errorLine1=" if (ATLEAST_S && targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="101"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
+ errorLine1=" && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="102"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
+ errorLine1=" && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="102"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
+ errorLine1=" spanX = targetCellWidth;"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="103"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
+ errorLine1=" spanY = targetCellHeight;"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
+ line="104"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 28 (current min is 26): `android.app.Person#getKey`"
+ errorLine1=" return people.stream().filter(person -> person.getKey() != null)"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
+ line="72"
+ column="56"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Method reference requires API level 28 (current min is 26): `Person::getKey`"
+ errorLine1=" .map(Person::getKey).sorted().toArray(String[]::new);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
+ line="73"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 26): `android.content.pm.LauncherActivityInfo#getLoadingProgress`"
+ errorLine1=" return (int) (100 * info.getLoadingProgress());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/util/PackageManagerHelper.java"
+ line="338"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`"
+ errorLine1=" AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+ line="1752"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`"
+ errorLine1=" : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+ line="1753"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`"
+ errorLine1=" AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+ line="1760"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`"
+ errorLine1=" : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+ line="1761"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `java.util.List#of`"
+ errorLine1=" private List<WidgetsListBaseEntry> mAllWidgets = List.of();"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
+ line="64"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `java.util.List#of`"
+ errorLine1=" private List<ItemInfo> mRecommendedWidgets = List.of();"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
+ line="66"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `new android.view.SurfaceControlViewHost`"
+ errorLine1=" .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="91"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.SurfaceControlViewHost#getSurfacePackage`"
+ errorLine1=" surfacePackage = mSurfaceControlViewHost.getSurfacePackage();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="93"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.SurfaceControlViewHost#setView`"
+ errorLine1=" host.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="127"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `SurfacePackage` to `Parcelable` requires API level 30 (current min is 26)"
+ errorLine1=" result.putParcelable(KEY_SURFACE_PACKAGE, surfacePackage);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="132"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.SurfaceControlViewHost#release`"
+ errorLine1=" mSurfaceControlViewHost.release();"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
+ line="149"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.graphics.Outline#setPath`"
+ errorLine1=" outline.setPath(mPath);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/popup/RoundedArrowDrawable.java"
+ line="88"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 26): `new android.widget.EdgeEffect`"
+ errorLine1=" ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
+ line="49"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 26): `new android.widget.EdgeEffect`"
+ errorLine1=" ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
+ line="51"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 29 (current min is 26): `android.view.WindowInsets#getTappableElementInsets`"
+ errorLine1=" return windowInsets.getTappableElementInsets().bottom > 0;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/SysUiScrim.java"
+ line="190"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 29 (current min is 26): `android.graphics.Insets#bottom`"
+ errorLine1=" return windowInsets.getTappableElementInsets().bottom > 0;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/SysUiScrim.java"
+ line="190"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 28 (current min is 26): `android.appwidget.AppWidgetProviderInfo#widgetFeatures`"
+ errorLine1=" int featureFlags = mProviderInfo.widgetFeatures;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetAddFlowHandler.java"
+ line="93"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#loadDescription`"
+ errorLine1=" CharSequence description = mItem.widgetInfo.loadDescription(getContext());"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
+ line="193"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
+ errorLine1=" && item.widgetInfo.previewLayout != Resources.ID_NULL) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
+ line="214"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
+ errorLine1=" launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
+ line="222"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.View#getWindowInsetsController`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+ line="558"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowInsets.Type#ime`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+ line="558"
+ column="60"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `android.view.WindowInsetsController#hide`"
+ errorLine1=" getWindowInsetsController().hide(WindowInsets.Type.ime());"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+ line="558"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level R (current min is 26): `java.util.List#of`"
+ errorLine1=" return new RecommendationTableData(List.of(), previewScale);"
+ errorLine2=" ~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java"
+ line="139"
+ column="53"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Method reference requires API level 28 (current min is 26): `Person::getKey`"
+ errorLine1=" : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/model/data/WorkspaceItemInfo.java"
+ line="178"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#setColorResources`"
+ errorLine1=" view.setColorResources(mWallpaperColorResources);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
+ line="381"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 27 (current min is 26): `android.app.WallpaperManager#getWallpaperColors`"
+ errorLine1=" : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
+ line="270"
+ column="61"/>
+ </issue>
+
+</issues>
diff --git a/lint-baseline-res-lib.xml b/lint-baseline-res-lib.xml
new file mode 100644
index 0000000..e52f8fb
--- /dev/null
+++ b/lint-baseline-res-lib.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+ <issue
+ id="NewApi"
+ message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
+ errorLine1=" android:topLeftRadius="?android:attr/dialogCornerRadius""
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
+ line="6"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
+ errorLine1=" android:topRightRadius="?android:attr/dialogCornerRadius" />"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
+ line="7"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`@android:style/Widget.DeviceDefault.Button.Colored` requires API level 28 (current min is 26)"
+ errorLine1=" <style name="Widget.DeviceDefault.Button.Rounded.Colored" parent="@android:style/Widget.DeviceDefault.Button.Colored">"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/values/styles.xml"
+ line="287"
+ column="63"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="`@android:dimen/system_app_widget_background_radius` requires API level 31 (current min is 26)"
+ errorLine1=" <corners android:radius="@android:dimen/system_app_widget_background_radius" />"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/apps/Launcher3/res/drawable/widget_resize_frame.xml"
+ line="20"
+ column="14"/>
+ </issue>
+
+</issues>
diff --git a/proguard.flags b/proguard.flags
index 37b8093..a450183 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -45,9 +45,10 @@
# BUG(70852369): Surpress additional warnings after changing from Proguard to R8
-dontwarn android.app.**
--dontwarn android.view.**
--dontwarn android.os.**
-dontwarn android.graphics.**
+-dontwarn android.os.**
+-dontwarn android.view.**
+-dontwarn android.window.**
# Ignore warnings for hidden utility classes referenced from the shared lib
-dontwarn com.android.internal.util.**
diff --git a/proto_overrides/launcher_log_extension.proto b/proto_overrides/launcher_log_extension.proto
deleted file mode 100644
index 2995aa2..0000000
--- a/proto_overrides/launcher_log_extension.proto
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-syntax = "proto2";
-
-option java_package = "com.android.launcher3.userevent";
-option java_outer_classname = "LauncherLogExtensions";
-
-package userevent;
-
-//
-// Use this to add any app specific extensions to the proto.
-//
-message LauncherEventExtension {
-}
-
-message TargetExtension {
-}
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index 5611969..6d49d75 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -18,6 +18,8 @@
option java_package = "com.android.launcher3.logger";
option java_outer_classname = "LauncherAtom";
+import "launcher_atom_extension.proto";
+
//
// ItemInfos
message ItemInfo {
@@ -27,6 +29,8 @@
Shortcut shortcut = 3;
Widget widget = 4;
FolderIcon folder_icon = 9;
+ Slice slice = 10;
+ SearchActionItem search_action_item = 11;
}
// When used for launch event, stores the global predictive rank
optional int32 rank = 5;
@@ -55,6 +59,7 @@
SettingsContainer settings_container = 9;
PredictedHotseatContainer predicted_hotseat_container = 10;
TaskSwitcherContainer task_switcher_container = 11;
+ ExtendedContainers extended_containers = 20;
}
}
@@ -120,6 +125,25 @@
// Folder's label is empty(i.e., title == "").
// Not eligible for auto-labeling.
EMPTY_LABEL = 12;
+
+ ALL_APPS_SEARCH_RESULT_APPLICATION = 13;
+ ALL_APPS_SEARCH_RESULT_SHORTCUT = 14;
+ ALL_APPS_SEARCH_RESULT_PEOPLE = 15;
+ ALL_APPS_SEARCH_RESULT_ACTION = 16;
+ ALL_APPS_SEARCH_RESULT_SETTING = 17;
+ ALL_APPS_SEARCH_RESULT_SCREENSHOT = 18;
+ ALL_APPS_SEARCH_RESULT_SLICE = 19;
+ ALL_APPS_SEARCH_RESULT_WIDGETS = 20;
+ ALL_APPS_SEARCH_RESULT_PLAY = 21;
+ ALL_APPS_SEARCH_RESULT_SUGGEST = 22;
+ ALL_APPS_SEARCH_RESULT_ASSISTANT = 23;
+ ALL_APPS_SEARCH_RESULT_CHROMETAB = 24;
+ ALL_APPS_SEARCH_RESULT_NAVVYSITE = 25;
+ ALL_APPS_SEARCH_RESULT_TIPS = 26;
+ ALL_APPS_SEARCH_RESULT_PEOPLE_TILE = 27;
+
+ WIDGETS_BOTTOM_TRAY = 28;
+ WIDGETS_TRAY_PREDICTION = 29;
}
// Main app icons
@@ -131,6 +155,7 @@
// Legacy shortcuts and shortcuts handled by ShortcutManager
message Shortcut {
optional string shortcut_name = 1;
+ optional string shortcut_id = 2;
}
// AppWidgets handled by AppWidgetManager
@@ -140,6 +165,7 @@
optional int32 app_widget_id = 3;
optional string package_name = 4; // only populated during snapshot if from workspace
optional string component_name = 5; // only populated during snapshot if from workspace
+ optional int32 widget_features = 6;
}
// Tasks handled by PackageManager
@@ -165,6 +191,17 @@
optional string label_info = 4;
}
+// Contains Slice details for logging.
+message Slice{
+ optional string uri = 1;
+}
+
+// Represents SearchAction with in launcher
+message SearchActionItem{
+ optional string package_name = 1;
+ optional string title = 2;
+}
+
//////////////////////////////////////////////
// Containers
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
deleted file mode 100644
index 9423cb2..0000000
--- a/protos/launcher_log.proto
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-syntax = "proto2";
-
-import "launcher_log_extension.proto";
-
-option java_package = "com.android.launcher3.userevent";
-option java_outer_classname = "LauncherLogProto";
-
-package userevent;
-
-message Target {
- enum Type {
- NONE = 0;
- ITEM = 1;
- CONTROL = 2;
- CONTAINER = 3;
- }
-
- optional Type type = 1;
-
- // For container type and item type
- // Used mainly for ContainerType.FOLDER, ItemType.*
- optional int32 page_index = 2;
- optional int32 rank = 3;
- optional int32 grid_x = 4;
- optional int32 grid_y = 5;
-
- // For container types only
- optional ContainerType container_type = 6;
- optional int32 cardinality = 7;
-
- // For control types only
- optional ControlType control_type = 8;
-
- // For item types only
- optional ItemType item_type = 9;
- optional int32 package_name_hash = 10;
- optional int32 component_hash = 11; // Used for ItemType.WIDGET
- optional int32 intent_hash = 12; // Used for ItemType.SHORTCUT
- optional int32 span_x = 13 [default = 1];// Used for ItemType.WIDGET
- optional int32 span_y = 14 [default = 1];// Used for ItemType.WIDGET
- optional int32 predictedRank = 15;
- optional TargetExtension extension = 16;
- optional TipType tip_type = 17;
- optional int32 search_query_length = 18;
- optional bool is_work_app = 19;
- optional FromFolderLabelState from_folder_label_state = 20;
- optional ToFolderLabelState to_folder_label_state = 21;
-
- // Note: proto does not support duplicate enum values, even if they belong to different enum type.
- // Hence "FROM" and "TO" prefix added.
- enum FromFolderLabelState {
- FROM_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
- FROM_EMPTY = 1;
- FROM_CUSTOM = 2;
- FROM_SUGGESTED = 3;
- }
-
- enum ToFolderLabelState {
- TO_FOLDER_LABEL_STATE_UNSPECIFIED = 0;
- TO_SUGGESTION0_WITH_VALID_PRIMARY = 1;
- TO_SUGGESTION1_WITH_VALID_PRIMARY = 2;
- TO_SUGGESTION1_WITH_EMPTY_PRIMARY = 3;
- TO_SUGGESTION2_WITH_VALID_PRIMARY = 4;
- TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 5;
- TO_SUGGESTION3_WITH_VALID_PRIMARY = 6;
- TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 7;
- TO_EMPTY_WITH_VALID_SUGGESTIONS = 8 [deprecated = true];
- TO_EMPTY_WITH_VALID_PRIMARY = 15;
- TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 16;
- TO_EMPTY_WITH_EMPTY_SUGGESTIONS = 9;
- TO_EMPTY_WITH_SUGGESTIONS_DISABLED = 10;
- TO_CUSTOM_WITH_VALID_SUGGESTIONS = 11 [deprecated = true];
- TO_CUSTOM_WITH_VALID_PRIMARY = 17;
- TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 18;
- TO_CUSTOM_WITH_EMPTY_SUGGESTIONS = 12;
- TO_CUSTOM_WITH_SUGGESTIONS_DISABLED = 13;
- UNCHANGED = 14;
- }
-}
-
-// Used to define what type of item a Target would represent.
-enum ItemType {
- DEFAULT_ITEMTYPE = 0;
- APP_ICON = 1;
- SHORTCUT = 2;
- WIDGET = 3;
- FOLDER_ICON = 4;
- DEEPSHORTCUT = 5;
- SEARCHBOX = 6;
- EDITTEXT = 7;
- NOTIFICATION = 8;
- TASK = 9; // Each page of Recents UI (QuickStep)
- WEB_APP = 10;
- TASK_ICON = 11;
-}
-
-// Used to define what type of container a Target would represent.
-enum ContainerType {
- DEFAULT_CONTAINERTYPE = 0;
- WORKSPACE = 1;
- HOTSEAT = 2;
- FOLDER = 3;
- ALLAPPS = 4;
- WIDGETS = 5;
- 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)
- OTHER_LAUNCHER_APP = 15;
-}
-
-// Used to define what type of control a Target would represent.
-enum ControlType {
- DEFAULT_CONTROLTYPE = 0;
- ALL_APPS_BUTTON = 1;
- WIDGETS_BUTTON = 2;
- WALLPAPER_BUTTON = 3;
- SETTINGS_BUTTON = 4;
- REMOVE_TARGET = 5;
- UNINSTALL_TARGET = 6;
- APPINFO_TARGET = 7;
- RESIZE_HANDLE = 8;
- VERTICAL_SCROLL = 9;
- HOME_INTENT = 10; // Deprecated, use enum Command instead
- BACK_BUTTON = 11;
- QUICK_SCRUB_BUTTON = 12;
- CLEAR_ALL_BUTTON = 13;
- CANCEL_TARGET = 14;
- TASK_PREVIEW = 15;
- SPLIT_SCREEN_TARGET = 16;
- REMOTE_ACTION_SHORTCUT = 17;
- APP_USAGE_SETTINGS = 18;
- BACK_GESTURE = 19;
- UNDO = 20;
- DISMISS_PREDICTION = 21;
- HYBRID_HOTSEAT_ACCEPTED = 22;
- HYBRID_HOTSEAT_CANCELED = 23;
- OVERVIEW_ACTIONS_SHARE_BUTTON = 24;
- OVERVIEW_ACTIONS_SCREENSHOT_BUTTON = 25;
- OVERVIEW_ACTIONS_SELECT_BUTTON = 26;
- SELECT_MODE_CLOSE_BUTTON = 27;
- SELECT_MODE_ITEM = 28;
-}
-
-enum TipType {
- DEFAULT_NONE = 0;
- BOUNCE = 1;
- SWIPE_UP_TEXT = 2;
- QUICK_SCRUB_TEXT = 3;
- PREDICTION_TEXT = 4;
- DWB_TOAST = 5;
- HYBRID_HOTSEAT = 6;
-}
-
-// Used to define the action component of the LauncherEvent.
-message Action {
- enum Type {
- TOUCH = 0;
- AUTOMATED = 1;
- COMMAND = 2;
- TIP = 3;
- SOFT_KEYBOARD = 4;
- // HARD_KEYBOARD, ASSIST
- }
-
- enum Touch {
- TAP = 0;
- LONGPRESS = 1;
- DRAGDROP = 2;
- SWIPE = 3;
- FLING = 4;
- PINCH = 5;
- SWIPE_NOOP = 6;
- }
-
- enum Direction {
- NONE = 0;
- UP = 1;
- DOWN = 2;
- LEFT = 3;
- RIGHT = 4;
- UPRIGHT = 5;
- UPLEFT = 6;
- }
- 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
- 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;
-}
-
-//
-// Context free grammar of typical user interaction:
-// Action (Touch) + Target
-// Action (Touch) + Target + Target
-//
-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;
-
- optional int64 action_duration_millis = 4;
- optional int64 elapsed_container_millis = 5;
- optional int64 elapsed_session_millis = 6;
-
- optional bool is_in_multi_window_mode = 7 [deprecated = true];
- optional bool is_in_landscape_mode = 8 [deprecated = true];
-
- optional LauncherEventExtension extension = 9;
-}
diff --git a/protos/launcher_trace.proto b/protos/launcher_trace.proto
index c6f3543..65fcfe5 100644
--- a/protos/launcher_trace.proto
+++ b/protos/launcher_trace.proto
@@ -28,4 +28,40 @@
message TouchInteractionServiceProto {
optional bool service_connected = 1;
+ optional OverviewComponentObserverProto overview_component_obvserver = 2;
+ optional InputConsumerProto input_consumer = 3;
+}
+
+message OverviewComponentObserverProto {
+
+ optional bool overview_activity_started = 1;
+ optional bool overview_activity_resumed = 2;
+}
+
+message InputConsumerProto {
+
+ optional string name = 1;
+ optional SwipeHandlerProto swipe_handler = 2;
+}
+
+message SwipeHandlerProto {
+
+ optional GestureStateProto gesture_state = 1;
+ optional bool is_recents_attached_to_app_window = 2;
+ optional int32 scroll_offset = 3;
+ // Swipe up progress from 0 (app) to 1 (overview); can be > 1 if swiping past overview.
+ optional float app_to_overview_progress = 4;
+}
+
+message GestureStateProto {
+
+ optional GestureEndTarget endTarget = 1 [default = UNSET];
+
+ enum GestureEndTarget {
+ UNSET = 0;
+ HOME = 1;
+ RECENTS = 2;
+ NEW_TASK = 3;
+ LAST_TASK = 4;
+ }
}
diff --git a/protos_overrides/launcher_atom_extension.proto b/protos_overrides/launcher_atom_extension.proto
new file mode 100644
index 0000000..a07daf8
--- /dev/null
+++ b/protos_overrides/launcher_atom_extension.proto
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto2";
+
+option java_package = "com.android.launcher3.logger";
+option java_outer_classname = "LauncherAtomExtensions";
+
+
+// This proto file contains placeholder messages that can be overridden by
+// other Launcher variants.
+// Variants could have its own launcher_atom_extension.proto file(should have
+// same messages declared here but with different implementation); when building
+// variant's apk launcher_atom.proto will reference variant's extension file,
+// essentially overriding this file.
+
+
+// Wrapper message for additional containers used in variants.
+message ExtendedContainers {}
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
new file mode 100644
index 0000000..38c9919
--- /dev/null
+++ b/quickstep/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "packages_apps_Launcher3_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["packages_apps_Launcher3_license"],
+}
+
+filegroup {
+ name: "launcher3-quickstep-robolectric-src",
+ path: "robolectric_tests",
+ srcs: ["robolectric_tests/src/**/*.java"],
+}
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 60afddb..6808222 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -49,10 +49,11 @@
android:stateNotNeeded="true"
android:windowSoftInputMode="adjustPan"
android:screenOrientation="unspecified"
- android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|density"
android:resizeableActivity="true"
android:resumeWhilePausing="true"
android:taskAffinity=""
+ android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index e49f2ec..b43d8d1 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -17,106 +17,130 @@
** limitations under the License.
*/
-->
-<manifest
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- package="com.android.launcher3" >
- <permission
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.launcher3">
+
+ <permission
android:name="${packageName}.permission.HOTSEAT_EDU"
android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
android:protectionLevel="signatureOrSystem" />
- <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
- <uses-permission android:name="android.permission.VIBRATE" />
- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+ <uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
+ <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+ <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS"/>
+ <uses-permission android:name="android.permission.REMOVE_TASKS"/>
+ <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
+ <uses-permission android:name="android.permission.STATUS_BAR"/>
+ <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
+ <uses-permission android:name="android.permission.SET_ORIENTATION"/>
+ <uses-permission android:name="android.permission.READ_FRAME_BUFFER"/>
+ <uses-permission android:name="android.permission.MANAGE_ACCESSIBILITY"/>
+ <uses-permission android:name="android.permission.MONITOR_INPUT"/>
+
<uses-permission android:name="${packageName}.permission.HOTSEAT_EDU" />
+ <uses-permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY" />
- <application
- android:backupAgent="com.android.launcher3.LauncherBackupAgent"
- android:fullBackupOnly="true"
- android:fullBackupContent="@xml/backupscheme"
- android:hardwareAccelerated="true"
- android:icon="@drawable/ic_launcher_home"
- android:label="@string/derived_app_name"
- android:theme="@style/AppTheme"
- android:largeHeap="@bool/config_largeHeap"
- android:restoreAnyVersion="true"
- android:supportsRtl="true" >
+ <application android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+ android:fullBackupOnly="true"
+ android:fullBackupContent="@xml/backupscheme"
+ android:hardwareAccelerated="true"
+ android:icon="@drawable/ic_launcher_home"
+ android:label="@string/derived_app_name"
+ android:theme="@style/AppTheme"
+ android:largeHeap="@bool/config_largeHeap"
+ android:restoreAnyVersion="true"
+ android:supportsRtl="true">
- <service
- android:name="com.android.quickstep.TouchInteractionService"
- android:permission="android.permission.STATUS_BAR_SERVICE"
- android:directBootAware="true" >
+ <service android:name="com.android.quickstep.TouchInteractionService"
+ android:permission="android.permission.STATUS_BAR_SERVICE"
+ android:directBootAware="true"
+ android:exported="true">
<intent-filter>
- <action android:name="android.intent.action.QUICKSTEP_SERVICE" />
+ <action android:name="android.intent.action.QUICKSTEP_SERVICE"/>
</intent-filter>
</service>
<activity android:name="com.android.quickstep.RecentsActivity"
- android:excludeFromRecents="true"
- android:launchMode="singleTask"
- android:clearTaskOnLaunch="true"
- android:stateNotNeeded="true"
- android:theme="@style/LauncherTheme"
- android:screenOrientation="unspecified"
- android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
- android:resizeableActivity="true"
- android:resumeWhilePausing="true"
- android:taskAffinity="" />
+ android:excludeFromRecents="true"
+ android:launchMode="singleTask"
+ android:clearTaskOnLaunch="true"
+ android:stateNotNeeded="true"
+ android:theme="@style/LauncherTheme"
+ android:screenOrientation="unspecified"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|density"
+ android:resizeableActivity="true"
+ android:resumeWhilePausing="true"
+ android:taskAffinity=""/>
<!-- Content provider to settings search. The autority should be same as the packageName -->
- <provider
- android:name="com.android.quickstep.LauncherSearchIndexablesProvider"
- android:authorities="${packageName}"
- android:grantUriPermissions="true"
- android:multiprocess="true"
- android:permission="android.permission.READ_SEARCH_INDEXABLES"
- android:exported="true">
+ <provider android:name="com.android.quickstep.LauncherSearchIndexablesProvider"
+ android:authorities="${packageName}"
+ android:grantUriPermissions="true"
+ android:multiprocess="true"
+ android:permission="android.permission.READ_SEARCH_INDEXABLES"
+ android:exported="true">
<intent-filter>
- <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" />
+ <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER"/>
</intent-filter>
</provider>
<!-- FileProvider used for sharing images. -->
- <provider
- android:name="androidx.core.content.FileProvider"
- android:authorities="${packageName}.overview.fileprovider"
- android:exported="false"
- android:grantUriPermissions="true">
- <meta-data
- android:name="android.support.FILE_PROVIDER_PATHS"
- android:resource="@xml/overview_file_provider_paths" />
+ <provider android:name="androidx.core.content.FileProvider"
+ android:authorities="${packageName}.overview.fileprovider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/overview_file_provider_paths"/>
</provider>
- <service
- android:name="com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompatVL$ColorExtractionService"
- tools:node="remove" />
+ <activity android:name="com.android.launcher3.proxy.ProxyActivityStarter"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
+ android:launchMode="singleTask"
+ android:clearTaskOnLaunch="true"
+ android:exported="false"/>
- <activity
- android:name="com.android.launcher3.proxy.ProxyActivityStarter"
- android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
- android:launchMode="singleTask"
- android:clearTaskOnLaunch="true"
- android:exported="false" />
-
- <activity
- android:name="com.android.quickstep.interaction.GestureSandboxActivity"
+ <activity android:name="com.android.quickstep.interaction.GestureSandboxActivity"
android:autoRemoveFromRecents="true"
android:excludeFromRecents="true"
- android:screenOrientation="portrait">
+ android:screenOrientation="portrait"
+ android:exported="true">
<intent-filter>
- <action android:name="com.android.quickstep.action.GESTURE_SANDBOX" />
- <category android:name="android.intent.category.DEFAULT" />
+ <action android:name="com.android.quickstep.action.GESTURE_SANDBOX"/>
+ <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
+
+ <!--
+ Activity following gesture nav onboarding.
+ It's protected by android.permission.REBOOT to ensure that only system apps can start it
+ (setup wizard already has this permission)
+ -->
+ <activity android:name="com.android.quickstep.interaction.AllSetActivity"
+ android:autoRemoveFromRecents="true"
+ android:excludeFromRecents="true"
+ android:screenOrientation="portrait"
+ android:permission="android.permission.REBOOT"
+ android:theme="@style/AllSetTheme"
+ android:label="@string/allset_title"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.quickstep.action.GESTURE_ONBOARDING_ALL_SET"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
<activity
android:name=".hybridhotseat.HotseatEduActivity"
android:theme="@android:style/Theme.NoDisplay"
android:noHistory="true"
android:launchMode="singleTask"
android:clearTaskOnLaunch="true"
- android:permission="${packageName}.permission.HOTSEAT_EDU">
+ android:permission="${packageName}.permission.HOTSEAT_EDU"
+ android:exported="true">
<intent-filter>
<action android:name="com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU"/>
<category android:name="android.intent.category.DEFAULT" />
diff --git a/quickstep/protos_overrides/launcher_atom_extension.proto b/quickstep/protos_overrides/launcher_atom_extension.proto
new file mode 100644
index 0000000..d2dc0cb
--- /dev/null
+++ b/quickstep/protos_overrides/launcher_atom_extension.proto
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto2";
+
+option java_package = "com.android.launcher3.logger";
+option java_outer_classname = "LauncherAtomExtensions";
+
+
+// Wrapper message for containers used at the quickstep level.
+// Message name should match with launcher_atom_extension.proto message at
+// the AOSP level.
+message ExtendedContainers {
+
+ oneof Container{
+ DeviceSearchResultContainer device_search_result_container = 1;
+ CorrectedDeviceSearchResultContainer corrected_device_search_result_container = 2;
+ }
+}
+
+// Represents on-device search result container.
+message DeviceSearchResultContainer{
+ optional int32 query_length = 1;
+}
+
+// Represents on-device search result container with results from spell-corrected query.
+message CorrectedDeviceSearchResultContainer{
+ optional int32 query_length = 1;
+}
+
diff --git a/quickstep/recents_ui_overrides/res/drawable/chip_scrim_gradient.xml b/quickstep/recents_ui_overrides/res/drawable/chip_scrim_gradient.xml
deleted file mode 100644
index 5a2dfb7..0000000
--- a/quickstep/recents_ui_overrides/res/drawable/chip_scrim_gradient.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <gradient
- android:angle="90"
- android:endColor="@android:color/transparent"
- android:startColor="@color/chip_scrim_start_color"
- android:type="linear" />
-</shape>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml b/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
deleted file mode 100644
index cd64a94..0000000
--- a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- 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.
--->
-<com.android.launcher3.LauncherRootView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fitsSystemWindows="true">
-
- <com.android.quickstep.fallback.RecentsDragLayer
- android:id="@+id/drag_layer"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipChildren="false">
-
- <com.android.quickstep.fallback.FallbackRecentsView
- android:id="@id/overview_panel"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:outlineProvider="none"
- android:theme="@style/HomeScreenElementTheme" />
-
- <include
- android:id="@+id/overview_actions_view"
- layout="@layout/overview_actions_container" />
-
- </com.android.quickstep.fallback.RecentsDragLayer>
-</com.android.launcher3.LauncherRootView>
diff --git a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
deleted file mode 100644
index fe57e9b..0000000
--- a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- 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.
--->
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-
- <com.android.quickstep.views.LauncherRecentsView
- android:id="@+id/overview_panel"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:accessibilityPaneTitle="@string/accessibility_recent_apps"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:theme="@style/HomeScreenElementTheme"
- android:visibility="invisible" />
-
- <include
- android:id="@+id/overview_actions_view"
- layout="@layout/overview_actions_container" />
-
-</merge>
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
deleted file mode 100644
index f03f118..0000000
--- a/quickstep/recents_ui_overrides/res/values/colors.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <color name="chip_hint_foreground_color">#fff</color>
- <color name="chip_scrim_start_color">#39000000</color>
-
- <color name="all_apps_label_text">#61000000</color>
- <color name="all_apps_label_text_dark">#61FFFFFF</color>
- <color name="all_apps_prediction_row_separator">#3c000000</color>
- <color name="all_apps_prediction_row_separator_dark">#3cffffff</color>
-</resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/config.xml b/quickstep/recents_ui_overrides/res/values/config.xml
deleted file mode 100644
index 120e034..0000000
--- a/quickstep/recents_ui_overrides/res/values/config.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <integer name="max_depth_blur_radius">150</integer>
-</resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
deleted file mode 100644
index 9266b06..0000000
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="chip_hint_border_width">1dp</dimen>
- <dimen name="chip_hint_corner_radius">20dp</dimen>
- <dimen name="chip_hint_outer_padding">20dp</dimen>
- <dimen name="chip_hint_start_padding">10dp</dimen>
- <dimen name="chip_hint_end_padding">12dp</dimen>
- <dimen name="chip_hint_horizontal_margin">20dp</dimen>
- <dimen name="chip_hint_vertical_offset">16dp</dimen>
- <dimen name="chip_hint_elevation">2dp</dimen>
- <dimen name="chip_icon_size">16dp</dimen>
- <dimen name="chip_text_height">26dp</dimen>
- <dimen name="chip_text_top_padding">4dp</dimen>
- <dimen name="chip_text_start_padding">10dp</dimen>
- <dimen name="chip_text_size">14sp</dimen>
-
- <dimen name="all_apps_prediction_row_divider_height">17dp</dimen>
- <dimen name="all_apps_label_top_padding">16dp</dimen>
- <dimen name="all_apps_label_bottom_padding">8dp</dimen>
- <dimen name="all_apps_label_text_size">14sp</dimen>
-
- <!-- Minimum distance to swipe to trigger accessibility gesture -->
- <dimen name="accessibility_gesture_min_swipe_distance">80dp</dimen>
-</resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
deleted file mode 100644
index 6aa9619..0000000
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<!-- Class overrides for launcher with quickstep. -->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_transition_manager_class" translatable="false">com.android.launcher3.LauncherAppTransitionManagerImpl</string>
-
- <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
-
- <string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
-
- <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
-
- <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherAppPredictionExtension</string>
-
- <string name="prediction_model_class" translatable="false">com.android.launcher3.hybridhotseat.HotseatPredictionModel</string>
-</resources>
-
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
deleted file mode 100644
index 38adf39..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
-import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * A {@link QuickstepAppTransitionManagerImpl} that also implements recents transitions from
- * {@link RecentsView}.
- */
-public final class LauncherAppTransitionManagerImpl extends QuickstepAppTransitionManagerImpl {
-
- public LauncherAppTransitionManagerImpl(Context context) {
- super(context);
- }
-
- @Override
- protected boolean isLaunchingFromRecents(@NonNull View v,
- @Nullable RemoteAnimationTargetCompat[] targets) {
- return mLauncher.getStateManager().getState().overviewUi
- && findTaskViewToLaunch(mLauncher, v, targets) != null;
- }
-
- @Override
- protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
- @NonNull RemoteAnimationTargetCompat[] appTargets,
- @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing) {
- RecentsView recentsView = mLauncher.getOverviewPanel();
- boolean skipLauncherChanges = !launcherClosing;
-
- TaskView taskView = findTaskViewToLaunch(mLauncher, v, appTargets);
- PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
- createRecentsWindowAnimator(taskView, skipLauncherChanges, appTargets, wallpaperTargets,
- mLauncher.getDepthController(), pa);
- anim.play(pa.buildAnim());
-
- 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);
- launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
- launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
-
- // Make sure recents gets fixed up by resetting task alphas and scales, etc.
- windowAnimEndListener = new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mLauncher.getStateManager().moveToRestState();
- mLauncher.getStateManager().reapplyState();
- }
- };
- } else {
- AnimatorPlaybackController controller =
- mLauncher.getStateManager().createAnimationToNewWorkspace(NORMAL,
- RECENTS_LAUNCH_DURATION);
- controller.dispatchOnStart();
- childStateAnimation = controller.getTarget();
- launcherAnim = controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION);
- windowAnimEndListener = new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mLauncher.getStateManager().goToState(NORMAL, false);
- }
- };
- }
- anim.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(anim, childStateAnimation);
- anim.addListener(windowAnimEndListener);
- }
-
- @Override
- protected Runnable composeViewContentAnimator(@NonNull AnimatorSet anim, float[] alphas,
- float[] trans) {
- RecentsView overview = mLauncher.getOverviewPanel();
- ObjectAnimator alpha = ObjectAnimator.ofFloat(overview,
- RecentsView.CONTENT_ALPHA, alphas);
- alpha.setDuration(CONTENT_ALPHA_DURATION);
- alpha.setInterpolator(LINEAR);
- anim.play(alpha);
- overview.setFreezeViewVisibility(true);
-
- ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans);
- transY.setInterpolator(AGGRESSIVE_EASE);
- transY.setDuration(CONTENT_TRANSLATION_DURATION);
- anim.play(transY);
-
- return () -> {
- overview.setFreezeViewVisibility(false);
- overview.setTranslationY(0);
- mLauncher.getStateManager().reapplyState();
- };
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
deleted file mode 100644
index 8477b10..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.appprediction;
-
-import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS;
-
-import android.os.UserManager;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.FloatingHeaderView;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.views.ArrowTipView;
-import com.android.systemui.shared.system.LauncherEventUtil;
-
-/**
- * ArrowTip helper aligned just above prediction apps, shown to users that enter all apps for the
- * first time.
- */
-public class AllAppsTipView {
-
- private static final String ALL_APPS_TIP_SEEN = "launcher.all_apps_tip_seen";
-
- private static boolean showAllAppsTipIfNecessary(Launcher launcher) {
- FloatingHeaderView floatingHeaderView = launcher.getAppsView().getFloatingHeaderView();
- if (!floatingHeaderView.hasVisibleContent()
- || AbstractFloatingView.getOpenView(launcher,
- TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE) != null
- || !launcher.isInState(ALL_APPS)
- || hasSeenAllAppsTip(launcher)
- || launcher.getSystemService(UserManager.class).isDemoUser()
- || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
- return false;
- }
-
- int[] coords = new int[2];
- floatingHeaderView.findFixedRowByType(PredictionRowView.class).getLocationOnScreen(coords);
- ArrowTipView arrowTipView = new ArrowTipView(launcher).setOnClosedCallback(() -> {
- launcher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
- launcher.getUserEventDispatcher().logActionTip(LauncherEventUtil.DISMISS,
- ALL_APPS_PREDICTION_TIPS);
- });
- arrowTipView.show(launcher.getString(R.string.all_apps_prediction_tip), coords[1]);
-
- return true;
- }
-
- private static boolean hasSeenAllAppsTip(Launcher launcher) {
- return launcher.getSharedPrefs().getBoolean(ALL_APPS_TIP_SEEN, false);
- }
-
- public static void scheduleShowIfNeeded(Launcher launcher) {
- if (!hasSeenAllAppsTip(launcher)) {
- launcher.getStateManager().addStateListener(new StateListener<LauncherState>() {
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- if (finalState == ALL_APPS) {
- if (showAllAppsTipIfNecessary(launcher)) {
- launcher.getStateManager().removeStateListener(this);
- }
- }
- }
- });
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
deleted file mode 100644
index 914d9e9..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ /dev/null
@@ -1,306 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.appprediction;
-
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-import static com.android.launcher3.LauncherState.ALL_APPS;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.text.Layout;
-import android.text.StaticLayout;
-import android.text.TextPaint;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.ColorInt;
-import androidx.core.content.ContextCompat;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.FloatingHeaderRow;
-import com.android.launcher3.allapps.FloatingHeaderView;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.util.Themes;
-
-/**
- * A view which shows a horizontal divider
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class AppsDividerView extends View implements StateListener<LauncherState>,
- FloatingHeaderRow {
-
- private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
- private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20;
-
- public enum DividerType {
- NONE,
- LINE,
- ALL_APPS_LABEL
- }
-
- private final Launcher mLauncher;
- private final TextPaint mPaint = new TextPaint();
- private DividerType mDividerType = DividerType.NONE;
-
- private final @ColorInt int mStrokeColor;
- private final @ColorInt int mAllAppsLabelTextColor;
-
- private Layout mAllAppsLabelLayout;
- private boolean mShowAllAppsLabel;
-
- private FloatingHeaderView mParent;
- private boolean mTabsHidden;
- private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS;
-
- private boolean mIsScrolledOut = false;
-
- public AppsDividerView(Context context) {
- this(context, null);
- }
-
- public AppsDividerView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mLauncher = Launcher.getLauncher(context);
-
- boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
- mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
-
- mStrokeColor = ContextCompat.getColor(context, isMainColorDark
- ? R.color.all_apps_prediction_row_separator_dark
- : R.color.all_apps_prediction_row_separator);
-
- mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark
- ? R.color.all_apps_label_text_dark
- : R.color.all_apps_label_text);
- }
-
- public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
- mParent = parent;
- mTabsHidden = tabsHidden;
- mRows = rows;
- updateDividerType();
- }
-
- @Override
- public int getExpectedHeight() {
- return getPaddingTop() + getPaddingBottom();
- }
-
- @Override
- public boolean shouldDraw() {
- return mDividerType != DividerType.NONE;
- }
-
- @Override
- public boolean hasVisibleContent() {
- return false;
- }
-
- private void updateDividerType() {
- final DividerType dividerType;
- if (!mTabsHidden) {
- dividerType = DividerType.NONE;
- } else {
- // Check how many sections above me.
- int sectionCount = 0;
- for (FloatingHeaderRow row : mRows) {
- if (row == this) {
- break;
- } else if (row.shouldDraw()) {
- sectionCount ++;
- }
- }
-
- if (mShowAllAppsLabel && sectionCount > 0) {
- dividerType = DividerType.ALL_APPS_LABEL;
- } else if (sectionCount == 1) {
- dividerType = DividerType.LINE;
- } else {
- dividerType = DividerType.NONE;
- }
- }
-
- if (mDividerType != dividerType) {
- mDividerType = dividerType;
- int topPadding;
- int bottomPadding;
- switch (dividerType) {
- case LINE:
- topPadding = 0;
- bottomPadding = getResources()
- .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
- mPaint.setColor(mStrokeColor);
- break;
- case ALL_APPS_LABEL:
- topPadding = getAllAppsLabelLayout().getHeight() + getResources()
- .getDimensionPixelSize(R.dimen.all_apps_label_top_padding);
- bottomPadding = getResources()
- .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
- mPaint.setColor(mAllAppsLabelTextColor);
- break;
- case NONE:
- default:
- topPadding = bottomPadding = 0;
- break;
- }
- setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding);
- updateViewVisibility();
- invalidate();
- requestLayout();
- if (mParent != null) {
- mParent.onHeightUpdated();
- }
- }
- }
-
- private void updateViewVisibility() {
- setVisibility(mDividerType == DividerType.NONE
- ? GONE
- : (mIsScrolledOut ? INVISIBLE : VISIBLE));
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (mDividerType == DividerType.LINE) {
- int side = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
- int y = getHeight() - (getPaddingBottom() / 2);
- int x1 = getPaddingLeft() + side;
- int x2 = getWidth() - getPaddingRight() - side;
- canvas.drawLine(x1, y, x2, y, mPaint);
- } else if (mDividerType == DividerType.ALL_APPS_LABEL) {
- Layout textLayout = getAllAppsLabelLayout();
- int x = getWidth() / 2 - textLayout.getWidth() / 2;
- int y = getHeight() - getPaddingBottom() - textLayout.getHeight();
- canvas.translate(x, y);
- textLayout.draw(canvas);
- canvas.translate(-x, -y);
- }
- }
-
- private Layout getAllAppsLabelLayout() {
- if (mAllAppsLabelLayout == null) {
- mPaint.setAntiAlias(true);
- mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
- mPaint.setTextSize(
- getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size));
-
- CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label);
- mAllAppsLabelLayout = StaticLayout.Builder.obtain(
- allAppsLabelText, 0, allAppsLabelText.length(), mPaint,
- Math.round(mPaint.measureText(allAppsLabelText.toString())))
- .setAlignment(Layout.Alignment.ALIGN_CENTER)
- .setMaxLines(1)
- .setIncludePad(true)
- .build();
- }
- return mAllAppsLabelLayout;
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getPaddingBottom() + getPaddingTop());
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (shouldShowAllAppsLabel()) {
- mShowAllAppsLabel = true;
- mLauncher.getStateManager().addStateListener(this);
- updateDividerType();
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mLauncher.getStateManager().removeStateListener(this);
- }
-
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- if (finalState == ALL_APPS) {
- setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
- } else {
- if (mShowAllAppsLabel != shouldShowAllAppsLabel()) {
- mShowAllAppsLabel = !mShowAllAppsLabel;
- updateDividerType();
- }
-
- if (!mShowAllAppsLabel) {
- mLauncher.getStateManager().removeStateListener(this);
- }
- }
- }
-
- private void setAllAppsVisitedCount(int count) {
- mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply();
- }
-
- private int getAllAppsVisitedCount() {
- return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0);
- }
-
- private boolean shouldShowAllAppsLabel() {
- return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT;
- }
-
- @Override
- public void setInsets(Rect insets, DeviceProfile grid) {
- int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
- + grid.cellLayoutPaddingLeftRightPx;
- setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
- }
-
- @Override
- public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
- PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
- // Don't use setViewAlpha as we want to control the visibility ourselves.
- setter.setFloat(this, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, allAppsFade);
- }
-
- @Override
- public void setVerticalScroll(int scroll, boolean isScrolledOut) {
- setTranslationY(scroll);
- mIsScrolledOut = isScrolledOut;
- updateViewVisibility();
- }
-
- @Override
- public Class<AppsDividerView> getTypeClass() {
- return AppsDividerView.class;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
deleted file mode 100644
index d200868..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.appprediction;
-
-import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
-
-import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.util.ComponentKey;
-
-public class ComponentKeyMapper {
-
- protected final ComponentKey componentKey;
- private final DynamicItemCache mCache;
-
- public ComponentKeyMapper(ComponentKey key, DynamicItemCache cache) {
- componentKey = key;
- mCache = cache;
- }
-
- public String getPackage() {
- return componentKey.componentName.getPackageName();
- }
-
- public String getComponentClass() {
- return componentKey.componentName.getClassName();
- }
-
- public ComponentKey getComponentKey() {
- return componentKey;
- }
-
- @Override
- public String toString() {
- return componentKey.toString();
- }
-
- public ItemInfoWithIcon getApp(AllAppsStore store) {
- AppInfo item = store.getApp(componentKey);
- if (item != null) {
- return item;
- } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) {
- return mCache.getInstantApp(componentKey.componentName.getPackageName());
- } else {
- return mCache.getShortcutInfo(componentKey);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
deleted file mode 100644
index ab96b1340..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.appprediction;
-
-import static android.content.pm.PackageManager.MATCH_INSTANT;
-
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ShortcutInfo;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.InstantAppResolver;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Utility class which loads and caches predicted items like instant apps and shortcuts, before
- * they can be displayed on the UI
- */
-public class DynamicItemCache {
-
- private static final String TAG = "DynamicItemCache";
- private static final boolean DEBUG = false;
- private static final String DEFAULT_URL = "default-url";
-
- private static final int BG_MSG_LOAD_SHORTCUTS = 1;
- private static final int BG_MSG_LOAD_INSTANT_APPS = 2;
-
- private static final int UI_MSG_UPDATE_SHORTCUTS = 1;
- private static final int UI_MSG_UPDATE_INSTANT_APPS = 2;
-
- private final Context mContext;
- private final Handler mWorker;
- private final Handler mUiHandler;
- private final InstantAppResolver mInstantAppResolver;
- private final Runnable mOnUpdateCallback;
- private final IconCache mIconCache;
-
- private final Map<ComponentKey, WorkspaceItemInfo> mShortcuts;
- private final Map<String, InstantAppItemInfo> mInstantApps;
-
- public DynamicItemCache(Context context, Runnable onUpdateCallback) {
- mContext = context;
- mWorker = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
- mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
- mInstantAppResolver = InstantAppResolver.newInstance(context);
- mOnUpdateCallback = onUpdateCallback;
- mIconCache = LauncherAppState.getInstance(mContext).getIconCache();
-
- mShortcuts = new HashMap<>();
- mInstantApps = new HashMap<>();
- }
-
- public void cacheItems(List<ShortcutKey> shortcutKeys, List<String> pkgNames) {
- if (!shortcutKeys.isEmpty()) {
- mWorker.removeMessages(BG_MSG_LOAD_SHORTCUTS);
- Message.obtain(mWorker, BG_MSG_LOAD_SHORTCUTS, shortcutKeys).sendToTarget();
- }
- if (!pkgNames.isEmpty()) {
- mWorker.removeMessages(BG_MSG_LOAD_INSTANT_APPS);
- Message.obtain(mWorker, BG_MSG_LOAD_INSTANT_APPS, pkgNames).sendToTarget();
- }
- }
-
- private boolean handleWorkerMessage(Message msg) {
- switch (msg.what) {
- case BG_MSG_LOAD_SHORTCUTS: {
- List<ShortcutKey> shortcutKeys = msg.obj != null ?
- (List<ShortcutKey>) msg.obj : Collections.EMPTY_LIST;
- Map<ShortcutKey, WorkspaceItemInfo> shortcutKeyAndInfos = new ArrayMap<>();
- for (ShortcutKey shortcutKey : shortcutKeys) {
- WorkspaceItemInfo workspaceItemInfo = loadShortcutWorker(shortcutKey);
- if (workspaceItemInfo != null) {
- shortcutKeyAndInfos.put(shortcutKey, workspaceItemInfo);
- }
- }
- Message.obtain(mUiHandler, UI_MSG_UPDATE_SHORTCUTS, shortcutKeyAndInfos)
- .sendToTarget();
- return true;
- }
- case BG_MSG_LOAD_INSTANT_APPS: {
- List<String> pkgNames = msg.obj != null ?
- (List<String>) msg.obj : Collections.EMPTY_LIST;
- List<InstantAppItemInfo> instantAppItemInfos = new ArrayList<>();
- for (String pkgName : pkgNames) {
- InstantAppItemInfo instantAppItemInfo = loadInstantApp(pkgName);
- if (instantAppItemInfo != null) {
- instantAppItemInfos.add(instantAppItemInfo);
- }
- }
- Message.obtain(mUiHandler, UI_MSG_UPDATE_INSTANT_APPS, instantAppItemInfos)
- .sendToTarget();
- return true;
- }
- }
-
- return false;
- }
-
- private boolean handleUiMessage(Message msg) {
- switch (msg.what) {
- case UI_MSG_UPDATE_SHORTCUTS: {
- mShortcuts.clear();
- mShortcuts.putAll((Map<ShortcutKey, WorkspaceItemInfo>) msg.obj);
- mOnUpdateCallback.run();
- return true;
- }
- case UI_MSG_UPDATE_INSTANT_APPS: {
- List<InstantAppItemInfo> instantAppItemInfos = (List<InstantAppItemInfo>) msg.obj;
- mInstantApps.clear();
- for (InstantAppItemInfo instantAppItemInfo : instantAppItemInfos) {
- mInstantApps.put(instantAppItemInfo.getTargetComponent().getPackageName(),
- instantAppItemInfo);
- }
- mOnUpdateCallback.run();
- if (DEBUG) {
- Log.d(TAG, String.format("Cache size: %d, Cache: %s",
- mInstantApps.size(), mInstantApps.toString()));
- }
- return true;
- }
- }
-
- return false;
- }
-
- @WorkerThread
- private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
- List<ShortcutInfo> details = shortcutKey.buildRequest(mContext).query(ShortcutRequest.ALL);
- if (!details.isEmpty()) {
- WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
- mIconCache.getShortcutIcon(si, details.get(0));
- return si;
- }
- if (DEBUG) {
- Log.d(TAG, "No shortcut found: " + shortcutKey.toString());
- }
- return null;
- }
-
- private InstantAppItemInfo loadInstantApp(String pkgName) {
- PackageManager pm = mContext.getPackageManager();
-
- try {
- ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
- if (!mInstantAppResolver.isInstantApp(ai)) {
- return null;
- }
- } catch (PackageManager.NameNotFoundException e) {
- return null;
- }
-
- String url = retrieveDefaultUrl(pkgName, pm);
- if (url == null) {
- Log.w(TAG, "no default-url available for pkg " + pkgName);
- return null;
- }
-
- Intent intent = new Intent(Intent.ACTION_VIEW)
- .addCategory(Intent.CATEGORY_BROWSABLE)
- .setData(Uri.parse(url));
- InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
- IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
- iconCache.getTitleAndIcon(info, false);
- if (info.bitmap.icon == null || iconCache.isDefaultIcon(info.bitmap, info.user)) {
- return null;
- }
- return info;
- }
-
- @Nullable
- public static String retrieveDefaultUrl(String pkgName, PackageManager pm) {
- Intent mainIntent = new Intent().setAction(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_LAUNCHER).setPackage(pkgName);
- List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
- mainIntent, MATCH_INSTANT | PackageManager.GET_META_DATA);
- String url = null;
- for (ResolveInfo resolveInfo : resolveInfos) {
- if (resolveInfo.activityInfo.metaData != null
- && resolveInfo.activityInfo.metaData.containsKey(DEFAULT_URL)) {
- url = resolveInfo.activityInfo.metaData.getString(DEFAULT_URL);
- }
- }
- return url;
- }
-
- @UiThread
- public InstantAppItemInfo getInstantApp(String pkgName) {
- return mInstantApps.get(pkgName);
- }
-
- @MainThread
- public WorkspaceItemInfo getShortcutInfo(ComponentKey key) {
- return mShortcuts.get(key);
- }
-
- /**
- * requests and caches icons for app targets
- */
- public void updateDependencies(List<ComponentKeyMapper> componentKeyMappers,
- AllAppsStore appsStore, IconCache.ItemInfoUpdateReceiver callback, int itemCount) {
- List<String> instantAppsToLoad = new ArrayList<>();
- List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
- int total = componentKeyMappers.size();
- for (int i = 0, count = 0; i < total && count < itemCount; i++) {
- ComponentKeyMapper mapper = componentKeyMappers.get(i);
- // Update instant apps
- if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
- instantAppsToLoad.add(mapper.getPackage());
- count++;
- } else if (mapper.getComponentKey() instanceof ShortcutKey) {
- shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
- count++;
- } else {
- // Reload high res icon
- AppInfo info = (AppInfo) mapper.getApp(appsStore);
- if (info != null) {
- if (info.usingLowResIcon()) {
- mIconCache.updateIconInBackground(callback, info);
- }
- count++;
- }
- }
- }
- cacheItems(shortcutsToLoad, instantAppsToLoad);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
deleted file mode 100644
index 4c7943b..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.appprediction;
-
-import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
-import android.annotation.TargetApi;
-import android.app.prediction.AppPredictionContext;
-import android.app.prediction.AppPredictionManager;
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.app.prediction.AppTargetEvent;
-import android.app.prediction.AppTargetId;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.systemui.plugins.AppLaunchEventsPlugin;
-import com.android.systemui.plugins.PluginListener;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Subclass of app tracker which publishes the data to the prediction engine and gets back results.
- */
-@TargetApi(Build.VERSION_CODES.Q)
-public class PredictionAppTracker extends AppLaunchTracker
- implements PluginListener<AppLaunchEventsPlugin> {
-
- private static final String TAG = "PredictionAppTracker";
- private static final boolean DBG = false;
-
- private static final int MSG_INIT = 0;
- private static final int MSG_DESTROY = 1;
- private static final int MSG_LAUNCH = 2;
- private static final int MSG_PREDICT = 3;
-
- protected final Context mContext;
- private final Handler mMessageHandler;
- private final List<AppLaunchEventsPlugin> mAppLaunchEventsPluginsList;
-
- // Accessed only on worker thread
- private AppPredictor mHomeAppPredictor;
- private AppPredictor mRecentsOverviewPredictor;
-
- public PredictionAppTracker(Context context) {
- mContext = context;
- mMessageHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessage);
- InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
-
- mMessageHandler.sendEmptyMessage(MSG_INIT);
-
- mAppLaunchEventsPluginsList = new ArrayList<>();
- PluginManagerWrapper.INSTANCE.get(context)
- .addPluginListener(this, AppLaunchEventsPlugin.class, true);
- }
-
- @UiThread
- private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
- if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
- // Reinitialize everything
- mMessageHandler.sendEmptyMessage(MSG_INIT);
- }
- }
-
- @WorkerThread
- private void destroy() {
- if (mHomeAppPredictor != null) {
- mHomeAppPredictor.destroy();
- mHomeAppPredictor = null;
- }
- if (mRecentsOverviewPredictor != null) {
- mRecentsOverviewPredictor.destroy();
- mRecentsOverviewPredictor = null;
- }
- }
-
- @WorkerThread
- private AppPredictor createPredictor(Client client, int count) {
- AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
-
- if (apm == null) {
- return null;
- }
-
- AppPredictor predictor = apm.createAppPredictionSession(
- new AppPredictionContext.Builder(mContext)
- .setUiSurface(client.id)
- .setPredictedTargetCount(count)
- .setExtras(getAppPredictionContextExtras(client))
- .build());
- predictor.registerPredictionUpdates(mContext.getMainExecutor(),
- PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client));
- predictor.requestPredictionUpdate();
- return predictor;
- }
-
- /**
- * Override to add custom extras.
- */
- @WorkerThread
- @Nullable
- public Bundle getAppPredictionContextExtras(Client client) {
- return null;
- }
-
- @WorkerThread
- private boolean handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_INIT: {
- // Destroy any existing clients
- destroy();
-
- // Initialize the clients
- int count = InvariantDeviceProfile.INSTANCE.get(mContext).numAllAppsColumns;
- mHomeAppPredictor = createPredictor(Client.HOME, count);
- mRecentsOverviewPredictor = createPredictor(Client.OVERVIEW, count);
- return true;
- }
- case MSG_DESTROY: {
- destroy();
- return true;
- }
- case MSG_LAUNCH: {
- if (mHomeAppPredictor != null) {
- mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj);
- }
- return true;
- }
- case MSG_PREDICT: {
- if (mHomeAppPredictor != null) {
- String client = (String) msg.obj;
- if (Client.HOME.id.equals(client)) {
- mHomeAppPredictor.requestPredictionUpdate();
- } else {
- mRecentsOverviewPredictor.requestPredictionUpdate();
- }
- }
- return true;
- }
- }
- return false;
- }
-
- @Override
- @UiThread
- public void onReturnedToHome() {
- String client = Client.HOME.id;
- mMessageHandler.removeMessages(MSG_PREDICT, client);
- Message.obtain(mMessageHandler, MSG_PREDICT, client).sendToTarget();
- if (DBG) {
- Log.d(TAG, String.format("Sent immediate message to update %s", client));
- }
-
- // Relay onReturnedToHome to every plugin.
- mAppLaunchEventsPluginsList.forEach(AppLaunchEventsPlugin::onReturnedToHome);
- }
-
- @Override
- @UiThread
- public void onStartShortcut(String packageName, String shortcutId, UserHandle user,
- String container) {
- // TODO: Use the full shortcut info
- AppTarget target = new AppTarget.Builder(
- new AppTargetId("shortcut:" + shortcutId), packageName, user)
- .setClassName(shortcutId)
- .build();
-
- sendLaunch(target, container);
-
- // Relay onStartShortcut info to every connected plugin.
- mAppLaunchEventsPluginsList
- .forEach(plugin -> plugin.onStartShortcut(
- packageName,
- shortcutId,
- user,
- container != null ? container : CONTAINER_DEFAULT)
- );
-
- }
-
- @Override
- @UiThread
- public void onStartApp(ComponentName cn, UserHandle user, String container) {
- if (cn != null) {
- AppTarget target = new AppTarget.Builder(
- new AppTargetId("app:" + cn), cn.getPackageName(), user)
- .setClassName(cn.getClassName())
- .build();
- sendLaunch(target, container);
-
- // Relay onStartApp to every connected plugin.
- mAppLaunchEventsPluginsList
- .forEach(plugin -> plugin.onStartApp(
- cn,
- user,
- container != null ? container : CONTAINER_DEFAULT)
- );
- }
- }
-
- @Override
- @UiThread
- public void onDismissApp(ComponentName cn, UserHandle user, String container) {
- if (cn == null) return;
- AppTarget target = new AppTarget.Builder(
- new AppTargetId("app: " + cn), cn.getPackageName(), user)
- .setClassName(cn.getClassName())
- .build();
- sendDismiss(target, container);
-
- // Relay onDismissApp to every connected plugin.
- mAppLaunchEventsPluginsList
- .forEach(plugin -> plugin.onDismissApp(
- cn,
- user,
- container != null ? container : CONTAINER_DEFAULT)
- );
- }
-
- @UiThread
- private void sendEvent(AppTarget target, String container, int eventId) {
- AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
- .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container)
- .build();
- Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget();
- }
-
- @UiThread
- private void sendLaunch(AppTarget target, String container) {
- sendEvent(target, container, AppTargetEvent.ACTION_LAUNCH);
- }
-
- @UiThread
- private void sendDismiss(AppTarget target, String container) {
- sendEvent(target, container, AppTargetEvent.ACTION_DISMISS);
- }
-
- @Override
- public void onPluginConnected(AppLaunchEventsPlugin appLaunchEventsPlugin, Context context) {
- mAppLaunchEventsPluginsList.add(appLaunchEventsPlugin);
- }
-
- @Override
- public void onPluginDisconnected(AppLaunchEventsPlugin appLaunchEventsPlugin) {
- mAppLaunchEventsPluginsList.remove(appLaunchEventsPlugin);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
deleted file mode 100644
index 44691d3..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ /dev/null
@@ -1,412 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.appprediction;
-
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.logging.LoggerUtils.newTarget;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Build;
-import android.util.AttributeSet;
-import android.util.IntProperty;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.animation.Interpolator;
-import android.widget.LinearLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.allapps.FloatingHeaderRow;
-import com.android.launcher3.allapps.FloatingHeaderView;
-import com.android.launcher3.anim.AlphaUpdateListener;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.keyboard.FocusIndicatorHelper;
-import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
-import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
-import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.Themes;
-import com.android.quickstep.AnimatedFloat;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-@TargetApi(Build.VERSION_CODES.P)
-public class PredictionRowView extends LinearLayout implements
- LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow {
-
- private static final String TAG = "PredictionRowView";
-
- private static final IntProperty<PredictionRowView> TEXT_ALPHA =
- new IntProperty<PredictionRowView>("textAlpha") {
- @Override
- public void setValue(PredictionRowView view, int alpha) {
- view.setTextAlpha(alpha);
- }
-
- @Override
- public Integer get(PredictionRowView view) {
- return view.mIconLastSetTextAlpha;
- }
- };
-
- private static final Interpolator ALPHA_FACTOR_INTERPOLATOR =
- (t) -> (t < 0.8f) ? 0 : (t - 0.8f) / 0.2f;
-
- private static final OnClickListener PREDICTION_CLICK_LISTENER =
- ItemClickHandler.getInstance(AppLaunchTracker.CONTAINER_PREDICTIONS);
-
- private final Launcher mLauncher;
- private final PredictionUiStateManager mPredictionUiStateManager;
- private int mNumPredictedAppsPerRow;
-
- // The set of predicted app component names
- private final List<ComponentKeyMapper> mPredictedAppComponents = new ArrayList<>();
- // The set of predicted apps resolved from the component names and the current set of apps
- private final ArrayList<ItemInfoWithIcon> mPredictedApps = new ArrayList<>();
- // Helper to drawing the focus indicator.
- private final FocusIndicatorHelper mFocusHelper;
-
- private final int mIconTextColor;
- private final int mIconFullTextAlpha;
- private int mIconLastSetTextAlpha;
- // Might use mIconFullTextAlpha instead of mIconLastSetTextAlpha if we are translucent.
- private int mIconCurrentTextAlpha;
-
- private FloatingHeaderView mParent;
- private boolean mScrolledOut;
-
- private float mScrollTranslation = 0;
- private final AnimatedFloat mContentAlphaFactor =
- new AnimatedFloat(this::updateTranslationAndAlpha);
- private final AnimatedFloat mOverviewScrollFactor =
- new AnimatedFloat(this::updateTranslationAndAlpha);
-
- private boolean mPredictionsEnabled = false;
-
- public PredictionRowView(@NonNull Context context) {
- this(context, null);
- }
-
- public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- setOrientation(LinearLayout.HORIZONTAL);
-
- mFocusHelper = new SimpleFocusIndicatorHelper(this);
-
- mNumPredictedAppsPerRow = LauncherAppState.getIDP(context).numAllAppsColumns;
- mLauncher = Launcher.getLauncher(context);
- mLauncher.addOnDeviceProfileChangeListener(this);
-
- mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(context);
-
- mIconTextColor = Themes.getAttrColor(context, android.R.attr.textColorSecondary);
- mIconFullTextAlpha = Color.alpha(mIconTextColor);
- mIconCurrentTextAlpha = mIconFullTextAlpha;
-
- updateVisibility();
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- mPredictionUiStateManager.setTargetAppsView(mLauncher.getAppsView());
- getAppsStore().registerIconContainer(this);
- AllAppsTipView.scheduleShowIfNeeded(mLauncher);
- }
-
- private AllAppsStore getAppsStore() {
- return mLauncher.getAppsView().getAppsStore();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- mPredictionUiStateManager.setTargetAppsView(null);
- getAppsStore().unregisterIconContainer(this);
- }
-
- public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
- mParent = parent;
- }
-
- private void updateVisibility() {
- setVisibility(mPredictionsEnabled ? VISIBLE : GONE);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
- MeasureSpec.EXACTLY));
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- mFocusHelper.draw(canvas);
- super.dispatchDraw(canvas);
- }
-
- @Override
- public int getExpectedHeight() {
- return getVisibility() == GONE ? 0 :
- Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx
- + getPaddingTop() + getPaddingBottom();
- }
-
- @Override
- public boolean shouldDraw() {
- return getVisibility() != GONE;
- }
-
- @Override
- public boolean hasVisibleContent() {
- return mPredictionsEnabled;
- }
-
- /**
- * Returns the predicted apps.
- */
- public List<ItemInfoWithIcon> getPredictedApps() {
- return mPredictedApps;
- }
-
- /**
- * Sets the current set of predicted apps.
- *
- * This can be called before we get the full set of applications, we should merge the results
- * only in onPredictionsUpdated() which is idempotent.
- *
- * If the number of predicted apps is the same as the previous list of predicted apps,
- * we can optimize by swapping them in place.
- */
- public void setPredictedApps(List<ComponentKeyMapper> apps) {
- mPredictedAppComponents.clear();
- mPredictedAppComponents.addAll(apps);
-
- mPredictedApps.clear();
- mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
- applyPredictionApps();
- }
-
- @Override
- public void onDeviceProfileChanged(DeviceProfile dp) {
- mNumPredictedAppsPerRow = dp.inv.numAllAppsColumns;
- removeAllViews();
- applyPredictionApps();
- }
-
- private void applyPredictionApps() {
- if (getChildCount() != mNumPredictedAppsPerRow) {
- while (getChildCount() > mNumPredictedAppsPerRow) {
- removeViewAt(0);
- }
- LayoutInflater inflater = mLauncher.getAppsView().getLayoutInflater();
- while (getChildCount() < mNumPredictedAppsPerRow) {
- BubbleTextView icon = (BubbleTextView) inflater.inflate(
- R.layout.all_apps_icon, this, false);
- icon.setOnClickListener(PREDICTION_CLICK_LISTENER);
- icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
- icon.setLongPressTimeoutFactor(1f);
- icon.setOnFocusChangeListener(mFocusHelper);
-
- LayoutParams lp = (LayoutParams) icon.getLayoutParams();
- // Ensure the all apps icon height matches the workspace icons in portrait mode.
- lp.height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
- lp.width = 0;
- lp.weight = 1;
- addView(icon);
- }
- }
-
- int predictionCount = mPredictedApps.size();
- int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
-
- for (int i = 0; i < getChildCount(); i++) {
- BubbleTextView icon = (BubbleTextView) getChildAt(i);
- icon.reset();
- if (predictionCount > i) {
- icon.setVisibility(View.VISIBLE);
- if (mPredictedApps.get(i) instanceof AppInfo) {
- icon.applyFromApplicationInfo((AppInfo) mPredictedApps.get(i));
- } else if (mPredictedApps.get(i) instanceof WorkspaceItemInfo) {
- icon.applyFromWorkspaceItem((WorkspaceItemInfo) mPredictedApps.get(i));
- }
- icon.setTextColor(iconColor);
- } else {
- icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
- }
- }
-
- boolean predictionsEnabled = predictionCount > 0;
- if (predictionsEnabled != mPredictionsEnabled) {
- mPredictionsEnabled = predictionsEnabled;
- mLauncher.reapplyUi(false /* cancelCurrentAnimation */);
- updateVisibility();
- }
- mParent.onHeightUpdated();
- }
-
- private List<ItemInfoWithIcon> processPredictedAppComponents(
- List<ComponentKeyMapper> components) {
- if (getAppsStore().getApps().length == 0) {
- // Apps have not been bound yet.
- return Collections.emptyList();
- }
-
- List<ItemInfoWithIcon> predictedApps = new ArrayList<>();
- for (ComponentKeyMapper mapper : components) {
- ItemInfoWithIcon info = mapper.getApp(getAppsStore());
- if (info != null) {
- ItemInfoWithIcon predictedApp = info.clone();
- predictedApp.container = LauncherSettings.Favorites.CONTAINER_PREDICTION;
- predictedApps.add(predictedApp);
- } else {
- if (FeatureFlags.IS_STUDIO_BUILD) {
- Log.e(TAG, "Predicted app not found: " + mapper);
- }
- }
- // Stop at the number of predicted apps
- if (predictedApps.size() == mNumPredictedAppsPerRow) {
- break;
- }
- }
- return predictedApps;
- }
-
- @Override
- public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
- ArrayList<LauncherLogProto.Target> parents) {
- for (int i = 0; i < mPredictedApps.size(); i++) {
- ItemInfoWithIcon appInfo = mPredictedApps.get(i);
- if (appInfo == childInfo) {
- child.predictedRank = i;
- break;
- }
- }
- parents.add(newContainerTarget(LauncherLogProto.ContainerType.PREDICTION));
-
- // include where the prediction is coming this used to be Launcher#modifyUserEvent
- LauncherLogProto.Target parent = newTarget(LauncherLogProto.Target.Type.CONTAINER);
- LauncherState state = mLauncher.getStateManager().getState();
- if (state == LauncherState.ALL_APPS) {
- parent.containerType = LauncherLogProto.ContainerType.ALLAPPS;
- } else if (state == OVERVIEW) {
- parent.containerType = LauncherLogProto.ContainerType.TASKSWITCHER;
- }
- parents.add(parent);
- }
-
- public void setTextAlpha(int textAlpha) {
- mIconLastSetTextAlpha = textAlpha;
- if (getAlpha() < 1 && textAlpha > 0) {
- // If the entire header is translucent, make sure the text is at full opacity so it's
- // not double-translucent. However, we support keeping the text invisible (alpha == 0).
- textAlpha = mIconFullTextAlpha;
- }
- mIconCurrentTextAlpha = textAlpha;
- int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
- for (int i = 0; i < getChildCount(); i++) {
- ((BubbleTextView) getChildAt(i)).setTextColor(iconColor);
- }
- }
-
- @Override
- public void setAlpha(float alpha) {
- super.setAlpha(alpha);
- // Reapply text alpha so that we update it to be full alpha if the row is now translucent.
- setTextAlpha(mIconLastSetTextAlpha);
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
-
- @Override
- public void setVerticalScroll(int scroll, boolean isScrolledOut) {
- mScrolledOut = isScrolledOut;
- updateTranslationAndAlpha();
- if (!isScrolledOut) {
- mScrollTranslation = scroll;
- updateTranslationAndAlpha();
- }
- }
-
- private void updateTranslationAndAlpha() {
- if (mPredictionsEnabled) {
- setTranslationY((1 - mOverviewScrollFactor.value) * mScrollTranslation);
-
- float factor = ALPHA_FACTOR_INTERPOLATOR.getInterpolation(mOverviewScrollFactor.value);
- float endAlpha = factor + (1 - factor) * (mScrolledOut ? 0 : 1);
- setAlpha(mContentAlphaFactor.value * endAlpha);
- AlphaUpdateListener.updateVisibility(this);
- }
- }
-
- @Override
- public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
- PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
- // Text follows all apps visibility
- int textAlpha = hasHeaderExtra && hasAllAppsContent ? mIconFullTextAlpha : 0;
- setter.setInt(this, TEXT_ALPHA, textAlpha, allAppsFade);
- setter.setFloat(mOverviewScrollFactor, AnimatedFloat.VALUE,
- (hasHeaderExtra && !hasAllAppsContent) ? 1 : 0, LINEAR);
- setter.setFloat(mContentAlphaFactor, AnimatedFloat.VALUE, hasHeaderExtra ? 1 : 0,
- headerFade);
- }
-
- @Override
- public void setInsets(Rect insets, DeviceProfile grid) {
- int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
- + grid.cellLayoutPaddingLeftRightPx;
- setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
- }
-
- @Override
- public Class<PredictionRowView> getTypeClass() {
- return PredictionRowView.class;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
deleted file mode 100644
index a0f6b04..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ /dev/null
@@ -1,370 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.appprediction;
-
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.content.ComponentName;
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
-import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.MainThreadInitializedObject;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.OptionalInt;
-import java.util.stream.IntStream;
-
-/**
- * Handler responsible to updating the UI due to predicted apps changes. Operations:
- * 1) Pushes the predicted apps to all-apps. If all-apps is visible, waits until it becomes
- * invisible again before applying the changes. This ensures that the UI does not change abruptly
- * in front of the user, even if an app launched and user pressed back button to return to the
- * all-apps UI again.
- * 2) Prefetch high-res icons for predicted apps. This ensures that we have the icons in memory
- * even if all-apps is not opened as they are shown in search UI as well
- * 3) Load instant app if it is not already in memory. As predictions are persisted on disk,
- * instant app will not be in memory when launcher starts.
- * 4) Maintains the current active client id (for the predictions) and all updates are performed on
- * that client id.
- */
-public class PredictionUiStateManager implements StateListener<LauncherState>,
- ItemInfoUpdateReceiver, OnIDPChangeListener, OnUpdateListener {
-
- public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
-
- // TODO (b/129421797): Update the client constants
- public enum Client {
- HOME("home"),
- OVERVIEW("overview");
-
- public final String id;
-
- Client(String id) {
- this.id = id;
- }
- }
-
- public static final MainThreadInitializedObject<PredictionUiStateManager> INSTANCE =
- new MainThreadInitializedObject<>(PredictionUiStateManager::new);
-
- private final Context mContext;
-
- private final DynamicItemCache mDynamicItemCache;
- private final List[] mPredictionServicePredictions;
-
- private int mMaxIconsPerRow;
- private Client mActiveClient;
-
- private AllAppsContainerView mAppsView;
-
- private PredictionState mPendingState;
- private PredictionState mCurrentState;
-
- private boolean mGettingValidPredictionResults;
-
- private PredictionUiStateManager(Context context) {
- mContext = context;
-
- mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated);
-
- mActiveClient = Client.HOME;
-
- InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
- mMaxIconsPerRow = idp.numColumns;
-
- idp.addOnChangeListener(this);
- mPredictionServicePredictions = new List[Client.values().length];
- for (int i = 0; i < mPredictionServicePredictions.length; i++) {
- mPredictionServicePredictions[i] = Collections.emptyList();
- }
- mGettingValidPredictionResults = Utilities.getDevicePrefs(context)
- .getBoolean(LAST_PREDICTION_ENABLED_STATE, true);
-
- // Call this last
- mCurrentState = parseLastState();
- }
-
- @Override
- public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
- mMaxIconsPerRow = profile.numColumns;
- }
-
- public Client getClient() {
- return mActiveClient;
- }
-
- public void switchClient(Client client) {
- if (client == mActiveClient) {
- return;
- }
- mActiveClient = client;
- dispatchOnChange(true);
- }
-
- public void setTargetAppsView(AllAppsContainerView appsView) {
- if (mAppsView != null) {
- mAppsView.getAppsStore().removeUpdateListener(this);
- }
- mAppsView = appsView;
- if (mAppsView != null) {
- mAppsView.getAppsStore().addUpdateListener(this);
- }
- if (mPendingState != null) {
- applyState(mPendingState);
- mPendingState = null;
- } else {
- applyState(mCurrentState);
- }
- updateDependencies(mCurrentState);
- }
-
- @Override
- public void reapplyItemInfo(ItemInfoWithIcon info) { }
-
- @Override
- public void onStateTransitionComplete(LauncherState state) {
- if (mAppsView == null) {
- return;
- }
- if (mPendingState != null && canApplyPredictions(mPendingState)) {
- applyState(mPendingState);
- mPendingState = null;
- }
- if (mPendingState == null) {
- Launcher.getLauncher(mAppsView.getContext()).getStateManager()
- .removeStateListener(this);
- }
- }
-
- private void scheduleApplyPredictedApps(PredictionState state) {
- boolean registerListener = mPendingState == null;
- mPendingState = state;
- if (registerListener) {
- // Add a listener and wait until appsView is invisible again.
- Launcher.getLauncher(mAppsView.getContext()).getStateManager().addStateListener(this);
- }
- }
-
- private void applyState(PredictionState state) {
- mCurrentState = state;
- if (mAppsView != null) {
- mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
- .setPredictedApps(mCurrentState.apps);
- }
- }
-
- private void updatePredictionStateAfterCallback() {
- boolean validResults = false;
- for (List l : mPredictionServicePredictions) {
- validResults |= l != null && !l.isEmpty();
- }
- if (validResults != mGettingValidPredictionResults) {
- mGettingValidPredictionResults = validResults;
- Utilities.getDevicePrefs(mContext).edit()
- .putBoolean(LAST_PREDICTION_ENABLED_STATE, true)
- .apply();
- }
- dispatchOnChange(true);
- }
-
- public AppPredictor.Callback appPredictorCallback(Client client) {
- return targets -> {
- mPredictionServicePredictions[client.ordinal()] = targets;
- updatePredictionStateAfterCallback();
- };
- }
-
- private void dispatchOnChange(boolean changed) {
- PredictionState newState = changed
- ? parseLastState()
- : mPendingState != null && canApplyPredictions(mPendingState)
- ? mPendingState
- : mCurrentState;
- if (changed && mAppsView != null && !canApplyPredictions(newState)) {
- scheduleApplyPredictedApps(newState);
- } else {
- applyState(newState);
- }
- }
-
- private PredictionState parseLastState() {
- PredictionState state = new PredictionState();
- state.isEnabled = mGettingValidPredictionResults;
- if (!state.isEnabled) {
- state.apps = Collections.EMPTY_LIST;
- return state;
- }
-
- state.apps = new ArrayList<>();
-
- List<AppTarget> appTargets = mPredictionServicePredictions[mActiveClient.ordinal()];
- if (!appTargets.isEmpty()) {
- for (AppTarget appTarget : appTargets) {
- ComponentKey key;
- if (appTarget.getShortcutInfo() != null) {
- key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
- } else {
- key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
- appTarget.getClassName()), appTarget.getUser());
- }
- state.apps.add(new ComponentKeyMapper(key, mDynamicItemCache));
- }
- }
- updateDependencies(state);
- return state;
- }
-
- private void updateDependencies(PredictionState state) {
- if (!state.isEnabled || mAppsView == null) {
- return;
- }
- mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this,
- mMaxIconsPerRow);
- }
-
- @Override
- public void onAppsUpdated() {
- dispatchOnChange(false);
- }
-
- private boolean canApplyPredictions(PredictionState newState) {
- if (mAppsView == null) {
- // If there is no apps view, no need to schedule.
- return true;
- }
- Launcher launcher = Launcher.getLauncher(mAppsView.getContext());
- PredictionRowView predictionRow = mAppsView.getFloatingHeaderView().
- findFixedRowByType(PredictionRowView.class);
- if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 ||
- launcher.isForceInvisible()) {
- return true;
- }
-
- if (mCurrentState.isEnabled != newState.isEnabled
- || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) {
- // If the visibility of the prediction row is changing, apply immediately.
- return true;
- }
-
- if (launcher.getDeviceProfile().isVerticalBarLayout()) {
- // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way
- return false;
- }
- if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) {
- // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI
- return false;
- }
-
- // Instead of checking against 1, we should check against (1 + delta), where delta accounts
- // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking
- // against 1, keeps the logic simple :)
- return launcher.getAllAppsController().getProgress() > 1;
- }
-
- public PredictionState getCurrentState() {
- return mCurrentState;
- }
-
- /**
- * Returns ranking info for the app within all apps prediction.
- * Only applicable when {@link ItemInfo#itemType} is one of the followings:
- * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
- * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
- * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
- */
- public OptionalInt getAllAppsRank(@Nullable ItemInfo itemInfo) {
- if (itemInfo == null || itemInfo.getTargetComponent() == null || itemInfo.user == null) {
- return OptionalInt.empty();
- }
-
- if (itemInfo.itemType == ITEM_TYPE_APPLICATION
- || itemInfo.itemType == ITEM_TYPE_SHORTCUT
- || itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
- ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(),
- itemInfo.user);
- final List<ComponentKeyMapper> apps = getCurrentState().apps;
- return IntStream.range(0, apps.size())
- .filter(index -> key.equals(apps.get(index).getComponentKey()))
- .findFirst();
- }
-
- return OptionalInt.empty();
- }
-
- /**
- * Fill in predicted_rank field based on app prediction.
- * Only applicable when {@link ItemInfo#itemType} is one of the followings:
- * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
- * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
- * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
- */
- public static void fillInPredictedRank(
- @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
-
- final PredictionUiStateManager manager = PredictionUiStateManager.INSTANCE.getNoCreate();
- if (manager == null || itemInfo.getTargetComponent() == null || itemInfo.user == null
- || (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
- && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
- && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
- return;
- }
- if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) {
- HotseatPredictionController.encodeHotseatLayoutIntoPredictionRank(itemInfo, target);
- return;
- }
-
- final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
- final List<ComponentKeyMapper> predictedApps = manager.getCurrentState().apps;
- IntStream.range(0, predictedApps.size())
- .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
- .findFirst()
- .ifPresent((rank) -> target.predictedRank = 0 - rank);
- }
-
- public static class PredictionState {
-
- public boolean isEnabled;
- public List<ComponentKeyMapper> apps;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
deleted file mode 100644
index c968de9..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.hybridhotseat;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.ActivityTracker;
-
-/**
- * Proxy activity to return user to home screen and show halfsheet education
- */
-public class HotseatEduActivity extends Activity {
-
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Intent homeIntent = new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setPackage(getPackageName())
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- new HotseatActivityTracker<>().addToIntent(homeIntent);
- startActivity(homeIntent);
- finish();
- }
-
- static class HotseatActivityTracker<T extends QuickstepLauncher> implements
- ActivityTracker.SchedulerCallback {
-
- @Override
- public boolean init(BaseActivity activity, boolean alreadyOnHome) {
- QuickstepLauncher launcher = (QuickstepLauncher) activity;
- if (launcher != null && launcher.getHotseatPredictionController() != null) {
- launcher.getHotseatPredictionController().showEdu();
- }
- return false;
- }
-
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
deleted file mode 100644
index 4f95254..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.hybridhotseat;
-
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
- .LAUNCHER_HOTSEAT_EDU_ONLY_TIP;
-
-import android.content.Intent;
-import android.view.View;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.Hotseat;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.R;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.util.GridOccupancy;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.views.ArrowTipView;
-import com.android.launcher3.views.Snackbar;
-
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.IntStream;
-
-/**
- * Controller class for managing user onboaridng flow for hybrid hotseat
- */
-public class HotseatEduController {
-
- public static final String HOTSEAT_EDU_ACTION =
- "com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU";
- public static final String SETTINGS_ACTION =
- "android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS";
-
- private final Launcher mLauncher;
- private final Hotseat mHotseat;
- private HotseatRestoreHelper mRestoreHelper;
- private List<WorkspaceItemInfo> mPredictedApps;
- private HotseatEduDialog mActiveDialog;
-
- private ArrayList<ItemInfo> mNewItems = new ArrayList<>();
- private IntArray mNewScreens = null;
- private Runnable mOnOnboardingComplete;
-
- HotseatEduController(Launcher launcher, HotseatRestoreHelper restoreHelper, Runnable runnable) {
- mLauncher = launcher;
- mHotseat = launcher.getHotseat();
- mRestoreHelper = restoreHelper;
- mOnOnboardingComplete = runnable;
- }
-
- /**
- * Checks what type of migration should be used and migrates hotseat
- */
- void migrate() {
- if (mRestoreHelper != null) {
- mRestoreHelper.createBackup();
- }
- if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
- migrateToFolder();
- } else {
- migrateHotseatWhole();
- }
- Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled,
- R.string.hotseat_prediction_settings, null,
- () -> mLauncher.startActivity(getSettingsIntent()));
- }
-
- /**
- * This migration places all non folder items in the hotseat into a folder and then moves
- * all folders in the hotseat to a workspace page that has enough empty spots.
- *
- * @return pageId that has accepted the items.
- */
- private int migrateToFolder() {
- ArrayDeque<FolderInfo> folders = new ArrayDeque<>();
- ArrayList<WorkspaceItemInfo> putIntoFolder = new ArrayList<>();
-
- //separate folders and items that can get in folders
- for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
- View view = mHotseat.getChildAt(i, 0);
- if (view == null) continue;
- ItemInfo info = (ItemInfo) view.getTag();
- if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
- folders.add((FolderInfo) info);
- } else if (info instanceof WorkspaceItemInfo && info.container == LauncherSettings
- .Favorites.CONTAINER_HOTSEAT) {
- putIntoFolder.add((WorkspaceItemInfo) info);
- }
- }
-
- // create a temp folder and add non folder items to it
- if (!putIntoFolder.isEmpty()) {
- ItemInfo firstItem = putIntoFolder.get(0);
- FolderInfo folderInfo = new FolderInfo();
- mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container,
- firstItem.screenId, firstItem.cellX, firstItem.cellY);
- folderInfo.setTitle("", mLauncher.getModelWriter());
- folderInfo.contents.addAll(putIntoFolder);
- for (int i = 0; i < folderInfo.contents.size(); i++) {
- ItemInfo item = folderInfo.contents.get(i);
- item.rank = i;
- mLauncher.getModelWriter().moveItemInDatabase(item, folderInfo.id, 0,
- item.cellX, item.cellY);
- }
- folders.add(folderInfo);
- }
- mNewItems.addAll(folders);
-
- return placeFoldersInWorkspace(folders);
- }
-
- private int placeFoldersInWorkspace(ArrayDeque<FolderInfo> folders) {
- if (folders.isEmpty()) return 0;
-
- Workspace workspace = mLauncher.getWorkspace();
- InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
-
- GridOccupancy[] occupancyList = new GridOccupancy[workspace.getChildCount()];
- for (int i = 0; i < occupancyList.length; i++) {
- occupancyList[i] = ((CellLayout) workspace.getChildAt(i)).cloneGridOccupancy();
- }
- //scan every screen to find available spots to place folders
- int occupancyIndex = 0;
- int[] itemXY = new int[2];
- while (occupancyIndex < occupancyList.length && !folders.isEmpty()) {
- GridOccupancy occupancy = occupancyList[occupancyIndex];
- if (occupancy.findVacantCell(itemXY, 1, 1)) {
- FolderInfo info = folders.poll();
- mLauncher.getModelWriter().moveItemInDatabase(info,
- LauncherSettings.Favorites.CONTAINER_DESKTOP,
- workspace.getScreenIdForPageIndex(occupancyIndex), itemXY[0], itemXY[1]);
- occupancy.markCells(info, true);
- } else {
- occupancyIndex++;
- }
- }
- if (folders.isEmpty()) return workspace.getScreenIdForPageIndex(occupancyIndex);
- int screenId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
- LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
- .getInt(LauncherSettings.Settings.EXTRA_VALUE);
- // if all screens are full and we still have folders left, put those on a new page
- FolderInfo folderInfo;
- int col = 0;
- while ((folderInfo = folders.poll()) != null) {
- mLauncher.getModelWriter().moveItemInDatabase(folderInfo,
- LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, col++,
- idp.numRows - 1);
- }
- mNewScreens = IntArray.wrap(screenId);
- return workspace.getPageCount();
- }
-
- /**
- * This migration option attempts to move the entire hotseat up to the first workspace that
- * has space to host items. If no such page is found, it moves items to a new page.
- *
- * @return pageId where items are migrated
- */
- private int migrateHotseatWhole() {
- Workspace workspace = mLauncher.getWorkspace();
-
- int pageId = -1;
- int toRow = 0;
- for (int i = 0; i < workspace.getPageCount(); i++) {
- CellLayout target = workspace.getScreenWithId(workspace.getScreenIdForPageIndex(i));
- if (target.makeSpaceForHotseatMigration(true)) {
- toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
- pageId = i;
- break;
- }
- }
- if (pageId == -1) {
- pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
- LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
- .getInt(LauncherSettings.Settings.EXTRA_VALUE);
- mNewScreens = IntArray.wrap(pageId);
- }
- for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
- View child = mHotseat.getChildAt(i, 0);
- if (child == null || child.getTag() == null) continue;
- ItemInfo tag = (ItemInfo) child.getTag();
- if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) continue;
- mLauncher.getModelWriter().moveItemInDatabase(tag,
- LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow);
- mNewItems.add(tag);
- }
- return pageId;
- }
-
- void moveHotseatItems() {
- mHotseat.removeAllViewsInLayout();
- if (!mNewItems.isEmpty()) {
- int lastPage = mNewItems.get(mNewItems.size() - 1).screenId;
- ArrayList<ItemInfo> animated = new ArrayList<>();
- ArrayList<ItemInfo> nonAnimated = new ArrayList<>();
-
- for (ItemInfo info : mNewItems) {
- if (info.screenId == lastPage) {
- animated.add(info);
- } else {
- nonAnimated.add(info);
- }
- }
- mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated);
- }
- }
-
- void finishOnboarding() {
- mOnOnboardingComplete.run();
- }
-
- void showDimissTip() {
- if (mHotseat.getShortcutsAndWidgets().getChildCount()
- < mLauncher.getDeviceProfile().inv.numHotseatIcons) {
- Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
- R.string.hotseat_prediction_settings, null,
- () -> mLauncher.startActivity(getSettingsIntent()));
- } else {
- new ArrowTipView(mLauncher).show(
- mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
- }
- }
-
- void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
- mPredictedApps = predictedApps;
- }
-
- void showEdu() {
- int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
- CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID);
- // hotseat is already empty and does not require migration. show edu tip
- boolean requiresMigration = IntStream.range(0, childCount).anyMatch(i -> {
- View v = mHotseat.getShortcutsAndWidgets().getChildAt(i);
- return v != null && v.getTag() != null && ((ItemInfo) v.getTag()).container
- != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
- });
- boolean canMigrateToFirstPage = cellLayout.makeSpaceForHotseatMigration(false);
- if (requiresMigration && canMigrateToFirstPage) {
- showDialog();
- } else {
- new ArrowTipView(mLauncher).show(mLauncher.getString(
- requiresMigration ? R.string.hotseat_tip_no_empty_slots
- : R.string.hotseat_auto_enrolled),
- mHotseat.getTop());
- mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ONLY_TIP);
- finishOnboarding();
- }
- }
-
- void showDialog() {
- if (mPredictedApps == null || mPredictedApps.isEmpty()) {
- return;
- }
- if (mActiveDialog != null) {
- mActiveDialog.handleClose(false);
- }
- mActiveDialog = HotseatEduDialog.getDialog(mLauncher);
- mActiveDialog.setHotseatEduController(this);
- mActiveDialog.show(mPredictedApps);
- }
-
- static Intent getSettingsIntent() {
- return new Intent(SETTINGS_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
-}
-
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
deleted file mode 100644
index 4b8e434..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.hybridhotseat;
-
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
- .LAUNCHER_HOTSEAT_EDU_ACCEPT;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN;
-
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.uioverrides.PredictedAppIcon;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.views.AbstractSlideInView;
-
-import java.util.List;
-
-/**
- * User education dialog for hybrid hotseat. Allows user to migrate hotseat items to a new page in
- * the workspace and shows predictions on the whole hotseat
- */
-public class HotseatEduDialog extends AbstractSlideInView implements Insettable {
-
- private static final int DEFAULT_CLOSE_DURATION = 200;
- protected static final int FINAL_SCRIM_BG_COLOR = 0x88000000;
-
- // we use this value to keep track of migration logs as we experiment with different migrations
- private static final int MIGRATION_EXPERIMENT_IDENTIFIER = 1;
-
- private final Rect mInsets = new Rect();
- private View mHotseatWrapper;
- private CellLayout mSampleHotseat;
- private Button mDismissBtn;
-
- public void setHotseatEduController(HotseatEduController hotseatEduController) {
- mHotseatEduController = hotseatEduController;
- }
-
- private HotseatEduController mHotseatEduController;
-
- public HotseatEduDialog(Context context, AttributeSet attr) {
- this(context, attr, 0);
- }
-
- public HotseatEduDialog(Context context, AttributeSet attrs,
- int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mContent = this;
- }
-
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
- mSampleHotseat = findViewById(R.id.sample_prediction);
-
- DeviceProfile grid = mLauncher.getDeviceProfile();
- Rect padding = grid.getHotseatLayoutPadding();
-
- mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
- mSampleHotseat.setGridSize(grid.inv.numHotseatIcons, 1);
- mSampleHotseat.setPadding(padding.left, 0, padding.right, 0);
-
- Button turnOnBtn = findViewById(R.id.turn_predictions_on);
- turnOnBtn.setOnClickListener(this::onAccept);
-
- mDismissBtn = findViewById(R.id.no_thanks);
- mDismissBtn.setOnClickListener(this::onDismiss);
-
- // update ui to reflect which migration method is going to be used
- if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
- ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
- R.string.hotseat_edu_message_migrate_alt);
- }
- }
-
- private void onAccept(View v) {
- mHotseatEduController.migrate();
- handleClose(true);
-
- mHotseatEduController.moveHotseatItems();
- mHotseatEduController.finishOnboarding();
- mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ACCEPT);
- }
-
- private void onDismiss(View v) {
- mHotseatEduController.showDimissTip();
- mHotseatEduController.finishOnboarding();
- mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_DENY);
- handleClose(true);
- }
-
- @Override
- public void logActionCommand(int command) {
- // Since this is on-boarding popup, it is not a user controlled action.
- }
-
- @Override
- public int getLogContainerType() {
- return LauncherLogProto.ContainerType.TIP;
- }
-
- @Override
- protected boolean isOfType(int type) {
- return (type & TYPE_ON_BOARD_POPUP) != 0;
- }
-
- @Override
- public void setInsets(Rect insets) {
- int leftInset = insets.left - mInsets.left;
- int rightInset = insets.right - mInsets.right;
- int bottomInset = insets.bottom - mInsets.bottom;
- mInsets.set(insets);
- if (mLauncher.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
- setPadding(leftInset, getPaddingTop(), rightInset, 0);
- mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
- mHotseatWrapper.getPaddingRight(), bottomInset);
- mHotseatWrapper.getLayoutParams().height =
- mLauncher.getDeviceProfile().hotseatBarSizePx + insets.bottom;
-
- } else {
- setPadding(0, getPaddingTop(), 0, 0);
- mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
- mHotseatWrapper.getPaddingRight(),
- (int) getResources().getDimension(R.dimen.bottom_sheet_edu_padding));
- ((TextView) findViewById(R.id.hotseat_edu_heading)).setText(
- R.string.hotseat_edu_title_migrate_landscape);
- ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
- R.string.hotseat_edu_message_migrate_landscape);
- }
- }
-
- private void animateOpen() {
- if (mIsOpen || mOpenCloseAnimator.isRunning()) {
- return;
- }
- mIsOpen = true;
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- mOpenCloseAnimator.start();
- }
-
- @Override
- protected void handleClose(boolean animate) {
- handleClose(true, DEFAULT_CLOSE_DURATION);
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- handleClose(false);
- }
-
- @Override
- protected int getScrimColor(Context context) {
- return FINAL_SCRIM_BG_COLOR;
- }
-
- private void populatePreview(List<WorkspaceItemInfo> predictions) {
- for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
- WorkspaceItemInfo info = predictions.get(i);
- PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info);
- icon.setEnabled(false);
- icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- icon.verifyHighRes();
- CellLayout.LayoutParams lp = new CellLayout.LayoutParams(i, 0, 1, 1);
- mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true);
- }
- }
-
- /**
- * Opens User education dialog with a list of suggested apps
- */
- public void show(List<WorkspaceItemInfo> predictions) {
- if (getParent() != null
- || predictions.size() < mLauncher.getDeviceProfile().inv.numHotseatIcons
- || mHotseatEduController == null) {
- return;
- }
- AbstractFloatingView.closeAllOpenViews(mLauncher);
- attachToContainer();
- mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_SEEN);
- animateOpen();
- populatePreview(predictions);
- }
-
- /**
- * Factory method for HotseatPredictionUserEdu dialog
- */
- public static HotseatEduDialog getDialog(Launcher launcher) {
- LayoutInflater layoutInflater = LayoutInflater.from(launcher);
- return (HotseatEduDialog) layoutInflater.inflate(
- R.layout.predicted_hotseat_edu, launcher.getDragLayer(),
- false);
-
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
deleted file mode 100644
index c15a596..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.hybridhotseat;
-
-import android.content.Context;
-import android.os.Handler;
-import android.util.Log;
-
-import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.MainThreadInitializedObject;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.PrintWriter;
-import java.text.DateFormat;
-import java.util.Calendar;
-import java.util.Date;
-
-/**
- * Helper class to allow hot seat file logging
- */
-public class HotseatFileLog {
-
- public static final int LOG_DAYS = 10;
- private static final String FILE_NAME_PREFIX = "hotseat-log-";
- private static final DateFormat DATE_FORMAT =
- DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
- public static final MainThreadInitializedObject<HotseatFileLog> INSTANCE =
- new MainThreadInitializedObject<>(HotseatFileLog::new);
-
-
- private final Handler mHandler = new Handler(
- Executors.createAndStartNewLooper("hotseat-logger"));
- private final File mLogsDir;
- private PrintWriter mCurrentWriter;
- private String mFileName;
-
- private HotseatFileLog(Context context) {
- mLogsDir = context.getFilesDir();
- }
-
- /**
- * Prints log values to disk
- */
- public void log(String tag, String msg) {
- String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
-
- mHandler.post(() -> {
- synchronized (this) {
- PrintWriter writer = getWriter();
- if (writer != null) {
- writer.println(out);
- }
- }
- });
- }
-
- private PrintWriter getWriter() {
- String fName = FILE_NAME_PREFIX + (LOG_DAYS % 10);
- if (fName.equals(mFileName)) return mCurrentWriter;
-
- Calendar cal = Calendar.getInstance();
-
- boolean append = false;
- File logFile = new File(mLogsDir, fName);
- if (logFile.exists()) {
- Calendar modifiedTime = Calendar.getInstance();
- modifiedTime.setTimeInMillis(logFile.lastModified());
-
- // If the file was modified more that 36 hours ago, purge the file.
- // We use instead of 24 to account for day-365 followed by day-1
- modifiedTime.add(Calendar.HOUR, 36);
- append = cal.before(modifiedTime);
- }
-
-
- if (mCurrentWriter != null) {
- mCurrentWriter.close();
- }
- try {
- mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
- mFileName = fName;
- } catch (Exception ex) {
- Log.e("HotseatLogs", "Error writing logs to file", ex);
- closeWriter();
- }
- return mCurrentWriter;
- }
-
-
- private synchronized void closeWriter() {
- mFileName = null;
- if (mCurrentWriter != null) {
- mCurrentWriter.close();
- }
- mCurrentWriter = null;
- }
-
-
- /**
- * Returns a list of all log files
- */
- public synchronized File[] getLogFiles() {
- File[] files = new File[LOG_DAYS + FileLog.LOG_DAYS];
- //include file log files here
- System.arraycopy(FileLog.getLogFiles(), 0, files, 0, FileLog.LOG_DAYS);
-
- closeWriter();
- for (int i = 0; i < LOG_DAYS; i++) {
- files[FileLog.LOG_DAYS + i] = new File(mLogsDir, FILE_NAME_PREFIX + i);
- }
- return files;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
deleted file mode 100644
index b94e633..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ /dev/null
@@ -1,723 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.hybridhotseat;
-
-import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.hybridhotseat.HotseatEduController.getSettingsIntent;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.app.prediction.AppPredictionContext;
-import android.app.prediction.AppPredictionManager;
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.app.prediction.AppTargetEvent;
-import android.content.ComponentName;
-import android.os.Process;
-import android.util.Log;
-import android.view.HapticFeedbackConstants;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DragSource;
-import com.android.launcher3.DropTarget;
-import com.android.launcher3.Hotseat;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.appprediction.ComponentKeyMapper;
-import com.android.launcher3.appprediction.DynamicItemCache;
-import com.android.launcher3.dragndrop.DragController;
-import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
-import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer;
-import com.android.launcher3.logging.InstanceId;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.uioverrides.PredictedAppIcon;
-import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.OnboardingPrefs;
-import com.android.launcher3.views.ArrowTipView;
-import com.android.launcher3.views.Snackbar;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.OptionalInt;
-import java.util.stream.IntStream;
-
-/**
- * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows
- * pinning of predicted apps and manages replacement of predicted apps with user drag.
- */
-public class HotseatPredictionController implements DragController.DragListener,
- View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>,
- InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener,
- IconCache.ItemInfoUpdateReceiver, DragSource {
-
- private static final String TAG = "PredictiveHotseat";
- private static final boolean DEBUG = false;
-
- private static final String PREDICTION_CLIENT = "hotseat";
- private DropTarget.DragObject mDragObject;
- private int mHotSeatItemsCount;
- private int mPredictedSpotsCount = 0;
-
- private Launcher mLauncher;
- private final Hotseat mHotseat;
-
- private final HotseatRestoreHelper mRestoreHelper;
-
- private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
-
- private DynamicItemCache mDynamicItemCache;
-
- private final HotseatPredictionModel mPredictionModel;
- private AppPredictor mAppPredictor;
- private AllAppsStore mAllAppsStore;
- private AnimatorSet mIconRemoveAnimators;
- private boolean mUIUpdatePaused = false;
- private boolean mIsDestroyed = false;
-
-
- private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
-
- private final View.OnLongClickListener mPredictionLongClickListener = v -> {
- if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
- if (mLauncher.getWorkspace().isSwitchingState()) return false;
- if (!mLauncher.getOnboardingPrefs().getBoolean(
- OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN)) {
- Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
- R.string.hotseat_prediction_settings, null,
- () -> mLauncher.startActivity(getSettingsIntent()));
- mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN);
- mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- return true;
- }
- // Start the drag
- mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions());
- return true;
- };
-
- public HotseatPredictionController(Launcher launcher) {
- mLauncher = launcher;
- mHotseat = launcher.getHotseat();
- mAllAppsStore = mLauncher.getAppsView().getAppsStore();
- LauncherAppState appState = LauncherAppState.getInstance(launcher);
- mPredictionModel = (HotseatPredictionModel) appState.getPredictionModel();
- mAllAppsStore.addUpdateListener(this);
- mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction);
- mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
- launcher.getDeviceProfile().inv.addOnChangeListener(this);
- mHotseat.addOnAttachStateChangeListener(this);
- mRestoreHelper = new HotseatRestoreHelper(mLauncher);
- if (mHotseat.isAttachedToWindow()) {
- onViewAttachedToWindow(mHotseat);
- }
- }
-
- /**
- * Shows appropriate hotseat education based on prediction enabled and migration states.
- */
- public void showEdu() {
- mLauncher.getStateManager().goToState(NORMAL, true, () -> {
- if (mComponentKeyMappers.isEmpty()) {
- // launcher has empty predictions set
- Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_disabled,
- R.string.hotseat_prediction_settings, null,
- () -> mLauncher.startActivity(getSettingsIntent()));
- } else if (getPredictedIcons().size() >= (mHotSeatItemsCount + 1) / 2) {
- showDiscoveryTip();
- } else {
- HotseatEduController eduController = new HotseatEduController(mLauncher,
- mRestoreHelper,
- this::createPredictor);
- eduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
- eduController.showEdu();
- }
- });
- }
-
- /**
- * Shows educational tip for hotseat if user does not go through Tips app.
- */
- private void showDiscoveryTip() {
- if (getPredictedIcons().isEmpty()) {
- new ArrowTipView(mLauncher).show(
- mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
- } else {
- Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
- R.string.hotseat_prediction_settings, null,
- () -> mLauncher.startActivity(getSettingsIntent()));
- }
- }
-
- /**
- * Returns if hotseat client has predictions
- */
- public boolean hasPredictions() {
- return !mComponentKeyMappers.isEmpty();
- }
-
- @Override
- public void onViewAttachedToWindow(View view) {
- mLauncher.getDragController().addDragListener(this);
- }
-
- @Override
- public void onViewDetachedFromWindow(View view) {
- mLauncher.getDragController().removeDragListener(this);
- }
-
- private void fillGapsWithPrediction() {
- fillGapsWithPrediction(false, null);
- }
-
- private void fillGapsWithPrediction(boolean animate, Runnable callback) {
- if (mUIUpdatePaused || mDragObject != null) {
- return;
- }
- List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
- if (mComponentKeyMappers.isEmpty() != predictedApps.isEmpty()) {
- // Safely ignore update as AppsList is not ready yet. This will called again once
- // apps are ready (HotseatPredictionController#onAppsUpdated)
- return;
- }
- int predictionIndex = 0;
- ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
- // make sure predicted icon removal and filling predictions don't step on each other
- if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) {
- mIconRemoveAnimators.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- fillGapsWithPrediction(animate, callback);
- mIconRemoveAnimators.removeListener(this);
- }
- });
- return;
- }
- for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
- View child = mHotseat.getChildAt(
- mHotseat.getCellXFromOrder(rank),
- mHotseat.getCellYFromOrder(rank));
-
- if (child != null && !isPredictedIcon(child)) {
- continue;
- }
- if (predictedApps.size() <= predictionIndex) {
- // Remove predicted apps from the past
- if (isPredictedIcon(child)) {
- mHotseat.removeView(child);
- }
- continue;
- }
- WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
- if (isPredictedIcon(child) && child.isEnabled()) {
- PredictedAppIcon icon = (PredictedAppIcon) child;
- icon.applyFromWorkspaceItem(predictedItem);
- icon.finishBinding(mPredictionLongClickListener);
- } else {
- newItems.add(predictedItem);
- }
- preparePredictionInfo(predictedItem, rank);
- }
- mPredictedSpotsCount = predictionIndex;
- bindItems(newItems, animate, callback);
- }
-
- private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback) {
- AnimatorSet animationSet = new AnimatorSet();
- for (WorkspaceItemInfo item : itemsToAdd) {
- PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
- mLauncher.getWorkspace().addInScreenFromBind(icon, item);
- icon.finishBinding(mPredictionLongClickListener);
- if (animate) {
- animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
- }
- }
- if (animate) {
- if (callback != null) {
- animationSet.addListener(AnimationSuccessListener.forRunnable(callback));
- }
- animationSet.start();
- } else {
- if (callback != null) callback.run();
- }
- }
-
- /**
- * Unregisters callbacks and frees resources
- */
- public void destroy() {
- mIsDestroyed = true;
- mAllAppsStore.removeUpdateListener(this);
- mLauncher.getDeviceProfile().inv.removeOnChangeListener(this);
- mHotseat.removeOnAttachStateChangeListener(this);
- if (mAppPredictor != null) {
- mAppPredictor.destroy();
- }
- }
-
- /**
- * start and pauses predicted apps update on the hotseat
- */
- public void setPauseUIUpdate(boolean paused) {
- mUIUpdatePaused = paused;
- if (!paused) {
- fillGapsWithPrediction();
- }
- }
-
- /**
- * Creates App Predictor with all the current apps pinned on the hotseat
- */
- public void createPredictor() {
- AppPredictionManager apm = mLauncher.getSystemService(AppPredictionManager.class);
- if (apm == null) {
- return;
- }
- if (mAppPredictor != null) {
- mAppPredictor.destroy();
- mAppPredictor = null;
- }
- WeakReference<HotseatPredictionController> controllerRef = new WeakReference<>(this);
-
-
- mPredictionModel.createBundle(bundle -> {
- if (mIsDestroyed) return;
- mAppPredictor = apm.createAppPredictionSession(
- new AppPredictionContext.Builder(mLauncher)
- .setUiSurface(PREDICTION_CLIENT)
- .setPredictedTargetCount(mHotSeatItemsCount)
- .setExtras(bundle)
- .build());
- mAppPredictor.registerPredictionUpdates(
- mLauncher.getApplicationContext().getMainExecutor(),
- list -> {
- if (controllerRef.get() != null) {
- controllerRef.get().setPredictedApps(list);
- }
- });
- mAppPredictor.requestPredictionUpdate();
- });
- setPauseUIUpdate(false);
- }
-
- /**
- * Create WorkspaceItemInfo objects and binds PredictedAppIcon views for cached predicted items.
- */
- public void showCachedItems(List<AppInfo> apps, IntArray ranks) {
- if (hasPredictions() && mAppPredictor != null) {
- mAppPredictor.requestPredictionUpdate();
- fillGapsWithPrediction();
- return;
- }
- int count = Math.min(ranks.size(), apps.size());
- List<WorkspaceItemInfo> items = new ArrayList<>(count);
- for (int i = 0; i < count; i++) {
- WorkspaceItemInfo item = new WorkspaceItemInfo(apps.get(i));
- ComponentKey componentKey = new ComponentKey(item.getTargetComponent(), item.user);
- preparePredictionInfo(item, ranks.get(i));
- items.add(item);
-
- mComponentKeyMappers.add(new ComponentKeyMapper(componentKey, mDynamicItemCache));
- }
- updateDependencies();
- bindItems(items, false, null);
- }
-
- private void setPredictedApps(List<AppTarget> appTargets) {
- mComponentKeyMappers.clear();
- if (appTargets.isEmpty()) {
- mRestoreHelper.restoreBackup();
- }
- StringBuilder predictionLog = new StringBuilder("predictedApps: [\n");
- ArrayList<ComponentKey> componentKeys = new ArrayList<>();
- for (AppTarget appTarget : appTargets) {
- ComponentKey key;
- if (appTarget.getShortcutInfo() != null) {
- key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
- } else {
- key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
- appTarget.getClassName()), appTarget.getUser());
- }
- componentKeys.add(key);
- predictionLog.append(key.toString());
- predictionLog.append(",rank:");
- predictionLog.append(appTarget.getRank());
- predictionLog.append("\n");
- mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
- }
- predictionLog.append("]");
- if (Utilities.IS_DEBUG_DEVICE) {
- HotseatFileLog.INSTANCE.get(mLauncher).log(TAG, predictionLog.toString());
- }
- updateDependencies();
- fillGapsWithPrediction();
- mPredictionModel.cachePredictionComponentKeys(componentKeys);
- }
-
- private void updateDependencies() {
- mDynamicItemCache.updateDependencies(mComponentKeyMappers, mAllAppsStore, this,
- mHotSeatItemsCount);
- }
-
- /**
- * Pins a predicted app icon into place.
- */
- public void pinPrediction(ItemInfo info) {
- PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt(
- mHotseat.getCellXFromOrder(info.rank),
- mHotseat.getCellYFromOrder(info.rank));
- if (icon == null) {
- return;
- }
- WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
- mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
- LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
- workspaceItemInfo.cellX, workspaceItemInfo.cellY);
- ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
- icon.pin(workspaceItemInfo);
- AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(workspaceItemInfo);
- if (appTarget != null) {
- notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
- AppTargetEvent.ACTION_PIN, workspaceItemInfo));
- }
- }
-
- private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
- List<ComponentKeyMapper> components) {
- AllAppsStore allAppsStore = mLauncher.getAppsView().getAppsStore();
- if (allAppsStore.getApps().length == 0) {
- return Collections.emptyList();
- }
-
- List<WorkspaceItemInfo> predictedApps = new ArrayList<>();
- for (ComponentKeyMapper mapper : components) {
- ItemInfoWithIcon info = mapper.getApp(allAppsStore);
- if (info instanceof AppInfo) {
- WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info);
- predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
- predictedApps.add(predictedApp);
- } else if (info instanceof WorkspaceItemInfo) {
- WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((WorkspaceItemInfo) info);
- predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
- predictedApps.add(predictedApp);
- } else {
- if (DEBUG) {
- Log.e(TAG, "Predicted app not found: " + mapper);
- }
- }
- // Stop at the number of hotseat items
- if (predictedApps.size() == mHotSeatItemsCount) {
- break;
- }
- }
- return predictedApps;
- }
-
- private List<PredictedAppIcon> getPredictedIcons() {
- List<PredictedAppIcon> icons = new ArrayList<>();
- ViewGroup vg = mHotseat.getShortcutsAndWidgets();
- for (int i = 0; i < vg.getChildCount(); i++) {
- View child = vg.getChildAt(i);
- if (isPredictedIcon(child)) {
- icons.add((PredictedAppIcon) child);
- }
- }
- return icons;
- }
-
- private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines,
- ItemInfo draggedInfo) {
- if (mIconRemoveAnimators != null) {
- mIconRemoveAnimators.end();
- }
- mIconRemoveAnimators = new AnimatorSet();
- removeOutlineDrawings();
- for (PredictedAppIcon icon : getPredictedIcons()) {
- if (!icon.isEnabled()) {
- continue;
- }
- if (icon.getTag().equals(draggedInfo)) {
- mHotseat.removeView(icon);
- continue;
- }
- int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
- outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
- mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
- icon.setEnabled(false);
- ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0);
- animator.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- if (icon.getParent() != null) {
- mHotseat.removeView(icon);
- }
- }
- });
- mIconRemoveAnimators.play(animator);
- }
- mIconRemoveAnimators.start();
- }
-
- private void notifyItemAction(AppTargetEvent event) {
- if (mAppPredictor != null) {
- mAppPredictor.notifyAppTargetEvent(event);
- }
- }
-
- @Override
- public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
- removePredictedApps(mOutlineDrawings, dragObject.dragInfo);
- mDragObject = dragObject;
- if (mOutlineDrawings.isEmpty()) return;
- for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
- mHotseat.addDelegatedCellDrawing(outlineDrawing);
- }
- mHotseat.invalidate();
- }
-
- /**
- * Unpins pinned app when it's converted into a folder
- */
- public void folderCreatedFromWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) {
- AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo);
- AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo);
- if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
- notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget,
- AppTargetEvent.ACTION_PIN, folderInfo));
- }
- // using folder info with isTrackedForPrediction as itemInfo.container is already changed
- // to folder by this point
- if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
- notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget,
- AppTargetEvent.ACTION_UNPIN, folderInfo
- ));
- }
- }
-
- /**
- * Pins workspace item created when all folder items are removed but one
- */
- public void folderConvertedToWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) {
- AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo);
- AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo);
- if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
- notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget,
- AppTargetEvent.ACTION_UNPIN, folderInfo));
- }
- if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(itemInfo)) {
- notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget,
- AppTargetEvent.ACTION_PIN, itemInfo));
- }
- }
-
- @Override
- public void onDragEnd() {
- if (mDragObject == null) {
- return;
- }
-
- ItemInfo dragInfo = mDragObject.dragInfo;
- if (mDragObject.isMoved()) {
- AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(dragInfo);
- //always send pin event first to prevent AiAi from predicting an item moved within
- // the same page
- if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction(dragInfo)) {
- notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
- AppTargetEvent.ACTION_PIN, dragInfo));
- }
- if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction(
- mDragObject.originalDragInfo)) {
- notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
- AppTargetEvent.ACTION_UNPIN, mDragObject.originalDragInfo));
- }
- }
- mDragObject = null;
- fillGapsWithPrediction(true, this::removeOutlineDrawings);
- }
-
-
- @Nullable
- @Override
- public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
- ItemInfo itemInfo) {
- if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
- return null;
- }
- return new PinPrediction(activity, itemInfo);
- }
-
- private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
- itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
- itemInfo.rank = rank;
- itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
- itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
- itemInfo.screenId = rank;
- }
-
- private void removeOutlineDrawings() {
- if (mOutlineDrawings.isEmpty()) return;
- for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
- mHotseat.removeDelegatedCellDrawing(outlineDrawing);
- }
- mHotseat.invalidate();
- mOutlineDrawings.clear();
- }
-
- @Override
- public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
- if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
- this.mHotSeatItemsCount = profile.numHotseatIcons;
- createPredictor();
- }
- }
-
- @Override
- public void onAppsUpdated() {
- fillGapsWithPrediction();
- }
-
- @Override
- public void reapplyItemInfo(ItemInfoWithIcon info) {
- }
-
- @Override
- public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
- //Does nothing
- }
-
- @Override
- public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
- ArrayList<LauncherLogProto.Target> parents) {
- mHotseat.fillInLogContainerData(childInfo, child, parents);
- }
-
- /**
- * Logs rank info based on current list of predicted items
- */
- public void logLaunchedAppRankingInfo(@NonNull ItemInfo itemInfo, InstanceId instanceId) {
- if (Utilities.IS_DEBUG_DEVICE) {
- final String pkg = itemInfo.getTargetComponent() != null
- ? itemInfo.getTargetComponent().getPackageName() : "unknown";
- HotseatFileLog.INSTANCE.get(mLauncher).log("UserEvent",
- "appLaunch: packageName:" + pkg + ",isWorkApp:" + (itemInfo.user != null
- && !Process.myUserHandle().equals(itemInfo.user))
- + ",launchLocation:" + itemInfo.container);
- }
-
- if (itemInfo.getTargetComponent() == null || itemInfo.user == null) {
- return;
- }
-
- final ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
-
- final List<ComponentKeyMapper> predictedApps = new ArrayList<>(mComponentKeyMappers);
- OptionalInt rank = IntStream.range(0, predictedApps.size())
- .filter(index -> key.equals(predictedApps.get(index).getComponentKey()))
- .findFirst();
- if (!rank.isPresent()) {
- return;
- }
-
- int cardinality = 0;
- for (PredictedAppIcon icon : getPredictedIcons()) {
- ItemInfo info = (ItemInfo) icon.getTag();
- cardinality |= 1 << info.screenId;
- }
-
- PredictedHotseatContainer.Builder containerBuilder = PredictedHotseatContainer.newBuilder();
- containerBuilder.setCardinality(cardinality);
- if (itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
- containerBuilder.setIndex(rank.getAsInt());
- }
- mLauncher.getStatsLogManager().logger()
- .withInstanceId(instanceId)
- .withRank(rank.getAsInt())
- .withContainerInfo(ContainerInfo.newBuilder()
- .setPredictedHotseatContainer(containerBuilder)
- .build())
- .log(LAUNCHER_HOTSEAT_RANKED);
- }
-
- private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
-
- private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) {
- super(R.drawable.ic_pin, R.string.pin_prediction, target,
- itemInfo);
- }
-
- @Override
- public void onClick(View view) {
- dismissTaskMenuView(mTarget);
- pinPrediction(mItemInfo);
- }
- }
-
- /**
- * Fill in predicted_rank field based on app prediction.
- * Only applicable when {@link ItemInfo#itemType} is PREDICTED_HOTSEAT
- */
- public static void encodeHotseatLayoutIntoPredictionRank(
- @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
- QuickstepLauncher launcher = QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
- if (launcher == null || launcher.getHotseatPredictionController() == null
- || itemInfo.getTargetComponent() == null) {
- return;
- }
- HotseatPredictionController controller = launcher.getHotseatPredictionController();
-
- final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
-
- final List<ComponentKeyMapper> predictedApps = controller.mComponentKeyMappers;
- OptionalInt rank = IntStream.range(0, predictedApps.size())
- .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
- .findFirst();
-
- target.predictedRank = 10000 + (controller.mPredictedSpotsCount * 100)
- + (rank.isPresent() ? rank.getAsInt() + 1 : 0);
- }
-
- private static boolean isPredictedIcon(View view) {
- return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo
- && ((WorkspaceItemInfo) view.getTag()).container
- == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
deleted file mode 100644
index 5a038d2..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.hybridhotseat;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.app.prediction.AppTarget;
-import android.app.prediction.AppTargetEvent;
-import android.app.prediction.AppTargetId;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.Bundle;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.model.AllAppsList;
-import com.android.launcher3.model.BaseModelUpdateTask;
-import com.android.launcher3.model.BgDataModel;
-import com.android.launcher3.model.PredictionModel;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.shortcuts.ShortcutKey;
-
-import java.util.ArrayList;
-import java.util.Locale;
-import java.util.function.Consumer;
-
-/**
- * Model helper for app predictions in workspace
- */
-public class HotseatPredictionModel extends PredictionModel {
- private static final String APP_LOCATION_HOTSEAT = "hotseat";
- private static final String APP_LOCATION_WORKSPACE = "workspace";
-
- private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events";
- private static final String BUNDLE_KEY_CURRENT_ITEMS = "current_items";
-
-
- public HotseatPredictionModel(Context context) { }
-
- /**
- * Creates and returns bundle using workspace items and cached items
- */
- public void createBundle(Consumer<Bundle> cb) {
- LauncherAppState appState = LauncherAppState.getInstance(mContext);
- appState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
- @Override
- public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
- Bundle bundle = new Bundle();
- ArrayList<AppTargetEvent> events = new ArrayList<>();
- ArrayList<ItemInfo> workspaceItems = new ArrayList<>(dataModel.workspaceItems);
- workspaceItems.addAll(dataModel.appWidgets);
- for (ItemInfo item : workspaceItems) {
- AppTarget target = getAppTargetFromInfo(item);
- if (target != null && !isTrackedForPrediction(item)) continue;
- events.add(wrapAppTargetWithLocation(target, AppTargetEvent.ACTION_PIN, item));
- }
- ArrayList<AppTarget> currentTargets = new ArrayList<>();
- for (ItemInfo itemInfo : dataModel.cachedPredictedItems) {
- AppTarget target = getAppTargetFromInfo(itemInfo);
- if (target != null) currentTargets.add(target);
- }
- bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, events);
- bundle.putParcelableArrayList(BUNDLE_KEY_CURRENT_ITEMS, currentTargets);
- MAIN_EXECUTOR.execute(() -> cb.accept(bundle));
- }
- });
- }
-
- /**
- * Creates and returns for {@link AppTarget} object given an {@link ItemInfo}. Returns null
- * if item is not supported prediction
- */
- public AppTarget getAppTargetFromInfo(ItemInfo info) {
- if (info == null) return null;
- if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
- && info instanceof LauncherAppWidgetInfo
- && ((LauncherAppWidgetInfo) info).providerName != null) {
- ComponentName cn = ((LauncherAppWidgetInfo) info).providerName;
- return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()),
- cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
- } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
- && info.getTargetComponent() != null) {
- ComponentName cn = info.getTargetComponent();
- return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
- cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
- } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
- && info instanceof WorkspaceItemInfo) {
- ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info);
- //TODO: switch to using full shortcut info
- return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()),
- shortcutKey.componentName.getPackageName(), shortcutKey.user).build();
- } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
- return new AppTarget.Builder(new AppTargetId("folder:" + info.id),
- mContext.getPackageName(), info.user).build();
- }
- return null;
- }
-
-
- /**
- * Creates and returns {@link AppTargetEvent} from an {@link AppTarget}, action, and item
- * location using {@link ItemInfo}
- */
- public AppTargetEvent wrapAppTargetWithLocation(AppTarget target, int action, ItemInfo info) {
- String location = String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]",
- info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
- ? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE,
- info.screenId, info.cellX, info.cellY, info.spanX, info.spanY);
- return new AppTargetEvent.Builder(target, action).setLaunchLocation(location).build();
- }
-
- /**
- * Helper method to determine if {@link ItemInfo} should be tracked and reported to predictors
- */
- public static boolean isTrackedForPrediction(ItemInfo info) {
- return info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || (
- info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
- && info.screenId == Workspace.FIRST_SCREEN_ID);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
deleted file mode 100644
index 8c1db4e..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.hybridhotseat;
-
-import static com.android.launcher3.LauncherSettings.Favorites.HYBRID_HOTSEAT_BACKUP_TABLE;
-import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.GridBackupTable;
-import com.android.launcher3.provider.LauncherDbUtils;
-
-/**
- * A helper class to manage migration revert restoration for hybrid hotseat
- */
-public class HotseatRestoreHelper {
- private final Launcher mLauncher;
- private boolean mBackupRestored = false;
-
- HotseatRestoreHelper(Launcher context) {
- mLauncher = context;
- }
-
- /**
- * Creates a snapshot backup of Favorite table for future restoration use.
- */
- public void createBackup() {
- MODEL_EXECUTOR.execute(() -> {
- try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
- LauncherSettings.Settings.call(
- mLauncher.getContentResolver(),
- LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
- .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
- InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
- GridBackupTable backupTable = new GridBackupTable(mLauncher,
- transaction.getDb(), idp.numHotseatIcons, idp.numColumns,
- idp.numRows);
- backupTable.createCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE);
- transaction.commit();
- LauncherSettings.Settings.call(mLauncher.getContentResolver(),
- LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE);
- }
- });
- }
-
- /**
- * Finds and restores a previously saved snapshow of Favorites table
- */
- public void restoreBackup() {
- if (mBackupRestored) return;
- MODEL_EXECUTOR.execute(() -> {
- try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
- LauncherSettings.Settings.call(
- mLauncher.getContentResolver(),
- LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
- .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
- if (!tableExists(transaction.getDb(), HYBRID_HOTSEAT_BACKUP_TABLE)) {
- return;
- }
- InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
- GridBackupTable backupTable = new GridBackupTable(mLauncher,
- transaction.getDb(), idp.numHotseatIcons, idp.numColumns,
- idp.numRows);
- backupTable.restoreFromCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE, true);
- transaction.commit();
- mBackupRestored = true;
- mLauncher.getModel().forceReload();
- }
- });
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
deleted file mode 100644
index 597c17b..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.PIN_PREDICTION;
-import static com.android.launcher3.graphics.IconShape.getShape;
-
-import android.content.Context;
-import android.graphics.BlurMaskFilter;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.Rect;
-import android.os.Process;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import androidx.core.graphics.ColorUtils;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.graphics.IconPalette;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
-import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.views.DoubleShadowBubbleTextView;
-
-/**
- * A BubbleTextView with a ring around it's drawable
- */
-public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
- LauncherAccessibilityDelegate.AccessibilityActionHandler {
-
- private static final int RING_SHADOW_COLOR = 0x99000000;
- private static final float RING_EFFECT_RATIO = 0.095f;
-
- boolean mIsDrawingDot = false;
- private final DeviceProfile mDeviceProfile;
- private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private final Path mRingPath = new Path();
- private boolean mIsPinned = false;
- private final int mNormalizedIconRadius;
- private final BlurMaskFilter mShadowFilter;
- private int mPlateColor;
- boolean mDrawForDrag = false;
-
- public PredictedAppIcon(Context context) {
- this(context, null, 0);
- }
-
- public PredictedAppIcon(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
- mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
- int shadowSize = context.getResources().getDimensionPixelSize(
- R.dimen.blur_size_thin_outline);
- mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- int count = canvas.save();
- if (!mIsPinned) {
- boolean isBadged = getTag() instanceof WorkspaceItemInfo
- && !Process.myUserHandle().equals(((ItemInfo) getTag()).user);
- drawEffect(canvas, isBadged);
- canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
- canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
- }
- super.onDraw(canvas);
- canvas.restoreToCount(count);
- }
-
- @Override
- protected void drawDotIfNecessary(Canvas canvas) {
- mIsDrawingDot = true;
- int count = canvas.save();
- canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO);
- canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO);
- super.drawDotIfNecessary(canvas);
- canvas.restoreToCount(count);
- mIsDrawingDot = false;
- }
-
- @Override
- public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
- super.applyFromWorkspaceItem(info);
- int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
- mPlateColor = ColorUtils.setAlphaComponent(color, 200);
- if (mIsPinned) {
- setContentDescription(info.contentDescription);
- } else {
- setContentDescription(
- getContext().getString(R.string.hotseat_prediction_content_description,
- info.contentDescription));
- }
- }
-
- /**
- * Removes prediction ring from app icon
- */
- public void pin(WorkspaceItemInfo info) {
- if (mIsPinned) return;
- mIsPinned = true;
- applyFromWorkspaceItem(info);
- setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
- ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
- invalidate();
- }
-
- /**
- * prepares prediction icon for usage after bind
- */
- public void finishBinding(OnLongClickListener longClickListener) {
- setOnLongClickListener(longClickListener);
- ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
- setTextVisibility(false);
- verifyHighRes();
- }
-
- @Override
- public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) {
- if (!mIsPinned) {
- accessibilityNodeInfo.addAction(
- new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
- getContext().getText(R.string.pin_prediction)));
- }
- }
-
- @Override
- public boolean performAccessibilityAction(int action, ItemInfo info) {
- QuickstepLauncher launcher = Launcher.cast(Launcher.getLauncher(getContext()));
- if (action == PIN_PREDICTION) {
- if (launcher == null || launcher.getHotseatPredictionController() == null) {
- return false;
- }
- HotseatPredictionController controller = launcher.getHotseatPredictionController();
- controller.pinPrediction(info);
- return true;
- }
- return false;
- }
-
- @Override
- public void getIconBounds(Rect outBounds) {
- super.getIconBounds(outBounds);
- if (!mIsPinned && !mIsDrawingDot) {
- int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
- outBounds.inset(predictionInset, predictionInset);
- }
- }
-
- private int getOutlineOffsetX() {
- return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
- }
-
- private int getOutlineOffsetY() {
- return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
- }
-
- private void drawEffect(Canvas canvas, boolean isBadged) {
- // Don't draw ring effect if item is about to be dragged.
- if (mDrawForDrag) {
- return;
- }
- mRingPath.reset();
- getShape().addToPath(mRingPath, getOutlineOffsetX(), getOutlineOffsetY(),
- mNormalizedIconRadius);
- if (isBadged) {
- float outlineSize = mNormalizedIconRadius * RING_EFFECT_RATIO * 2;
- float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
- float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
- float badgeInset = mNormalizedIconRadius * 2 - badgeSize;
- getShape().addToPath(mRingPath, getOutlineOffsetX() + badgeInset,
- getOutlineOffsetY() + badgeInset, badgeSize / 2);
-
- }
- mIconRingPaint.setColor(RING_SHADOW_COLOR);
- mIconRingPaint.setMaskFilter(mShadowFilter);
- canvas.drawPath(mRingPath, mIconRingPaint);
- mIconRingPaint.setColor(mPlateColor);
- mIconRingPaint.setMaskFilter(null);
- canvas.drawPath(mRingPath, mIconRingPaint);
- }
-
- @Override
- public void getSourceVisualDragBounds(Rect bounds) {
- super.getSourceVisualDragBounds(bounds);
- if (!mIsPinned) {
- int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO);
- bounds.inset(internalSize, internalSize);
- }
- }
-
- @Override
- public SafeCloseable prepareDrawDragView() {
- mDrawForDrag = true;
- invalidate();
- SafeCloseable r = super.prepareDrawDragView();
- return () -> {
- r.close();
- mDrawForDrag = false;
- };
- }
-
- /**
- * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
- */
- public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
- PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
- .inflate(R.layout.predicted_app_icon, parent, false);
- icon.applyFromWorkspaceItem(info);
- icon.setOnClickListener(ItemClickHandler.INSTANCE);
- icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler());
- return icon;
- }
-
- /**
- * Draws Predicted Icon outline on cell layout
- */
- public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
-
- private int mOffsetX;
- private int mOffsetY;
- private int mIconRadius;
- private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
- public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
- mDelegateCellX = cellX;
- mDelegateCellY = cellY;
- mOffsetX = icon.getOutlineOffsetX();
- mOffsetY = icon.getOutlineOffsetY();
- mIconRadius = icon.mNormalizedIconRadius;
- mOutlinePaint.setStyle(Paint.Style.FILL);
- mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
- }
-
- /**
- * Draws predicted app icon outline under CellLayout
- */
- @Override
- public void drawUnderItem(Canvas canvas) {
- getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
- }
-
- /**
- * Draws PredictedAppIcon outline over CellLayout
- */
- @Override
- public void drawOverItem(Canvas canvas) {
- // Does nothing
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
deleted file mode 100644
index 9a03d92..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
-import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
-import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
-import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
-import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-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.anim.AnimatorPlaybackController;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.folder.Folder;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
-import com.android.launcher3.logging.InstanceId;
-import com.android.launcher3.logging.StatsLogManager.StatsLogger;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
-import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
-import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.OverviewToAllAppsTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.util.UiThreadHelper;
-import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.OptionalInt;
-import java.util.stream.Stream;
-
-public class QuickstepLauncher extends BaseQuickstepLauncher {
-
- public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
- /**
- * Reusable command for applying the shelf height on the background thread.
- */
- public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
- SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.createPredictor();
- }
- }
-
- @Override
- protected void setupViews() {
- super.setupViews();
- if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
- mHotseatPredictionController = new HotseatPredictionController(this);
- }
- }
-
- @Override
- protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
- StatsLogger logger = getStatsLogManager()
- .logger().withItemInfo(info).withInstanceId(instanceId);
- OptionalInt allAppsRank = PredictionUiStateManager.INSTANCE.get(this).getAllAppsRank(info);
- allAppsRank.ifPresent(logger::withRank);
- logger.log(LAUNCHER_APP_LAUNCH_TAP);
-
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
- }
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- onStateOrResumeChanging(false /* inTransition */);
- }
-
- @Override
- public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
- @Nullable String sourceContainer) {
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.setPauseUIUpdate(true);
- }
- return super.startActivitySafely(v, intent, item, sourceContainer);
- }
-
- @Override
- protected void onActivityFlagsChanged(int changeBits) {
- super.onActivityFlagsChanged(changeBits);
- if ((changeBits & (ACTIVITY_STATE_DEFERRED_RESUMED | ACTIVITY_STATE_STARTED
- | ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
- onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
- }
-
- if (mHotseatPredictionController != null && ((changeBits & ACTIVITY_STATE_STARTED) != 0
- || (changeBits & getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
- mHotseatPredictionController.setPauseUIUpdate(false);
- }
- }
-
- @Override
- public void folderCreatedFromItem(Folder folder, WorkspaceItemInfo itemInfo) {
- super.folderCreatedFromItem(folder, itemInfo);
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.folderCreatedFromWorkspaceItem(itemInfo, folder.getInfo());
- }
- }
-
- @Override
- public void folderConvertedToItem(Folder folder, WorkspaceItemInfo itemInfo) {
- super.folderConvertedToItem(folder, itemInfo);
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.folderConvertedToWorkspaceItem(itemInfo, folder.getInfo());
- }
- }
-
- @Override
- public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
- if (mHotseatPredictionController != null) {
- return Stream.concat(super.getSupportedShortcuts(),
- Stream.of(mHotseatPredictionController));
- } else {
- return super.getSupportedShortcuts();
- }
- }
-
- /**
- * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
- */
- private void onStateOrResumeChanging(boolean inTransition) {
- LauncherState state = getStateManager().getState();
- DeviceProfile profile = getDeviceProfile();
- boolean willUserBeActive = (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
- boolean visible = (state == NORMAL || state == OVERVIEW)
- && (willUserBeActive || isUserActive())
- && !profile.isVerticalBarLayout()
- && profile.isPhone && !profile.isLandscape;
- UiThreadHelper.runAsyncCommand(this, SET_SHELF_HEIGHT, visible ? 1 : 0,
- profile.hotseatBarSizePx);
- if (state == NORMAL && !inTransition) {
- ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
- }
- }
-
- @Override
- public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) {
- super.bindPredictedItems(appInfos, ranks);
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.showCachedItems(appInfos, ranks);
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.destroy();
- mHotseatPredictionController = null;
- }
- }
-
- @Override
- public void onStateSetEnd(LauncherState state) {
- super.onStateSetEnd(state);
-
- switch (state.ordinal) {
- case HINT_STATE_ORDINAL: {
- Workspace workspace = getWorkspace();
- boolean willMoveScreens = workspace.getNextPage() != Workspace.DEFAULT_PAGE;
- getStateManager().goToState(NORMAL, true,
- willMoveScreens ? null : getScrimView()::startDragHandleEducationAnim);
- if (willMoveScreens) {
- workspace.post(workspace::moveToDefaultScreen);
- }
- break;
- }
- case OVERVIEW_STATE_ORDINAL: {
- RecentsView recentsView = getOverviewPanel();
- DiscoveryBounce.showForOverviewIfNeeded(this,
- recentsView.getPagedOrientationHandler());
- RecentsView rv = getOverviewPanel();
- sendCustomAccessibilityEvent(
- rv.getPageAt(rv.getCurrentPage()), TYPE_VIEW_FOCUSED, null);
- break;
- }
- case QUICK_SWITCH_STATE_ORDINAL: {
- RecentsView rv = getOverviewPanel();
- TaskView tasktolaunch = rv.getTaskViewAt(0);
- if (tasktolaunch != null) {
- tasktolaunch.launchTask(false, success -> {
- if (!success) {
- getStateManager().goToState(OVERVIEW);
- tasktolaunch.notifyTaskLaunchFailed(TAG);
- } else {
- getStateManager().moveToRestState();
- }
- }, MAIN_EXECUTOR.getHandler());
- } else {
- getStateManager().goToState(NORMAL);
- }
- break;
- }
-
- }
- }
-
- @Override
- public TouchController[] createTouchControllers() {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.1");
- }
- Mode mode = SysUINavigationMode.getMode(this);
-
- ArrayList<TouchController> list = new ArrayList<>();
- list.add(getDragController());
- if (mode == NO_BUTTON) {
- list.add(new NoButtonQuickSwitchTouchController(this));
- list.add(new NavBarToHomeTouchController(this));
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.2");
- }
- if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.3");
- }
- list.add(new NoButtonNavbarToOverviewTouchController(this));
- } else {
- list.add(new FlingAndHoldTouchController(this));
- }
- } else {
- if (getDeviceProfile().isVerticalBarLayout()) {
- list.add(new OverviewToAllAppsTouchController(this));
- list.add(new LandscapeEdgeSwipeController(this));
- if (mode.hasGestures) {
- list.add(new TransposedQuickSwitchTouchController(this));
- }
- } else {
- list.add(new PortraitStatesTouchController(this,
- mode.hasGestures /* allowDragToOverview */));
- if (mode.hasGestures) {
- list.add(new QuickSwitchTouchController(this));
- }
- }
- }
-
- if (!getDeviceProfile().isMultiWindowMode) {
- list.add(new StatusBarTouchController(this));
- }
-
- list.add(new LauncherTaskViewController(this));
- return list.toArray(new TouchController[list.size()]);
- }
-
- @Override
- public AtomicAnimationFactory createAtomicAnimationFactory() {
- return new QuickstepAtomicAnimationFactory(this);
- }
-
- private static final class LauncherTaskViewController extends
- TaskViewTouchController<Launcher> {
-
- LauncherTaskViewController(Launcher activity) {
- super(activity);
- }
-
- @Override
- protected boolean isRecentsInteractive() {
- return mActivity.isInState(OVERVIEW) || mActivity.isInState(OVERVIEW_MODAL_TASK);
- }
-
- @Override
- protected boolean isRecentsModal() {
- return mActivity.isInState(OVERVIEW_MODAL_TASK);
- }
-
- @Override
- protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
- mActivity.getStateManager().setCurrentUserControlledAnimation(animController);
- }
- }
-
- @Override
- public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
- super.dump(prefix, fd, writer, args);
- RecentsView recentsView = getOverviewPanel();
- writer.println("\nQuickstepLauncher:");
- writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" :
- recentsView.getPagedViewOrientedState()));
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
deleted file mode 100644
index 085b9b3..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
-import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
-import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
-
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.util.FloatProperty;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.quickstep.views.ClearAllButton;
-import com.android.quickstep.views.LauncherRecentsView;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
- * the basic view properties, this class also manages changes in the task visuals.
- */
-@TargetApi(Build.VERSION_CODES.O)
-public final class RecentsViewStateController extends
- BaseRecentsViewStateController<LauncherRecentsView> {
-
- public RecentsViewStateController(BaseQuickstepLauncher launcher) {
- super(launcher);
- }
-
- @Override
- public void setState(@NonNull LauncherState state) {
- super.setState(state);
- if (state.overviewUi) {
- mRecentsView.updateEmptyMessage();
- mRecentsView.resetTaskVisuals();
- }
- setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state);
- mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
- }
-
- @Override
- void setStateWithAnimationInternal(@NonNull LauncherState toState,
- @NonNull StateAnimationConfig config, @NonNull PendingAnimation builder) {
- super.setStateWithAnimationInternal(toState, config, builder);
-
- if (toState.overviewUi) {
- // While animating into recents, update the visible task data as needed
- builder.addOnFrameCallback(mRecentsView::loadVisibleTaskData);
- mRecentsView.updateEmptyMessage();
- } else {
- builder.addListener(
- AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
- }
-
- setAlphas(builder, toState);
- builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
- toState.getOverviewFullscreenProgress(), LINEAR);
- }
-
- private void setAlphas(PropertySetter propertySetter, LauncherState state) {
- float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
- propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
- buttonAlpha, LINEAR);
- propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
- MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
- }
-
- @Override
- FloatProperty<RecentsView> getTaskModalnessProperty() {
- return TASK_MODALNESS;
- }
-
- @Override
- FloatProperty<RecentsView> getContentAlphaProperty() {
- return CONTENT_ALPHA;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
deleted file mode 100644
index 8ff05f2..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.states;
-
-import android.content.Context;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * State indicating that the Launcher is behind an app
- */
-public class BackgroundAppState extends OverviewState {
-
- private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI
- | FLAG_WORKSPACE_INACCESSIBLE | FLAG_NON_INTERACTIVE | FLAG_CLOSE_POPUPS;
-
- public BackgroundAppState(int id) {
- this(id, LauncherLogProto.ContainerType.TASKSWITCHER);
- }
-
- protected BackgroundAppState(int id, int logContainer) {
- super(id, logContainer, STATE_FLAGS);
- }
-
- @Override
- public float getVerticalProgress(Launcher launcher) {
- if (launcher.getDeviceProfile().isVerticalBarLayout()) {
- return super.getVerticalProgress(launcher);
- }
- RecentsView recentsView = launcher.getOverviewPanel();
- int transitionLength = LayoutUtils.getShelfTrackingDistance(launcher,
- launcher.getDeviceProfile(),
- recentsView.getPagedOrientationHandler());
- AllAppsTransitionController controller = launcher.getAllAppsController();
- float scrollRange = Math.max(controller.getShiftRange(), 1);
- float progressDelta = (transitionLength / scrollRange);
- return super.getVerticalProgress(launcher) + progressDelta;
- }
-
- @Override
- public float[] getOverviewScaleAndOffset(Launcher launcher) {
- return getOverviewScaleAndOffsetForBackgroundState(launcher);
- }
-
- @Override
- public float getOverviewFullscreenProgress() {
- return 1;
- }
-
- @Override
- public int getVisibleElements(Launcher launcher) {
- return super.getVisibleElements(launcher)
- & ~OVERVIEW_BUTTONS & ~VERTICAL_SWIPE_INDICATOR;
- }
-
- @Override
- public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
- if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
- // Translate hotseat offscreen if we show it in overview.
- RecentsView recentsView = launcher.getOverviewPanel();
- ScaleAndTranslation scaleAndTranslation = super.getHotseatScaleAndTranslation(launcher);
- scaleAndTranslation.translationY += LayoutUtils.getShelfTrackingDistance(launcher,
- launcher.getDeviceProfile(),
- recentsView.getPagedOrientationHandler());
- return scaleAndTranslation;
- }
- return super.getHotseatScaleAndTranslation(launcher);
- }
-
- @Override
- protected float getDepthUnchecked(Context context) {
- return 1f;
- }
-
- public static float[] getOverviewScaleAndOffsetForBackgroundState(
- BaseDraggingActivity activity) {
- return new float[] {
- ((RecentsView) activity.getOverviewPanel()).getMaxScaleForFullScreen(),
- NO_OFFSET};
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
deleted file mode 100644
index fc0dcd5..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.states;
-
-import android.content.Context;
-import android.graphics.Rect;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * An Overview state that shows the current task in a modal fashion. Modal state is where the
- * current task is shown on its own without other tasks visible.
- */
-public class OverviewModalTaskState extends OverviewState {
-
- private static final int STATE_FLAGS =
- FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_WORKSPACE_INACCESSIBLE;
-
- public OverviewModalTaskState(int id) {
- super(id, ContainerType.OVERVIEW, STATE_FLAGS);
- }
-
- @Override
- public int getTransitionDuration(Context launcher) {
- return 300;
- }
-
- @Override
- public int getVisibleElements(Launcher launcher) {
- return OVERVIEW_BUTTONS;
- }
-
- @Override
- public float[] getOverviewScaleAndOffset(Launcher launcher) {
- return getOverviewScaleAndOffsetForModalState(launcher);
- }
-
- @Override
- public float getOverviewModalness() {
- return 1.0f;
- }
-
- @Override
- public void onBackPressed(Launcher launcher) {
- launcher.getStateManager().goToState(LauncherState.OVERVIEW);
- RecentsView recentsView = launcher.<RecentsView>getOverviewPanel();
- if (recentsView != null) {
- recentsView.resetModalVisuals();
- } else {
- super.onBackPressed(launcher);
- }
- }
-
- public static float[] getOverviewScaleAndOffsetForModalState(BaseDraggingActivity activity) {
- Rect out = new Rect();
- activity.<RecentsView>getOverviewPanel().getTaskSize(out);
- int taskHeight = out.height();
- activity.<RecentsView>getOverviewPanel().getModalTaskSize(out);
- int newHeight = out.height();
-
- float scale = (float) newHeight / taskHeight;
-
- return new float[] {scale, NO_OFFSET};
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
deleted file mode 100644
index fc9a11b..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.states;
-
-import com.android.launcher3.Launcher;
-
-public class OverviewPeekState extends OverviewState {
- private static final float OVERVIEW_OFFSET = 0.7f;
-
- public OverviewPeekState(int id) {
- super(id);
- }
-
- @Override
- public float[] getOverviewScaleAndOffset(Launcher launcher) {
- return new float[] {NO_SCALE, OVERVIEW_OFFSET};
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
deleted file mode 100644
index d174bfd..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.states;
-
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.view.View;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-
-/**
- * Definition for overview state
- */
-public class OverviewState extends LauncherState {
-
- protected static final Rect sTempRect = new Rect();
-
- private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED
- | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_WORKSPACE_INACCESSIBLE
- | FLAG_CLOSE_POPUPS;
-
- public OverviewState(int id) {
- this(id, STATE_FLAGS);
- }
-
- protected OverviewState(int id, int stateFlags) {
- this(id, ContainerType.TASKSWITCHER, stateFlags);
- }
-
- protected OverviewState(int id, int logContainer, int stateFlags) {
- super(id, logContainer, stateFlags);
- }
-
- @Override
- public int getTransitionDuration(Context context) {
- // In no-button mode, overview comes in all the way from the left, so give it more time.
- boolean isNoButtonMode = SysUINavigationMode.INSTANCE.get(context).getMode() == NO_BUTTON;
- return isNoButtonMode && ENABLE_OVERVIEW_ACTIONS.get() ? 380 : 250;
- }
-
- @Override
- public ScaleAndTranslation 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 ScaleAndTranslation(scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor);
- }
-
- @Override
- public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
- if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
- DeviceProfile dp = launcher.getDeviceProfile();
- if (dp.allAppsIconSizePx >= dp.iconSizePx) {
- return new ScaleAndTranslation(1, 0, 0);
- } else {
- float scale = ((float) dp.allAppsIconSizePx) / dp.iconSizePx;
- // Distance between the screen center (which is the pivotY for hotseat) and the
- // bottom of the hotseat (which we want to preserve)
- float distanceFromBottom = dp.heightPx / 2 - dp.hotseatBarBottomPaddingPx;
- // On scaling, the bottom edge is moved closer to the pivotY. We move the
- // hotseat back down so that the bottom edge's position is preserved.
- float translationY = distanceFromBottom * (1 - scale);
- return new ScaleAndTranslation(scale, 0, translationY);
- }
- }
- return getWorkspaceScaleAndTranslation(launcher);
- }
-
- @Override
- public float[] getOverviewScaleAndOffset(Launcher launcher) {
- return new float[] {NO_SCALE, NO_OFFSET};
- }
-
- @Override
- public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
- if (this == OVERVIEW && ENABLE_OVERVIEW_ACTIONS.get()
- && removeShelfFromOverview(launcher)) {
- // Treat the QSB as part of the hotseat so they move together.
- return getHotseatScaleAndTranslation(launcher);
- }
- return super.getQsbScaleAndTranslation(launcher);
- }
-
- @Override
- public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
- return new PageAlphaProvider(DEACCEL_2) {
- @Override
- public float getPageAlpha(int pageIndex) {
- return 0;
- }
- };
- }
-
- @Override
- public int getVisibleElements(Launcher launcher) {
- RecentsView recentsView = launcher.getOverviewPanel();
- if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(launcher) ||
- hideShelfInTwoButtonLandscape(launcher, recentsView.getPagedOrientationHandler())) {
- return OVERVIEW_BUTTONS;
- } else if (launcher.getDeviceProfile().isVerticalBarLayout()) {
- return VERTICAL_SWIPE_INDICATOR | OVERVIEW_BUTTONS;
- } else {
- boolean hasAllAppsHeaderExtra = launcher.getAppsView() != null
- && launcher.getAppsView().getFloatingHeaderView().hasVisibleContent();
- return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | OVERVIEW_BUTTONS
- | (hasAllAppsHeaderExtra ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
- }
- }
-
- @Override
- public float getOverviewScrimAlpha(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 getDefaultVerticalProgress(launcher);
- }
-
- public static float getDefaultVerticalProgress(Launcher launcher) {
- return 1 - (getDefaultSwipeHeight(launcher)
- / launcher.getAllAppsController().getShiftRange());
- }
-
- @Override
- public String getDescription(Launcher launcher) {
- return launcher.getString(R.string.accessibility_recent_apps);
- }
-
- public static float getDefaultSwipeHeight(Launcher launcher) {
- return LayoutUtils.getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
- }
-
- @Override
- protected float getDepthUnchecked(Context context) {
- return 1f;
- }
-
- @Override
- public void onBackPressed(Launcher launcher) {
- TaskView taskView = launcher.<RecentsView>getOverviewPanel().getRunningTaskView();
- if (taskView != null) {
- launcher.getUserEventDispatcher().logActionCommand(Action.Command.BACK,
- newContainerTarget(ContainerType.OVERVIEW));
- taskView.launchTask(true);
- } else {
- super.onBackPressed(launcher);
- }
- }
-
- public static OverviewState newBackgroundState(int id) {
- return new BackgroundAppState(id);
- }
-
- public static OverviewState newPeekState(int id) {
- return new OverviewPeekState(id);
- }
-
- public static OverviewState newSwitchState(int id) {
- return new QuickSwitchState(id);
- }
-
- /**
- * New Overview substate that represents the overview in modal mode (one task shown on its own)
- */
- public static OverviewState newModalTaskState(int id) {
- return new OverviewModalTaskState(id);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
deleted file mode 100644
index 2c7373e..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.states;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-/**
- * State to indicate we are about to launch a recent task. Note that this state is only used when
- * quick switching from launcher; quick switching from an app uses LauncherSwipeHandler.
- * @see com.android.quickstep.GestureState.GestureEndTarget#NEW_TASK
- */
-public class QuickSwitchState extends BackgroundAppState {
-
- public QuickSwitchState(int id) {
- super(id, LauncherLogProto.ContainerType.APP);
- }
-
- @Override
- public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
- float shiftRange = launcher.getAllAppsController().getShiftRange();
- float shiftProgress = getVerticalProgress(launcher) - NORMAL.getVerticalProgress(launcher);
- float translationY = shiftProgress * shiftRange;
- return new ScaleAndTranslation(1, 0, translationY);
- }
-
- @Override
- public int getVisibleElements(Launcher launcher) {
- return NONE;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
deleted file mode 100644
index a0af797..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.states;
-
-import static android.view.View.VISIBLE;
-
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.HINT_STATE;
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
-import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCRIM_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.Hotseat;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.RecentsAtomicAnimationFactory;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * Animation factory for quickstep specific transitions
- */
-public class QuickstepAtomicAnimationFactory extends
- RecentsAtomicAnimationFactory<Launcher, LauncherState> {
-
- // Scale recents takes before animating in
- private static final float RECENTS_PREPARE_SCALE = 1.33f;
-
- public static final int INDEX_SHELF_ANIM = RecentsAtomicAnimationFactory.NEXT_INDEX + 0;
- public static final int INDEX_PAUSE_TO_OVERVIEW_ANIM =
- RecentsAtomicAnimationFactory.NEXT_INDEX + 1;
-
- private static final int MY_ANIM_COUNT = 2;
- protected static final int NEXT_INDEX = RecentsAtomicAnimationFactory.NEXT_INDEX
- + MY_ANIM_COUNT;
-
- // Due to use of physics, duration may differ between devices so we need to calculate and
- // cache the value.
- private int mHintToNormalDuration = -1;
-
- public static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
-
- public QuickstepAtomicAnimationFactory(QuickstepLauncher activity) {
- super(activity, MY_ANIM_COUNT);
- }
-
- @Override
- public Animator createStateElementAnimation(int index, float... values) {
- switch (index) {
- case INDEX_SHELF_ANIM: {
- AllAppsTransitionController aatc = mActivity.getAllAppsController();
- Animator springAnim = aatc.createSpringAnimation(values);
-
- if ((OVERVIEW.getVisibleElements(mActivity) & HOTSEAT_ICONS) != 0) {
- // Translate hotseat with the shelf until reaching overview.
- float overviewProgress = OVERVIEW.getVerticalProgress(mActivity);
- ScaleAndTranslation sat = OVERVIEW.getHotseatScaleAndTranslation(mActivity);
- float shiftRange = aatc.getShiftRange();
- if (values.length == 1) {
- values = new float[] {aatc.getProgress(), values[0]};
- }
- ValueAnimator hotseatAnim = ValueAnimator.ofFloat(values);
- hotseatAnim.addUpdateListener(anim -> {
- float progress = (Float) anim.getAnimatedValue();
- if (progress >= overviewProgress || mActivity.isInState(BACKGROUND_APP)) {
- float hotseatShift = (progress - overviewProgress) * shiftRange;
- mActivity.getHotseat().setTranslationY(hotseatShift + sat.translationY);
- }
- });
- hotseatAnim.setInterpolator(LINEAR);
- hotseatAnim.setDuration(springAnim.getDuration());
-
- AnimatorSet anim = new AnimatorSet();
- anim.play(hotseatAnim);
- anim.play(springAnim);
- return anim;
- }
-
- return springAnim;
- }
- case INDEX_PAUSE_TO_OVERVIEW_ANIM: {
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
-
- config.setInterpolator(ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
- config.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_3);
- if ((OVERVIEW.getVisibleElements(mActivity) & HOTSEAT_ICONS) != 0) {
- config.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
- config.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
- }
-
- StateManager<LauncherState> stateManager = mActivity.getStateManager();
- return stateManager.createAtomicAnimation(
- stateManager.getCurrentStableState(), OVERVIEW, config);
- }
- default:
- return super.createStateElementAnimation(index, values);
- }
- }
-
- @Override
- public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
- StateAnimationConfig config) {
- if (toState == NORMAL && fromState == OVERVIEW) {
- config.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL);
- config.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL);
- config.setInterpolator(ANIM_ALL_APPS_FADE, ACCEL);
- config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
- config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL);
- config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
- Workspace workspace = mActivity.getWorkspace();
-
- // Start from a higher workspace scale, but only if we're invisible so we don't jump.
- boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE;
- if (isWorkspaceVisible) {
- CellLayout currentChild = (CellLayout) workspace.getChildAt(
- workspace.getCurrentPage());
- isWorkspaceVisible = currentChild.getVisibility() == VISIBLE
- && currentChild.getShortcutsAndWidgets().getAlpha() > 0;
- }
- if (!isWorkspaceVisible) {
- workspace.setScaleX(0.92f);
- workspace.setScaleY(0.92f);
- }
- Hotseat hotseat = mActivity.getHotseat();
- boolean isHotseatVisible = hotseat.getVisibility() == VISIBLE && hotseat.getAlpha() > 0;
- if (!isHotseatVisible) {
- hotseat.setScaleX(0.92f);
- hotseat.setScaleY(0.92f);
- if (ENABLE_OVERVIEW_ACTIONS.get()) {
- AllAppsContainerView qsbContainer = mActivity.getAppsView();
- View qsb = qsbContainer.getSearchView();
- boolean qsbVisible = qsb.getVisibility() == VISIBLE && qsb.getAlpha() > 0;
- if (!qsbVisible) {
- qsbContainer.setScaleX(0.92f);
- qsbContainer.setScaleY(0.92f);
- }
- }
- }
- } else if (toState == NORMAL && fromState == OVERVIEW_PEEK) {
- // Keep fully visible until the very end (when overview is offscreen) to make invisible.
- config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME);
- } else if (toState == OVERVIEW_PEEK && fromState == NORMAL) {
- config.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
- config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
- config.setInterpolator(ANIM_OVERVIEW_SCRIM_FADE, FAST_OUT_SLOW_IN);
- } else if ((fromState == NORMAL || fromState == HINT_STATE) && toState == OVERVIEW) {
- if (SysUINavigationMode.getMode(mActivity) == NO_BUTTON) {
- config.setInterpolator(ANIM_WORKSPACE_SCALE,
- fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
- config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
- } else {
- config.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
-
- // Scale up the recents, if it is not coming from the side
- RecentsView overview = mActivity.getOverviewPanel();
- if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
- SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
- }
- }
- config.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
- config.setInterpolator(ANIM_ALL_APPS_FADE, OVERSHOOT_1_2);
- config.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
- config.setInterpolator(ANIM_DEPTH, OVERSHOOT_1_2);
- Interpolator translationInterpolator = ENABLE_OVERVIEW_ACTIONS.get()
- && removeShelfFromOverview(mActivity)
- ? OVERSHOOT_1_2
- : OVERSHOOT_1_7;
- config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator);
- config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, translationInterpolator);
- config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
- } else if (fromState == HINT_STATE && toState == NORMAL) {
- config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
- if (mHintToNormalDuration == -1) {
- ValueAnimator va = getSpringScaleAnimator(mActivity, mActivity.getWorkspace(),
- toState.getWorkspaceScaleAndTranslation(mActivity).scale);
- mHintToNormalDuration = (int) va.getDuration();
- }
- config.duration = Math.max(config.duration, mHintToNormalDuration);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
deleted file mode 100644
index fac478e..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ /dev/null
@@ -1,279 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-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.LauncherState.OVERVIEW_PEEK;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_HEADER_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_PAUSE_TO_OVERVIEW_ANIM;
-import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.MotionPauseDetector;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * Touch controller which handles swipe and hold to go to Overview
- */
-public class FlingAndHoldTouchController extends PortraitStatesTouchController {
-
- private static final long PEEK_IN_ANIM_DURATION = 240;
- private static final long PEEK_OUT_ANIM_DURATION = 100;
- private static final float MAX_DISPLACEMENT_PERCENT = 0.75f;
-
- protected final MotionPauseDetector mMotionPauseDetector;
- private final float mMotionPauseMinDisplacement;
- private final float mMotionPauseMaxDisplacement;
-
- private AnimatorSet mPeekAnim;
-
- public FlingAndHoldTouchController(Launcher l) {
- super(l, false /* allowDragToOverview */);
- mMotionPauseDetector = new MotionPauseDetector(l);
- mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
- mMotionPauseMaxDisplacement = getMotionPauseMaxDisplacement();
- }
-
- protected float getMotionPauseMaxDisplacement() {
- return getShiftRange() * MAX_DISPLACEMENT_PERCENT;
- }
-
- @Override
- protected long getAtomicDuration() {
- return QuickstepAtomicAnimationFactory.ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
- }
-
- @Override
- public void onDragStart(boolean start, float startDisplacement) {
- mMotionPauseDetector.clear();
-
- super.onDragStart(start, startDisplacement);
-
- if (handlingOverviewAnim()) {
- mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
- }
-
- if (mAtomicAnim != null) {
- mAtomicAnim.cancel();
- }
- }
-
- protected void onMotionPauseChanged(boolean isPaused) {
- RecentsView recentsView = mLauncher.getOverviewPanel();
- recentsView.setOverviewStateEnabled(isPaused);
- if (mPeekAnim != null) {
- mPeekAnim.cancel();
- }
- LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK;
- LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL;
- long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION;
-
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = peekDuration;
- config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
- mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(
- fromState, toState, config);
- mPeekAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mPeekAnim = null;
- }
- });
- mPeekAnim.start();
- VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
-
- mLauncher.getDragLayer().getScrim().createSysuiMultiplierAnim(isPaused ? 0 : 1)
- .setDuration(peekDuration).start();
- }
-
- /**
- * @return Whether we are handling the overview animation, rather than
- * having it as part of the existing animation to the target state.
- */
- protected boolean handlingOverviewAnim() {
- int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
- return mStartState == NORMAL && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
- }
-
- @Override
- protected StateAnimationConfig getConfigForStates(
- LauncherState fromState, LauncherState toState) {
- if (fromState == NORMAL && toState == ALL_APPS) {
- StateAnimationConfig builder = new StateAnimationConfig();
- // Fade in prediction icons quickly, then rest of all apps after reaching overview.
- float progressToReachOverview = NORMAL.getVerticalProgress(mLauncher)
- - OVERVIEW.getVerticalProgress(mLauncher);
- builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(
- ACCEL,
- 0,
- ALL_APPS_CONTENT_FADE_THRESHOLD));
- builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(
- ACCEL,
- progressToReachOverview,
- progressToReachOverview + ALL_APPS_CONTENT_FADE_THRESHOLD));
-
- // Get workspace out of the way quickly, to prepare for potential pause.
- builder.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL_3);
- builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, DEACCEL_3);
- builder.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_3);
- return builder;
- } else if (fromState == ALL_APPS && toState == NORMAL) {
- StateAnimationConfig builder = new StateAnimationConfig();
- // Keep all apps/predictions opaque until the very end of the transition.
- float progressToReachOverview = OVERVIEW.getVerticalProgress(mLauncher);
- builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(
- DEACCEL,
- progressToReachOverview - ALL_APPS_CONTENT_FADE_THRESHOLD,
- progressToReachOverview));
- builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(
- DEACCEL,
- 1 - ALL_APPS_CONTENT_FADE_THRESHOLD,
- 1));
- return builder;
- }
- return super.getConfigForStates(fromState, toState);
- }
-
- @Override
- public boolean onDrag(float displacement, MotionEvent event) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "FlingAndHoldTouchController");
- }
- float upDisplacement = -displacement;
- mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim()
- || upDisplacement < mMotionPauseMinDisplacement
- || upDisplacement > mMotionPauseMaxDisplacement);
- mMotionPauseDetector.addPosition(event);
- return super.onDrag(displacement, event);
- }
-
- @Override
- public void onDragEnd(float velocity) {
- if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) {
- goToOverviewOnDragEnd(velocity);
- } else {
- super.onDragEnd(velocity);
- }
-
- View searchView = mLauncher.getAppsView().getSearchView();
- if (searchView instanceof FeedbackHandler) {
- ((FeedbackHandler) searchView).resetFeedback();
- }
- mMotionPauseDetector.clear();
- }
-
- protected void goToOverviewOnDragEnd(float velocity) {
- if (mPeekAnim != null) {
- mPeekAnim.cancel();
- }
-
- Animator overviewAnim = mLauncher.createAtomicAnimationFactory()
- .createStateElementAnimation(INDEX_PAUSE_TO_OVERVIEW_ANIM);
- mAtomicAnim = new AnimatorSet();
- mAtomicAnim.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- if (mCancelled) {
- StateAnimationConfig config = new StateAnimationConfig();
- config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
- config.duration = PEEK_OUT_ANIM_DURATION;
- mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(
- mFromState, mToState, config);
- mPeekAnim.start();
- }
- mAtomicAnim = null;
- }
- });
- mAtomicAnim.play(overviewAnim);
- mAtomicAnim.start();
- }
-
- @Override
- protected void goToTargetState(LauncherState targetState, int logAction) {
- if (mPeekAnim != null && mPeekAnim.isStarted()) {
- // Don't jump to the target state until overview is no longer peeking.
- mPeekAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- FlingAndHoldTouchController.super.goToTargetState(targetState, logAction);
- }
- });
- } else {
- super.goToTargetState(targetState, logAction);
- }
- }
-
- @Override
- @AnimationFlags
- protected int updateAnimComponentsOnReinit(@AnimationFlags int animComponents) {
- if (handlingOverviewAnim()) {
- // We don't want the state transition to all apps to animate overview,
- // as that will cause a jump after our atomic animation.
- return animComponents | SKIP_OVERVIEW;
- } else {
- return animComponents;
- }
- }
-
- /**
- * Interface for views with feedback animation requiring reset
- */
- public interface FeedbackHandler {
-
- /**
- * reset searchWidget feedback
- */
- void resetFeedback();
- }
-
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
deleted file mode 100644
index c1a585e..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
-import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-
-import android.animation.ValueAnimator;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.TouchController;
-import com.android.quickstep.util.AssistantUtilities;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-/**
- * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
- */
-public class NavBarToHomeTouchController implements TouchController,
- SingleAxisSwipeDetector.Listener {
-
- private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
-
- private final Launcher mLauncher;
- private final SingleAxisSwipeDetector mSwipeDetector;
- private final float mPullbackDistance;
-
- private boolean mNoIntercept;
- private LauncherState mStartState;
- private LauncherState mEndState = NORMAL;
- private AnimatorPlaybackController mCurrentAnimation;
-
- public NavBarToHomeTouchController(Launcher launcher) {
- mLauncher = launcher;
- mSwipeDetector = new SingleAxisSwipeDetector(mLauncher, this,
- SingleAxisSwipeDetector.VERTICAL);
- mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
- }
-
- @Override
- public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mStartState = mLauncher.getStateManager().getState();
- mNoIntercept = !canInterceptTouch(ev);
- if (mNoIntercept) {
- return false;
- }
- mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_POSITIVE,
- false /* ignoreSlop */);
- }
-
- if (mNoIntercept) {
- return false;
- }
-
- onControllerTouchEvent(ev);
- return mSwipeDetector.isDraggingOrSettling();
- }
-
- private boolean canInterceptTouch(MotionEvent ev) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NavBarToHomeTouchController.canInterceptTouch "
- + ev);
- }
- boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
- if (!cameFromNavBar) {
- return false;
- }
- if (mStartState.overviewUi || mStartState == ALL_APPS) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED,
- "NavBarToHomeTouchController.canInterceptTouch true 1 "
- + mStartState.overviewUi + " " + (mStartState == ALL_APPS));
- }
- return true;
- }
- int typeToClose = ENABLE_ALL_APPS_EDU.get() ? TYPE_ALL & ~TYPE_ALL_APPS_EDU : TYPE_ALL;
- if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, typeToClose) != null) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED,
- "NavBarToHomeTouchController.canInterceptTouch true 2 "
- + AbstractFloatingView.getTopOpenView(mLauncher), new Exception());
- }
- return true;
- }
- if (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
- && AssistantUtilities.isExcludedAssistantRunning()) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED,
- "NavBarToHomeTouchController.canInterceptTouch true 3");
- }
- return true;
- }
- return false;
- }
-
- @Override
- public final boolean onControllerTouchEvent(MotionEvent ev) {
- return mSwipeDetector.onTouchEvent(ev);
- }
-
- private float getShiftRange() {
- return mLauncher.getDeviceProfile().heightPx;
- }
-
- @Override
- public void onDragStart(boolean start, float startDisplacement) {
- initCurrentAnimation();
- }
-
- private void initCurrentAnimation() {
- long accuracy = (long) (getShiftRange() * 2);
- final PendingAnimation builder = new PendingAnimation(accuracy);
- if (mStartState.overviewUi) {
- RecentsView recentsView = mLauncher.getOverviewPanel();
- builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
- -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- builder.addOnFrameCallback(
- () -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
- }
- } else if (mStartState == ALL_APPS) {
- AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
- builder.setFloat(allAppsController, ALL_APPS_PROGRESS,
- -mPullbackDistance / allAppsController.getShiftRange(), PULLBACK_INTERPOLATOR);
-
- // Slightly fade out all apps content to further distinguish from scrolling.
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = accuracy;
- config.setInterpolator(StateAnimationConfig.ANIM_ALL_APPS_FADE, Interpolators
- .mapToProgress(PULLBACK_INTERPOLATOR, 0, 0.5f));
-
- allAppsController.setAlphas(mEndState, config, builder);
- }
- AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mLauncher);
- if (topView != null) {
- topView.addHintCloseAnim(mPullbackDistance, PULLBACK_INTERPOLATOR, builder);
- }
- mCurrentAnimation = builder.createPlaybackController()
- .setOnCancelRunnable(this::clearState);
- }
-
- private void clearState() {
- mCurrentAnimation = null;
- mSwipeDetector.finishedScrolling();
- mSwipeDetector.setDetectableScrollConditions(0, false);
- }
-
- @Override
- public boolean onDrag(float displacement) {
- // Only allow swipe up.
- displacement = Math.min(0, displacement);
- float progress = Utilities.getProgress(displacement, 0, getShiftRange());
- mCurrentAnimation.setPlayFraction(progress);
- return true;
- }
-
- @Override
- public void onDragEnd(float velocity) {
- boolean fling = mSwipeDetector.isFling(velocity);
- final int logAction = fling ? Touch.FLING : Touch.SWIPE;
- float progress = mCurrentAnimation.getProgressFraction();
- float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress);
- boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
- || (velocity < 0 && fling);
- if (success) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- RecentsView recentsView = mLauncher.getOverviewPanel();
- recentsView.switchToScreenshot(null,
- () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
- }
- mLauncher.getStateManager().goToState(mEndState, true,
- () -> onSwipeInteractionCompleted(mEndState));
- if (mStartState != mEndState) {
- logStateChange(mStartState.containerType, logAction);
- }
- AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher);
- if (topOpenView != null) {
- AbstractFloatingView.closeAllOpenViews(mLauncher);
- logStateChange(topOpenView.getLogContainerType(), logAction);
- }
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
- } else {
- // Quickly return to the state we came from (we didn't move far).
- ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
- anim.setFloatValues(progress, 0);
- anim.addListener(AnimationSuccessListener.forRunnable(
- () -> onSwipeInteractionCompleted(mStartState)));
- anim.setDuration(80).start();
- }
- }
-
- private void onSwipeInteractionCompleted(LauncherState targetState) {
- clearState();
- mLauncher.getStateManager().goToState(targetState, false /* animated */);
- AccessibilityManagerCompat.sendStateEventToTest(mLauncher, targetState.ordinal);
- }
-
- private void logStateChange(int startContainerType, int logAction) {
- mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
- LauncherLogProto.Action.Direction.UP,
- mSwipeDetector.getDownX(), mSwipeDetector.getDownY(),
- LauncherLogProto.ContainerType.NAVBAR,
- startContainerType,
- mEndState.containerType,
- mLauncher.getWorkspace().getCurrentPage());
- mLauncher.getStatsLogManager().logger()
- .withSrcState(StatsLogManager.containerTypeToAtomState(mStartState.containerType))
- .withDstState(StatsLogManager.containerTypeToAtomState(mEndState.containerType))
- .log(LAUNCHER_HOME_GESTURE);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
deleted file mode 100644
index 9316938..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.LauncherState.HINT_STATE;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
-import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.graphics.PointF;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
-import com.android.quickstep.views.RecentsView;
-
-/**
- * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above
- * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the
- * first home screen instead of to Overview.
- */
-public class NoButtonNavbarToOverviewTouchController extends FlingAndHoldTouchController {
-
-
- // How much of the movement to use for translating overview after swipe and hold.
- private static final float OVERVIEW_MOVEMENT_FACTOR = 0.25f;
- private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
- private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
-
- private final RecentsView mRecentsView;
-
- private boolean mDidTouchStartInNavBar;
- private boolean mReachedOverview;
- private boolean mIsOverviewRehidden;
- private boolean mIsHomeStaggeredAnimFinished;
- // The last recorded displacement before we reached overview.
- private PointF mStartDisplacement = new PointF();
-
- // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
- private ObjectAnimator mNormalToHintOverviewScrimAnimator;
-
- public NoButtonNavbarToOverviewTouchController(Launcher l) {
- super(l);
- mRecentsView = l.getOverviewPanel();
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NoButtonNavbarToOverviewTouchController.ctor");
- }
- }
-
- @Override
- protected float getMotionPauseMaxDisplacement() {
- // No need to disallow pause when swiping up all the way up the screen (unlike
- // FlingAndHoldTouchController where user is probably intending to go to all apps).
- return Float.MAX_VALUE;
- }
-
- @Override
- protected boolean canInterceptTouch(MotionEvent ev) {
- mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
- return super.canInterceptTouch(ev);
- }
-
- @Override
- protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
- if (fromState == NORMAL && mDidTouchStartInNavBar) {
- return HINT_STATE;
- } else if (fromState == OVERVIEW && isDragTowardPositive) {
- // Don't allow swiping up to all apps.
- return OVERVIEW;
- }
- return super.getTargetState(fromState, isDragTowardPositive);
- }
-
- @Override
- protected float initCurrentAnimation(int animComponents) {
- float progressMultiplier = super.initCurrentAnimation(animComponents);
- if (mToState == HINT_STATE) {
- // Track the drag across the entire height of the screen.
- progressMultiplier = -1 / getShiftRange();
- }
- return progressMultiplier;
- }
-
- @Override
- public void onDragStart(boolean start, float startDisplacement) {
- super.onDragStart(start, startDisplacement);
- if (mFromState == NORMAL && mToState == HINT_STATE) {
- mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofFloat(
- mLauncher.getDragLayer().getOverviewScrim(),
- OverviewScrim.SCRIM_PROGRESS,
- mFromState.getOverviewScrimAlpha(mLauncher),
- mToState.getOverviewScrimAlpha(mLauncher));
- }
- mReachedOverview = false;
- }
-
- @Override
- protected void updateProgress(float fraction) {
- super.updateProgress(fraction);
- if (mNormalToHintOverviewScrimAnimator != null) {
- mNormalToHintOverviewScrimAnimator.setCurrentFraction(fraction);
- }
- }
-
- @Override
- public void onDragEnd(float velocity) {
- super.onDragEnd(velocity);
- mNormalToHintOverviewScrimAnimator = null;
- }
-
- @Override
- protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
- LauncherState targetState, float velocity, boolean isFling) {
- super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity,
- isFling);
- if (targetState == HINT_STATE) {
- // Normally we compute the duration based on the velocity and distance to the given
- // state, but since the hint state tracks the entire screen without a clear endpoint, we
- // need to manually set the duration to a reasonable value.
- animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher));
- }
- }
-
- @Override
- protected void onMotionPauseChanged(boolean isPaused) {
- if (mCurrentAnimation == null) {
- return;
- }
- mNormalToHintOverviewScrimAnimator = null;
- mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> {
- mLauncher.getStateManager().goToState(OVERVIEW, true, () -> {
- mReachedOverview = true;
- maybeSwipeInteractionToOverviewComplete();
- });
- });
- VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
- }
-
- private void maybeSwipeInteractionToOverviewComplete() {
- if (mReachedOverview && mDetector.isSettlingState()) {
- onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
- }
- }
-
- // Used if flinging back to home after reaching overview
- private void maybeSwipeInteractionToHomeComplete() {
- if (mIsHomeStaggeredAnimFinished && mIsOverviewRehidden) {
- onSwipeInteractionCompleted(NORMAL, Touch.FLING);
- }
- }
-
- @Override
- protected boolean handlingOverviewAnim() {
- return mDidTouchStartInNavBar && super.handlingOverviewAnim();
- }
-
- @Override
- public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NoButtonNavbarToOverviewTouchController");
- }
- if (mMotionPauseDetector.isPaused()) {
- if (!mReachedOverview) {
- mStartDisplacement.set(xDisplacement, yDisplacement);
- } else {
- mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
- * OVERVIEW_MOVEMENT_FACTOR);
- mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
- * OVERVIEW_MOVEMENT_FACTOR);
- }
- // Stay in Overview.
- return true;
- }
- return super.onDrag(yDisplacement, xDisplacement, event);
- }
-
- @Override
- protected void goToOverviewOnDragEnd(float velocity) {
- float velocityDp = dpiFromPx(velocity);
- boolean isFling = Math.abs(velocityDp) > 1;
- StateManager<LauncherState> stateManager = mLauncher.getStateManager();
- boolean goToHomeInsteadOfOverview = isFling;
- if (goToHomeInsteadOfOverview) {
- if (velocity > 0) {
- stateManager.goToState(NORMAL, true,
- () -> onSwipeInteractionCompleted(NORMAL, Touch.FLING));
- } else {
- mIsHomeStaggeredAnimFinished = mIsOverviewRehidden = false;
-
- StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
- mLauncher, velocity, false /* animateOverviewScrim */);
- staggeredWorkspaceAnim.addAnimatorListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- mIsHomeStaggeredAnimFinished = true;
- maybeSwipeInteractionToHomeComplete();
- }
- }).start();
-
- // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here.
- stateManager.cancelAnimation();
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = OVERVIEW.getTransitionDuration(mLauncher);
- config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
- AnimatorSet anim = stateManager.createAtomicAnimation(
- stateManager.getState(), NORMAL, config);
- anim.addListener(AnimationSuccessListener.forRunnable(() -> {
- mIsOverviewRehidden = true;
- maybeSwipeInteractionToHomeComplete();
- }));
- anim.start();
- }
- }
- if (mReachedOverview) {
- float distanceDp = dpiFromPx(Math.max(
- Math.abs(mRecentsView.getTranslationX()),
- Math.abs(mRecentsView.getTranslationY())));
- long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
- distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
- mRecentsView.animate()
- .translationX(0)
- .translationY(0)
- .setInterpolator(ACCEL_DEACCEL)
- .setDuration(duration)
- .withEndAction(goToHomeInsteadOfOverview
- ? null
- : this::maybeSwipeInteractionToOverviewComplete);
- }
- }
-
- private float dpiFromPx(float pixels) {
- return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics());
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
deleted file mode 100644
index 1b439d1..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ /dev/null
@@ -1,474 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
-import static com.android.launcher3.LauncherState.QUICK_SWITCH;
-import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
-import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_5;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
-import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
-import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
-import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
-import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_PAUSE_TO_OVERVIEW_ANIM;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
-import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.CANCEL;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
-import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.graphics.PointF;
-import android.view.MotionEvent;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.touch.BaseSwipeDetector;
-import com.android.launcher3.touch.BothAxesSwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.MotionPauseDetector;
-import com.android.quickstep.util.ShelfPeekAnim;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
-import com.android.quickstep.views.LauncherRecentsView;
-
-/**
- * Handles quick switching to a recent task from the home screen. To give as much flexibility to
- * the user as possible, also handles swipe up and hold to go to overview and swiping back home.
- */
-public class NoButtonQuickSwitchTouchController implements TouchController,
- BothAxesSwipeDetector.Listener, MotionPauseDetector.OnMotionPauseListener {
-
- /** The minimum progress of the scale/translationY animation until drag end. */
- private static final float Y_ANIM_MIN_PROGRESS = 0.15f;
- private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_5;
- private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
- private static final Interpolator SCALE_DOWN_INTERPOLATOR = DEACCEL;
-
- private final BaseQuickstepLauncher mLauncher;
- private final BothAxesSwipeDetector mSwipeDetector;
- private final ShelfPeekAnim mShelfPeekAnim;
- private final float mXRange;
- private final float mYRange;
- private final MotionPauseDetector mMotionPauseDetector;
- private final float mMotionPauseMinDisplacement;
- private final LauncherRecentsView mRecentsView;
-
- private boolean mNoIntercept;
- private LauncherState mStartState;
-
- private boolean mIsHomeScreenVisible = true;
-
- // As we drag, we control 3 animations: one to get non-overview components out of the way,
- // and the other two to set overview properties based on x and y progress.
- private AnimatorPlaybackController mNonOverviewAnim;
- private AnimatorPlaybackController mXOverviewAnim;
- private AnimatorPlaybackController mYOverviewAnim;
-
- public NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher) {
- mLauncher = launcher;
- mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
- mShelfPeekAnim = mLauncher.getShelfPeekAnim();
- mRecentsView = mLauncher.getOverviewPanel();
- mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
- mYRange = LayoutUtils.getShelfTrackingDistance(
- mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
- mMotionPauseDetector = new MotionPauseDetector(mLauncher);
- mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
- R.dimen.motion_pause_detector_min_displacement_from_app);
- }
-
- @Override
- public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mNoIntercept = !canInterceptTouch(ev);
- if (mNoIntercept) {
- return false;
- }
-
- // Only detect horizontal swipe for intercept, then we will allow swipe up as well.
- mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
- false /* ignoreSlopWhenSettling */);
- }
-
- if (mNoIntercept) {
- return false;
- }
-
- onControllerTouchEvent(ev);
- return mSwipeDetector.isDraggingOrSettling();
- }
-
- @Override
- public boolean onControllerTouchEvent(MotionEvent ev) {
- return mSwipeDetector.onTouchEvent(ev);
- }
-
- private boolean canInterceptTouch(MotionEvent ev) {
- if (!mLauncher.isInState(LauncherState.NORMAL)) {
- return false;
- }
- if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
- return false;
- }
- int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
- if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
- return false;
- }
- return true;
- }
-
- @Override
- public void onDragStart(boolean start) {
- mMotionPauseDetector.clear();
- if (start) {
- mStartState = mLauncher.getStateManager().getState();
-
- mMotionPauseDetector.setOnMotionPauseListener(this);
-
- // We have detected horizontal drag start, now allow swipe up as well.
- mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP,
- false /* ignoreSlopWhenSettling */);
-
- setupAnimators();
- }
- }
-
- @Override
- public void onMotionPauseChanged(boolean isPaused) {
- VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
-
- if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
- return;
- }
-
- ShelfAnimState shelfState = isPaused ? PEEK : HIDE;
- if (shelfState == PEEK) {
- // Some shelf elements (e.g. qsb) were hidden, but we need them visible when peeking.
- AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
- allAppsController.setAlphas(
- NORMAL, new StateAnimationConfig(), NO_ANIM_PROPERTY_SETTER);
-
- if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
- // Hotseat was hidden, but we need it visible when peeking.
- mLauncher.getHotseat().setAlpha(1);
- }
- }
- mShelfPeekAnim.setShelfState(shelfState, ShelfPeekAnim.INTERPOLATOR,
- ShelfPeekAnim.DURATION);
- }
-
- private void setupAnimators() {
- // Animate the non-overview components (e.g. workspace, shelf) out of the way.
- StateAnimationConfig nonOverviewBuilder = new StateAnimationConfig();
- nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR);
- nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR);
- nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, TRANSLATE_OUT_INTERPOLATOR);
- nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR);
- updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder);
- mNonOverviewAnim.dispatchOnStart();
-
- if (mRecentsView.getTaskViewCount() == 0) {
- mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
- if (!isEmpty && mSwipeDetector.isDraggingState()) {
- // We have loaded tasks, update the animators to start at the correct scale etc.
- setupOverviewAnimators();
- }
- });
- }
-
- setupOverviewAnimators();
- }
-
- /** Create state animation to control non-overview components. */
- private void updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config) {
- config.duration = (long) (Math.max(mXRange, mYRange) * 2);
- config.animFlags = config.animFlags | SKIP_OVERVIEW;
- mNonOverviewAnim = mLauncher.getStateManager()
- .createAnimationToNewWorkspace(toState, config)
- .setOnCancelRunnable(this::clearState);
- }
-
- private void setupOverviewAnimators() {
- final LauncherState fromState = QUICK_SWITCH;
- final LauncherState toState = OVERVIEW;
-
- // Set RecentView's initial properties.
- SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
- ADJACENT_PAGE_OFFSET.set(mRecentsView, 1f);
- mRecentsView.setContentAlpha(1);
- mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
- mLauncher.getActionsView().getVisibilityAlpha().setValue(
- (fromState.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1f : 0f);
-
- float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
- // As we drag right, animate the following properties:
- // - RecentsView translationX
- // - OverviewScrim
- PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
- xAnim.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1], LINEAR);
- xAnim.setFloat(mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
- toState.getOverviewScrimAlpha(mLauncher), LINEAR);
- mXOverviewAnim = xAnim.createPlaybackController();
- mXOverviewAnim.dispatchOnStart();
-
- // As we drag up, animate the following properties:
- // - RecentsView scale
- // - RecentsView fullscreenProgress
- PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
- yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
- yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
- toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
- mYOverviewAnim = yAnim.createPlaybackController();
- mYOverviewAnim.dispatchOnStart();
- }
-
- @Override
- public boolean onDrag(PointF displacement, MotionEvent ev) {
- float xProgress = Math.max(0, displacement.x) / mXRange;
- float yProgress = Math.max(0, -displacement.y) / mYRange;
- yProgress = Utilities.mapRange(yProgress, Y_ANIM_MIN_PROGRESS, 1f);
-
- boolean wasHomeScreenVisible = mIsHomeScreenVisible;
- if (wasHomeScreenVisible && mNonOverviewAnim != null) {
- mNonOverviewAnim.setPlayFraction(xProgress);
- }
- mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress)
- <= 1 - ALPHA_CUTOFF_THRESHOLD;
-
- if (wasHomeScreenVisible && !mIsHomeScreenVisible) {
- // Get the shelf all the way offscreen so it pops up when we decide to peek it.
- mShelfPeekAnim.setShelfState(HIDE, LINEAR, 0);
- }
-
- // Only allow motion pause if the home screen is invisible, since some
- // home screen elements will appear in the shelf on motion pause.
- mMotionPauseDetector.setDisallowPause(mIsHomeScreenVisible
- || -displacement.y < mMotionPauseMinDisplacement);
- mMotionPauseDetector.addPosition(ev);
-
- if (mIsHomeScreenVisible) {
- // Cancel the shelf anim so it doesn't clobber mNonOverviewAnim.
- mShelfPeekAnim.setShelfState(CANCEL, LINEAR, 0);
- }
-
- if (mXOverviewAnim != null) {
- mXOverviewAnim.setPlayFraction(xProgress);
- }
- if (mYOverviewAnim != null) {
- mYOverviewAnim.setPlayFraction(yProgress);
- }
- return true;
- }
-
- @Override
- public void onDragEnd(PointF velocity) {
- boolean horizontalFling = mSwipeDetector.isFling(velocity.x);
- boolean verticalFling = mSwipeDetector.isFling(velocity.y);
- boolean noFling = !horizontalFling && !verticalFling;
- int logAction = noFling ? Touch.SWIPE : Touch.FLING;
- if (mMotionPauseDetector.isPaused() && noFling) {
- cancelAnimations();
-
- Animator overviewAnim = mLauncher.createAtomicAnimationFactory()
- .createStateElementAnimation(INDEX_PAUSE_TO_OVERVIEW_ANIM);
- overviewAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- onAnimationToStateCompleted(OVERVIEW, logAction);
- }
- });
- overviewAnim.start();
- return;
- }
-
- final LauncherState targetState;
- if (horizontalFling && verticalFling) {
- if (velocity.x < 0) {
- // Flinging left and up or down both go back home.
- targetState = NORMAL;
- } else {
- if (velocity.y > 0) {
- // Flinging right and down goes to quick switch.
- targetState = QUICK_SWITCH;
- } else {
- // Flinging up and right could go either home or to quick switch.
- // Determine the target based on the higher velocity.
- targetState = Math.abs(velocity.x) > Math.abs(velocity.y)
- ? QUICK_SWITCH : NORMAL;
- }
- }
- } else if (horizontalFling) {
- targetState = velocity.x > 0 ? QUICK_SWITCH : NORMAL;
- } else if (verticalFling) {
- targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL;
- } else {
- // If user isn't flinging, just snap to the closest state based on x progress.
- boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
- targetState = passedHorizontalThreshold ? QUICK_SWITCH : NORMAL;
- }
-
- // Animate the various components to the target state.
-
- float xProgress = mXOverviewAnim.getProgressFraction();
- float startXProgress = Utilities.boundToRange(xProgress
- + velocity.x * getSingleFrameMs(mLauncher) / mXRange, 0f, 1f);
- final float endXProgress = targetState == NORMAL ? 0 : 1;
- long xDuration = BaseSwipeDetector.calculateDuration(velocity.x,
- Math.abs(endXProgress - startXProgress));
- ValueAnimator xOverviewAnim = mXOverviewAnim.getAnimationPlayer();
- xOverviewAnim.setFloatValues(startXProgress, endXProgress);
- xOverviewAnim.setDuration(xDuration)
- .setInterpolator(scrollInterpolatorForVelocity(velocity.x));
- mXOverviewAnim.dispatchOnStart();
-
- boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
-
- float yProgress = mYOverviewAnim.getProgressFraction();
- float startYProgress = Utilities.boundToRange(yProgress
- - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, 1f);
- final float endYProgress;
- if (flingUpToNormal) {
- endYProgress = 1;
- } else if (targetState == NORMAL) {
- // Keep overview at its current scale/translationY as it slides off the screen.
- endYProgress = startYProgress;
- } else {
- endYProgress = 0;
- }
- long yDuration = BaseSwipeDetector.calculateDuration(velocity.y,
- Math.abs(endYProgress - startYProgress));
- ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer();
- yOverviewAnim.setFloatValues(startYProgress, endYProgress);
- yOverviewAnim.setDuration(yDuration);
- mYOverviewAnim.dispatchOnStart();
-
- ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
- if (flingUpToNormal && !mIsHomeScreenVisible) {
- // We are flinging to home while workspace is invisible, run the same staggered
- // animation as from an app.
- StateAnimationConfig config = new StateAnimationConfig();
- // Update mNonOverviewAnim to do nothing so it doesn't interfere.
- config.animFlags = 0;
- updateNonOverviewAnim(targetState, config);
- nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
-
- new StaggeredWorkspaceAnim(mLauncher, velocity.y, false /* animateOverviewScrim */)
- .start();
- } else {
- boolean canceled = targetState == NORMAL;
- if (canceled) {
- // Let the state manager know that the animation didn't go to the target state,
- // but don't clean up yet (we already clean up when the animation completes).
- mNonOverviewAnim.dispatchOnCancelWithoutCancelRunnable();
- }
- float startProgress = mNonOverviewAnim.getProgressFraction();
- float endProgress = canceled ? 0 : 1;
- nonOverviewAnim.setFloatValues(startProgress, endProgress);
- mNonOverviewAnim.dispatchOnStart();
- }
-
- nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
- mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState, logAction));
-
- cancelAnimations();
- xOverviewAnim.start();
- yOverviewAnim.start();
- nonOverviewAnim.start();
- }
-
- private void onAnimationToStateCompleted(LauncherState targetState, int logAction) {
- mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
- getDirectionForLog(), mSwipeDetector.getDownX(), mSwipeDetector.getDownY(),
- LauncherLogProto.ContainerType.NAVBAR,
- mStartState.containerType,
- targetState.containerType,
- mLauncher.getWorkspace().getCurrentPage());
- mLauncher.getStatsLogManager().logger()
- .withSrcState(LAUNCHER_STATE_HOME)
- .withDstState(StatsLogManager.containerTypeToAtomState(targetState.containerType))
- .log(getLauncherAtomEvent(mStartState.containerType, targetState.containerType,
- targetState.ordinal > mStartState.ordinal
- ? LAUNCHER_UNKNOWN_SWIPEUP
- : LAUNCHER_UNKNOWN_SWIPEDOWN));
- mLauncher.getStateManager().goToState(targetState, false, this::clearState);
- }
-
- private int getDirectionForLog() {
- return Utilities.isRtl(mLauncher.getResources()) ? Direction.LEFT : Direction.RIGHT;
- }
-
- private void cancelAnimations() {
- if (mNonOverviewAnim != null) {
- mNonOverviewAnim.getAnimationPlayer().cancel();
- }
- if (mXOverviewAnim != null) {
- mXOverviewAnim.getAnimationPlayer().cancel();
- }
- if (mYOverviewAnim != null) {
- mYOverviewAnim.getAnimationPlayer().cancel();
- }
- mShelfPeekAnim.setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
- mMotionPauseDetector.clear();
- }
-
- private void clearState() {
- cancelAnimations();
- mNonOverviewAnim = null;
- mXOverviewAnim = null;
- mYOverviewAnim = null;
- mIsHomeScreenVisible = true;
- mSwipeDetector.finishedScrolling();
- mRecentsView.setOnEmptyMessageUpdatedListener(null);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/OverviewToAllAppsTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/OverviewToAllAppsTouchController.java
deleted file mode 100644
index 9091168..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/OverviewToAllAppsTouchController.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-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.Utilities;
-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, true /* allowDragToOverview */);
- }
-
- @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 (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0;
- } 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(MotionEvent ev) {
- return LauncherLogProto.ContainerType.WORKSPACE;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
deleted file mode 100644
index 845699a..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController.isTouchOverHotseat;
-
-import android.view.MotionEvent;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-
-/**
- * Helper class for {@link PortraitStatesTouchController} that determines swipeable regions and
- * animations on the overview state that depend on the recents implementation.
- */
-public final class PortraitOverviewStateTouchHelper {
-
- RecentsView mRecentsView;
- Launcher mLauncher;
-
- public PortraitOverviewStateTouchHelper(Launcher launcher) {
- mLauncher = launcher;
- mRecentsView = launcher.getOverviewPanel();
- }
-
- /**
- * Whether or not {@link PortraitStatesTouchController} should intercept the touch when on the
- * overview state.
- *
- * @param ev the motion event
- * @return true if we should intercept the motion event
- */
- boolean canInterceptTouch(MotionEvent ev) {
- if (mRecentsView.getTaskViewCount() > 0) {
- // Allow swiping up in the gap between the hotseat and overview.
- return ev.getY() >= mRecentsView.getTaskViewAt(0).getBottom();
- } else {
- // If there are no tasks, we only intercept if we're below the hotseat height.
- return isTouchOverHotseat(mLauncher, ev);
- }
- }
-
- /**
- * Whether or not swiping down to leave overview state should return to the currently running
- * task app.
- *
- * @return true if going back should take the user to the currently running task
- */
- boolean shouldSwipeDownReturnToApp() {
- TaskView taskView = mRecentsView.getNextPageTaskView();
- return taskView != null && mRecentsView.shouldSwipeDownLaunchApp();
- }
-
- /**
- * Create the animation for going from overview to the task app via swiping. Should only be
- * called when {@link #shouldSwipeDownReturnToApp()} returns true.
- *
- * @param duration how long the animation should be
- * @return the animation
- */
- PendingAnimation createSwipeDownToTaskAppAnimation(long duration, Interpolator interpolator) {
- mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
- TaskView taskView = mRecentsView.getCurrentPageTaskView();
- if (taskView == null) {
- throw new IllegalStateException("There is no task view to animate to.");
- }
- return mRecentsView.createTaskLaunchAnimation(taskView, duration, interpolator);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
deleted file mode 100644
index c643858..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.QUICK_SWITCH;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-
-import android.view.MotionEvent;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-/**
- * Handles quick switching to a recent task from the home screen.
- */
-public class QuickSwitchTouchController extends AbstractStateChangeTouchController {
-
- protected final RecentsView mOverviewPanel;
-
- public QuickSwitchTouchController(Launcher launcher) {
- this(launcher, SingleAxisSwipeDetector.HORIZONTAL);
- }
-
- protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
- super(l, dir);
- mOverviewPanel = l.getOverviewPanel();
- }
-
- @Override
- protected boolean canInterceptTouch(MotionEvent ev) {
- if (mCurrentAnimation != null) {
- return true;
- }
- if (!mLauncher.isInState(LauncherState.NORMAL)) {
- return false;
- }
- if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
- return false;
- }
- return true;
- }
-
- @Override
- protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
- int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
- if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
- return NORMAL;
- }
- return isDragTowardPositive ? QUICK_SWITCH : NORMAL;
- }
-
- @Override
- public void onDragStart(boolean start, float startDisplacement) {
- super.onDragStart(start, startDisplacement);
- mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
- }
-
- @Override
- protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
- super.onSwipeInteractionCompleted(targetState, logAction);
- }
-
- @Override
- protected float initCurrentAnimation(int animComponents) {
- StateAnimationConfig config = new StateAnimationConfig();
- setupInterpolators(config);
- config.duration = (long) (getShiftRange() * 2);
- mCurrentAnimation = mLauncher.getStateManager()
- .createAnimationToNewWorkspace(mToState, config)
- .setOnCancelRunnable(this::clearState);
- mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator ->
- updateFullscreenProgress((Float) valueAnimator.getAnimatedValue()));
- return 1 / getShiftRange();
- }
-
- private void setupInterpolators(StateAnimationConfig stateAnimationConfig) {
- stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_2);
- stateAnimationConfig.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_2);
- if (SysUINavigationMode.getMode(mLauncher) == Mode.NO_BUTTON) {
- // Overview lives to the left of workspace, so translate down later than over
- stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL_2);
- stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCEL_2);
- stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_SCALE, ACCEL_2);
- stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, ACCEL_2);
- stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
- } else {
- stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, LINEAR);
- stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, LINEAR);
- }
- }
-
- @Override
- protected void updateProgress(float progress) {
- super.updateProgress(progress);
- updateFullscreenProgress(Utilities.boundToRange(progress, 0, 1));
- }
-
- private void updateFullscreenProgress(float progress) {
- mOverviewPanel.setFullscreenProgress(progress);
- int sysuiFlags = 0;
- if (progress > UPDATE_SYSUI_FLAGS_THRESHOLD) {
- TaskView tv = mOverviewPanel.getTaskViewAt(0);
- if (tv != null) {
- sysuiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
- }
- }
- mLauncher.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
- }
-
- @Override
- protected float getShiftRange() {
- return mLauncher.getDeviceProfile().widthPx / 2f;
- }
-
- @Override
- protected int getLogContainerTypeForNormalState(MotionEvent ev) {
- return LauncherLogProto.ContainerType.NAVBAR;
- }
-
- @Override
- protected int getDirectionForLog() {
- return Utilities.isRtl(mLauncher.getResources()) ? Direction.LEFT : Direction.RIGHT;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
deleted file mode 100644
index 0ee5d04..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ /dev/null
@@ -1,329 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.view.MotionEvent;
-import android.view.View;
-
-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.anim.PendingAnimation;
-import com.android.launcher3.touch.BaseSwipeDetector;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.FlingBlockCheck;
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-
-/**
- * Touch controller for handling task view card swipes
- */
-public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
- extends AnimatorListenerAdapter implements TouchController,
- SingleAxisSwipeDetector.Listener {
-
- // Progress after which the transition is assumed to be a success in case user does not fling
- public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
-
- protected final T mActivity;
- private final SingleAxisSwipeDetector mDetector;
- private final RecentsView mRecentsView;
- private final int[] mTempCords = new int[2];
- private final boolean mIsRtl;
-
- 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();
- mIsRtl = Utilities.isRtl(activity.getResources());
- SingleAxisSwipeDetector.Direction dir =
- mRecentsView.getPagedOrientationHandler().getOppositeSwipeDirection();
- mDetector = new SingleAxisSwipeDetector(activity, this, dir);
- }
-
- private boolean canInterceptTouch() {
- if (mCurrentAnimation != null) {
- mCurrentAnimation.forceFinishIfCloseToEnd();
- }
- if (mCurrentAnimation != null) {
- // If we are already animating from a previous state, we can intercept.
- return true;
- }
- if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
- return false;
- }
- return isRecentsInteractive();
- }
-
- protected abstract boolean isRecentsInteractive();
-
- /** Is recents view showing a single task in a modal way. */
- protected abstract boolean isRecentsModal();
-
- 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_UP || ev.getAction() == MotionEvent.ACTION_CANCEL)
- && mCurrentAnimation == null) {
- clearState();
- }
- 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 = DIRECTION_BOTH;
- ignoreSlopWhenSettling = true;
- } else {
- mTaskBeingDragged = null;
-
- for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
- TaskView view = mRecentsView.getTaskViewAt(i);
-
- if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
- .isEventOverView(view, ev)) {
- // Disable swiping up and down if the task overlay is modal.
- if (isRecentsModal()) {
- mTaskBeingDragged = null;
- break;
- }
- mTaskBeingDragged = view;
- if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
- // Don't allow swipe down to open if we don't support swipe up
- // to enter overview.
- directionsToDetectScroll = 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()
- ? DIRECTION_BOTH : 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 & DIRECTION_POSITIVE) == 0)
- || !goingUp && ((scrollDirections & 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;
- }
-
- PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
- mCurrentAnimationIsGoingUp = goingUp;
- BaseDragLayer dl = mActivity.getDragLayer();
- final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
- long maxDuration = 2 * secondaryLayerDimension;
- int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
- int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
- if (goingUp) {
- mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
- true /* animateTaskView */, true /* removeTask */, maxDuration);
-
- mEndDisplacement = -secondaryTaskDimension;
- } else {
- mPendingAnimation = mRecentsView.createTaskLaunchAnimation(
- mTaskBeingDragged, maxDuration, Interpolators.ZOOM_IN);
-
- // Since the thumbnail is what is filling the screen, based the end displacement on it.
- View thumbnailView = mTaskBeingDragged.getThumbnail();
- mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
- dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
- mEndDisplacement = secondaryLayerDimension - mTempCords[1];
- }
- mEndDisplacement *= verticalFactor;
-
- if (mCurrentAnimation != null) {
- mCurrentAnimation.setOnCancelRunnable(null);
- }
- mCurrentAnimation = mPendingAnimation.createPlaybackController()
- .setOnCancelRunnable(this::clearState);
- onUserControlledAnimationCreated(mCurrentAnimation);
- mCurrentAnimation.getTarget().addListener(this);
- mCurrentAnimation.dispatchOnStart();
- mProgressMultiplier = 1 / mEndDisplacement;
- }
-
- @Override
- public void onDragStart(boolean start, float startDisplacement) {
- PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
- if (mCurrentAnimation == null) {
- reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
- mDisplacementShift = 0;
- } else {
- mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
- mCurrentAnimation.pause();
- }
- mFlingBlockCheck.unblockFling();
- }
-
- @Override
- public boolean onDrag(float displacement) {
- PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
- float totalDisplacement = displacement + mDisplacementShift;
- boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
- orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
- if (isGoingUp != mCurrentAnimationIsGoingUp) {
- reInitAnimationController(isGoingUp);
- mFlingBlockCheck.blockFling();
- } else {
- mFlingBlockCheck.onEvent();
- }
- mCurrentAnimation.setPlayFraction(Utilities.boundToRange(
- totalDisplacement * mProgressMultiplier, 0, 1));
-
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- if (mRecentsView.getCurrentPage() != 0 || isGoingUp) {
- mRecentsView.redrawLiveTile(true);
- }
- }
- return true;
- }
-
- @Override
- public void onDragEnd(float velocity) {
- boolean fling = mDetector.isFling(velocity);
- final boolean goingToEnd;
- final int logAction;
- boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
- if (blockedFling) {
- fling = false;
- }
- PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
- float progress = mCurrentAnimation.getProgressFraction();
- float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
- if (fling) {
- logAction = Touch.FLING;
- boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
- goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
- } else {
- logAction = Touch.SWIPE;
- goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
- }
- long animationDuration = BaseSwipeDetector.calculateDuration(
- velocity, goingToEnd ? (1 - progress) : progress);
- if (blockedFling && !goingToEnd) {
- animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
- }
-
- mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> {
- if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
- mRecentsView.redrawLiveTile(true);
- }
- });
- }
- mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
- velocity, mEndDisplacement, animationDuration);
- }
-
- 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/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
deleted file mode 100644
index 0ed5291..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-
-public class TransposedQuickSwitchTouchController extends QuickSwitchTouchController {
-
- public TransposedQuickSwitchTouchController(Launcher launcher) {
- super(launcher, SingleAxisSwipeDetector.VERTICAL);
- }
-
- @Override
- protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
- return super.getTargetState(fromState,
- isDragTowardPositive ^ mLauncher.getDeviceProfile().isSeascape());
- }
-
- @Override
- protected float initCurrentAnimation(int animComponents) {
- float multiplier = super.initCurrentAnimation(animComponents);
- return mLauncher.getDeviceProfile().isSeascape() ? multiplier : -multiplier;
- }
-
- @Override
- protected float getShiftRange() {
- return mLauncher.getDeviceProfile().heightPx / 2f;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
deleted file mode 100644
index de83caf..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
-import static com.android.launcher3.statehandlers.DepthController.DEPTH;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.util.Log;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.SurfaceTransactionApplier;
-import com.android.quickstep.util.TaskViewSimulator;
-import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Provider for the atomic (for 3-button mode) remote window animation from the app to the overview.
- *
- * @param <T> activity that contains the overview
- */
-final class AppToOverviewAnimationProvider<T extends StatefulActivity<?>> extends
- RemoteAnimationProvider {
-
- private static final long RECENTS_LAUNCH_DURATION = 250;
- private static final String TAG = "AppToOverviewAnimationProvider";
-
- private final BaseActivityInterface<?, T> mActivityInterface;
- // The id of the currently running task that is transitioning to overview.
- private final int mTargetTaskId;
- private final RecentsAnimationDeviceState mDeviceState;
-
- private T mActivity;
- private RecentsView mRecentsView;
-
- AppToOverviewAnimationProvider(
- BaseActivityInterface<?, T> activityInterface, int targetTaskId,
- RecentsAnimationDeviceState deviceState) {
- mActivityInterface = activityInterface;
- mTargetTaskId = targetTaskId;
- mDeviceState = deviceState;
- }
-
- /**
- * Callback for when the activity is ready/initialized.
- *
- * @param activity the activity that is ready
- * @param wasVisible true if it was visible before
- */
- boolean onActivityReady(T activity, Boolean wasVisible) {
- activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTaskId);
- AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
- BaseActivityInterface.AnimationFactory factory = mActivityInterface.prepareRecentsUI(
- mDeviceState,
- wasVisible, (controller) -> {
- controller.dispatchOnStart();
- controller.getAnimationPlayer().end();
- });
- factory.createActivityInterface(RECENTS_LAUNCH_DURATION);
- factory.setRecentsAttachedToAppWindow(true, false);
- mActivity = activity;
- mRecentsView = mActivity.getOverviewPanel();
- return false;
- }
-
- /**
- * Create remote window animation from the currently running app to the overview panel.
- *
- * @param appTargets the target apps
- * @return animation from app to overview
- */
- @Override
- public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets) {
- PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
- if (mActivity == null) {
- Log.e(TAG, "Animation created, before activity");
- return pa.buildAnim();
- }
-
- mRecentsView.setRunningTaskIconScaledDown(true);
- pa.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- mActivityInterface.onSwipeUpToRecentsComplete();
- mRecentsView.animateUpRunningTaskIconScale();
- }
- });
-
- DepthController depthController = mActivityInterface.getDepthController();
- if (depthController != null) {
- pa.addFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(mActivity),
- OVERVIEW.getDepth(mActivity), TOUCH_RESPONSE_INTERPOLATOR);
- }
-
- RemoteAnimationTargets targets = new RemoteAnimationTargets(appTargets,
- wallpaperTargets, MODE_CLOSING);
-
- // Use the top closing app to determine the insets for the animation
- RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mTargetTaskId);
- if (runningTaskTarget == null) {
- Log.e(TAG, "No closing app");
- return pa.buildAnim();
- }
-
- TaskViewSimulator tsv = new TaskViewSimulator(mActivity, mRecentsView.getSizeStrategy());
- tsv.setDp(mActivity.getDeviceProfile());
- tsv.setPreview(runningTaskTarget);
- tsv.setLayoutRotation(mRecentsView.getPagedViewOrientedState().getTouchRotation(),
- mRecentsView.getPagedViewOrientedState().getDisplayRotation());
-
- TransformParams params = new TransformParams()
- .setTargetSet(targets)
- .setSyncTransactionApplier(new SurfaceTransactionApplier(mActivity.getRootView()));
-
- AnimatedFloat recentsAlpha = new AnimatedFloat(() -> { });
- params.setBaseBuilderProxy((builder, app, p)
- -> builder.withAlpha(recentsAlpha.value));
-
- Interpolator taskInterpolator;
- if (targets.isAnimatingHome()) {
- params.setHomeBuilderProxy((builder, app, p) -> builder.withAlpha(1 - p.getProgress()));
-
- taskInterpolator = TOUCH_RESPONSE_INTERPOLATOR;
- pa.addFloat(recentsAlpha, AnimatedFloat.VALUE, 0, 1, TOUCH_RESPONSE_INTERPOLATOR);
- } else {
- // When animation from app to recents, the recents layer is drawn on top of the app. To
- // prevent the overlap, we animate the task first and then quickly fade in the recents.
- taskInterpolator = clampToProgress(TOUCH_RESPONSE_INTERPOLATOR, 0, 0.8f);
- pa.addFloat(recentsAlpha, AnimatedFloat.VALUE, 0, 1,
- clampToProgress(TOUCH_RESPONSE_INTERPOLATOR, 0.8f, 1));
- }
-
- pa.addFloat(params, TransformParams.PROGRESS, 0, 1, taskInterpolator);
- tsv.addAppToOverviewAnim(pa, taskInterpolator);
- pa.addOnFrameCallback(() -> tsv.apply(params));
- return pa.buildAnim();
- }
-
- /**
- * Get duration of animation from app to overview.
- *
- * @return duration of animation
- */
- long getRecentsLaunchDuration() {
- return RECENTS_LAUNCH_DURATION;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
deleted file mode 100644
index a63f3a8..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ /dev/null
@@ -1,385 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.Build;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.VibratorWrapper;
-import com.android.launcher3.util.WindowBounds;
-import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.SurfaceTransactionApplier;
-import com.android.quickstep.util.TransformParams;
-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.InputConsumerController;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-/**
- * Base class for swipe up handler with some utility methods
- */
-@TargetApi(Build.VERSION_CODES.Q)
-public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
- extends SwipeUpAnimationLogic implements RecentsAnimationListener {
-
- private static final String TAG = "BaseSwipeUpHandler";
-
- protected final BaseActivityInterface<?, T> mActivityInterface;
- protected final InputConsumerController mInputConsumer;
-
- protected final ActivityInitListener mActivityInitListener;
-
- protected RecentsAnimationController mRecentsAnimationController;
- protected RecentsAnimationTargets mRecentsAnimationTargets;
-
- // Callbacks to be made once the recents animation starts
- private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
-
- protected T mActivity;
- protected Q mRecentsView;
-
- protected Runnable mGestureEndCallback;
-
- protected MultiStateCallback mStateCallback;
-
- protected boolean mCanceled;
-
- private boolean mRecentsViewScrollLinked = false;
-
- protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
- GestureState gestureState, InputConsumerController inputConsumer) {
- super(context, deviceState, gestureState, new TransformParams());
- mActivityInterface = gestureState.getActivityInterface();
- mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
- mInputConsumer = inputConsumer;
- }
-
- /**
- * To be called at the end of constructor of subclasses. This calls various methods which can
- * depend on proper class initialization.
- */
- protected void initAfterSubclassConstructor() {
- initTransitionEndpoints(
- mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
- }
-
- protected void performHapticFeedback() {
- VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
- }
-
- public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
- return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
- }
-
- public void setGestureEndCallback(Runnable gestureEndCallback) {
- mGestureEndCallback = gestureEndCallback;
- }
-
- public abstract Intent getLaunchIntent();
-
- protected void linkRecentsViewScroll() {
- SurfaceTransactionApplier.create(mRecentsView, applier -> {
- mTransformParams.setSyncTransactionApplier(applier);
- runOnRecentsAnimationStart(() ->
- mRecentsAnimationTargets.addReleaseCheck(applier));
- });
-
- mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
- if (moveWindowWithRecentsScroll()) {
- updateFinalShift();
- }
- });
- runOnRecentsAnimationStart(() ->
- mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
- mRecentsAnimationTargets));
- mRecentsViewScrollLinked = true;
- }
-
- protected void startNewTask(Consumer<Boolean> resultCallback) {
- // Launch the task user scrolled to (mRecentsView.getNextPage()).
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- // We finish recents animation inside launchTask() when live tile is enabled.
- mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
- true /* freezeTaskList */);
- } else {
- int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
- if (!mCanceled) {
- TaskView nextTask = mRecentsView.getTaskView(taskId);
- if (nextTask != null) {
- mGestureState.updateLastStartedTaskId(taskId);
- boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
- .contains(taskId);
- nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
- success -> {
- resultCallback.accept(success);
- if (success) {
- if (hasTaskPreviouslyAppeared) {
- onRestartPreviouslyAppearedTask();
- }
- } else {
- mActivityInterface.onLaunchTaskFailed();
- nextTask.notifyTaskLaunchFailed(TAG);
- mRecentsAnimationController.finish(true /* toRecents */, null);
- }
- }, MAIN_EXECUTOR.getHandler());
- }
- }
- mCanceled = false;
- }
- }
-
- /**
- * Called when we successfully startNewTask() on the task that was previously running. Normally
- * we call resumeLastTask() when returning to the previously running task, but this handles a
- * specific edge case: if we switch from A to B, and back to A before B appears, we need to
- * start A again to ensure it stays on top.
- */
- @CallSuper
- protected void onRestartPreviouslyAppearedTask() {
- // Finish the controller here, since we won't get onTaskAppeared() for a task that already
- // appeared.
- if (mRecentsAnimationController != null) {
- mRecentsAnimationController.finish(false, null);
- }
- }
-
- /**
- * Runs the given {@param action} if the recents animation has already started, or queues it to
- * be run when it is next started.
- */
- protected void runOnRecentsAnimationStart(Runnable action) {
- if (mRecentsAnimationTargets == null) {
- mRecentsAnimationStartCallbacks.add(action);
- } else {
- action.run();
- }
- }
-
- /**
- * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
- * @return whether the recents animation has started and there are valid app targets.
- */
- protected boolean hasTargets() {
- return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
- }
-
- @Override
- public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController,
- RecentsAnimationTargets targets) {
- mRecentsAnimationController = recentsAnimationController;
- mRecentsAnimationTargets = targets;
- mTransformParams.setTargetSet(mRecentsAnimationTargets);
- RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
- mGestureState.getRunningTaskId());
-
- if (runningTaskTarget != null) {
- mTaskViewSimulator.setPreview(runningTaskTarget);
- }
-
- // Only initialize the device profile, if it has not been initialized before, as in some
- // configurations targets.homeContentInsets may not be correct.
- if (mActivity == null) {
- DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
- if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
- Rect overviewStackBounds = mActivityInterface
- .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
- dp = dp.getMultiWindowProfile(mContext,
- new WindowBounds(overviewStackBounds, targets.homeContentInsets));
- } else {
- // If we are not in multi-window mode, home insets should be same as system insets.
- dp = dp.copy(mContext);
- }
- dp.updateInsets(targets.homeContentInsets);
- dp.updateIsSeascape(mContext);
- initTransitionEndpoints(dp);
- }
-
- // Notify when the animation starts
- if (!mRecentsAnimationStartCallbacks.isEmpty()) {
- for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
- action.run();
- }
- mRecentsAnimationStartCallbacks.clear();
- }
- }
-
- @Override
- public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
- mRecentsAnimationController = null;
- mRecentsAnimationTargets = null;
- if (mRecentsView != null) {
- mRecentsView.setRecentsAnimationTargets(null, null);
- }
- }
-
- @Override
- public void onRecentsAnimationFinished(RecentsAnimationController controller) {
- mRecentsAnimationController = null;
- mRecentsAnimationTargets = null;
- if (mRecentsView != null) {
- mRecentsView.setRecentsAnimationTargets(null, null);
- }
- }
-
- @Override
- public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
- if (mRecentsAnimationController != null) {
- if (handleTaskAppeared(appearedTaskTarget)) {
- mRecentsAnimationController.finish(false /* toRecents */,
- null /* onFinishComplete */);
- mActivityInterface.onLaunchTaskSuccess();
- ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
- }
- }
- }
-
- /** @return Whether this was the task we were waiting to appear, and thus handled it. */
- protected abstract boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget);
-
- /**
- * @return The index of the TaskView in RecentsView whose taskId matches the task that will
- * resume if we finish the controller.
- */
- protected int getLastAppearedTaskIndex() {
- return mGestureState.getLastAppearedTaskId() != -1
- ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
- : mRecentsView.getRunningTaskIndex();
- }
-
- /**
- * @return Whether we are continuing a gesture that already landed on a new task,
- * but before that task appeared.
- */
- protected boolean hasStartedNewTask() {
- return mGestureState.getLastStartedTaskId() != -1;
- }
-
- /**
- * Return true if the window should be translated horizontally if the recents view scrolls
- */
- protected abstract boolean moveWindowWithRecentsScroll();
-
- protected boolean onActivityInit(Boolean alreadyOnHome) {
- T createdActivity = mActivityInterface.getCreatedActivity();
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
- }
- if (createdActivity != null) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
- }
- initTransitionEndpoints(createdActivity.getDeviceProfile());
- }
- return true;
- }
-
- /**
- * Called to create a input proxy for the running task
- */
- @UiThread
- protected abstract InputConsumer createNewInputProxyHandler();
-
- /**
- * Called when the value of {@link #mCurrentShift} changes
- */
- @UiThread
- public abstract void updateFinalShift();
-
- /**
- * Called when motion pause is detected
- */
- public abstract void onMotionPauseChanged(boolean isPaused);
-
- @UiThread
- public void onGestureStarted(boolean isLikelyToStartNewTask) { }
-
- @UiThread
- public abstract void onGestureCancelled();
-
- @UiThread
- public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
-
- public abstract void onConsumerAboutToBeSwitched();
-
- public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
-
- /**
- * Registers a callback to run when the activity is ready.
- * @param intent The intent that will be used to start the activity if it doesn't exist already.
- */
- public void initWhenReady(Intent intent) {
- // Preload the plan
- RecentsModel.INSTANCE.get(mContext).getTasks(null);
-
- mActivityInitListener.register(intent);
- }
-
- /**
- * Applies the transform on the recents animation
- */
- protected void applyWindowTransform() {
- if (mWindowTransitionController != null) {
- float progress = mCurrentShift.value / mDragLengthFactor;
- mWindowTransitionController.setPlayFraction(progress);
- }
- if (mRecentsAnimationTargets != null) {
- if (mRecentsViewScrollLinked) {
- mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
- }
- mTaskViewSimulator.apply(mTransformParams);
- }
- }
-
- @Override
- protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
- HomeAnimationFactory homeAnimationFactory) {
- RectFSpringAnim anim =
- super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
- if (mRecentsAnimationTargets != null) {
- mRecentsAnimationTargets.addReleaseCheck(anim);
- }
- return anim;
- }
-
- public interface Factory {
-
- BaseSwipeUpHandler newHandler(
- GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
deleted file mode 100644
index 6c4c5d3..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
+++ /dev/null
@@ -1,1335 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
-import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
-import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
-import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
-import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
-import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
-import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
-import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
-import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-
-import android.animation.Animator;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.os.Build;
-import android.os.SystemClock;
-import android.view.View;
-import android.view.View.OnApplyWindowInsetsListener;
-import android.view.ViewTreeObserver.OnDrawListener;
-import android.view.WindowInsets;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.statemanager.StatefulActivity;
-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.TraceHelper;
-import com.android.quickstep.BaseActivityInterface.AnimationFactory;
-import com.android.quickstep.GestureState.GestureEndTarget;
-import com.android.quickstep.inputconsumers.OverviewInputConsumer;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.ShelfPeekAnim;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
-import com.android.quickstep.views.LiveTileOverlay;
-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.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.TaskInfoCompat;
-import com.android.systemui.shared.system.TaskStackChangeListener;
-
-/**
- * Handles the navigation gestures when Launcher is the default home activity.
- * TODO: Merge this with BaseSwipeUpHandler
- */
-@TargetApi(Build.VERSION_CODES.O)
-public abstract class BaseSwipeUpHandlerV2<T extends StatefulActivity<?>, Q extends RecentsView>
- extends BaseSwipeUpHandler<T, Q> implements OnApplyWindowInsetsListener {
- private static final String TAG = BaseSwipeUpHandlerV2.class.getSimpleName();
-
- private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
-
- private static int getFlagForIndex(int index, String name) {
- if (DEBUG_STATES) {
- STATE_NAMES[index] = name;
- }
- return 1 << index;
- }
-
- // Launcher UI related states
- protected static final int STATE_LAUNCHER_PRESENT =
- getFlagForIndex(0, "STATE_LAUNCHER_PRESENT");
- protected static final int STATE_LAUNCHER_STARTED =
- getFlagForIndex(1, "STATE_LAUNCHER_STARTED");
- protected static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN");
-
- // Internal initialization states
- private static final int STATE_APP_CONTROLLER_RECEIVED =
- getFlagForIndex(3, "STATE_APP_CONTROLLER_RECEIVED");
-
- // Interaction finish states
- private static final int STATE_SCALED_CONTROLLER_HOME =
- getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME");
- private static final int STATE_SCALED_CONTROLLER_RECENTS =
- getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
-
- protected static final int STATE_HANDLER_INVALIDATED =
- getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
- private static final int STATE_GESTURE_STARTED =
- getFlagForIndex(7, "STATE_GESTURE_STARTED");
- private static final int STATE_GESTURE_CANCELLED =
- getFlagForIndex(8, "STATE_GESTURE_CANCELLED");
- private static final int STATE_GESTURE_COMPLETED =
- getFlagForIndex(9, "STATE_GESTURE_COMPLETED");
-
- private static final int STATE_CAPTURE_SCREENSHOT =
- getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT");
- protected static final int STATE_SCREENSHOT_CAPTURED =
- getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED");
- private static final int STATE_SCREENSHOT_VIEW_SHOWN =
- getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN");
-
- private static final int STATE_RESUME_LAST_TASK =
- getFlagForIndex(13, "STATE_RESUME_LAST_TASK");
- private static final int STATE_START_NEW_TASK =
- getFlagForIndex(14, "STATE_START_NEW_TASK");
- private static final int STATE_CURRENT_TASK_FINISHED =
- getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED");
-
- private static final int LAUNCHER_UI_STATES =
- STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
-
- public static final long MAX_SWIPE_DURATION = 350;
- public static final long MIN_OVERSHOOT_DURATION = 120;
-
- public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
- private static final float SWIPE_DURATION_MULTIPLIER =
- Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
- private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
-
- public static final long RECENTS_ATTACH_DURATION = 300;
-
- /**
- * Used as the page index for logging when we return to the last task at the end of the gesture.
- */
- private static final int LOG_NO_OP_PAGE_INDEX = -1;
-
- protected final TaskAnimationManager mTaskAnimationManager;
-
- // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
- private RunningWindowAnim mRunningWindowAnim;
- private boolean mIsShelfPeeking;
-
- private boolean mContinuingLastGesture;
-
- private ThumbnailData mTaskSnapshot;
-
- // Used to control launcher components throughout the swipe gesture.
- private AnimatorPlaybackController mLauncherTransitionController;
- private boolean mHasLauncherTransitionControllerStarted;
-
- private AnimationFactory mAnimationFactory = (t) -> { };
-
- private boolean mWasLauncherAlreadyVisible;
-
- private boolean mPassedOverviewThreshold;
- private boolean mGestureStarted;
- private int mLogAction = Touch.SWIPE;
- private int mLogDirection = Direction.UP;
- private PointF mDownPos;
- private boolean mIsLikelyToStartNewTask;
-
- private final long mTouchTimeMs;
- private long mLauncherFrameDrawnTime;
-
- private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
-
- public BaseSwipeUpHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
- TaskAnimationManager taskAnimationManager, GestureState gestureState,
- long touchTimeMs, boolean continuingLastGesture,
- InputConsumerController inputConsumer) {
- super(context, deviceState, gestureState, inputConsumer);
- mTaskAnimationManager = taskAnimationManager;
- mTouchTimeMs = touchTimeMs;
- mContinuingLastGesture = continuingLastGesture;
-
- initAfterSubclassConstructor();
- initStateCallbacks();
- }
-
- private void initStateCallbacks() {
- mStateCallback = new MultiStateCallback(STATE_NAMES);
-
- mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
- this::onLauncherPresentAndGestureStarted);
-
- mStateCallback.runOnceAtState(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
- this::initializeLauncherAnimationController);
-
- mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
- this::launcherFrameDrawn);
-
- mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
- | STATE_GESTURE_CANCELLED,
- this::resetStateForAnimationCancel);
-
- mStateCallback.runOnceAtState(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
- this::resumeLastTask);
- mStateCallback.runOnceAtState(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
- this::startNewTask);
-
- mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
- | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT,
- this::switchToScreenshot);
-
- mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
- | STATE_SCALED_CONTROLLER_RECENTS,
- this::finishCurrentTransitionToRecents);
-
- mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
- | STATE_SCALED_CONTROLLER_HOME,
- this::finishCurrentTransitionToHome);
- mStateCallback.runOnceAtState(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
- this::reset);
-
- mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
- | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
- | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
- | STATE_GESTURE_STARTED,
- this::setupLauncherUiAfterSwipeUpToRecentsAnimation);
-
- mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED,
- this::continueComputingRecentsScrollIfNecessary);
- mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED
- | STATE_RECENTS_SCROLLING_FINISHED,
- this::onSettledOnEndTarget);
-
- mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
- mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
- this::invalidateHandlerWithLauncher);
- mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
- this::notifyTransitionCancelled);
-
- mGestureState.runOnceAtState(STATE_END_TARGET_SET,
- () -> mDeviceState.onEndTargetCalculated(mGestureState.getEndTarget(),
- mActivityInterface));
-
- if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
- | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
- (b) -> mRecentsView.setRunningTaskHidden(!b));
- }
- }
-
- @Override
- protected boolean onActivityInit(Boolean alreadyOnHome) {
- super.onActivityInit(alreadyOnHome);
- final T activity = mActivityInterface.getCreatedActivity();
- 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);
- }
- 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(STATE_HANDLER_INVISIBILITY_FLAGS);
- } else {
- mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
- }
-
- mRecentsView = activity.getOverviewPanel();
- mRecentsView.setOnPageTransitionEndCallback(null);
- addLiveTileOverlay();
-
- mStateCallback.setState(STATE_LAUNCHER_PRESENT);
- if (alreadyOnHome) {
- onLauncherStart();
- } else {
- activity.runOnceOnStart(this::onLauncherStart);
- }
-
- setupRecentsViewUi();
-
- if (mDeviceState.getNavMode() == TWO_BUTTONS) {
- // If the device is in two button mode, swiping up will show overview with predictions
- // so we need to kick off switching to the overview predictions as soon as possible
- mActivityInterface.updateOverviewPredictionState();
- }
- linkRecentsViewScroll();
-
- return true;
- }
-
- @Override
- protected boolean moveWindowWithRecentsScroll() {
- return mGestureState.getEndTarget() != HOME;
- }
-
- private void onLauncherStart() {
- final T activity = mActivityInterface.getCreatedActivity();
- if (mActivity != activity) {
- return;
- }
- if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
- return;
- }
- mTaskViewSimulator.setRecentsConfiguration(mActivity.getResources().getConfiguration());
-
- // If we've already ended the gesture and are going home, don't prepare recents UI,
- // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
- if (mGestureState.getEndTarget() != HOME) {
- Runnable initAnimFactory = () -> {
- mAnimationFactory = mActivityInterface.prepareRecentsUI(mDeviceState,
- mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated);
- maybeUpdateRecentsAttachedState(false /* animate */);
- };
- if (mWasLauncherAlreadyVisible) {
- // Launcher is visible, but might be about to stop. Thus, if we prepare recents
- // now, it might get overridden by moveToRestState() in onStop(). To avoid this,
- // wait until the next gesture (and possibly launcher) starts.
- mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, initAnimFactory);
- } else {
- initAnimFactory.run();
- }
- }
- AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible,
- AbstractFloatingView.TYPE_LISTENER);
-
- if (mWasLauncherAlreadyVisible) {
- mStateCallback.setState(STATE_LAUNCHER_DRAWN);
- } else {
- Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init");
- View dragLayer = activity.getDragLayer();
- dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
- boolean mHandled = false;
-
- @Override
- public void onDraw() {
- if (mHandled) {
- return;
- }
- mHandled = true;
-
- TraceHelper.INSTANCE.endSection(traceToken);
- dragLayer.post(() ->
- dragLayer.getViewTreeObserver().removeOnDrawListener(this));
- if (activity != mActivity) {
- return;
- }
-
- mStateCallback.setState(STATE_LAUNCHER_DRAWN);
- }
- });
- }
-
- activity.getRootView().setOnApplyWindowInsetsListener(this);
- mStateCallback.setState(STATE_LAUNCHER_STARTED);
- }
-
- private void onLauncherPresentAndGestureStarted() {
- // Re-setup the recents UI when gesture starts, as the state could have been changed during
- // that time by a previous window transition.
- setupRecentsViewUi();
-
- // For the duration of the gesture, in cases where an activity is launched while the
- // activity is not yet resumed, finish the animation to ensure we get resumed
- mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback(
- mOnDeferredActivityLaunch);
-
- notifyGestureStartedAsync();
- }
-
- private void onDeferredActivityLaunch() {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- mActivityInterface.switchRunningTaskViewToScreenshot(
- null, () -> {
- mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
- });
- } else {
- mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
- }
- }
-
- private void setupRecentsViewUi() {
- if (mContinuingLastGesture) {
- updateSysUiFlags(mCurrentShift.value);
- return;
- }
- notifyGestureAnimationStartToRecents();
- }
-
- protected void notifyGestureAnimationStartToRecents() {
- mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
- }
-
- private void launcherFrameDrawn() {
- mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
- }
-
- private void initializeLauncherAnimationController() {
- buildAnimationController();
-
- Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents",
- TraceHelper.FLAG_IGNORE_BINDERS);
- // Only used in debug builds
- if (LatencyTrackerCompat.isEnabled(mContext)) {
- LatencyTrackerCompat.logToggleRecents(
- (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
- }
- TraceHelper.INSTANCE.endSection(traceToken);
-
- // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
- // high-res thumbnail loader here once we are sure that we will end up in an overview state
- RecentsModel.INSTANCE.get(mContext).getThumbnailCache()
- .getHighResLoadingState().setVisible(true);
- }
-
- @Override
- public void onMotionPauseChanged(boolean isPaused) {
- setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
-
- if (mDeviceState.isFullyGesturalNavMode() && isPaused) {
- // In fully gestural nav mode, switch to overview predictions once the user has paused
- // (this is a no-op if the predictions are already in that state)
- mActivityInterface.updateOverviewPredictionState();
- }
- }
-
- public void maybeUpdateRecentsAttachedState() {
- maybeUpdateRecentsAttachedState(true /* animate */);
- }
-
- /**
- * Determines whether to show or hide RecentsView. The window is always
- * synchronized with its corresponding TaskView in RecentsView, so if
- * RecentsView is shown, it will appear to be attached to the window.
- *
- * Note this method has no effect unless the navigation mode is NO_BUTTON.
- */
- private void maybeUpdateRecentsAttachedState(boolean animate) {
- if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
- return;
- }
- RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
- ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
- : null;
- final boolean recentsAttachedToAppWindow;
- if (mGestureState.getEndTarget() != null) {
- recentsAttachedToAppWindow = mGestureState.getEndTarget().recentsAttachedToAppWindow;
- } else if (mContinuingLastGesture
- && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
- recentsAttachedToAppWindow = true;
- } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
- // The window is going away so make sure recents is always visible in this case.
- recentsAttachedToAppWindow = true;
- } else {
- recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
- }
- mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
- }
-
- @Override
- public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
- setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
- }
-
- private void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask, boolean animate) {
- if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
- mIsLikelyToStartNewTask = isLikelyToStartNewTask;
- maybeUpdateRecentsAttachedState(animate);
- }
- }
-
- @UiThread
- public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
- mAnimationFactory.setShelfState(shelfState, interpolator, duration);
- boolean wasShelfPeeking = mIsShelfPeeking;
- mIsShelfPeeking = shelfState == PEEK;
- if (mIsShelfPeeking != wasShelfPeeking) {
- maybeUpdateRecentsAttachedState();
- }
- if (shelfState.shouldPreformHaptic) {
- performHapticFeedback();
- }
- }
-
- private void buildAnimationController() {
- if (!canCreateNewOrUpdateExistingLauncherTransitionController()) {
- return;
- }
- initTransitionEndpoints(mActivity.getDeviceProfile());
- mAnimationFactory.createActivityInterface(mTransitionDragLength);
- }
-
- /**
- * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
- * (it has its own animation) or if we're already animating the current controller.
- * @return Whether we can create the launcher controller or update its progress.
- */
- private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
- return mGestureState.getEndTarget() != HOME && !mHasLauncherTransitionControllerStarted;
- }
-
- @Override
- public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
- WindowInsets result = view.onApplyWindowInsets(windowInsets);
- buildAnimationController();
- return result;
- }
-
- private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
- mLauncherTransitionController = anim;
- mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
- mLauncherTransitionController.dispatchOnStart();
- updateLauncherTransitionProgress();
- }
-
- @Override
- public Intent getLaunchIntent() {
- return mGestureState.getOverviewIntent();
- }
-
- @Override
- public void updateFinalShift() {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- if (mRecentsAnimationTargets != null) {
- LiveTileOverlay.INSTANCE.update(
- mTaskViewSimulator.getCurrentCropRect(),
- mTaskViewSimulator.getCurrentCornerRadius());
- }
- }
-
- final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
- if (passed != mPassedOverviewThreshold) {
- mPassedOverviewThreshold = passed;
- if (!mDeviceState.isFullyGesturalNavMode()) {
- performHapticFeedback();
- }
- }
-
- updateSysUiFlags(mCurrentShift.value);
- applyWindowTransform();
- updateLauncherTransitionProgress();
- }
-
- private void updateLauncherTransitionProgress() {
- if (mLauncherTransitionController == null
- || !canCreateNewOrUpdateExistingLauncherTransitionController()) {
- return;
- }
- // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
- // anyway. The controller mimics the drag length factor by applying it to its interpolators.
- float progress = mCurrentShift.value / mDragLengthFactor;
- mLauncherTransitionController.setPlayFraction(progress);
- }
-
- /**
- * @param windowProgress 0 == app, 1 == overview
- */
- private void updateSysUiFlags(float windowProgress) {
- if (mRecentsAnimationController != null && mRecentsView != null) {
- TaskView runningTask = mRecentsView.getRunningTaskView();
- TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
- int centermostTaskFlags = centermostTask == null ? 0
- : centermostTask.getThumbnail().getSysUiStatusNavFlags();
- boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
- boolean quickswitchThresholdPassed = centermostTask != runningTask;
-
- // We will handle the sysui flags based on the centermost task view.
- mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed
- || (quickswitchThresholdPassed && centermostTaskFlags != 0));
- mRecentsAnimationController.setSplitScreenMinimized(swipeUpThresholdPassed);
-
- int sysuiFlags = swipeUpThresholdPassed ? 0 : centermostTaskFlags;
- mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
- }
- }
-
- @Override
- public void onRecentsAnimationStart(RecentsAnimationController controller,
- RecentsAnimationTargets targets) {
- ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length);
- super.onRecentsAnimationStart(controller, targets);
-
- // Only add the callback to enable the input consumer after we actually have the controller
- mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
- mRecentsAnimationController::enableInputConsumer);
- mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
-
- mPassedOverviewThreshold = false;
- }
-
- @Override
- public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
- ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
- mActivityInitListener.unregister();
- mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
-
- // Defer clearing the controller and the targets until after we've updated the state
- super.onRecentsAnimationCanceled(thumbnailData);
- }
-
- @Override
- public void onGestureStarted(boolean isLikelyToStartNewTask) {
- notifyGestureStartedAsync();
- setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
- mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
- mGestureStarted = true;
- }
-
- /**
- * Notifies the launcher that the swipe gesture has started. This can be called multiple times.
- */
- @UiThread
- private void notifyGestureStartedAsync() {
- final T curActivity = mActivity;
- if (curActivity != null) {
- // Once the gesture starts, we can no longer transition home through the button, so
- // reset the force override of the activity visibility
- mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
- }
- }
-
- /**
- * Called as a result on ACTION_CANCEL to return the UI to the start state.
- */
- @Override
- public void onGestureCancelled() {
- updateDisplacement(0);
- mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
- mLogAction = Touch.SWIPE_NOOP;
- handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */);
- }
-
- /**
- * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
- * @param velocity The x and y components of the velocity when the gesture ends.
- * @param downPos The x and y value of where the gesture started.
- */
- @Override
- public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
- float flingThreshold = mContext.getResources()
- .getDimension(R.dimen.quickstep_fling_threshold_velocity);
- boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
- mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
-
- mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
- boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
- if (isVelocityVertical) {
- mLogDirection = velocity.y < 0 ? Direction.UP : Direction.DOWN;
- } else {
- mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT;
- }
- mDownPos = downPos;
- handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
- }
-
- @Override
- protected InputConsumer createNewInputProxyHandler() {
- endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
- endLauncherTransitionController();
-
- StatefulActivity activity = mActivityInterface.getCreatedActivity();
- return activity == null ? InputConsumer.NO_OP
- : new OverviewInputConsumer(mGestureState, activity, null, true);
- }
-
- private void endRunningWindowAnim(boolean cancel) {
- if (mRunningWindowAnim != null) {
- if (cancel) {
- mRunningWindowAnim.cancel();
- } else {
- mRunningWindowAnim.end();
- }
- }
- }
-
- private void onSettledOnEndTarget() {
- switch (mGestureState.getEndTarget()) {
- case HOME:
- mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
- // Notify swipe-to-home (recents animation) is finished
- SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished();
- break;
- case RECENTS:
- mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
- | STATE_SCREENSHOT_VIEW_SHOWN);
- break;
- case NEW_TASK:
- mStateCallback.setState(STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT);
- break;
- case LAST_TASK:
- mStateCallback.setState(STATE_RESUME_LAST_TASK);
- break;
- }
- ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + mGestureState.getEndTarget());
- }
-
- @Override
- protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
- if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
- return false;
- }
- if (mStateCallback.hasStates(STATE_START_NEW_TASK)
- && appearedTaskTarget.taskId == mGestureState.getLastStartedTaskId()) {
- reset();
- return true;
- }
- return false;
- }
-
- private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling,
- boolean isCancel) {
- final GestureEndTarget endTarget;
- final boolean goingToNewTask;
- if (mRecentsView != null) {
- if (!hasTargets()) {
- // If there are no running tasks, then we can assume that this is a continuation of
- // the last gesture, but after the recents animation has finished
- goingToNewTask = true;
- } else {
- final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
- final int taskToLaunch = mRecentsView.getNextPage();
- goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
- }
- } else {
- goingToNewTask = false;
- }
- final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
- if (!isFling) {
- if (isCancel) {
- endTarget = LAST_TASK;
- } else if (mDeviceState.isFullyGesturalNavMode()) {
- if (mIsShelfPeeking) {
- endTarget = RECENTS;
- } else if (goingToNewTask) {
- endTarget = NEW_TASK;
- } else {
- endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME;
- }
- } else {
- endTarget = reachedOverviewThreshold && mGestureStarted
- ? RECENTS
- : goingToNewTask
- ? NEW_TASK
- : LAST_TASK;
- }
- } else {
- // If swiping at a diagonal, base end target on the faster velocity.
- boolean isSwipeUp = endVelocity < 0;
- boolean willGoToNewTaskOnSwipeUp =
- goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity);
-
- if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
- endTarget = HOME;
- } else if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !mIsShelfPeeking) {
- // If swiping at a diagonal, base end target on the faster velocity.
- endTarget = NEW_TASK;
- } else if (isSwipeUp) {
- endTarget = !reachedOverviewThreshold && willGoToNewTaskOnSwipeUp
- ? NEW_TASK : RECENTS;
- } else {
- endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
- }
- }
-
- if (mDeviceState.isOverviewDisabled() && (endTarget == RECENTS || endTarget == LAST_TASK)) {
- return LAST_TASK;
- }
- return endTarget;
- }
-
- @UiThread
- private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity,
- boolean isCancel) {
- PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000);
- long duration = MAX_SWIPE_DURATION;
- float currentShift = mCurrentShift.value;
- final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
- isFling, isCancel);
- float endShift = endTarget.isLauncher ? 1 : 0;
- final float startShift;
- Interpolator interpolator = DEACCEL;
- if (!isFling) {
- long expectedDuration = Math.abs(Math.round((endShift - currentShift)
- * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
- duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
- startShift = currentShift;
- interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
- } else {
- startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y
- * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
- float minFlingVelocity = mContext.getResources()
- .getDimension(R.dimen.quickstep_fling_min_velocity);
- if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
- if (endTarget == RECENTS && !mDeviceState.isFullyGesturalNavMode()) {
- Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
- startShift, endShift, endShift, endVelocity / 1000,
- mTransitionDragLength, mContext);
- endShift = overshoot.end;
- interpolator = overshoot.interpolator;
- duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION,
- MAX_SWIPE_DURATION);
- } else {
- float distanceToTravel = (endShift - currentShift) * mTransitionDragLength;
-
- // we want the page's snap velocity to approximately match the velocity at
- // which the user flings, so we scale the duration by a value near to the
- // derivative of the scroll interpolator at zero, ie. 2.
- long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
- duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
-
- if (endTarget == RECENTS) {
- interpolator = OVERSHOOT_1_2;
- }
- }
- }
- }
-
- if (endTarget.isLauncher && mRecentsAnimationController != null) {
- mRecentsAnimationController.enableInputProxy(mInputConsumer,
- this::createNewInputProxyHandler);
- }
-
- if (endTarget == HOME) {
- setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
- duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
- } else if (endTarget == RECENTS) {
- LiveTileOverlay.INSTANCE.startIconAnimation();
- if (mRecentsView != null) {
- int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
- if (mRecentsView.getNextPage() != nearestPage) {
- // We shouldn't really scroll to the next page when swiping up to recents.
- // Only allow settling on the next page if it's nearest to the center.
- mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration));
- }
- if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) {
- mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION);
- }
- duration = Math.max(duration, mRecentsView.getScroller().getDuration());
- }
- if (mDeviceState.isFullyGesturalNavMode()) {
- setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
- }
- }
-
- // Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
- // or resumeLastTask().
- if (mRecentsView != null) {
- mRecentsView.setOnPageTransitionEndCallback(
- () -> mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED));
- } else {
- mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
- }
-
- animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
- }
-
- private void doLogGesture(GestureEndTarget endTarget) {
- DeviceProfile dp = mDp;
- if (dp == null || mDownPos == null) {
- // We probably never received an animation controller, skip logging.
- return;
- }
-
- int pageIndex = endTarget == LAST_TASK
- ? LOG_NO_OP_PAGE_INDEX
- : mRecentsView.getNextPage();
- UserEventDispatcher.newInstance(mContext).logStateChangeAction(
- mLogAction, mLogDirection,
- (int) mDownPos.x, (int) mDownPos.y,
- ContainerType.NAVBAR, ContainerType.APP,
- endTarget.containerType,
- pageIndex);
- StatsLogManager.EventEnum event;
- switch (endTarget) {
- case HOME:
- event = LAUNCHER_HOME_GESTURE;
- break;
- case RECENTS:
- event = LAUNCHER_OVERVIEW_GESTURE;
- break;
- case LAST_TASK:
- case NEW_TASK:
- event = (mLogDirection == Direction.LEFT)
- ? LAUNCHER_QUICKSWITCH_LEFT
- : LAUNCHER_QUICKSWITCH_RIGHT;
- break;
- default:
- event = IGNORE;
- }
- StatsLogManager.newInstance(mContext).logger()
- .withSrcState(LAUNCHER_STATE_BACKGROUND)
- .withDstState(StatsLogManager.containerTypeToAtomState(endTarget.containerType))
- .log(event);
- }
-
- /** Animates to the given progress, where 0 is the current app and 1 is overview. */
- @UiThread
- private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
- GestureEndTarget target, PointF velocityPxPerMs) {
- runOnRecentsAnimationStart(() -> animateToProgressInternal(start, end, duration,
- interpolator, target, velocityPxPerMs));
- }
-
- protected abstract HomeAnimationFactory createHomeAnimationFactory(long duration);
-
- private TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() {
- @Override
- public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
- boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
- if (task.taskId == mGestureState.getRunningTaskId()
- && TaskInfoCompat.getActivityType(task) != ACTIVITY_TYPE_HOME) {
- // Since this is an edge case, just cancel and relaunch with default activity
- // options (since we don't know if there's an associated app icon to launch from)
- endRunningWindowAnim(true /* cancel */);
- ActivityManagerWrapper.getInstance().unregisterTaskStackListener(
- mActivityRestartListener);
- ActivityManagerWrapper.getInstance().startActivityFromRecents(task.taskId, null);
- }
- }
- };
-
- @UiThread
- private void animateToProgressInternal(float start, float end, long duration,
- Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
- // Set the state, but don't notify until the animation completes
- mGestureState.setEndTarget(target, false /* isAtomic */);
- maybeUpdateRecentsAttachedState();
-
- // If we are transitioning to launcher, then listen for the activity to be restarted while
- // the transition is in progress
- if (mGestureState.getEndTarget().isLauncher) {
- ActivityManagerWrapper.getInstance().registerTaskStackListener(
- mActivityRestartListener);
- }
-
- if (mGestureState.getEndTarget() == HOME) {
- HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration);
- RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
- windowAnim.addAnimatorListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- if (mRecentsAnimationController == null) {
- // If the recents animation is interrupted, we still end the running
- // animation (not canceled) so this is still called. In that case, we can
- // skip doing any future work here for the current gesture.
- return;
- }
- // Finalize the state and notify of the change
- mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
- }
- });
- getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
- windowAnim.start(mContext, velocityPxPerMs);
- homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
- mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
- mLauncherTransitionController = null;
- } else {
- ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
- windowAnim.setDuration(duration).setInterpolator(interpolator);
- windowAnim.addUpdateListener(valueAnimator -> {
- computeRecentsScrollIfInvisible();
- });
- windowAnim.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- if (mRecentsAnimationController == null) {
- // If the recents animation is interrupted, we still end the running
- // animation (not canceled) so this is still called. In that case, we can
- // skip doing any future work here for the current gesture.
- return;
- }
- if (mRecentsView != null) {
- int taskToLaunch = mRecentsView.getNextPage();
- int runningTask = getLastAppearedTaskIndex();
- boolean hasStartedNewTask = hasStartedNewTask();
- if (target == NEW_TASK && taskToLaunch == runningTask
- && !hasStartedNewTask) {
- // We are about to launch the current running task, so use LAST_TASK
- // state instead of NEW_TASK. This could happen, for example, if our
- // scroll is aborted after we determined the target to be NEW_TASK.
- mGestureState.setEndTarget(LAST_TASK);
- } else if (target == LAST_TASK && hasStartedNewTask) {
- // We are about to re-launch the previously running task, but we can't
- // just finish the controller like we normally would because that would
- // instead resume the last task that appeared, and not ensure that this
- // task is restored to the top. To address this, re-launch the task as
- // if it were a new task.
- mGestureState.setEndTarget(NEW_TASK);
- }
- }
- mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
- }
- });
- windowAnim.start();
- mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
- }
- // Always play the entire launcher animation when going home, since it is separate from
- // the animation that has been controlled thus far.
- if (mGestureState.getEndTarget() == HOME) {
- start = 0;
- }
-
- // We want to use the same interpolator as the window, but need to adjust it to
- // interpolate over the remaining progress (end - start).
- TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
- interpolator, start, end);
- if (mLauncherTransitionController == null) {
- return;
- }
- if (start == end || duration <= 0) {
- mLauncherTransitionController.dispatchSetInterpolator(t -> end);
- } else {
- mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
- }
- mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
-
- if (UNSTABLE_SPRINGS.get()) {
- mLauncherTransitionController.dispatchOnStart();
- }
- mLauncherTransitionController.getAnimationPlayer().start();
- mHasLauncherTransitionControllerStarted = true;
- }
-
- private void computeRecentsScrollIfInvisible() {
- if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
- // Views typically don't compute scroll when invisible as an optimization,
- // but in our case we need to since the window offset depends on the scroll.
- mRecentsView.computeScroll();
- }
- }
-
- private void continueComputingRecentsScrollIfNecessary() {
- if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)
- && !mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)
- && !mCanceled) {
- computeRecentsScrollIfInvisible();
- mRecentsView.postOnAnimation(this::continueComputingRecentsScrollIfNecessary);
- }
- }
-
- /**
- * Creates an animation that transforms the current app window into the home app.
- * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
- * @param homeAnimationFactory The home animation factory.
- */
- @Override
- protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
- HomeAnimationFactory homeAnimationFactory) {
- RectFSpringAnim anim =
- super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
- anim.addOnUpdateListener((r, p) -> {
- updateSysUiFlags(Math.max(p, mCurrentShift.value));
- });
- anim.addAnimatorListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationStart(Animator animation) {
- if (mActivity != null) {
- removeLiveTileOverlay();
- }
- }
-
- @Override
- public void onAnimationSuccess(Animator animator) {
- if (mRecentsView != null) {
- mRecentsView.post(mRecentsView::resetTaskVisuals);
- }
- // Make sure recents is in its final state
- maybeUpdateRecentsAttachedState(false);
- mActivityInterface.onSwipeUpToHomeComplete(mDeviceState);
- }
- });
- return anim;
- }
-
- @Override
- public void onConsumerAboutToBeSwitched() {
- if (mActivity != null) {
- // In the off chance that the gesture ends before Launcher is started, we should clear
- // the callback here so that it doesn't update with the wrong state
- mActivity.clearRunOnceOnStartCallback();
- resetLauncherListenersAndOverlays();
- }
- if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
- cancelCurrentAnimation();
- } else {
- reset();
- }
- }
-
- public boolean isCanceled() {
- return mCanceled;
- }
-
- @UiThread
- private void resumeLastTask() {
- mRecentsAnimationController.finish(false /* toRecents */, null);
- ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
- doLogGesture(LAST_TASK);
- reset();
- }
-
- @UiThread
- private void startNewTask() {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- mRecentsAnimationController.finish(true /* toRecents */, this::startNewTaskInternal);
- } else {
- startNewTaskInternal();
- }
- }
-
- @UiThread
- private void startNewTaskInternal() {
- startNewTask(success -> {
- if (!success) {
- reset();
- // We couldn't launch the task, so take user to overview so they can
- // decide what to do instead of staying in this broken state.
- endLauncherTransitionController();
- updateSysUiFlags(1 /* windowProgress == overview */);
- }
- doLogGesture(NEW_TASK);
- });
- }
-
- @Override
- protected void onRestartPreviouslyAppearedTask() {
- super.onRestartPreviouslyAppearedTask();
- reset();
- }
-
- private void reset() {
- mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
- }
-
- /**
- * Cancels any running animation so that the active target can be overriden by a new swipe
- * handle (in case of quick switch).
- */
- private void cancelCurrentAnimation() {
- mCanceled = true;
- mCurrentShift.cancelAnimation();
- if (mLauncherTransitionController != null && mLauncherTransitionController
- .getAnimationPlayer().isStarted()) {
- mLauncherTransitionController.getAnimationPlayer().cancel();
- }
- }
-
- private void invalidateHandler() {
- endRunningWindowAnim(false /* cancel */);
-
- if (mGestureEndCallback != null) {
- mGestureEndCallback.run();
- }
-
- mActivityInitListener.unregister();
- ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mActivityRestartListener);
- mTaskSnapshot = null;
- }
-
- private void invalidateHandlerWithLauncher() {
- endLauncherTransitionController();
-
- mRecentsView.onGestureAnimationEnd();
- resetLauncherListenersAndOverlays();
- }
-
- private void endLauncherTransitionController() {
- setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
- if (mLauncherTransitionController != null) {
- mLauncherTransitionController.getAnimationPlayer().end();
- mLauncherTransitionController = null;
- }
- }
-
- private void resetLauncherListenersAndOverlays() {
- // Reset the callback for deferred activity launches
- if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- mActivityInterface.setOnDeferredActivityLaunchCallback(null);
- }
- mActivity.getRootView().setOnApplyWindowInsetsListener(null);
- removeLiveTileOverlay();
- }
-
- private void notifyTransitionCancelled() {
- mAnimationFactory.onTransitionCancelled();
- }
-
- private void resetStateForAnimationCancel() {
- boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
- mActivityInterface.onTransitionCancelled(wasVisible);
-
- // Leave the pending invisible flag, as it may be used by wallpaper open animation.
- mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
- }
-
- protected void switchToScreenshot() {
- final int runningTaskId = mGestureState.getRunningTaskId();
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- if (mRecentsAnimationController != null) {
- mRecentsAnimationController.getController().setWillFinishToHome(true);
- // Update the screenshot of the task
- if (mTaskSnapshot == null) {
- mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
- }
- mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, false /* refreshNow */);
- }
- mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
- } else if (!hasTargets()) {
- // If there are no targets, then we don't need to capture anything
- mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
- } else {
- boolean finishTransitionPosted = false;
- if (mRecentsAnimationController != null) {
- // Update the screenshot of the task
- if (mTaskSnapshot == null) {
- mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
- }
- final TaskView taskView;
- if (mGestureState.getEndTarget() == HOME) {
- // Capture the screenshot before finishing the transition to home to ensure it's
- // taken in the correct orientation, but no need to update the thumbnail.
- taskView = null;
- } else {
- taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot);
- }
- if (taskView != null && !mCanceled) {
- // Defer finishing the animation until the next launcher frame with the
- // new thumbnail
- finishTransitionPosted = ViewUtils.postDraw(taskView,
- () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
- this::isCanceled);
- }
- }
- if (!finishTransitionPosted) {
- // If we haven't posted a draw callback, set the state immediately.
- Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
- TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
- mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
- TraceHelper.INSTANCE.endSection(traceToken);
- }
- }
- }
-
- private void finishCurrentTransitionToRecents() {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
- } else if (!hasTargets() || mRecentsAnimationController == null) {
- // If there are no targets or the animation not started, then there is nothing to finish
- mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
- } else {
- mRecentsAnimationController.finish(true /* toRecents */,
- () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
- }
- ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
- }
-
- private void finishCurrentTransitionToHome() {
- if (!hasTargets() || mRecentsAnimationController == null) {
- // If there are no targets or the animation not started, then there is nothing to finish
- mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
- } else {
- finishRecentsControllerToHome(
- () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
- }
- ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
- doLogGesture(HOME);
- }
-
- protected abstract void finishRecentsControllerToHome(Runnable callback);
-
- private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
- endLauncherTransitionController();
- mActivityInterface.onSwipeUpToRecentsComplete();
- if (mRecentsAnimationController != null) {
- mRecentsAnimationController.setDeferCancelUntilNextTransition(true /* defer */,
- true /* screenshot */);
- }
- mRecentsView.onSwipeUpAnimationSuccess();
-
- SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG);
- doLogGesture(RECENTS);
- reset();
- }
-
- private void addLiveTileOverlay() {
- if (LiveTileOverlay.INSTANCE.attach(mActivity.getRootView().getOverlay())) {
- mRecentsView.setLiveTileOverlayAttached(true);
- }
- }
-
- private void removeLiveTileOverlay() {
- LiveTileOverlay.INSTANCE.detach(mActivity.getRootView().getOverlay());
- mRecentsView.setLiveTileOverlayAttached(false);
- }
-
- private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
- return app.isNotInRecents
- || app.activityType == ACTIVITY_TYPE_HOME;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
deleted file mode 100644
index 33b9cde..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-import static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
-import static com.android.quickstep.fallback.RecentsState.DEFAULT;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.view.MotionEvent;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.fallback.RecentsState;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * {@link BaseActivityInterface} for recents when the default launcher is different than the
- * currently running one and apps should interact with the {@link RecentsActivity} as opposed
- * to the in-launcher one.
- */
-public final class FallbackActivityInterface extends
- BaseActivityInterface<RecentsState, RecentsActivity> {
-
- public static final FallbackActivityInterface INSTANCE = new FallbackActivityInterface();
-
- private FallbackActivityInterface() {
- super(false, DEFAULT, BACKGROUND_APP);
- }
-
- /** 2 */
- @Override
- public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
- PagedOrientationHandler orientationHandler) {
- calculateTaskSize(context, dp, outRect, orientationHandler);
- if (dp.isVerticalBarLayout()
- && SysUINavigationMode.INSTANCE.get(context).getMode() != NO_BUTTON) {
- Rect targetInsets = dp.getInsets();
- int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
- return dp.hotseatBarSizePx + hotseatInset;
- } else {
- return dp.heightPx - outRect.bottom;
- }
- }
-
- /** 4 */
- @Override
- public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
- onSwipeUpToRecentsComplete();
- }
-
- /** 5 */
- @Override
- public void onAssistantVisibilityChanged(float visibility) {
- // This class becomes active when the screen is locked.
- // Rather than having it handle assistant visibility changes, the assistant visibility is
- // set to zero prior to this class becoming active.
- }
-
- /** 6 */
- @Override
- public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
- boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
- DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
- factory.initUI();
- return factory;
- }
-
- @Override
- public ActivityInitListener createActivityInitListener(
- Predicate<Boolean> onInitListener) {
- return new ActivityInitListener<>((activity, alreadyOnHome) ->
- onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
- }
-
- @Nullable
- @Override
- public RecentsActivity getCreatedActivity() {
- return RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
- }
-
- @Nullable
- @Override
- public RecentsView getVisibleRecentsView() {
- RecentsActivity activity = getCreatedActivity();
- if (activity != null && activity.hasWindowFocus()) {
- return activity.getOverviewPanel();
- }
- return null;
- }
-
- @Override
- public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
- return false;
- }
-
- @Override
- public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
- // TODO: Remove this once b/77875376 is fixed
- return target.screenSpaceBounds;
- }
-
- @Override
- public boolean allowMinimizeSplitScreen() {
- // TODO: Remove this once b/77875376 is fixed
- return false;
- }
-
- @Override
- public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
- // In non-gesture mode, user might be clicking on the home button which would directly
- // start the home activity instead of going through recents. In that case, defer starting
- // recents until we are sure it is a gesture.
- return !deviceState.isFullyGesturalNavMode()
- || super.deferStartingActivity(deviceState, ev);
- }
-
- @Override
- public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
- // no-op, fake landscape not supported for 3P
- }
-
- @Override
- public int getContainerType() {
- RecentsActivity activity = getCreatedActivity();
- boolean visible = activity != null && activity.isStarted() && activity.hasWindowFocus();
- return visible
- ? LauncherLogProto.ContainerType.OTHER_LAUNCHER_APP
- : LauncherLogProto.ContainerType.APP;
- }
-
- @Override
- public boolean isInLiveTileMode() {
- return false;
- }
-
- @Override
- public void onLaunchTaskFailed() {
- // TODO: probably go back to overview instead.
- RecentsActivity activity = getCreatedActivity();
- if (activity == null) {
- return;
- }
- activity.<RecentsView>getOverviewPanel().startHome();
- }
-
- @Override
- protected float getExtraSpace(Context context, DeviceProfile dp,
- PagedOrientationHandler orientationHandler) {
- return showOverviewActions(context)
- ? context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height)
- : 0;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
deleted file mode 100644
index fc7a119..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-
-import android.animation.ObjectAnimator;
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Matrix;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.util.TransformParams.BuilderProxy;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.InputConsumerController;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-
-/**
- * Handles the navigation gestures when a 3rd party launcher is the default home activity.
- */
-public class FallbackSwipeHandler extends
- BaseSwipeUpHandlerV2<RecentsActivity, FallbackRecentsView> {
-
- private FallbackHomeAnimationFactory mActiveAnimationFactory;
- private final boolean mRunningOverHome;
-
- private final Matrix mTmpMatrix = new Matrix();
- private float mMaxLauncherScale = 1;
-
- public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
- TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
- boolean continuingLastGesture, InputConsumerController inputConsumer) {
- super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
- continuingLastGesture, inputConsumer);
-
- mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
- if (mRunningOverHome) {
- mTransformParams.setHomeBuilderProxy(this::updateHomeActivityTransformDuringSwipeUp);
- }
- }
-
- @Override
- protected void initTransitionEndpoints(DeviceProfile dp) {
- super.initTransitionEndpoints(dp);
- if (mRunningOverHome) {
- mMaxLauncherScale = 1 / mTaskViewSimulator.getFullScreenScale();
- }
- }
-
- private void updateHomeActivityTransformDuringSwipeUp(SurfaceParams.Builder builder,
- RemoteAnimationTargetCompat app, TransformParams params) {
- setHomeScaleAndAlpha(builder, app, mCurrentShift.value,
- Utilities.boundToRange(1 - mCurrentShift.value, 0, 1));
- }
-
- private void setHomeScaleAndAlpha(SurfaceParams.Builder builder,
- RemoteAnimationTargetCompat app, float verticalShift, float alpha) {
- float scale = Utilities.mapRange(verticalShift, 1, mMaxLauncherScale);
- mTmpMatrix.setScale(scale, scale,
- app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
- builder.withMatrix(mTmpMatrix).withAlpha(alpha);
- }
-
- @Override
- protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
- mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
- ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
- mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle());
- return mActiveAnimationFactory;
- }
-
- @Override
- protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
- if (mActiveAnimationFactory != null
- && mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTarget)) {
- mActiveAnimationFactory = null;
- return false;
- }
-
- return super.handleTaskAppeared(appearedTaskTarget);
- }
-
- @Override
- protected void finishRecentsControllerToHome(Runnable callback) {
- mRecentsAnimationController.finish(
- false /* toRecents */, callback, true /* sendUserLeaveHint */);
- }
-
- @Override
- protected void switchToScreenshot() {
- if (mRunningOverHome) {
- // When the current task is home, then we don't need to capture anything
- mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
- } else {
- super.switchToScreenshot();
- }
- }
-
- @Override
- protected void notifyGestureAnimationStartToRecents() {
- if (mRunningOverHome) {
- mRecentsView.onGestureAnimationStartOnHome(mGestureState.getRunningTask());
- } else {
- super.notifyGestureAnimationStartToRecents();
- }
- }
-
- private class FallbackHomeAnimationFactory extends HomeAnimationFactory {
-
- private final TransformParams mHomeAlphaParams = new TransformParams();
- private final AnimatedFloat mHomeAlpha;
-
- private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat();
-
- private final AnimatedFloat mRecentsAlpha = new AnimatedFloat();
-
- private final long mDuration;
- FallbackHomeAnimationFactory(long duration) {
- super(null);
- mDuration = duration;
-
- if (mRunningOverHome) {
- mHomeAlpha = new AnimatedFloat();
- mHomeAlpha.value = Utilities.boundToRange(1 - mCurrentShift.value, 0, 1);
- mVerticalShiftForScale.value = mCurrentShift.value;
- mTransformParams.setHomeBuilderProxy(
- this::updateHomeActivityTransformDuringHomeAnim);
- } else {
- mHomeAlpha = new AnimatedFloat(this::updateHomeAlpha);
- mHomeAlpha.value = 0;
-
- mHomeAlphaParams.setHomeBuilderProxy(
- this::updateHomeActivityTransformDuringHomeAnim);
- }
-
- mRecentsAlpha.value = 1;
- mTransformParams.setBaseBuilderProxy(
- this::updateRecentsActivityTransformDuringHomeAnim);
- }
-
- private void updateRecentsActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
- RemoteAnimationTargetCompat app, TransformParams params) {
- builder.withAlpha(mRecentsAlpha.value);
- }
-
- private void updateHomeActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
- RemoteAnimationTargetCompat app, TransformParams params) {
- setHomeScaleAndAlpha(builder, app, mVerticalShiftForScale.value, mHomeAlpha.value);
- }
-
- @NonNull
- @Override
- public AnimatorPlaybackController createActivityAnimationToHome() {
- PendingAnimation pa = new PendingAnimation(mDuration);
- pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCEL);
- return pa.createPlaybackController();
- }
-
- private void updateHomeAlpha() {
- if (mHomeAlphaParams.getTargetSet() != null) {
- mHomeAlphaParams.applySurfaceParams(
- mHomeAlphaParams.createSurfaceParams(BuilderProxy.NO_OP));
- }
- }
-
- public boolean handleHomeTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
- if (appearedTaskTarget.activityType == ACTIVITY_TYPE_HOME) {
- RemoteAnimationTargets targets = new RemoteAnimationTargets(
- new RemoteAnimationTargetCompat[] {appearedTaskTarget},
- new RemoteAnimationTargetCompat[0], appearedTaskTarget.mode);
- mHomeAlphaParams.setTargetSet(targets);
- updateHomeAlpha();
- return true;
- }
- return false;
- }
-
- @Override
- public void playAtomicAnimation(float velocity) {
- ObjectAnimator alphaAnim = mHomeAlpha.animateToValue(mHomeAlpha.value, 1);
- alphaAnim.setDuration(mDuration).setInterpolator(ACCEL);
- alphaAnim.start();
-
- if (mRunningOverHome) {
- // Spring back launcher scale
- new SpringAnimationBuilder(mContext)
- .setStartValue(mVerticalShiftForScale.value)
- .setEndValue(0)
- .setStartVelocity(-velocity / mTransitionDragLength)
- .setMinimumVisibleChange(1f / mDp.heightPx)
- .setDampingRatio(0.6f)
- .setStiffness(800)
- .build(mVerticalShiftForScale, AnimatedFloat.VALUE)
- .start();
- }
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
deleted file mode 100644
index ba8ba33..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static android.content.Intent.EXTRA_STREAM;
-import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.quickstep.util.ImageActionUtils.persistBitmapAndStartActivity;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.BuildConfig;
-import com.android.quickstep.util.ImageActionUtils;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.function.Supplier;
-
-/**
- * Contains image selection functions necessary to complete overview action button functions.
- */
-public class ImageActionsApi {
-
- private static final String TAG = BuildConfig.APPLICATION_ID + "ImageActionsApi";
-
- protected final Context mContext;
- protected final Supplier<Bitmap> mBitmapSupplier;
- protected final SystemUiProxy mSystemUiProxy;
-
- public ImageActionsApi(Context context, Supplier<Bitmap> bitmapSupplier) {
- mContext = context;
- mBitmapSupplier = bitmapSupplier;
- mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
- }
-
- /**
- * Share the image this api was constructed with using the provided intent. The implementation
- * should add an {@link Intent#EXTRA_STREAM} with the URI pointing to the image to the intent.
- */
- @UiThread
- public void shareWithExplicitIntent(@Nullable Rect crop, Intent intent) {
- if (mBitmapSupplier.get() == null) {
- Log.e(TAG, "No snapshot available, not starting share.");
- return;
- }
-
- UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(mContext,
- mBitmapSupplier.get(), crop, intent, (uri, intentForUri) -> {
- intentForUri
- .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
- .putExtra(EXTRA_STREAM, uri);
- return new Intent[]{intentForUri};
- }, TAG));
-
- }
-
- /**
- * Share the image this api was constructed with.
- */
- @UiThread
- public void startShareActivity() {
- ImageActionUtils.startShareActivity(mContext, mBitmapSupplier, null, null, TAG);
- }
-
- /**
- * @param screenshot to be saved to the media store.
- * @param screenshotBounds the location of where the bitmap was laid out on the screen in
- * screen coordinates.
- * @param visibleInsets that are used to draw the screenshot within the bounds.
- * @param task of the task that the screenshot was taken of.
- */
- public void saveScreenshot(Bitmap screenshot, Rect screenshotBounds,
- Insets visibleInsets, Task.TaskKey task) {
- ImageActionUtils.saveScreenshot(mSystemUiProxy, screenshot, screenshotBounds, visibleInsets,
- task);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
deleted file mode 100644
index edefbe1..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ /dev/null
@@ -1,345 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_SHELF_ANIM;
-import static com.android.quickstep.SysUINavigationMode.getMode;
-import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
-import static com.android.quickstep.util.LayoutUtils.getDefaultSwipeHeight;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.util.Log;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherInitListener;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.plugins.shared.LauncherOverlayManager;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * {@link BaseActivityInterface} for the in-launcher recents.
- */
-public final class LauncherActivityInterface extends
- BaseActivityInterface<LauncherState, BaseQuickstepLauncher> {
-
- public static final LauncherActivityInterface INSTANCE = new LauncherActivityInterface();
-
- private LauncherActivityInterface() {
- super(true, OVERVIEW, BACKGROUND_APP);
- }
-
- @Override
- public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
- PagedOrientationHandler orientationHandler) {
- calculateTaskSize(context, dp, outRect, orientationHandler);
- if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
- Rect targetInsets = dp.getInsets();
- int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
- return dp.hotseatBarSizePx + hotseatInset;
- } else {
- return LayoutUtils.getShelfTrackingDistance(context, dp, orientationHandler);
- }
- }
-
- @Override
- public void onSwipeUpToRecentsComplete() {
- super.onSwipeUpToRecentsComplete();
- Launcher launcher = getCreatedActivity();
- if (launcher != null) {
- RecentsView recentsView = launcher.getOverviewPanel();
- DiscoveryBounce.showForOverviewIfNeeded(launcher,
- recentsView.getPagedOrientationHandler());
- }
- }
-
- @Override
- public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
- Launcher launcher = getCreatedActivity();
- if (launcher == null) {
- return;
- }
- // Ensure recents is at the correct position for NORMAL state. For example, when we detach
- // recents, we assume the first task is invisible, making translation off by one task.
- launcher.getStateManager().reapplyState();
- launcher.getRootView().setForceHideBackArrow(false);
- notifyRecentsOfOrientation(deviceState);
- }
-
- @Override
- public void onAssistantVisibilityChanged(float visibility) {
- Launcher launcher = getCreatedActivity();
- if (launcher == null) {
- return;
- }
- launcher.onAssistantVisibilityChanged(visibility);
- }
-
- @Override
- public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
- boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
- notifyRecentsOfOrientation(deviceState);
- DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
- @Override
- public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
- long duration) {
- mActivity.getShelfPeekAnim().setShelfState(shelfState, interpolator, duration);
- }
-
- @Override
- protected void createBackgroundToOverviewAnim(BaseQuickstepLauncher activity,
- PendingAnimation pa) {
- super.createBackgroundToOverviewAnim(activity, pa);
-
- if (!activity.getDeviceProfile().isVerticalBarLayout()
- && SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
- // Don't animate the shelf when the mode is NO_BUTTON, because we
- // update it atomically.
- pa.add(activity.getStateManager().createStateElementAnimation(
- INDEX_SHELF_ANIM,
- BACKGROUND_APP.getVerticalProgress(activity),
- OVERVIEW.getVerticalProgress(activity)));
- }
-
- // Animate the blur and wallpaper zoom
- float fromDepthRatio = BACKGROUND_APP.getDepth(activity);
- float toDepthRatio = OVERVIEW.getDepth(activity);
- pa.addFloat(getDepthController(),
- new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
- fromDepthRatio, toDepthRatio, LINEAR);
-
- }
- };
-
- BaseQuickstepLauncher launcher = factory.initUI();
- // Since all apps is not visible, we can safely reset the scroll position.
- // This ensures then the next swipe up to all-apps starts from scroll 0.
- launcher.getAppsView().reset(false /* animate */);
- return factory;
- }
-
- @Override
- public ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
- return new LauncherInitListener((activity, alreadyOnHome) ->
- onInitListener.test(alreadyOnHome));
- }
-
- @Override
- public void setOnDeferredActivityLaunchCallback(Runnable r) {
- Launcher launcher = getCreatedActivity();
- if (launcher == null) {
- return;
- }
- launcher.setOnDeferredActivityLaunchCallback(r);
- }
-
- @Nullable
- @Override
- public BaseQuickstepLauncher getCreatedActivity() {
- return BaseQuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
- }
-
- @Nullable
- @Override
- public DepthController getDepthController() {
- BaseQuickstepLauncher launcher = getCreatedActivity();
- if (launcher == null) {
- return null;
- }
- return launcher.getDepthController();
- }
-
- @Nullable
- @Override
- public RecentsView getVisibleRecentsView() {
- Launcher launcher = getVisibleLauncher();
- return launcher != null && launcher.getStateManager().getState().overviewUi
- ? launcher.getOverviewPanel() : null;
- }
-
- @Nullable
- @UiThread
- private Launcher getVisibleLauncher() {
- Launcher launcher = getCreatedActivity();
- return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus()
- ? launcher : null;
- }
-
- @Override
- public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "switchToRecentsIfVisible");
- }
- Launcher launcher = getVisibleLauncher();
- if (launcher == null) {
- return false;
- }
-
- launcher.getUserEventDispatcher().logActionCommand(
- LauncherLogProto.Action.Command.RECENTS_BUTTON,
- getContainerType(),
- LauncherLogProto.ContainerType.TASKSWITCHER);
- launcher.getStateManager().goToState(OVERVIEW,
- launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
- return true;
- }
-
-
- @Override
- public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
- final StateManager<LauncherState> stateManager = getCreatedActivity().getStateManager();
- stateManager.addStateListener(
- new StateManager.StateListener<LauncherState>() {
- @Override
- public void onStateTransitionComplete(LauncherState toState) {
- // Are we going from Recents to Workspace?
- if (toState == LauncherState.NORMAL) {
- exitRunnable.run();
- notifyRecentsOfOrientation(deviceState);
- stateManager.removeStateListener(this);
- }
- }
- });
- }
-
- private void notifyRecentsOfOrientation(RecentsAnimationDeviceState deviceState) {
- // reset layout on swipe to home
- RecentsView recentsView = getCreatedActivity().getOverviewPanel();
- recentsView.setLayoutRotation(deviceState.getCurrentActiveRotation(),
- deviceState.getDisplayRotation());
- }
-
- @Override
- public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
- return homeBounds;
- }
-
- @Override
- public boolean allowMinimizeSplitScreen() {
- return true;
- }
-
- @Override
- public void updateOverviewPredictionState() {
- Launcher launcher = getCreatedActivity();
- if (launcher == null) {
- return;
- }
- PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
- PredictionUiStateManager.Client.OVERVIEW);
- }
-
- @Override
- public int getContainerType() {
- final Launcher launcher = getVisibleLauncher();
- return launcher != null ? launcher.getStateManager().getState().containerType
- : LauncherLogProto.ContainerType.APP;
- }
-
- @Override
- public boolean isInLiveTileMode() {
- Launcher launcher = getCreatedActivity();
- return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
- launcher.isStarted();
- }
-
- @Override
- public void onLaunchTaskFailed() {
- Launcher launcher = getCreatedActivity();
- if (launcher == null) {
- return;
- }
- launcher.getStateManager().goToState(OVERVIEW);
- }
-
- @Override
- public void closeOverlay() {
- Launcher launcher = getCreatedActivity();
- if (launcher == null) {
- return;
- }
- LauncherOverlayManager om = launcher.getOverlayManager();
- if (!launcher.isStarted() || launcher.isForceInvisible()) {
- om.hideOverlay(false /* animate */);
- } else {
- om.hideOverlay(150);
- }
- }
-
- @Override
- protected float getExtraSpace(Context context, DeviceProfile dp,
- PagedOrientationHandler orientationHandler) {
- if ((dp.isVerticalBarLayout() && !showOverviewActions(context))
- || hideShelfInTwoButtonLandscape(context, orientationHandler)) {
- return 0;
- } else {
- Resources res = context.getResources();
- if (showOverviewActions(context)) {
- //TODO: this needs to account for the swipe gesture height and accessibility
- // UI when shown.
- float actionsBottomMargin = 0;
- if (!dp.isVerticalBarLayout()) {
- if (getMode(context) == Mode.THREE_BUTTONS) {
- actionsBottomMargin = res.getDimensionPixelSize(
- R.dimen.overview_actions_bottom_margin_three_button);
- } else {
- actionsBottomMargin = res.getDimensionPixelSize(
- R.dimen.overview_actions_bottom_margin_gesture);
- }
- }
- float actionsHeight = actionsBottomMargin
- + res.getDimensionPixelSize(R.dimen.overview_actions_height);
- return actionsHeight;
- } else {
- return getDefaultSwipeHeight(context, dp) + dp.workspacePageIndicatorHeight
- + res.getDimensionPixelSize(
- R.dimen.dynamic_grid_hotseat_extra_vertical_size)
- + res.getDimensionPixelSize(
- R.dimen.dynamic_grid_hotseat_bottom_padding);
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
deleted file mode 100644
index fa7d268..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-
-import android.animation.AnimatorSet;
-import android.content.Context;
-import android.graphics.RectF;
-import android.os.UserHandle;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.views.FloatingIconView;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.InputConsumerController;
-
-/**
- * Temporary class to allow easier refactoring
- */
-public class LauncherSwipeHandlerV2 extends
- BaseSwipeUpHandlerV2<BaseQuickstepLauncher, RecentsView> {
-
- public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
- TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
- boolean continuingLastGesture, InputConsumerController inputConsumer) {
- super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
- continuingLastGesture, inputConsumer);
- }
-
-
- @Override
- protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
- HomeAnimationFactory homeAnimFactory;
- if (mActivity != null) {
- final TaskView runningTaskView = mRecentsView.getRunningTaskView();
- final View workspaceView;
- if (runningTaskView != null
- && runningTaskView.getTask().key.getComponent() != null) {
- workspaceView = mActivity.getWorkspace().getFirstMatchForAppClose(
- runningTaskView.getTask().key.getComponent().getPackageName(),
- UserHandle.of(runningTaskView.getTask().key.userId));
- } else {
- workspaceView = null;
- }
- final RectF iconLocation = new RectF();
- boolean canUseWorkspaceView =
- workspaceView != null && workspaceView.isAttachedToWindow();
- FloatingIconView floatingIconView = canUseWorkspaceView
- ? FloatingIconView.getFloatingIconView(mActivity, workspaceView,
- true /* hideOriginal */, iconLocation, false /* isOpening */)
- : null;
-
- mActivity.getRootView().setForceHideBackArrow(true);
- mActivity.setHintUserWillBeActive();
-
- homeAnimFactory = new HomeAnimationFactory(floatingIconView) {
-
- @Override
- public RectF getWindowTargetRect() {
- if (canUseWorkspaceView) {
- return iconLocation;
- } else {
- return super.getWindowTargetRect();
- }
- }
-
- @NonNull
- @Override
- public AnimatorPlaybackController createActivityAnimationToHome() {
- // Return an empty APC here since we have an non-user controlled animation
- // to home.
- long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
- return mActivity.getStateManager().createAnimationToNewWorkspace(
- NORMAL, accuracy, 0 /* animComponents */);
- }
-
- @Override
- public void playAtomicAnimation(float velocity) {
- new StaggeredWorkspaceAnim(mActivity, velocity,
- true /* animateOverviewScrim */).start();
- }
- };
-
- } else {
- homeAnimFactory = new HomeAnimationFactory(null) {
- @Override
- public AnimatorPlaybackController createActivityAnimationToHome() {
- return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
- }
- };
- mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
- isPresent -> mRecentsView.startHome());
- }
- return homeAnimFactory;
- }
-
- @Override
- protected void finishRecentsControllerToHome(Runnable callback) {
- mRecentsAnimationController.finish(
- true /* toRecents */, callback, true /* sendUserLeaveHint */);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
deleted file mode 100644
index 434a929..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.SystemClock;
-import android.view.ViewConfiguration;
-
-import androidx.annotation.BinderThread;
-
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.LatencyTrackerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Helper class to handle various atomic commands for switching between Overview.
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class OverviewCommandHelper {
-
- private final Context mContext;
- private final RecentsAnimationDeviceState mDeviceState;
- private final RecentsModel mRecentsModel;
- private final OverviewComponentObserver mOverviewComponentObserver;
-
- private long mLastToggleTime;
-
- public OverviewCommandHelper(Context context, RecentsAnimationDeviceState deviceState,
- OverviewComponentObserver observer) {
- mContext = context;
- mDeviceState = deviceState;
- mRecentsModel = RecentsModel.INSTANCE.get(mContext);
- mOverviewComponentObserver = observer;
- }
-
- @BinderThread
- public void onOverviewToggle() {
- // If currently screen pinning, do not enter overview
- if (mDeviceState.isScreenPinningActive()) {
- return;
- }
-
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
- MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
- }
-
- @BinderThread
- public void onOverviewShown(boolean triggeredFromAltTab) {
- if (triggeredFromAltTab) {
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
- }
- MAIN_EXECUTOR.execute(new ShowRecentsCommand(triggeredFromAltTab));
- }
-
- @BinderThread
- public void onOverviewHidden() {
- MAIN_EXECUTOR.execute(new HideRecentsCommand());
- }
-
- @BinderThread
- public void onTip(int actionType, int viewType) {
- MAIN_EXECUTOR.execute(() ->
- UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
- }
-
- private class ShowRecentsCommand extends RecentsActivityCommand {
-
- private final boolean mTriggeredFromAltTab;
-
- ShowRecentsCommand(boolean triggeredFromAltTab) {
- mTriggeredFromAltTab = triggeredFromAltTab;
- }
-
- @Override
- protected boolean handleCommand(long elapsedTime) {
- // TODO: Go to the next page if started from alt-tab.
- return mActivityInterface.getVisibleRecentsView() != null;
- }
-
- @Override
- protected void onTransitionComplete() {
- // TODO(b/138729100) This doesn't execute first time launcher is run
- if (mTriggeredFromAltTab) {
- RecentsView rv = mActivityInterface.getVisibleRecentsView();
- if (rv == null) {
- return;
- }
-
- // Ensure that recents view has focus so that it receives the followup key inputs
- TaskView taskView = rv.getNextTaskView();
- if (taskView == null) {
- if (rv.getTaskViewCount() > 0) {
- taskView = rv.getTaskViewAt(0);
- taskView.requestFocus();
- } else {
- rv.requestFocus();
- }
- } else {
- taskView.requestFocus();
- }
- }
- }
- }
-
- private class HideRecentsCommand extends RecentsActivityCommand {
-
- @Override
- protected boolean handleCommand(long elapsedTime) {
- RecentsView recents = mActivityInterface.getVisibleRecentsView();
- if (recents == null) {
- return false;
- }
- int currentPage = recents.getNextPage();
- if (currentPage >= 0 && currentPage < recents.getTaskViewCount()) {
- ((TaskView) recents.getPageAt(currentPage)).launchTask(true);
- } else {
- recents.startHome();
- }
- return true;
- }
- }
-
- private class RecentsActivityCommand<T extends StatefulActivity<?>> implements Runnable {
-
- protected final BaseActivityInterface<?, T> mActivityInterface;
- private final long mCreateTime;
- private final AppToOverviewAnimationProvider<T> mAnimationProvider;
-
- private final long mToggleClickedTime = SystemClock.uptimeMillis();
- private boolean mUserEventLogged;
- private ActivityInitListener mListener;
-
- public RecentsActivityCommand() {
- mActivityInterface = mOverviewComponentObserver.getActivityInterface();
- mCreateTime = SystemClock.elapsedRealtime();
- mAnimationProvider = new AppToOverviewAnimationProvider<>(mActivityInterface,
- RecentsModel.getRunningTaskId(), mDeviceState);
-
- // Preload the plan
- mRecentsModel.getTasks(null);
- }
-
- @Override
- public void run() {
- long elapsedTime = mCreateTime - mLastToggleTime;
- mLastToggleTime = mCreateTime;
-
- if (handleCommand(elapsedTime)) {
- // Command already handled.
- return;
- }
-
- if (mActivityInterface.switchToRecentsIfVisible(this::onTransitionComplete)) {
- // If successfully switched, then return
- return;
- }
-
- // Otherwise, start overview.
- mListener = mActivityInterface.createActivityInitListener(this::onActivityReady);
- mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
- new RemoteAnimationProvider() {
- @Override
- public AnimatorSet createWindowAnimation(
- RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets) {
- return RecentsActivityCommand.this.createWindowAnimation(appTargets,
- wallpaperTargets);
- }
- }, mContext, MAIN_EXECUTOR.getHandler(),
- mAnimationProvider.getRecentsLaunchDuration());
- }
-
- 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 = mActivityInterface.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(Boolean wasVisible) {
- final T activity = mActivityInterface.getCreatedActivity();
- if (!mUserEventLogged) {
- activity.getUserEventDispatcher().logActionCommand(
- LauncherLogProto.Action.Command.RECENTS_BUTTON,
- mActivityInterface.getContainerType(),
- LauncherLogProto.ContainerType.TASKSWITCHER);
- mUserEventLogged = true;
- }
-
- // Switch prediction client to overview
- PredictionUiStateManager.INSTANCE.get(activity).switchClient(
- PredictionUiStateManager.Client.OVERVIEW);
- return mAnimationProvider.onActivityReady(activity, wasVisible);
- }
-
- private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets) {
- if (LatencyTrackerCompat.isEnabled(mContext)) {
- LatencyTrackerCompat.logToggleRecents(
- (int) (SystemClock.uptimeMillis() - mToggleClickedTime));
- }
-
- mListener.unregister();
-
- AnimatorSet animatorSet = mAnimationProvider.createWindowAnimation(appTargets,
- wallpaperTargets);
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- onTransitionComplete();
- }
- });
- return animatorSet;
- }
-
- protected void onTransitionComplete() { }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
deleted file mode 100644
index 6f4d34c..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.android.quickstep;
-
-import android.app.Activity;
-import android.content.Context;
-import android.os.Bundle;
-
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.testing.TestInformationHandler;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
-import com.android.quickstep.util.LayoutUtils;
-
-public class QuickstepTestInformationHandler extends TestInformationHandler {
-
- protected final Context mContext;
-
- public QuickstepTestInformationHandler(Context context) {
- mContext = context;
- }
-
- @Override
- public Bundle call(String method) {
- final Bundle response = new Bundle();
- switch (method) {
- case TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT: {
- return getLauncherUIProperty(Bundle::putInt, l -> {
- final float progress = LauncherState.OVERVIEW.getVerticalProgress(l)
- - LauncherState.ALL_APPS.getVerticalProgress(l);
- final float distance = l.getAllAppsController().getShiftRange() * progress;
- return (int) distance;
- });
- }
-
- case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
- final float swipeHeight =
- LayoutUtils.getDefaultSwipeHeight(mContext, mDeviceProfile);
- response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
- return response;
- }
-
- case TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT: {
- final float swipeHeight =
- LayoutUtils.getShelfTrackingDistance(mContext, mDeviceProfile,
- PagedOrientationHandler.PORTRAIT);
- response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
- return response;
- }
-
- case TestProtocol.REQUEST_HOTSEAT_TOP: {
- return getLauncherUIProperty(
- Bundle::putInt, PortraitStatesTouchController::getHotseatTop);
- }
-
- case TestProtocol.REQUEST_OVERVIEW_ACTIONS_ENABLED: {
- response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
- FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
- return response;
- }
- }
-
- return super.call(method);
- }
-
- @Override
- protected Activity getCurrentActivity() {
- RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
- OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
- try {
- return observer.getActivityInterface().getCreatedActivity();
- } finally {
- observer.onDestroy();
- rads.destroy();
- }
- }
-
- @Override
- protected boolean isLauncherInitialized() {
- return super.isLauncherInitialized() && TouchInteractionService.isInitialized();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
deleted file mode 100644
index 6f2f9fb..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
-import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
-
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.STATUS_BAR_TRANSITION_PRE_DELAY;
-import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
-import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
-import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
-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.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAnimationRunner;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
-import com.android.launcher3.statemanager.StateManager.StateHandler;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.util.ActivityTracker;
-import com.android.launcher3.util.SystemUiController;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.fallback.FallbackRecentsStateController;
-import com.android.quickstep.fallback.FallbackRecentsView;
-import com.android.quickstep.fallback.RecentsDragLayer;
-import com.android.quickstep.fallback.RecentsState;
-import com.android.quickstep.util.RecentsAtomicAnimationFactory;
-import com.android.quickstep.views.OverviewActionsView;
-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 recents activity that shows the recently launched tasks as swipable task cards.
- * See {@link com.android.quickstep.views.RecentsView}.
- */
-public final class RecentsActivity extends StatefulActivity<RecentsState> {
-
- public static final ActivityTracker<RecentsActivity> ACTIVITY_TRACKER =
- new ActivityTracker<>();
-
- private Handler mUiHandler = new Handler(Looper.getMainLooper());
-
- private RecentsDragLayer mDragLayer;
- private FallbackRecentsView mFallbackRecentsView;
- private OverviewActionsView mActionsView;
-
- private Configuration mOldConfig;
-
- private StateManager<RecentsState> mStateManager;
-
- /**
- * Init drag layer and overview panel views.
- */
- protected void setupViews() {
- inflateRootView(R.layout.fallback_recents_activity);
- setContentView(getRootView());
- mDragLayer = findViewById(R.id.drag_layer);
- mFallbackRecentsView = findViewById(R.id.overview_panel);
- mActionsView = findViewById(R.id.overview_actions_view);
-
- mDragLayer.recreateControllers();
- mFallbackRecentsView.init(mActionsView);
- }
-
- @Override
- public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
- onHandleConfigChanged();
- super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- ACTIVITY_TRACKER.handleNewIntent(this, intent);
- }
-
- /**
- * Logic for when device configuration changes (rotation, screen size change, multi-window,
- * etc.)
- */
- protected void onHandleConfigChanged() {
- mUserEventDispatcher = null;
- initDeviceProfile();
-
- AbstractFloatingView.closeOpenViews(this, true,
- AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
- dispatchDeviceProfileChanged();
-
- reapplyUi();
- mDragLayer.recreateControllers();
- }
-
- /**
- * Generate the device profile to use in this activity.
- * @return device profile
- */
- protected DeviceProfile createDeviceProfile() {
- DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this);
-
- // In case we are reusing IDP, create a copy so that we don't conflict with Launcher
- // activity.
- return (mDragLayer != null) && isInMultiWindowMode()
- ? dp.getMultiWindowProfile(this, getMultiWindowDisplaySize())
- : dp.copy(this);
- }
-
- @Override
- public BaseDragLayer getDragLayer() {
- return mDragLayer;
- }
-
- @Override
- public <T extends View> T getOverviewPanel() {
- return (T) mFallbackRecentsView;
- }
-
- public OverviewActionsView getActionsView() {
- return mActionsView;
- }
-
- @Override
- public void returnToHomescreen() {
- super.returnToHomescreen();
- // TODO(b/137318995) This should go home, but doing so removes freeform windows
- }
-
- @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[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
- AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
- wallpaperTargets);
- anim.addListener(resetStateListener());
- result.setAnimation(anim, RecentsActivity.this);
- }
- };
- return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
- runner, RECENTS_LAUNCH_DURATION,
- RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
- - STATUS_BAR_TRANSITION_PRE_DELAY));
- }
-
- /**
- * Composes the animations for a launch from the recents list if possible.
- */
- private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
- RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets) {
- AnimatorSet target = new AnimatorSet();
- boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
- PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
- createRecentsWindowAnimator(taskView, !activityClosing, appTargets,
- wallpaperTargets, null /* depthController */, pa);
- target.play(pa.buildAnim());
-
- // Found a visible recents task that matches the opening app, lets launch the app from there
- if (activityClosing) {
- Animator adjacentAnimation = mFallbackRecentsView
- .createAdjacentPageAnimForTaskLaunch(taskView);
- adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
- adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION);
- adjacentAnimation.addListener(resetStateListener());
- target.play(adjacentAnimation);
- }
- return target;
- }
-
- @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();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
-
- // Workaround for b/78520668, explicitly trim memory once UI is hidden
- onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- AccessibilityManagerCompat.sendStateEventToTest(getBaseContext(), OVERVIEW_STATE_ORDINAL);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mStateManager = new StateManager<>(this, RecentsState.DEFAULT);
-
- mOldConfig = new Configuration(getResources().getConfiguration());
- initDeviceProfile();
- setupViews();
-
- getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
- Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
- ACTIVITY_TRACKER.handleCreate(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);
- }
-
- /**
- * Initialize/update the device profile.
- */
- private void initDeviceProfile() {
- mDeviceProfile = createDeviceProfile();
- onDeviceProfileInitiated();
- }
-
- @Override
- public void onEnterAnimationComplete() {
- super.onEnterAnimationComplete();
- // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
- // as a part of quickstep, so that high-res thumbnails can load the next time we enter
- // overview
- RecentsModel.INSTANCE.get(this).getThumbnailCache()
- .getHighResLoadingState().setVisible(true);
- }
-
- @Override
- public void onTrimMemory(int level) {
- super.onTrimMemory(level);
- RecentsModel.INSTANCE.get(this).onTrimMemory(level);
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- ACTIVITY_TRACKER.onActivityDestroyed(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
- protected StateHandler<RecentsState>[] createStateHandlers() {
- return new StateHandler[] { new FallbackRecentsStateController(this) };
- }
-
- @Override
- public StateManager<RecentsState> getStateManager() {
- return mStateManager;
- }
-
- @Override
- public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
- super.dump(prefix, fd, writer, args);
- writer.println(prefix + "Misc:");
- dumpMisc(prefix + "\t", writer);
- }
-
- @Override
- public AtomicAnimationFactory<RecentsState> createAtomicAnimationFactory() {
- return new RecentsAtomicAnimationFactory<>(this, 0);
- }
-
- private AnimatorListenerAdapter resetStateListener() {
- return new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mFallbackRecentsView.resetTaskVisuals();
- mStateManager.reapplyState();
- }
- };
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
deleted file mode 100644
index dc8f1c5..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
-
-import android.animation.Animator;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.Matrix.ScaleToFit;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.views.FloatingIconView;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.TaskViewSimulator;
-import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.util.TransformParams.BuilderProxy;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
-
-public abstract class SwipeUpAnimationLogic {
-
- protected static final Rect TEMP_RECT = new Rect();
- private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
-
- protected DeviceProfile mDp;
-
- protected final Context mContext;
- protected final RecentsAnimationDeviceState mDeviceState;
- protected final GestureState mGestureState;
- protected final TaskViewSimulator mTaskViewSimulator;
-
- protected final TransformParams mTransformParams;
-
- // 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.
- protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
-
- // The distance needed to drag to reach the task size in recents.
- protected int mTransitionDragLength;
- // How much further we can drag past recents, as a factor of mTransitionDragLength.
- protected float mDragLengthFactor = 1;
- // Start resisting when swiping past this factor of mTransitionDragLength.
- private float mDragLengthFactorStartPullback = 1f;
- // This is how far down we can scale down, where 0f is full screen and 1f is recents.
- private float mDragLengthFactorMaxPullback = 1f;
-
- protected AnimatorPlaybackController mWindowTransitionController;
-
- public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
- GestureState gestureState, TransformParams transformParams) {
- mContext = context;
- mDeviceState = deviceState;
- mGestureState = gestureState;
- mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
- mTransformParams = transformParams;
-
- mTaskViewSimulator.setLayoutRotation(
- mDeviceState.getCurrentActiveRotation(), mDeviceState.getDisplayRotation());
- }
-
- protected void initTransitionEndpoints(DeviceProfile dp) {
- mDp = dp;
-
- mTaskViewSimulator.setDp(dp);
- mTransitionDragLength = mGestureState.getActivityInterface().getSwipeUpDestinationAndLength(
- dp, mContext, TEMP_RECT,
- mTaskViewSimulator.getOrientationState().getOrientationHandler());
-
- if (mDeviceState.isFullyGesturalNavMode()) {
- // We can drag all the way to the top of the screen.
- mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
-
- float startScale = mTaskViewSimulator.getFullScreenScale();
- // Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
- mDragLengthFactorStartPullback = (0.75f - startScale) / (1 - startScale);
- mDragLengthFactorMaxPullback = (0.5f - startScale) / (1 - startScale);
- } else {
- mDragLengthFactor = 1;
- mDragLengthFactorStartPullback = mDragLengthFactorMaxPullback = 1;
- }
-
- PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
- mTaskViewSimulator.addAppToOverviewAnim(pa, t -> t * mDragLengthFactor);
- mWindowTransitionController = pa.createPlaybackController();
- }
-
- @UiThread
- public void updateDisplacement(float displacement) {
- // We are moving in the negative x/y direction
- displacement = -displacement;
- float shift;
- if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
- shift = mDragLengthFactor;
- } else {
- float translation = Math.max(displacement, 0);
- shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
- if (shift > mDragLengthFactorStartPullback) {
- float pullbackProgress = Utilities.getProgress(shift,
- mDragLengthFactorStartPullback, mDragLengthFactor);
- pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
- shift = mDragLengthFactorStartPullback + pullbackProgress
- * (mDragLengthFactorMaxPullback - mDragLengthFactorStartPullback);
- }
- }
-
- mCurrentShift.updateValue(shift);
- }
-
- /**
- * Called when the value of {@link #mCurrentShift} changes
- */
- @UiThread
- public abstract void updateFinalShift();
-
- protected PagedOrientationHandler getOrientationHandler() {
- return mTaskViewSimulator.getOrientationState().getOrientationHandler();
- }
-
- protected abstract class HomeAnimationFactory {
-
- public FloatingIconView mIconView;
-
- public HomeAnimationFactory(@Nullable FloatingIconView iconView) {
- mIconView = iconView;
- }
-
- public @NonNull RectF getWindowTargetRect() {
- PagedOrientationHandler orientationHandler = getOrientationHandler();
- DeviceProfile dp = mDp;
- final int halfIconSize = dp.iconSizePx / 2;
- float primaryDimension = orientationHandler
- .getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx);
- float secondaryDimension = orientationHandler
- .getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx);
- final float targetX = primaryDimension / 2f;
- final float targetY = secondaryDimension - dp.hotseatBarSizePx;
- // Fallback to animate to center of screen.
- return new RectF(targetX - halfIconSize, targetY - halfIconSize,
- targetX + halfIconSize, targetY + halfIconSize);
- }
-
- public abstract @NonNull AnimatorPlaybackController createActivityAnimationToHome();
-
- public void playAtomicAnimation(float velocity) {
- // No-op
- }
- }
-
- /**
- * Creates an animation that transforms the current app window into the home app.
- * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
- * @param homeAnimationFactory The home animation factory.
- */
- protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
- HomeAnimationFactory homeAnimationFactory) {
- final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
- final FloatingIconView fiv = homeAnimationFactory.mIconView;
- final boolean isFloatingIconView = fiv != null;
-
- mWindowTransitionController.setPlayFraction(startProgress / mDragLengthFactor);
- mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
- RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
-
- // Matrix to map a rect in Launcher space to window space
- Matrix homeToWindowPositionMap = new Matrix();
- mTaskViewSimulator.applyWindowToHomeRotation(homeToWindowPositionMap);
-
- final RectF startRect = new RectF(cropRectF);
- mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
- // Move the startRect to Launcher space as floatingIconView runs in Launcher
- Matrix windowToHomePositionMap = new Matrix();
- homeToWindowPositionMap.invert(windowToHomePositionMap);
- windowToHomePositionMap.mapRect(startRect);
-
- RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext);
- if (isFloatingIconView) {
- anim.addAnimatorListener(fiv);
- fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
- fiv.setFastFinishRunnable(anim::end);
- }
-
- SpringAnimationRunner runner = new SpringAnimationRunner(
- homeAnimationFactory, cropRectF, homeToWindowPositionMap);
- anim.addOnUpdateListener(runner);
- anim.addAnimatorListener(runner);
- return anim;
- }
-
- /**
- * @param progress The progress of the animation to the home screen.
- * @return The current alpha to set on the animating app window.
- */
- protected float getWindowAlpha(float progress) {
- // Alpha interpolates between [1, 0] between progress values [start, end]
- final float start = 0f;
- final float end = 0.85f;
-
- if (progress <= start) {
- return 1f;
- }
- if (progress >= end) {
- return 0f;
- }
- return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
- }
-
- protected class SpringAnimationRunner extends AnimationSuccessListener
- implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
-
- final Rect mCropRect = new Rect();
- final Matrix mMatrix = new Matrix();
-
- final RectF mWindowCurrentRect = new RectF();
- final Matrix mHomeToWindowPositionMap;
-
- final FloatingIconView mFIV;
- final AnimatorPlaybackController mHomeAnim;
- final RectF mCropRectF;
-
- final float mStartRadius;
- final float mEndRadius;
- final float mWindowAlphaThreshold;
-
- SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
- Matrix homeToWindowPositionMap) {
- mHomeAnim = factory.createActivityAnimationToHome();
- mCropRectF = cropRectF;
- mHomeToWindowPositionMap = homeToWindowPositionMap;
-
- cropRectF.roundOut(mCropRect);
- mFIV = factory.mIconView;
-
- // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
- // rounding at the end of the animation.
- mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
- mEndRadius = cropRectF.width() / 2f;
-
- // We want the window alpha to be 0 once this threshold is met, so that the
- // FolderIconView can be seen morphing into the icon shape.
- mWindowAlphaThreshold = mFIV != null ? 1f - SHAPE_PROGRESS_DURATION : 1f;
- }
-
- @Override
- public void onUpdate(RectF currentRect, float progress) {
- mHomeAnim.setPlayFraction(progress);
- mHomeToWindowPositionMap.mapRect(mWindowCurrentRect, currentRect);
-
- mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
- float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius);
- mTransformParams
- .setTargetAlpha(getWindowAlpha(progress))
- .setCornerRadius(cornerRadius);
-
- mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
- if (mFIV != null) {
- mFIV.update(currentRect, 1f, progress,
- mWindowAlphaThreshold, mMatrix.mapRadius(cornerRadius), false);
- }
- }
-
- @Override
- public void onBuildTargetParams(
- Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
- builder.withMatrix(mMatrix)
- .withWindowCrop(mCropRect)
- .withCornerRadius(params.getCornerRadius());
- }
-
- @Override
- public void onCancel() {
- if (mFIV != null) {
- mFIV.fastFinish();
- }
- }
-
- @Override
- public void onAnimationStart(Animator animation) {
- mHomeAnim.dispatchOnStart();
- }
-
- @Override
- public void onAnimationSuccess(Animator animator) {
- mHomeAnim.getAnimationPlayer().end();
- }
- }
-
- public interface RunningWindowAnim {
- void end();
-
- void cancel();
-
- static RunningWindowAnim wrap(Animator animator) {
- return new RunningWindowAnim() {
- @Override
- public void end() {
- animator.end();
- }
-
- @Override
- public void cancel() {
- animator.cancel();
- }
- };
- }
-
- static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
- return new RunningWindowAnim() {
- @Override
- public void end() {
- rectFSpringAnim.end();
- }
-
- @Override
- public void cancel() {
- rectFSpringAnim.cancel();
- }
- };
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
deleted file mode 100644
index e9614d1..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static android.view.Surface.ROTATION_0;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Insets;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.os.Build;
-import android.view.View;
-import android.widget.Toast;
-
-import androidx.annotation.RequiresApi;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.R;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.quickstep.util.RecentsOrientedState;
-import com.android.quickstep.views.OverviewActionsView;
-import com.android.quickstep.views.TaskThumbnailView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.plugins.OverscrollPlugin;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Factory class to create and add an overlays on the TaskView
- */
-public class TaskOverlayFactory implements ResourceBasedOverride {
-
- public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView) {
- final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
- final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
- for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
- SystemShortcut shortcut = menuOption.getShortcut(activity, taskView);
- if (shortcut != null) {
- shortcuts.add(shortcut);
- }
- }
- RecentsOrientedState orientedState = taskView.getRecentsView().getPagedViewOrientedState();
- boolean canLauncherRotate = orientedState.canRecentsActivityRotate();
- boolean isInLandscape = orientedState.getTouchRotation() != ROTATION_0;
-
- // Add overview actions to the menu when in in-place rotate landscape mode.
- if (!canLauncherRotate && isInLandscape) {
- // Add screenshot action to task menu.
- SystemShortcut screenshotShortcut = TaskShortcutFactory.SCREENSHOT
- .getShortcut(activity, taskView);
- if (screenshotShortcut != null) {
- shortcuts.add(screenshotShortcut);
- }
-
- // Add modal action only if display orientation is the same as the device orientation.
- if (orientedState.getDisplayRotation() == ROTATION_0) {
- SystemShortcut modalShortcut = TaskShortcutFactory.MODAL
- .getShortcut(activity, taskView);
- if (modalShortcut != null) {
- shortcuts.add(modalShortcut);
- }
- }
- }
- return shortcuts;
- }
-
- public static final MainThreadInitializedObject<TaskOverlayFactory> INSTANCE =
- forOverride(TaskOverlayFactory.class, R.string.task_overlay_factory_class);
-
- /**
- * @return a launcher-provided OverscrollPlugin if available, otherwise null
- */
- public OverscrollPlugin getLocalOverscrollPlugin() {
- return null;
- }
-
- public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
- return new TaskOverlay(thumbnailView);
- }
-
- /** Note that these will be shown in order from top to bottom, if available for the task. */
- private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
- TaskShortcutFactory.APP_INFO,
- TaskShortcutFactory.SPLIT_SCREEN,
- TaskShortcutFactory.PIN,
- TaskShortcutFactory.INSTALL,
- TaskShortcutFactory.FREE_FORM,
- TaskShortcutFactory.WELLBEING
- };
-
- /**
- * Overlay on each task handling Overview Action Buttons.
- */
- public static class TaskOverlay<T extends OverviewActionsView> {
-
- private final Context mApplicationContext;
- protected final TaskThumbnailView mThumbnailView;
-
- private T mActionsView;
- private ImageActionsApi mImageApi;
- private boolean mIsAllowedByPolicy;
-
- protected TaskOverlay(TaskThumbnailView taskThumbnailView) {
- mApplicationContext = taskThumbnailView.getContext().getApplicationContext();
- mThumbnailView = taskThumbnailView;
- mImageApi = new ImageActionsApi(
- mApplicationContext, mThumbnailView::getThumbnail);
- }
-
- protected T getActionsView() {
- if (mActionsView == null) {
- mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById(
- R.id.overview_actions_view);
- }
- return mActionsView;
- }
-
- /**
- * Called when the current task is interactive for the user
- */
- public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
- boolean rotated) {
- final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
-
- getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
-
- getActionsView().setCallbacks(new OverlayUICallbacks() {
- @Override
- public void onShare() {
- if (isAllowedByPolicy) {
- mImageApi.startShareActivity();
- } else {
- showBlockedByPolicyMessage();
- }
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onScreenshot() {
- saveScreenshot(task);
- }
- });
- }
-
- /**
- * Called to save screenshot of the task thumbnail.
- */
- @SuppressLint("NewApi")
- private void saveScreenshot(Task task) {
- if (mThumbnailView.isRealSnapshot()) {
- mImageApi.saveScreenshot(mThumbnailView.getThumbnail(),
- getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
- } else {
- showBlockedByPolicyMessage();
- }
- }
-
- /**
- * Called when the overlay is no longer used.
- */
- public void reset() {
- }
-
- /**
- * Called when the system wants to reset the modal visuals.
- */
- public void resetModalVisuals() {
- }
-
- /**
- * Gets the modal state system shortcut.
- */
- public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo) {
- return null;
- }
-
- /**
- * Gets the system shortcut for the screenshot that will be added to the task menu.
- */
- public SystemShortcut getScreenshotShortcut(BaseDraggingActivity activity,
- ItemInfo iteminfo) {
- return new ScreenshotSystemShortcut(activity, iteminfo);
- }
- /**
- * Gets the task snapshot as it is displayed on the screen.
- *
- * @return the bounds of the snapshot in screen coordinates.
- */
- public Rect getTaskSnapshotBounds() {
- int[] location = new int[2];
- mThumbnailView.getLocationOnScreen(location);
-
- return new Rect(location[0], location[1], mThumbnailView.getWidth() + location[0],
- mThumbnailView.getHeight() + location[1]);
- }
-
- /**
- * Gets the insets that the snapshot is drawn with.
- *
- * @return the insets in screen coordinates.
- */
- @RequiresApi(api = Build.VERSION_CODES.Q)
- public Insets getTaskSnapshotInsets() {
- return mThumbnailView.getScaledInsets();
- }
-
- private void showBlockedByPolicyMessage() {
- Toast.makeText(
- mThumbnailView.getContext(),
- R.string.blocked_by_policy,
- Toast.LENGTH_LONG).show();
- }
-
- private class ScreenshotSystemShortcut extends SystemShortcut {
-
- private final BaseDraggingActivity mActivity;
-
- ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo) {
- super(R.drawable.ic_screenshot, R.string.action_screenshot, activity, itemInfo);
- mActivity = activity;
- }
-
- @Override
- public void onClick(View view) {
- saveScreenshot(mThumbnailView.getTaskView().getTask());
- dismissTaskMenuView(mActivity);
- }
- }
- }
-
- /**
- * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
- * controller.
- */
- public interface OverlayUICallbacks {
- /** User has indicated they want to share the current task. */
- void onShare();
-
- /** User has indicated they want to screenshot the current task. */
- void onScreenshot();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
deleted file mode 100644
index ff051b6..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SELECTIONS;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
-
-import android.app.Activity;
-import android.app.ActivityOptions;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
-import android.view.View;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
-import com.android.launcher3.model.WellbeingModel;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.popup.SystemShortcut.AppInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.Executors;
-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.model.Task;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
-import com.android.systemui.shared.recents.view.RecentsTransition;
-import com.android.systemui.shared.system.ActivityCompat;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.WindowManagerWrapper;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * Represents a system shortcut that can be shown for a recent task.
- */
-public interface TaskShortcutFactory {
-
- SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view);
-
- TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, view.getItemInfo());
-
- abstract class MultiWindowFactory implements TaskShortcutFactory {
-
- private final int mIconRes;
- private final int mTextRes;
- private final LauncherEvent mLauncherEvent;
-
- MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent) {
- mIconRes = iconRes;
- mTextRes = textRes;
- mLauncherEvent = launcherEvent;
- }
-
- protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
- protected abstract ActivityOptions makeLaunchOptions(Activity activity);
- protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
-
- @Override
- public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
- final Task task = taskView.getTask();
- if (!task.isDockable) {
- return null;
- }
- if (!isAvailable(activity, task.key.displayId)) {
- return null;
- }
- return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this,
- mLauncherEvent);
- }
- }
-
- class MultiWindowSystemShortcut extends SystemShortcut {
-
- private Handler mHandler;
-
- private final RecentsView mRecentsView;
- private final TaskThumbnailView mThumbnailView;
- private final TaskView mTaskView;
- private final MultiWindowFactory mFactory;
- private final LauncherEvent mLauncherEvent;
-
- public MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
- TaskView taskView, MultiWindowFactory factory, LauncherEvent launcherEvent) {
- super(iconRes, textRes, activity, taskView.getItemInfo());
- mLauncherEvent = launcherEvent;
- mHandler = new Handler(Looper.getMainLooper());
- mTaskView = taskView;
- mRecentsView = activity.getOverviewPanel();
- mThumbnailView = taskView.getThumbnail();
- mFactory = factory;
- }
-
- @Override
- public void onClick(View view) {
- Task.TaskKey taskKey = mTaskView.getTask().key;
- final int taskId = taskKey.id;
-
- final View.OnLayoutChangeListener onLayoutChangeListener =
- new View.OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View v, int l, int t, int r, int b,
- int oldL, int oldT, int oldR, int oldB) {
- mTaskView.getRootView().removeOnLayoutChangeListener(this);
- mRecentsView.clearIgnoreResetTask(taskId);
-
- // Start animating in the side pages once launcher has been resized
- mRecentsView.dismissTask(mTaskView, false, false);
- }
- };
-
- final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
- new DeviceProfile.OnDeviceProfileChangeListener() {
- @Override
- public void onDeviceProfileChanged(DeviceProfile dp) {
- mTarget.removeOnDeviceProfileChangeListener(this);
- if (dp.isMultiWindowMode) {
- mTaskView.getRootView().addOnLayoutChangeListener(
- onLayoutChangeListener);
- }
- }
- };
-
- dismissTaskMenuView(mTarget);
-
- ActivityOptions options = mFactory.makeLaunchOptions(mTarget);
- if (options != null
- && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
- options)) {
- if (!mFactory.onActivityStarted(mTarget)) {
- return;
- }
- // Add a device profile change listener to kick off animating the side tasks
- // once we enter multiwindow mode and relayout
- mTarget.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
-
- final Runnable animStartedListener = () -> {
- // Hide the task view and wait for the window to be resized
- // TODO: Consider animating in launcher and do an in-place start activity
- // afterwards
- mRecentsView.setIgnoreResetTask(taskId);
- mTaskView.setAlpha(0f);
- };
-
- final int[] position = new int[2];
- mThumbnailView.getLocationOnScreen(position);
- final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
- final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
- final Rect taskBounds = new Rect(position[0], position[1],
- position[0] + width, position[1] + height);
-
- // Take the thumbnail of the task without a scrim and apply it back after
- float alpha = mThumbnailView.getDimAlpha();
- mThumbnailView.setDimAlpha(0);
- Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
- taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
- Color.BLACK);
- mThumbnailView.setDimAlpha(alpha);
-
- AppTransitionAnimationSpecsFuture future =
- new AppTransitionAnimationSpecsFuture(mHandler) {
- @Override
- public List<AppTransitionAnimationSpecCompat> composeSpecs() {
- return Collections.singletonList(new AppTransitionAnimationSpecCompat(
- taskId, thumbnail, taskBounds));
- }
- };
- WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
- future, animStartedListener, mHandler, true /* scaleUp */,
- taskKey.displayId);
- mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
- .log(mLauncherEvent);
- }
- }
- }
-
- TaskShortcutFactory SPLIT_SCREEN = new MultiWindowFactory(R.drawable.ic_split_screen,
- R.string.recent_task_option_split_screen, LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP) {
-
- @Override
- protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
- // Don't show menu-item if already in multi-window and the task is from
- // the secondary display.
- // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
- // implementation is enabled
- return !activity.getDeviceProfile().isMultiWindowMode
- && (displayId == -1 || displayId == DEFAULT_DISPLAY);
- }
-
- @Override
- protected ActivityOptions makeLaunchOptions(Activity activity) {
- final ActivityCompat act = new ActivityCompat(activity);
- final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
- act.getDisplayId());
- if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
- return null;
- }
- boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
- return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
- }
-
- @Override
- protected boolean onActivityStarted(BaseDraggingActivity activity) {
- SystemUiProxy.INSTANCE.get(activity).onSplitScreenInvoked();
- activity.getUserEventDispatcher().logActionOnControl(TAP,
- LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
- return true;
- }
- };
-
- TaskShortcutFactory FREE_FORM = new MultiWindowFactory(R.drawable.ic_split_screen,
- R.string.recent_task_option_freeform, LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP) {
-
- @Override
- protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
- return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
- }
-
- @Override
- protected ActivityOptions makeLaunchOptions(Activity activity) {
- ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
- // Arbitrary bounds only because freeform is in dev mode right now
- Rect r = new Rect(50, 50, 200, 200);
- activityOptions.setLaunchBounds(r);
- return activityOptions;
- }
-
- @Override
- protected boolean onActivityStarted(BaseDraggingActivity activity) {
- activity.returnToHomescreen();
- return true;
- }
- };
-
- TaskShortcutFactory PIN = (activity, tv) -> {
- if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
- return null;
- }
- if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
- return null;
- }
- if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
- // We shouldn't be able to pin while an app is locked.
- return null;
- }
- return new PinSystemShortcut(activity, tv);
- };
-
- class PinSystemShortcut extends SystemShortcut {
-
- private static final String TAG = "PinSystemShortcut";
-
- private final TaskView mTaskView;
-
- public PinSystemShortcut(BaseDraggingActivity target, TaskView tv) {
- super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, tv.getItemInfo());
- mTaskView = tv;
- }
-
- @Override
- public void onClick(View view) {
- Consumer<Boolean> resultCallback = success -> {
- if (success) {
- SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(
- mTaskView.getTask().key.id);
- } else {
- mTaskView.notifyTaskLaunchFailed(TAG);
- }
- };
- mTaskView.launchTask(true, resultCallback, Executors.MAIN_EXECUTOR.getHandler());
- dismissTaskMenuView(mTarget);
- mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
- .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
- }
- }
-
- TaskShortcutFactory INSTALL = (activity, view) ->
- InstantAppResolver.newInstance(activity).isInstantApp(activity,
- view.getTask().getTopComponent().getPackageName())
- ? new SystemShortcut.Install(activity, view.getItemInfo()) : null;
-
- TaskShortcutFactory WELLBEING = (activity, view) ->
- WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, view.getItemInfo());
-
- TaskShortcutFactory SCREENSHOT = (activity, tv) -> {
- if (ENABLE_OVERVIEW_ACTIONS.get()) {
- return tv.getThumbnail().getTaskOverlay()
- .getScreenshotShortcut(activity, tv.getItemInfo());
- }
- return null;
- };
-
- TaskShortcutFactory MODAL = (activity, tv) -> {
- if (ENABLE_OVERVIEW_ACTIONS.get() && ENABLE_OVERVIEW_SELECTIONS.get()) {
- return tv.getThumbnail().getTaskOverlay().getModalStateSystemShortcut(tv.getItemInfo());
- }
- return null;
- };
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
deleted file mode 100644
index e2e25f3..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
-import static com.android.launcher3.statehandlers.DepthController.DEPTH;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.TargetApi;
-import android.content.ComponentName;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.Matrix.ScaleToFit;
-import android.graphics.RectF;
-import android.os.Build;
-import android.view.View;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.quickstep.util.SurfaceTransactionApplier;
-import com.android.quickstep.util.TaskViewSimulator;
-import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskThumbnailView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Utility class for helpful methods related to {@link TaskView} objects and their tasks.
- */
-@TargetApi(Build.VERSION_CODES.R)
-public final class TaskViewUtils {
-
- private TaskViewUtils() {}
-
- /**
- * 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) {
- RecentsView recentsView = activity.getOverviewPanel();
- if (v instanceof TaskView) {
- TaskView taskView = (TaskView) v;
- return recentsView.isTaskViewVisible(taskView) ? taskView : null;
- }
-
- // 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.getTaskViewCount(); i++) {
- TaskView taskView = recentsView.getTaskViewAt(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;
- }
-
- /**
- * Creates an animation that controls the window of the opening targets for the recents launch
- * animation.
- */
- public static void createRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
- RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, DepthController depthController,
- PendingAnimation out) {
-
- SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
- final RemoteAnimationTargets targets =
- new RemoteAnimationTargets(appTargets, wallpaperTargets, MODE_OPENING);
- targets.addReleaseCheck(applier);
-
- TransformParams params = new TransformParams()
- .setSyncTransactionApplier(applier)
- .setTargetSet(targets);
-
- final RecentsView recentsView = v.getRecentsView();
- int taskIndex = recentsView.indexOfChild(v);
- boolean parallaxCenterAndAdjacentTask = taskIndex != recentsView.getCurrentPage();
- int startScroll = recentsView.getScrollOffset(taskIndex);
-
- Context context = v.getContext();
- DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
- // RecentsView never updates the display rotation until swipe-up so the value may be stale.
- // Use the display value instead.
- int displayRotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
-
- TaskViewSimulator topMostSimulator = null;
- if (targets.apps.length > 0) {
- TaskViewSimulator tsv = new TaskViewSimulator(context, recentsView.getSizeStrategy());
- tsv.setDp(dp);
- tsv.setLayoutRotation(displayRotation, displayRotation);
- tsv.setPreview(targets.apps[targets.apps.length - 1]);
- tsv.fullScreenProgress.value = 0;
- tsv.recentsViewScale.value = 1;
- tsv.setScroll(startScroll);
-
- out.setFloat(tsv.fullScreenProgress,
- AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
- out.setFloat(tsv.recentsViewScale,
- AnimatedFloat.VALUE, tsv.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
- out.setInt(tsv, TaskViewSimulator.SCROLL, 0, TOUCH_RESPONSE_INTERPOLATOR);
-
- out.addOnFrameCallback(() -> tsv.apply(params));
- topMostSimulator = tsv;
- }
-
- // Fade in the task during the initial 20% of the animation
- out.addFloat(params, TransformParams.TARGET_ALPHA, 0, 1, clampToProgress(LINEAR, 0, 0.2f));
-
- if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulator != null) {
- out.addFloat(v, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
-
- TaskViewSimulator simulatorToCopy = topMostSimulator;
- simulatorToCopy.apply(params);
-
- // Mt represents the overall transformation on the thumbnailView relative to the
- // Launcher's rootView
- // K(t) represents transformation on the running window by the taskViewSimulator at
- // any time t.
- // at t = 0, we know that the simulator matches the thumbnailView. So if we apply K(0)`
- // on the Launcher's rootView, the thumbnailView would match the full running task
- // window. If we apply "K(0)` K(t)" thumbnailView will match the final transformed
- // window at any time t. This gives the overall matrix on thumbnailView to be:
- // Mt K(0)` K(t)
- // During animation we apply transformation on the thumbnailView (and not the rootView)
- // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is:
- // Mt K(0)` K(t) Mt`
- TaskThumbnailView ttv = v.getThumbnail();
- RectF tvBounds = new RectF(0, 0, ttv.getWidth(), ttv.getHeight());
- float[] tvBoundsMapped = new float[]{0, 0, ttv.getWidth(), ttv.getHeight()};
- getDescendantCoordRelativeToAncestor(ttv, ttv.getRootView(), tvBoundsMapped, false);
- RectF tvBoundsInRoot = new RectF(
- tvBoundsMapped[0], tvBoundsMapped[1],
- tvBoundsMapped[2], tvBoundsMapped[3]);
-
- Matrix mt = new Matrix();
- mt.setRectToRect(tvBounds, tvBoundsInRoot, ScaleToFit.FILL);
-
- Matrix mti = new Matrix();
- mt.invert(mti);
-
- Matrix k0i = new Matrix();
- simulatorToCopy.getCurrentMatrix().invert(k0i);
-
- Matrix animationMatrix = new Matrix();
- out.addOnFrameCallback(() -> {
- animationMatrix.set(mt);
- animationMatrix.postConcat(k0i);
- animationMatrix.postConcat(simulatorToCopy.getCurrentMatrix());
- animationMatrix.postConcat(mti);
- ttv.setAnimationMatrix(animationMatrix);
- });
-
- out.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- ttv.setAnimationMatrix(null);
- }
- });
- }
-
- out.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- targets.release();
- }
- });
-
- if (depthController != null) {
- out.setFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(context),
- TOUCH_RESPONSE_INTERPOLATOR);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
deleted file mode 100644
index 6e0b517..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ /dev/null
@@ -1,911 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static android.content.Intent.ACTION_CHOOSER;
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_UP;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.quickstep.GestureState.DEFAULT_STATE;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
-
-import android.annotation.TargetApi;
-import android.app.ActivityManager;
-import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.app.Service;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Looper;
-import android.util.Log;
-import android.view.Choreographer;
-import android.view.InputEvent;
-import android.view.MotionEvent;
-import android.view.accessibility.AccessibilityManager;
-
-import androidx.annotation.BinderThread;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.provider.RestoreDbTask;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.tracing.nano.LauncherTraceProto;
-import com.android.launcher3.tracing.nano.TouchInteractionServiceProto;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.util.OnboardingPrefs;
-import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.util.WindowBounds;
-import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
-import com.android.quickstep.inputconsumers.AssistantInputConsumer;
-import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
-import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
-import com.android.quickstep.inputconsumers.OverscrollInputConsumer;
-import com.android.quickstep.inputconsumers.OverviewInputConsumer;
-import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
-import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
-import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
-import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.AssistantUtilities;
-import com.android.quickstep.util.ProtoTracer;
-import com.android.quickstep.util.SplitScreenBounds;
-import com.android.systemui.plugins.OverscrollPlugin;
-import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.shared.recents.IOverviewProxy;
-import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
-import com.android.systemui.shared.system.InputConsumerController;
-import com.android.systemui.shared.system.InputMonitorCompat;
-import com.android.systemui.shared.tracing.ProtoTraceable;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * Wrapper around a list for processing arguments.
- */
-class ArgList extends LinkedList<String> {
- public ArgList(List<String> l) {
- super(l);
- }
-
- public String peekArg() {
- return peekFirst();
- }
-
- public String nextArg() {
- return pollFirst().toLowerCase();
- }
-}
-
-/**
- * Service connected by system-UI for handling touch interaction.
- */
-@TargetApi(Build.VERSION_CODES.R)
-public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin>,
- ProtoTraceable<LauncherTraceProto> {
-
- private static final String TAG = "TouchInteractionService";
-
- private static final String KEY_BACK_NOTIFICATION_COUNT = "backNotificationCount";
- private static final String NOTIFY_ACTION_BACK = "com.android.quickstep.action.BACK_GESTURE";
- private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
- private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
-
- /**
- * System Action ID to show all apps.
- * TODO: Use AccessibilityService's corresponding global action constant in S
- */
- private static final int SYSTEM_ACTION_ID_ALL_APPS = 14;
-
- private int mBackGestureNotificationCounter = -1;
- @Nullable
- private OverscrollPlugin mOverscrollPlugin;
-
- private final IBinder mMyBinder = new IOverviewProxy.Stub() {
-
- @BinderThread
- public void onInitialize(Bundle bundle) {
- ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
- bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
- MAIN_EXECUTOR.execute(() -> {
- SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy);
- TouchInteractionService.this.initInputMonitor();
- preloadOverview(true /* fromInit */);
- });
- sIsInitialized = true;
- }
-
- @BinderThread
- @Override
- public void onOverviewToggle() {
- TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
- mOverviewCommandHelper.onOverviewToggle();
- }
-
- @BinderThread
- @Override
- public void onOverviewShown(boolean triggeredFromAltTab) {
- mOverviewCommandHelper.onOverviewShown(triggeredFromAltTab);
- }
-
- @BinderThread
- @Override
- public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
- if (triggeredFromAltTab && !triggeredFromHomeKey) {
- // onOverviewShownFromAltTab hides the overview and ends at the target app
- mOverviewCommandHelper.onOverviewHidden();
- }
- }
-
- @BinderThread
- @Override
- public void onTip(int actionType, int viewType) {
- mOverviewCommandHelper.onTip(actionType, viewType);
- }
-
- @BinderThread
- @Override
- public void onAssistantAvailable(boolean available) {
- MAIN_EXECUTOR.execute(() -> {
- mDeviceState.setAssistantAvailable(available);
- TouchInteractionService.this.onAssistantVisibilityChanged();
- });
- }
-
- @BinderThread
- @Override
- public void onAssistantVisibilityChanged(float visibility) {
- MAIN_EXECUTOR.execute(() -> {
- mDeviceState.setAssistantVisibility(visibility);
- TouchInteractionService.this.onAssistantVisibilityChanged();
- });
- }
-
- @BinderThread
- public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
- boolean gestureSwipeLeft) {
- if (mOverviewComponentObserver == null) {
- return;
- }
-
- final BaseActivityInterface activityInterface =
- mOverviewComponentObserver.getActivityInterface();
- UserEventDispatcher.newInstance(getBaseContext()).logActionBack(completed, downX, downY,
- isButton, gestureSwipeLeft, activityInterface.getContainerType());
-
- if (completed && !isButton && shouldNotifyBackGesture()) {
- UI_HELPER_EXECUTOR.execute(TouchInteractionService.this::tryNotifyBackGesture);
- }
- }
-
- @BinderThread
- public void onSystemUiStateChanged(int stateFlags) {
- MAIN_EXECUTOR.execute(() -> {
- mDeviceState.setSystemUiFlags(stateFlags);
- TouchInteractionService.this.onSystemUiFlagsChanged();
- });
- }
-
- @BinderThread
- public void onActiveNavBarRegionChanges(Region region) {
- MAIN_EXECUTOR.execute(() -> mDeviceState.setDeferredGestureRegion(region));
- }
-
- public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
- WindowBounds wb = new WindowBounds(bounds, insets);
- MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
- }
-
- /** Deprecated methods **/
- public void onQuickStep(MotionEvent motionEvent) { }
-
- public void onQuickScrubEnd() { }
-
- public void onQuickScrubProgress(float progress) { }
-
- public void onQuickScrubStart() { }
-
- public void onPreMotionEvent(int downHitTarget) { }
-
- public void onMotionEvent(MotionEvent ev) {
- ev.recycle();
- }
-
- public void onBind(ISystemUiProxy iSystemUiProxy) { }
- };
-
- private static boolean sConnected = false;
- private static boolean sIsInitialized = false;
-
- public static boolean isConnected() {
- return sConnected;
- }
-
- public static boolean isInitialized() {
- return sIsInitialized;
- }
-
- private final BaseSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
- this::createLauncherSwipeHandler;
- private final BaseSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
- this::createFallbackSwipeHandler;
-
- private ActivityManagerWrapper mAM;
- private OverviewCommandHelper mOverviewCommandHelper;
- private OverviewComponentObserver mOverviewComponentObserver;
- private InputConsumerController mInputConsumer;
- private RecentsAnimationDeviceState mDeviceState;
- private TaskAnimationManager mTaskAnimationManager;
-
- private InputConsumer mUncheckedConsumer = InputConsumer.NO_OP;
- private InputConsumer mConsumer = InputConsumer.NO_OP;
- private Choreographer mMainChoreographer;
- private InputConsumer mResetGestureInputConsumer;
- private GestureState mGestureState = DEFAULT_STATE;
-
- private InputMonitorCompat mInputMonitorCompat;
- private InputEventReceiver mInputEventReceiver;
-
- @Override
- public void onCreate() {
- super.onCreate();
- // Initialize anything here that is needed in direct boot mode.
- // Everything else should be initialized in onUserUnlocked() below.
- mMainChoreographer = Choreographer.getInstance();
- mAM = ActivityManagerWrapper.getInstance();
- mDeviceState = new RecentsAnimationDeviceState(this);
- mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
- mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
- ProtoTracer.INSTANCE.get(this).add(this);
-
- sConnected = true;
- }
-
- private void disposeEventHandlers() {
- if (mInputEventReceiver != null) {
- mInputEventReceiver.dispose();
- mInputEventReceiver = null;
- }
- if (mInputMonitorCompat != null) {
- mInputMonitorCompat.dispose();
- mInputMonitorCompat = null;
- }
- }
-
- private void initInputMonitor() {
- disposeEventHandlers();
- if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
- return;
- }
-
- Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
- mDeviceState.getDisplayId());
- mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
- mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
- mMainChoreographer, this::onInputEvent);
-
- mDeviceState.updateGestureTouchRegions();
- }
-
- /**
- * Called when the navigation mode changes, guaranteed to be after the device state has updated.
- */
- private void onNavigationModeChanged(SysUINavigationMode.Mode mode) {
- initInputMonitor();
- resetHomeBounceSeenOnQuickstepEnabledFirstTime();
- }
-
- @UiThread
- public void onUserUnlocked() {
- mTaskAnimationManager = new TaskAnimationManager();
- mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
- mOverviewCommandHelper = new OverviewCommandHelper(this, mDeviceState,
- mOverviewComponentObserver);
- mResetGestureInputConsumer = new ResetGestureInputConsumer(mTaskAnimationManager);
- mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
- mInputConsumer.registerInputConsumer();
- onSystemUiFlagsChanged();
- onAssistantVisibilityChanged();
-
- // Temporarily disable model preload
- // new ModelPreload().start(this);
- mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this)
- .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT));
- resetHomeBounceSeenOnQuickstepEnabledFirstTime();
-
- PluginManagerWrapper.INSTANCE.get(getBaseContext()).addPluginListener(this,
- OverscrollPlugin.class, false /* allowMultiple */);
-
- mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
- onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame());
- }
-
- private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
- if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) {
- // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation
- // mode doesn't have gestures
- return;
- }
-
- // Reset home bounce seen on quick step enabled for first time
- SharedPreferences sharedPrefs = Utilities.getPrefs(this);
- if (!sharedPrefs.getBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)) {
- sharedPrefs.edit()
- .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)
- .putBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN, false)
- .apply();
- }
- }
-
- private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
- AccessibilityManager am = getSystemService(AccessibilityManager.class);
-
- if (isHomeAndOverviewSame) {
- Intent intent = new Intent(mOverviewComponentObserver.getHomeIntent())
- .setAction(Intent.ACTION_ALL_APPS);
- RemoteAction allAppsAction = new RemoteAction(
- Icon.createWithResource(this, R.drawable.ic_apps),
- getString(R.string.all_apps_label),
- getString(R.string.all_apps_label),
- PendingIntent.getActivity(this, SYSTEM_ACTION_ID_ALL_APPS, intent,
- PendingIntent.FLAG_UPDATE_CURRENT));
- am.registerSystemAction(allAppsAction, SYSTEM_ACTION_ID_ALL_APPS);
- } else {
- am.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
- }
- }
-
- @UiThread
- private void onSystemUiFlagsChanged() {
- if (mDeviceState.isUserUnlocked()) {
- SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(
- mDeviceState.getSystemUiStateFlags());
- mOverviewComponentObserver.onSystemUiStateChanged();
-
- // Update the tracing state
- if ((mDeviceState.getSystemUiStateFlags() & SYSUI_STATE_TRACING_ENABLED) != 0) {
- ProtoTracer.INSTANCE.get(TouchInteractionService.this).start();
- } else {
- ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
- }
- }
- }
-
- @UiThread
- private void onAssistantVisibilityChanged() {
- if (mDeviceState.isUserUnlocked()) {
- mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged(
- mDeviceState.getAssistantVisibility());
- }
- }
-
- @Override
- public void onDestroy() {
- sIsInitialized = false;
- if (mDeviceState.isUserUnlocked()) {
- mInputConsumer.unregisterInputConsumer();
- mOverviewComponentObserver.onDestroy();
- PluginManagerWrapper.INSTANCE.get(getBaseContext()).removePluginListener(this);
- }
- disposeEventHandlers();
- mDeviceState.destroy();
- SystemUiProxy.INSTANCE.get(this).setProxy(null);
- ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
- ProtoTracer.INSTANCE.get(this).remove(this);
-
- getSystemService(AccessibilityManager.class)
- .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
-
- sConnected = false;
- super.onDestroy();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- Log.d(TAG, "Touch service connected");
- return mMyBinder;
- }
-
- private void onInputEvent(InputEvent ev) {
- if (!(ev instanceof MotionEvent)) {
- Log.e(TAG, "Unknown event " + ev);
- return;
- }
- MotionEvent event = (MotionEvent) ev;
-
- TestLogging.recordMotionEvent(
- TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
-
- if (!mDeviceState.isUserUnlocked()) {
- return;
- }
-
- Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride(
- TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
-
- final int action = event.getAction();
- if (action == ACTION_DOWN) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_SWIPE_TO_HOME, "TouchInteractionService.onInputEvent:DOWN");
- }
- mDeviceState.setOrientationTransformIfNeeded(event);
-
- if (mDeviceState.isInSwipeUpTouchRegion(event)) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_SWIPE_TO_HOME,
- "TouchInteractionService.onInputEvent:isInSwipeUpTouchRegion");
- }
- // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
- // onConsumerInactive and wipe the previous gesture state
- GestureState prevGestureState = new GestureState(mGestureState);
- GestureState newGestureState = createGestureState(mGestureState);
- mConsumer.onConsumerAboutToBeSwitched();
- mGestureState = newGestureState;
- mConsumer = newConsumer(prevGestureState, mGestureState, event);
-
- ActiveGestureLog.INSTANCE.addLog("setInputConsumer: " + mConsumer.getName());
- mUncheckedConsumer = mConsumer;
- } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode()) {
- mGestureState = createGestureState(mGestureState);
- ActivityManager.RunningTaskInfo runningTask = mGestureState.getRunningTask();
- if (mDeviceState.canTriggerAssistantAction(event, runningTask)) {
- // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
- // should not interrupt it. QuickSwitch assumes that interruption can only
- // happen if the next gesture is also quick switch.
- mUncheckedConsumer = new AssistantInputConsumer(
- this,
- mGestureState,
- InputConsumer.NO_OP, mInputMonitorCompat,
- mOverviewComponentObserver.assistantGestureIsConstrained());
- } else {
- mUncheckedConsumer = InputConsumer.NO_OP;
- }
- } else {
- mUncheckedConsumer = InputConsumer.NO_OP;
- }
- } else {
- // Other events
- if (mUncheckedConsumer != InputConsumer.NO_OP) {
- // Only transform the event if we are handling it in a proper consumer
- mDeviceState.setOrientationTransformIfNeeded(event);
- }
- }
-
- if (mUncheckedConsumer != InputConsumer.NO_OP) {
- switch (event.getActionMasked()) {
- case ACTION_DOWN:
- case ACTION_UP:
- ActiveGestureLog.INSTANCE.addLog("onMotionEvent("
- + (int) event.getRawX() + ", " + (int) event.getRawY() + ")",
- event.getActionMasked());
- break;
- default:
- ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
- break;
- }
- }
-
- boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
- && mConsumer != null
- && !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
- mUncheckedConsumer.onMotionEvent(event);
-
- if (cleanUpConsumer) {
- reset();
- }
- TraceHelper.INSTANCE.endFlagsOverride(traceToken);
- }
-
- private GestureState createGestureState(GestureState previousGestureState) {
- GestureState gestureState = new GestureState(mOverviewComponentObserver,
- ActiveGestureLog.INSTANCE.generateAndSetLogId());
- if (mTaskAnimationManager.isRecentsAnimationRunning()) {
- gestureState.updateRunningTask(previousGestureState.getRunningTask());
- gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId());
- gestureState.updatePreviouslyAppearedTaskIds(
- previousGestureState.getPreviouslyAppearedTaskIds());
- } else {
- gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
- () -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
- }
- return gestureState;
- }
-
- private InputConsumer newConsumer(GestureState previousGestureState,
- GestureState newGestureState, MotionEvent event) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_SWIPE_TO_HOME, "newConsumer");
- }
- boolean canStartSystemGesture = mDeviceState.canStartSystemGesture();
-
- if (!mDeviceState.isUserUnlocked()) {
- if (canStartSystemGesture) {
- // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
- // launched while device is locked even after exiting direct boot mode (e.g. camera).
- return createDeviceLockedInputConsumer(newGestureState);
- } else {
- return mResetGestureInputConsumer;
- }
- }
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_SWIPE_TO_HOME, "newConsumer:user is unlocked");
- }
-
- // When there is an existing recents animation running, bypass systemState check as this is
- // a followup gesture and the first gesture started in a valid system state.
- InputConsumer base = canStartSystemGesture
- || previousGestureState.isRecentsAnimationRunning()
- ? newBaseConsumer(previousGestureState, newGestureState, event)
- : mResetGestureInputConsumer;
- if (mDeviceState.isGesturalNavMode()) {
- handleOrientationSetup(base);
- }
- if (mDeviceState.isFullyGesturalNavMode()) {
- if (mDeviceState.canTriggerAssistantAction(event, newGestureState.getRunningTask())) {
- base = new AssistantInputConsumer(
- this,
- newGestureState,
- base,
- mInputMonitorCompat,
- mOverviewComponentObserver.assistantGestureIsConstrained());
- }
-
- if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
- OverscrollPlugin plugin = null;
- if (FeatureFlags.FORCE_LOCAL_OVERSCROLL_PLUGIN.get()) {
- TaskOverlayFactory factory =
- TaskOverlayFactory.INSTANCE.get(getApplicationContext());
- plugin = factory.getLocalOverscrollPlugin(); // may be null
- }
-
- // If not local plugin was forced, use the actual overscroll plugin if available.
- if (plugin == null && mOverscrollPlugin != null && mOverscrollPlugin.isActive()) {
- plugin = mOverscrollPlugin;
- }
-
- if (plugin != null) {
- // Put the overscroll gesture as higher priority than the Assistant or base
- // gestures
- base = new OverscrollInputConsumer(this, newGestureState, base,
- mInputMonitorCompat, plugin);
- }
- }
-
- // If Bubbles is expanded, use the overlay input consumer, which will close Bubbles
- // instead of going all the way home when a swipe up is detected.
- if (mDeviceState.isBubblesExpanded() || mDeviceState.isGlobalActionsShowing()) {
- base = new SysUiOverlayInputConsumer(
- getBaseContext(), mDeviceState, mInputMonitorCompat);
- }
-
- if (mDeviceState.isScreenPinningActive()) {
- // Note: we only allow accessibility to wrap this, and it replaces the previous
- // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
- base = new ScreenPinnedInputConsumer(this, newGestureState);
- }
-
- if (mDeviceState.isAccessibilityMenuAvailable()) {
- base = new AccessibilityInputConsumer(this, mDeviceState, base,
- mInputMonitorCompat);
- }
- } else {
- if (mDeviceState.isScreenPinningActive()) {
- base = mResetGestureInputConsumer;
- }
- }
- return base;
- }
-
- private void handleOrientationSetup(InputConsumer baseInputConsumer) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "handleOrientationSetup.1");
- }
-
- baseInputConsumer.notifyOrientationSetup();
- }
-
- private InputConsumer newBaseConsumer(GestureState previousGestureState,
- GestureState gestureState, MotionEvent event) {
- if (mDeviceState.isKeyguardShowingOccluded()) {
- // This handles apps showing over the lockscreen (e.g. camera)
- return createDeviceLockedInputConsumer(gestureState);
- }
-
- // Use overview input consumer for sharesheets on top of home.
- boolean forceOverviewInputConsumer = gestureState.getActivityInterface().isStarted()
- && gestureState.getRunningTask() != null
- && ACTION_CHOOSER.equals(gestureState.getRunningTask().baseIntent.getAction());
- if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) {
- // In the case where we are in the excluded assistant state, ignore it and treat the
- // running activity as the task behind the assistant
- gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.assistant",
- () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
- ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
- ComponentName runningComponent =
- gestureState.getRunningTask().baseIntent.getComponent();
- forceOverviewInputConsumer =
- runningComponent != null && runningComponent.equals(homeComponent);
- }
-
- if (gestureState.getRunningTask() == null) {
- return mResetGestureInputConsumer;
- } else if (previousGestureState.isRunningAnimationToLauncher()
- || gestureState.getActivityInterface().isResumed()
- || forceOverviewInputConsumer) {
- return createOverviewInputConsumer(
- previousGestureState, gestureState, event, forceOverviewInputConsumer);
- } else if (ENABLE_QUICKSTEP_LIVE_TILE.get()
- && gestureState.getActivityInterface().isInLiveTileMode()) {
- return createOverviewInputConsumer(
- previousGestureState, gestureState, event, forceOverviewInputConsumer);
- } else if (mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask())) {
- return mResetGestureInputConsumer;
- } else {
- return createOtherActivityInputConsumer(gestureState, event);
- }
- }
-
- private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
- MotionEvent event) {
-
- final BaseSwipeUpHandler.Factory factory;
- if (!mOverviewComponentObserver.isHomeAndOverviewSame()) {
- factory = mFallbackSwipeHandlerFactory;
- } else {
- factory = mLauncherSwipeHandlerFactory;
- }
-
- final boolean shouldDefer = !mOverviewComponentObserver.isHomeAndOverviewSame()
- || gestureState.getActivityInterface().deferStartingActivity(mDeviceState, event);
- final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
- return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager,
- gestureState, shouldDefer, this::onConsumerInactive,
- mInputMonitorCompat, disableHorizontalSwipe, factory);
- }
-
- private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState) {
- if (mDeviceState.isFullyGesturalNavMode() && gestureState.getRunningTask() != null) {
- return new DeviceLockedInputConsumer(this, mDeviceState, mTaskAnimationManager,
- gestureState, mInputMonitorCompat);
- } else {
- return mResetGestureInputConsumer;
- }
- }
-
- public InputConsumer createOverviewInputConsumer(GestureState previousGestureState,
- GestureState gestureState, MotionEvent event,
- boolean forceOverviewInputConsumer) {
- StatefulActivity activity = gestureState.getActivityInterface().getCreatedActivity();
- if (activity == null) {
- return mResetGestureInputConsumer;
- }
-
- if (activity.getRootView().hasWindowFocus()
- || previousGestureState.isRunningAnimationToLauncher()
- || (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
- && forceOverviewInputConsumer)) {
- return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
- false /* startingInActivityBounds */);
- } else {
- final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
- return new OverviewWithoutFocusInputConsumer(activity, mDeviceState, gestureState,
- mInputMonitorCompat, disableHorizontalSwipe);
- }
- }
-
- /**
- * To be called by the consumer when it's no longer active. This can be called by any consumer
- * in the hierarchy at any point during the gesture (ie. if a delegate consumer starts
- * intercepting touches, the base consumer can try to call this).
- */
- private void onConsumerInactive(InputConsumer caller) {
- if (mConsumer != null && mConsumer.getActiveConsumerInHierarchy() == caller) {
- reset();
- }
- }
-
- private void reset() {
- mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
- mGestureState = DEFAULT_STATE;
- }
-
- private void preloadOverview(boolean fromInit) {
- if (!mDeviceState.isUserUnlocked()) {
- return;
- }
-
- if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
- // Prevent the overview from being started before the real home on first boot.
- return;
- }
-
- if (RestoreDbTask.isPending(this) || !mDeviceState.isUserSetupComplete()) {
- // Preloading while a restore is pending may cause launcher to start the restore
- // too early.
- return;
- }
-
- final BaseActivityInterface activityInterface =
- mOverviewComponentObserver.getActivityInterface();
- final Intent overviewIntent = new Intent(
- mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
- if (activityInterface.getCreatedActivity() == null) {
- // Make sure that UI states will be initialized.
- activityInterface.createActivityInitListener((wasVisible) -> {
- AppLaunchTracker.INSTANCE.get(TouchInteractionService.this);
- return false;
- }).register(overviewIntent);
- } else if (fromInit) {
- // The activity has been created before the initialization of overview service. It is
- // usually happens when booting or launcher is the top activity, so we should already
- // have the latest state.
- return;
- }
-
- mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- if (!mDeviceState.isUserUnlocked()) {
- return;
- }
- final BaseActivityInterface activityInterface =
- mOverviewComponentObserver.getActivityInterface();
- final BaseDraggingActivity activity = activityInterface.getCreatedActivity();
- if (activity == null || activity.isStarted()) {
- // We only care about the existing background activity.
- return;
- }
- if (mOverviewComponentObserver.canHandleConfigChanges(activity.getComponentName(),
- activity.getResources().getConfiguration().diff(newConfig))) {
- return;
- }
-
- preloadOverview(false /* fromInit */);
- }
-
- @Override
- protected void dump(FileDescriptor fd, PrintWriter pw, String[] rawArgs) {
- if (rawArgs.length > 0 && Utilities.IS_DEBUG_DEVICE) {
- ArgList args = new ArgList(Arrays.asList(rawArgs));
- switch (args.nextArg()) {
- case "cmd":
- if (args.peekArg() == null) {
- printAvailableCommands(pw);
- } else {
- onCommand(pw, args);
- }
- break;
- }
- } else {
- // Dump everything
- FeatureFlags.dump(pw);
- if (mDeviceState.isUserUnlocked()) {
- PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw);
- }
- mDeviceState.dump(pw);
- if (mOverviewComponentObserver != null) {
- mOverviewComponentObserver.dump(pw);
- }
- if (mGestureState != null) {
- mGestureState.dump(pw);
- }
- SysUINavigationMode.INSTANCE.get(this).dump(pw);
- pw.println("TouchState:");
- BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
- : mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
- boolean resumed = mOverviewComponentObserver != null
- && mOverviewComponentObserver.getActivityInterface().isResumed();
- pw.println(" createdOverviewActivity=" + createdOverviewActivity);
- pw.println(" resumed=" + resumed);
- pw.println(" mConsumer=" + mConsumer.getName());
- ActiveGestureLog.INSTANCE.dump("", pw);
- pw.println("ProtoTrace:");
- pw.println(" file="
- + ProtoTracer.INSTANCE.get(TouchInteractionService.this).getTraceFile());
- }
- }
-
- private void printAvailableCommands(PrintWriter pw) {
- pw.println("Available commands:");
- pw.println(" clear-touch-log: Clears the touch interaction log");
- }
-
- private void onCommand(PrintWriter pw, ArgList args) {
- switch (args.nextArg()) {
- case "clear-touch-log":
- ActiveGestureLog.INSTANCE.clear();
- break;
- }
- }
-
- private BaseSwipeUpHandler createLauncherSwipeHandler(
- GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
- return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
- gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
- }
-
- private BaseSwipeUpHandler createFallbackSwipeHandler(
- GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
- return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
- gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
- }
-
- protected boolean shouldNotifyBackGesture() {
- return mBackGestureNotificationCounter > 0 &&
- !mDeviceState.getGestureBlockedActivityPackages().isEmpty();
- }
-
- @WorkerThread
- protected void tryNotifyBackGesture() {
- if (shouldNotifyBackGesture()) {
- mBackGestureNotificationCounter--;
- Utilities.getDevicePrefs(this).edit()
- .putInt(KEY_BACK_NOTIFICATION_COUNT, mBackGestureNotificationCounter).apply();
- mDeviceState.getGestureBlockedActivityPackages().forEach(blockedPackage ->
- sendBroadcast(new Intent(NOTIFY_ACTION_BACK).setPackage(blockedPackage)));
- }
- }
-
- @Override
- public void onPluginConnected(OverscrollPlugin overscrollPlugin, Context context) {
- mOverscrollPlugin = overscrollPlugin;
- }
-
- @Override
- public void onPluginDisconnected(OverscrollPlugin overscrollPlugin) {
- mOverscrollPlugin = null;
- }
-
- @Override
- public void writeToProto(LauncherTraceProto proto) {
- if (proto.touchInteractionService == null) {
- proto.touchInteractionService = new TouchInteractionServiceProto();
- }
- proto.touchInteractionService.serviceConnected = true;
- proto.touchInteractionService.serviceConnected = true;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/ViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/ViewUtils.java
deleted file mode 100644
index cbb6ad4..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/ViewUtils.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.graphics.Canvas;
-import android.view.View;
-
-import com.android.systemui.shared.system.WindowCallbacksCompat;
-
-import java.util.function.BooleanSupplier;
-
-/**
- * Utility class for helpful methods related to {@link View} objects.
- */
-public class ViewUtils {
-
- /** See {@link #postDraw(View, Runnable, BooleanSupplier)}} */
- public static boolean postDraw(View view, Runnable onFinishRunnable) {
- return postDraw(view, onFinishRunnable, () -> false);
- }
-
- /**
- * Inject some addition logic in order to make sure that the view is updated smoothly post
- * draw, and allow addition task to be run after view update.
- *
- * @param onFinishRunnable runnable to be run right after the view finishes drawing.
- */
- public static boolean postDraw(View view, Runnable onFinishRunnable, BooleanSupplier canceled) {
- // Defer finishing the animation until the next launcher frame with the
- // new thumbnail
- return new WindowCallbacksCompat(view) {
- // The number of frames to defer until we actually finish the animation
- private int mDeferFrameCount = 2;
-
- @Override
- public void onPostDraw(Canvas canvas) {
- // If we were cancelled after this was attached, do not update
- // the state.
- if (canceled.getAsBoolean()) {
- detach();
- return;
- }
-
- if (mDeferFrameCount > 0) {
- mDeferFrameCount--;
- // Workaround, detach and reattach to invalidate the root node for
- // another draw
- detach();
- attach();
- view.invalidate();
- return;
- }
-
- if (onFinishRunnable != null) {
- onFinishRunnable.run();
- }
- detach();
- }
- }.attach();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
deleted file mode 100644
index be3fdde..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import android.graphics.PointF;
-import android.view.MotionEvent;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.launcher3.util.TouchController;
-import com.android.quickstep.RecentsActivity;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.NavBarPosition;
-import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
-
-/**
- * In 0-button mode, intercepts swipe up from the nav bar on FallbackRecentsView to go home.
- */
-public class FallbackNavBarTouchController implements TouchController,
- TriggerSwipeUpTouchTracker.OnSwipeUpListener {
-
- private final RecentsActivity mActivity;
- @Nullable
- private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
-
- public FallbackNavBarTouchController(RecentsActivity activity) {
- mActivity = activity;
- SysUINavigationMode.Mode sysUINavigationMode = SysUINavigationMode.getMode(mActivity);
- if (sysUINavigationMode == SysUINavigationMode.Mode.NO_BUTTON) {
- NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode,
- DefaultDisplay.INSTANCE.get(mActivity).getInfo());
- mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
- true /* disableHorizontalSwipe */, navBarPosition,
- null /* onInterceptTouch */, this);
- } else {
- mTriggerSwipeUpTracker = null;
- }
- }
-
- @Override
- public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
- if (cameFromNavBar && mTriggerSwipeUpTracker != null) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mTriggerSwipeUpTracker.init();
- }
- onControllerTouchEvent(ev);
- return mTriggerSwipeUpTracker.interceptedTouch();
- }
- return false;
- }
-
- @Override
- public boolean onControllerTouchEvent(MotionEvent ev) {
- if (mTriggerSwipeUpTracker != null) {
- mTriggerSwipeUpTracker.onMotionEvent(ev);
- return true;
- }
- return false;
- }
-
- @Override
- public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
- mActivity.<FallbackRecentsView>getOverviewPanel().startHome();
- }
-
- @Override
- public void onSwipeUpCancelled() {}
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
deleted file mode 100644
index 3f1e7ba..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
-import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
-import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
-
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.statemanager.StateManager.StateHandler;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.quickstep.RecentsActivity;
-import com.android.quickstep.views.ClearAllButton;
-
-/**
- * State controller for fallback recents activity
- */
-public class FallbackRecentsStateController implements StateHandler<RecentsState> {
-
- private final StateAnimationConfig mNoConfig = new StateAnimationConfig();
- private final RecentsActivity mActivity;
- private final FallbackRecentsView mRecentsView;
-
- public FallbackRecentsStateController(RecentsActivity activity) {
- mActivity = activity;
- mRecentsView = activity.getOverviewPanel();
- }
-
- @Override
- public void setState(RecentsState state) {
- mRecentsView.updateEmptyMessage();
- mRecentsView.resetTaskVisuals();
- setProperties(state, mNoConfig, PropertySetter.NO_ANIM_PROPERTY_SETTER);
- }
-
- @Override
- public void setStateWithAnimation(RecentsState toState, StateAnimationConfig config,
- PendingAnimation setter) {
- if (!config.hasAnimationFlag(PLAY_ATOMIC_OVERVIEW_PEEK | PLAY_ATOMIC_OVERVIEW_SCALE)) {
- // The entire recents animation is played atomically.
- return;
- }
- if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
- return;
- }
- // While animating into recents, update the visible task data as needed
- setter.addOnFrameCallback(mRecentsView::loadVisibleTaskData);
- mRecentsView.updateEmptyMessage();
-
- setProperties(toState, config, setter);
- }
-
- private void setProperties(RecentsState state, StateAnimationConfig config,
- PropertySetter setter) {
- float buttonAlpha = state.hasButtons() ? 1 : 0;
- setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
- buttonAlpha, LINEAR);
- setter.setFloat(mActivity.getActionsView().getVisibilityAlpha(),
- MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
-
- float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
- setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
- config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
- setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
- config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
-
- setter.setFloat(mRecentsView, TASK_MODALNESS, state.getOverviewModalness(),
- config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
- setter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, state.isFullScreen() ? 1 : 0, LINEAR);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
deleted file mode 100644
index d20bbe9..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import static com.android.quickstep.fallback.RecentsState.DEFAULT;
-import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
-
-import android.annotation.TargetApi;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.content.Context;
-import android.os.Build;
-import android.util.AttributeSet;
-
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.quickstep.FallbackActivityInterface;
-import com.android.quickstep.RecentsActivity;
-import com.android.quickstep.views.OverviewActionsView;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-
-import java.util.ArrayList;
-
-@TargetApi(Build.VERSION_CODES.R)
-public class FallbackRecentsView extends RecentsView<RecentsActivity>
- implements StateListener<RecentsState> {
-
- private RunningTaskInfo mHomeTaskInfo;
-
- public FallbackRecentsView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr, FallbackActivityInterface.INSTANCE);
- mActivity.getStateManager().addStateListener(this);
- }
-
- @Override
- public void init(OverviewActionsView actionsView) {
- super.init(actionsView);
- setOverviewStateEnabled(true);
- setOverlayEnabled(true);
- }
-
- @Override
- public void startHome() {
- mActivity.startHome();
- }
-
- @Override
- public boolean shouldUseMultiWindowTaskSizeStrategy() {
- // Just use the activity task size for multi-window as well.
- return false;
- }
-
- /**
- * When starting gesture interaction from home, we add a temporary invisible tile corresponding
- * to the home task. This allows us to handle quick-switch similarly to a quick-switching
- * from a foreground task.
- */
- public void onGestureAnimationStartOnHome(RunningTaskInfo homeTaskInfo) {
- mHomeTaskInfo = homeTaskInfo;
- onGestureAnimationStart(homeTaskInfo == null ? -1 : homeTaskInfo.taskId);
- }
-
- /**
- * When the gesture ends and recents view become interactive, we also remove the temporary
- * invisible tile added for the home task. This also pushes the remaining tiles back
- * to the center.
- */
- @Override
- public void onGestureAnimationEnd() {
- super.onGestureAnimationEnd();
- if (mHomeTaskInfo != null) {
- TaskView tv = getTaskView(mHomeTaskInfo.taskId);
- if (tv != null) {
- PendingAnimation pa = createTaskDismissAnimation(tv, true, false, 150);
- pa.addEndListener(e -> setCurrentTask(-1));
- runDismissAnimation(pa);
- }
- }
- }
-
- @Override
- public void setCurrentTask(int runningTaskId) {
- super.setCurrentTask(runningTaskId);
- if (mHomeTaskInfo != null && mHomeTaskInfo.taskId != runningTaskId) {
- mHomeTaskInfo = null;
- setRunningTaskHidden(false);
- }
- }
-
- @Override
- protected boolean shouldAddDummyTaskView(int runningTaskId) {
- if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == runningTaskId
- && getTaskViewCount() == 0) {
- // Do not add a dummy task if we are running over home with empty recents, so that we
- // show the empty recents message instead of showing a dummy task and later removing it.
- return false;
- }
- return super.shouldAddDummyTaskView(runningTaskId);
- }
-
- @Override
- protected void applyLoadPlan(ArrayList<Task> tasks) {
- // When quick-switching on 3p-launcher, we add a "dummy" tile corresponding to Launcher
- // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
- // track the index of the next task appropriately, as if we are switching on any other app.
- if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId && !tasks.isEmpty()) {
- // Check if the task list has running task
- boolean found = false;
- for (Task t : tasks) {
- if (t.key.id == mRunningTaskId) {
- found = true;
- break;
- }
- }
- if (!found) {
- ArrayList<Task> newList = new ArrayList<>(tasks.size() + 1);
- newList.addAll(tasks);
- newList.add(Task.from(new TaskKey(mHomeTaskInfo), mHomeTaskInfo, false));
- tasks = newList;
- }
- }
- super.applyLoadPlan(tasks);
- }
-
- @Override
- public void setRunningTaskHidden(boolean isHidden) {
- if (mHomeTaskInfo != null) {
- // Always keep the home task hidden
- isHidden = true;
- }
- super.setRunningTaskHidden(isHidden);
- }
-
- @Override
- public void setModalStateEnabled(boolean isModalState) {
- super.setModalStateEnabled(isModalState);
- if (isModalState) {
- mActivity.getStateManager().goToState(RecentsState.MODAL_TASK);
- } else {
- if (mActivity.isInState(RecentsState.MODAL_TASK)) {
- mActivity.getStateManager().goToState(DEFAULT);
- }
- }
- }
-
- @Override
- public void onStateTransitionStart(RecentsState toState) {
- setOverviewStateEnabled(true);
- setFreezeViewVisibility(true);
- }
-
- @Override
- public void onStateTransitionComplete(RecentsState finalState) {
- setOverlayEnabled(finalState == DEFAULT || finalState == MODAL_TASK);
- setFreezeViewVisibility(false);
- }
-
- @Override
- public void setOverviewStateEnabled(boolean enabled) {
- super.setOverviewStateEnabled(enabled);
- if (enabled) {
- RecentsState state = mActivity.getStateManager().getState();
- setDisallowScrollToClearAll(!state.hasButtons());
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java
deleted file mode 100644
index a00015a..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-
-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;
-
-/**
- * Drag layer for fallback recents activity
- */
-public class RecentsDragLayer extends BaseDragLayer<RecentsActivity> {
-
- public RecentsDragLayer(Context context, AttributeSet attrs) {
- super(context, attrs, 1 /* alphaChannelCount */);
- }
-
- @Override
- public void recreateControllers() {
- mControllers = new TouchController[] {
- new RecentsTaskController(mActivity),
- new FallbackNavBarTouchController(mActivity),
- };
- }
-
- @Override
- public void setInsets(Rect insets) {
- super.setInsets(insets);
- setBackground(insets.top == 0 || !mAllowSysuiScrims
- ? null
- : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
deleted file mode 100644
index 211a30c..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
-import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
-
-import android.content.Context;
-
-import com.android.launcher3.statemanager.BaseState;
-import com.android.quickstep.RecentsActivity;
-
-/**
- * State definition for Fallback recents
- */
-public class RecentsState implements BaseState<RecentsState> {
-
- private static final int FLAG_MODAL = BaseState.getFlag(0);
- private static final int FLAG_HAS_BUTTONS = BaseState.getFlag(1);
- private static final int FLAG_FULL_SCREEN = BaseState.getFlag(2);
-
- public static final RecentsState DEFAULT = new RecentsState(0, FLAG_HAS_BUTTONS);
- public static final RecentsState MODAL_TASK = new ModalState(1,
- FLAG_DISABLE_RESTORE | FLAG_HAS_BUTTONS | FLAG_MODAL);
- public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2,
- FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN);
-
- public final int ordinal;
- private final int mFlags;
-
- private static final float NO_OFFSET = 0;
- private static final float NO_SCALE = 1;
-
- public RecentsState(int id, int flags) {
- this.ordinal = id;
- this.mFlags = flags;
- }
-
-
- @Override
- public String toString() {
- return "Ordinal-" + ordinal;
- }
-
- @Override
- public final boolean hasFlag(int mask) {
- return (mFlags & mask) != 0;
- }
-
- @Override
- public int getTransitionDuration(Context context) {
- return 250;
- }
-
- @Override
- public RecentsState getHistoryForState(RecentsState previousState) {
- return DEFAULT;
- }
-
- /**
- * For this state, how modal should over view been shown. 0 modalness means all tasks drawn,
- * 1 modalness means the current task is show on its own.
- */
- public float getOverviewModalness() {
- return hasFlag(FLAG_MODAL) ? 1 : 0;
- }
-
- public boolean isFullScreen() {
- return hasFlag(FLAG_FULL_SCREEN);
- }
-
- public boolean hasButtons() {
- return hasFlag(FLAG_HAS_BUTTONS);
- }
-
- public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
- return new float[] { NO_SCALE, NO_OFFSET };
- }
-
-
- private static class ModalState extends RecentsState {
-
- public ModalState(int id, int flags) {
- super(id, flags);
- }
-
- @Override
- public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
- return getOverviewScaleAndOffsetForModalState(activity);
- }
- }
-
- private static class BackgroundAppState extends RecentsState {
- public BackgroundAppState(int id, int flags) {
- super(id, flags);
- }
-
- @Override
- public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
- return getOverviewScaleAndOffsetForBackgroundState(activity);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
deleted file mode 100644
index d7458d2..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
-import com.android.quickstep.RecentsActivity;
-
-public class RecentsTaskController extends TaskViewTouchController<RecentsActivity> {
-
- public RecentsTaskController(RecentsActivity activity) {
- super(activity);
- }
-
- @Override
- protected boolean isRecentsInteractive() {
- return mActivity.hasWindowFocus();
- }
-
- @Override
- protected boolean isRecentsModal() {
- return false;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
deleted file mode 100644
index 5ad48eb..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.inputconsumers;
-
-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 android.content.Context;
-import android.view.Display;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.R;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.MotionPauseDetector;
-import com.android.systemui.shared.system.InputMonitorCompat;
-
-/**
- * Input consumer for two finger swipe actions for accessibility actions
- */
-public class AccessibilityInputConsumer extends DelegateInputConsumer {
-
- private static final String TAG = "A11yInputConsumer";
-
- private final Context mContext;
- private final VelocityTracker mVelocityTracker;
- private final MotionPauseDetector mMotionPauseDetector;
- private final RecentsAnimationDeviceState mDeviceState;
-
- private final float mMinGestureDistance;
- private final float mMinFlingVelocity;
-
- private int mActivePointerId = -1;
- private float mDownY;
- private float mTotalY;
-
- public AccessibilityInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
- InputConsumer delegate, InputMonitorCompat inputMonitor) {
- super(delegate, inputMonitor);
- mContext = context;
- mVelocityTracker = VelocityTracker.obtain();
- mMinGestureDistance = context.getResources()
- .getDimension(R.dimen.accessibility_gesture_min_swipe_distance);
- mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
- mDeviceState = deviceState;
-
- mMotionPauseDetector = new MotionPauseDetector(context);
- }
-
- @Override
- public int getType() {
- return TYPE_ACCESSIBILITY | mDelegate.getType();
- }
-
- @Override
- public void onMotionEvent(MotionEvent ev) {
- if (mState != STATE_DELEGATE_ACTIVE) {
- mVelocityTracker.addMovement(ev);
- }
-
- switch (ev.getActionMasked()) {
- case ACTION_DOWN: {
- break;
- }
- case ACTION_POINTER_UP: {
- if (mState == STATE_ACTIVE) {
- int pointerIndex = ev.getActionIndex();
- int pointerId = ev.getPointerId(pointerIndex);
- if (pointerId == mActivePointerId) {
- final int newPointerIdx = pointerIndex == 0 ? 1 : 0;
-
- mTotalY += (ev.getY(pointerIndex) - mDownY);
- mDownY = ev.getY(newPointerIdx);
- mActivePointerId = ev.getPointerId(newPointerIdx);
- }
- }
- break;
- }
- case ACTION_POINTER_DOWN: {
- if (mState == STATE_INACTIVE) {
- int pointerIndex = ev.getActionIndex();
- if (mDeviceState.isInSwipeUpTouchRegion(ev, pointerIndex)
- && mDelegate.allowInterceptByParent()) {
- setActive(ev);
-
- mActivePointerId = ev.getPointerId(pointerIndex);
- mDownY = ev.getY(pointerIndex);
- } else {
- mState = STATE_DELEGATE_ACTIVE;
- }
- }
- break;
- }
- case ACTION_MOVE: {
- if (mState == STATE_ACTIVE && mDeviceState.isAccessibilityMenuShortcutAvailable()) {
- int pointerIndex = ev.findPointerIndex(mActivePointerId);
- if (pointerIndex == -1) {
- break;
- }
- mMotionPauseDetector.addPosition(ev, pointerIndex);
- }
- break;
- }
- case ACTION_UP:
- if (mState == STATE_ACTIVE) {
- if (mDeviceState.isAccessibilityMenuShortcutAvailable()
- && mMotionPauseDetector.isPaused()) {
- SystemUiProxy.INSTANCE.get(mContext).notifyAccessibilityButtonLongClicked();
- } else {
- mTotalY += (ev.getY() - mDownY);
- mVelocityTracker.computeCurrentVelocity(1000);
-
- if ((-mTotalY) > mMinGestureDistance
- || (-mVelocityTracker.getYVelocity()) > mMinFlingVelocity) {
- SystemUiProxy.INSTANCE.get(mContext).notifyAccessibilityButtonClicked(
- Display.DEFAULT_DISPLAY);
- }
- }
- }
- // Follow through
- case ACTION_CANCEL: {
- mVelocityTracker.recycle();
- mMotionPauseDetector.clear();
- break;
- }
- }
-
- if (mState != STATE_ACTIVE) {
- mDelegate.onMotionEvent(ev);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
deleted file mode 100644
index 89e6931..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
+++ /dev/null
@@ -1,285 +0,0 @@
-
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.inputconsumers;
-
-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.launcher3.Utilities.squaredHypot;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction.UPLEFT;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction.UPRIGHT;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.FLING;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.SWIPE_NOOP;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.NAVBAR;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.PointF;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.view.GestureDetector;
-import android.view.GestureDetector.SimpleOnGestureListener;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.GestureState;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.SystemUiProxy;
-import com.android.systemui.shared.system.InputMonitorCompat;
-
-/**
- * Touch consumer for handling events to launch assistant from launcher
- */
-public class AssistantInputConsumer extends DelegateInputConsumer {
-
- private static final String TAG = "AssistantInputConsumer";
- private static final long RETRACT_ANIMATION_DURATION_MS = 300;
-
- // From //java/com/google/android/apps/gsa/search/shared/util/OpaContract.java.
- private static final String OPA_BUNDLE_TRIGGER = "triggered_by";
- // From //java/com/google/android/apps/gsa/assistant/shared/proto/opa_trigger.proto.
- private static final int OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE = 83;
- private static final String INVOCATION_TYPE_KEY = "invocation_type";
- private static final int INVOCATION_TYPE_GESTURE = 1;
-
- private final PointF mDownPos = new PointF();
- private final PointF mLastPos = new PointF();
- private final PointF mStartDragPos = new PointF();
-
- private int mActivePointerId = -1;
- private boolean mPassedSlop;
- private boolean mLaunchedAssistant;
- private float mDistance;
- private float mTimeFraction;
- private long mDragTime;
- private float mLastProgress;
- private int mDirection;
- private BaseActivityInterface mActivityInterface;
-
- private final float mDragDistThreshold;
- private final float mFlingDistThreshold;
- private final long mTimeThreshold;
- private final int mAngleThreshold;
- private final float mSquaredSlop;
- private final Context mContext;
- private final GestureDetector mGestureDetector;
- private final boolean mIsAssistGestureConstrained;
-
- public AssistantInputConsumer(
- Context context,
- GestureState gestureState,
- InputConsumer delegate,
- InputMonitorCompat inputMonitor,
- boolean isAssistGestureConstrained) {
- super(delegate, inputMonitor);
- final Resources res = context.getResources();
- mContext = context;
- mIsAssistGestureConstrained = isAssistGestureConstrained;
- mDragDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
- mFlingDistThreshold = res.getDimension(R.dimen.gestures_assistant_fling_threshold);
- mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
- mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
-
- float slop = ViewConfiguration.get(context).getScaledTouchSlop();
-
- mSquaredSlop = slop * slop;
- mActivityInterface = gestureState.getActivityInterface();
-
- mGestureDetector = new GestureDetector(context, new AssistantGestureListener());
- }
-
- @Override
- public int getType() {
- return TYPE_ASSISTANT | mDelegate.getType();
- }
-
- @Override
- public void onMotionEvent(MotionEvent ev) {
- // TODO add logging
- switch (ev.getActionMasked()) {
- case ACTION_DOWN: {
- mActivePointerId = ev.getPointerId(0);
- mDownPos.set(ev.getX(), ev.getY());
- mLastPos.set(mDownPos);
- mTimeFraction = 0;
- break;
- }
- case ACTION_POINTER_DOWN: {
- if (mState != STATE_ACTIVE) {
- mState = STATE_DELEGATE_ACTIVE;
- }
- 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: {
- if (mState == STATE_DELEGATE_ACTIVE) {
- break;
- }
- if (!mDelegate.allowInterceptByParent()) {
- mState = STATE_DELEGATE_ACTIVE;
- break;
- }
- int pointerIndex = ev.findPointerIndex(mActivePointerId);
- if (pointerIndex == -1) {
- break;
- }
- mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
-
- if (!mPassedSlop) {
- // Normal gesture, ensure we pass the slop before we start tracking the gesture
- if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
- > mSquaredSlop) {
-
- mPassedSlop = true;
- mStartDragPos.set(mLastPos.x, mLastPos.y);
- mDragTime = SystemClock.uptimeMillis();
-
- if (isValidAssistantGestureAngle(
- mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) {
- setActive(ev);
- } else {
- mState = STATE_DELEGATE_ACTIVE;
- }
- }
- } else {
- // Movement
- mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
- mLastPos.y - mStartDragPos.y);
- if (mDistance >= 0) {
- final long diff = SystemClock.uptimeMillis() - mDragTime;
- mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
- updateAssistantProgress();
- }
- }
- break;
- }
- case ACTION_CANCEL:
- case ACTION_UP:
- if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) {
- ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0)
- .setDuration(RETRACT_ANIMATION_DURATION_MS);
- UserEventDispatcher.newInstance(mContext).logActionOnContainer(
- SWIPE_NOOP, mDirection, NAVBAR);
- animator.addUpdateListener(valueAnimator -> {
- float progress = (float) valueAnimator.getAnimatedValue();
- SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(progress);
- });
- animator.setInterpolator(Interpolators.DEACCEL_2);
- animator.start();
- }
- mPassedSlop = false;
- mState = STATE_INACTIVE;
- break;
- }
-
- mGestureDetector.onTouchEvent(ev);
-
- if (mState != STATE_ACTIVE) {
- mDelegate.onMotionEvent(ev);
- }
- }
-
- private void updateAssistantProgress() {
- if (!mLaunchedAssistant) {
- mLastProgress = Math.min(mDistance * 1f / mDragDistThreshold, 1) * mTimeFraction;
- if (mDistance >= mDragDistThreshold && mTimeFraction >= 1) {
- SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion(0);
- startAssistantInternal(SWIPE);
-
- Bundle args = new Bundle();
- args.putInt(OPA_BUNDLE_TRIGGER, OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE);
- args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
- SystemUiProxy.INSTANCE.get(mContext).startAssistant(args);
- mLaunchedAssistant = true;
- } else {
- SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(mLastProgress);
- }
- }
- }
-
- private void startAssistantInternal(int gestureType) {
- UserEventDispatcher.newInstance(mContext)
- .logActionOnContainer(gestureType, mDirection, NAVBAR);
-
- BaseDraggingActivity launcherActivity = mActivityInterface.getCreatedActivity();
- if (launcherActivity != null) {
- launcherActivity.getRootView().performHapticFeedback(
- 13, // HapticFeedbackConstants.GESTURE_END
- HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
- }
- }
-
- /**
- * Determine if angle is larger than threshold for assistant detection
- */
- private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) {
- float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
- mDirection = angle > 90 ? UPLEFT : UPRIGHT;
-
- // normalize so that angle is measured clockwise from horizontal in the bottom right corner
- // and counterclockwise from horizontal in the bottom left corner
- angle = angle > 90 ? 180 - angle : angle;
- return (angle > mAngleThreshold && angle < 90);
- }
-
- private class AssistantGestureListener extends SimpleOnGestureListener {
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- if (!mIsAssistGestureConstrained
- && isValidAssistantGestureAngle(velocityX, -velocityY)
- && mDistance >= mFlingDistThreshold
- && !mLaunchedAssistant
- && mState != STATE_DELEGATE_ACTIVE) {
- mLastProgress = 1;
- SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion(
- (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY));
- startAssistantInternal(FLING);
-
- Bundle args = new Bundle();
- args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
- SystemUiProxy.INSTANCE.get(mContext).startAssistant(args);
- mLaunchedAssistant = true;
- }
- return true;
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
deleted file mode 100644
index 67a15a7..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.android.quickstep.inputconsumers;
-
-import android.view.MotionEvent;
-
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.quickstep.InputConsumer;
-import com.android.systemui.shared.system.InputMonitorCompat;
-
-public abstract class DelegateInputConsumer implements InputConsumer {
-
- protected static final int STATE_INACTIVE = 0;
- protected static final int STATE_ACTIVE = 1;
- protected static final int STATE_DELEGATE_ACTIVE = 2;
-
- protected final InputConsumer mDelegate;
- protected final InputMonitorCompat mInputMonitor;
-
- protected int mState;
-
- public DelegateInputConsumer(InputConsumer delegate, InputMonitorCompat inputMonitor) {
- mDelegate = delegate;
- mInputMonitor = inputMonitor;
- mState = STATE_INACTIVE;
- }
-
- @Override
- public InputConsumer getActiveConsumerInHierarchy() {
- if (mState == STATE_ACTIVE) {
- return this;
- }
- return mDelegate.getActiveConsumerInHierarchy();
- }
-
- @Override
- public boolean allowInterceptByParent() {
- return mDelegate.allowInterceptByParent() && mState != STATE_ACTIVE;
- }
-
- @Override
- public void onConsumerAboutToBeSwitched() {
- mDelegate.onConsumerAboutToBeSwitched();
- }
-
- protected void setActive(MotionEvent ev) {
- mState = STATE_ACTIVE;
- TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
- mInputMonitor.pilferPointers();
-
- // Send cancel event
- MotionEvent event = MotionEvent.obtain(ev);
- event.setAction(MotionEvent.ACTION_CANCEL);
- mDelegate.onMotionEvent(event);
- event.recycle();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
deleted file mode 100644
index 3a97216..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.inputconsumers;
-
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_POINTER_DOWN;
-import static android.view.MotionEvent.ACTION_UP;
-
-import static com.android.launcher3.Utilities.squaredHypot;
-import static com.android.launcher3.Utilities.squaredTouchSlop;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.MIN_PROGRESS_FOR_OVERVIEW;
-import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Matrix;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.quickstep.AnimatedFloat;
-import com.android.quickstep.GestureState;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.MultiStateCallback;
-import com.android.quickstep.RecentsAnimationCallbacks;
-import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.RecentsAnimationTargets;
-import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.util.TransformParams.BuilderProxy;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.InputMonitorCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
-
-/**
- * A dummy input consumer used when the device is still locked, e.g. from secure camera.
- */
-public class DeviceLockedInputConsumer implements InputConsumer,
- RecentsAnimationCallbacks.RecentsAnimationListener, BuilderProxy {
-
- private static final String[] STATE_NAMES = DEBUG_STATES ? new String[2] : null;
- private static int getFlagForIndex(int index, String name) {
- if (DEBUG_STATES) {
- STATE_NAMES[index] = name;
- }
- return 1 << index;
- }
-
- private static final int STATE_TARGET_RECEIVED =
- getFlagForIndex(0, "STATE_TARGET_RECEIVED");
- private static final int STATE_HANDLER_INVALIDATED =
- getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
-
- private final Context mContext;
- private final RecentsAnimationDeviceState mDeviceState;
- private final TaskAnimationManager mTaskAnimationManager;
- private final GestureState mGestureState;
- private final float mTouchSlopSquared;
- private final InputMonitorCompat mInputMonitorCompat;
-
- private final PointF mTouchDown = new PointF();
- private final TransformParams mTransformParams;
- private final MultiStateCallback mStateCallback;
-
- private final Point mDisplaySize;
- private final Matrix mMatrix = new Matrix();
- private final float mMaxTranslationY;
-
- private VelocityTracker mVelocityTracker;
- private final AnimatedFloat mProgress = new AnimatedFloat(this::applyTransform);
-
- private boolean mThresholdCrossed = false;
- private boolean mHomeLaunched = false;
-
- private RecentsAnimationController mRecentsAnimationController;
-
- public DeviceLockedInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
- TaskAnimationManager taskAnimationManager, GestureState gestureState,
- InputMonitorCompat inputMonitorCompat) {
- mContext = context;
- mDeviceState = deviceState;
- mTaskAnimationManager = taskAnimationManager;
- mGestureState = gestureState;
- mTouchSlopSquared = squaredTouchSlop(context);
- mTransformParams = new TransformParams();
- mInputMonitorCompat = inputMonitorCompat;
- mMaxTranslationY = context.getResources().getDimensionPixelSize(
- R.dimen.device_locked_y_offset);
-
- // Do not use DeviceProfile as the user data might be locked
- mDisplaySize = DefaultDisplay.INSTANCE.get(context).getInfo().realSize;
-
- // Init states
- mStateCallback = new MultiStateCallback(STATE_NAMES);
- mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
- this::endRemoteAnimation);
-
- mVelocityTracker = VelocityTracker.obtain();
- }
-
- @Override
- public int getType() {
- return TYPE_DEVICE_LOCKED;
- }
-
- @Override
- public void onMotionEvent(MotionEvent ev) {
- if (mVelocityTracker == null) {
- return;
- }
- mVelocityTracker.addMovement(ev);
-
- float x = ev.getX();
- float y = ev.getY();
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mTouchDown.set(x, y);
- break;
- case ACTION_POINTER_DOWN: {
- if (!mThresholdCrossed) {
- // Cancel interaction in case of multi-touch interaction
- int ptrIdx = ev.getActionIndex();
- if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
- int action = ev.getAction();
- ev.setAction(ACTION_CANCEL);
- finishTouchTracking(ev);
- ev.setAction(action);
- }
- }
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- if (!mThresholdCrossed) {
- if (squaredHypot(x - mTouchDown.x, y - mTouchDown.y) > mTouchSlopSquared) {
- startRecentsTransition();
- }
- } else {
- float dy = Math.max(mTouchDown.y - y, 0);
- mProgress.updateValue(dy / mDisplaySize.y);
- }
- break;
- }
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- finishTouchTracking(ev);
- break;
- }
- }
-
- /**
- * 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 (mThresholdCrossed && ev.getAction() == ACTION_UP) {
- mVelocityTracker.computeCurrentVelocity(1000,
- ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
-
- float velocityY = mVelocityTracker.getYVelocity();
- float flingThreshold = mContext.getResources()
- .getDimension(R.dimen.quickstep_fling_threshold_velocity);
-
- boolean dismissTask;
- if (Math.abs(velocityY) > flingThreshold) {
- // Is fling
- dismissTask = velocityY < 0;
- } else {
- dismissTask = mProgress.value >= (1 - MIN_PROGRESS_FOR_OVERVIEW);
- }
-
- // Animate back to fullscreen before finishing
- ObjectAnimator animator = mProgress.animateToValue(mProgress.value, 0);
- animator.setDuration(100);
- animator.setInterpolator(Interpolators.ACCEL);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- if (dismissTask) {
- // For now, just start the home intent so user is prompted to unlock the device.
- mContext.startActivity(new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
- mHomeLaunched = true;
- }
- mStateCallback.setState(STATE_HANDLER_INVALIDATED);
- }
- });
- animator.start();
- } else {
- mStateCallback.setState(STATE_HANDLER_INVALIDATED);
- }
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
-
- private void startRecentsTransition() {
- mThresholdCrossed = true;
- mHomeLaunched = false;
- TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
- mInputMonitorCompat.pilferPointers();
-
- Intent intent = mGestureState.getHomeIntent()
- .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
- mTaskAnimationManager.startRecentsAnimation(mGestureState, intent, this);
- }
-
- @Override
- public void onRecentsAnimationStart(RecentsAnimationController controller,
- RecentsAnimationTargets targets) {
- mRecentsAnimationController = controller;
- mTransformParams.setTargetSet(targets);
- applyTransform();
- mStateCallback.setState(STATE_TARGET_RECEIVED);
- }
-
- @Override
- public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
- mRecentsAnimationController = null;
- mTransformParams.setTargetSet(null);
- }
-
- private void endRemoteAnimation() {
- if (mHomeLaunched) {
- ActivityManagerWrapper.getInstance().cancelRecentsAnimation(false);
- } else if (mRecentsAnimationController != null) {
- mRecentsAnimationController.finishController(
- false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
- }
- }
-
- private void applyTransform() {
- mTransformParams.setProgress(mProgress.value);
- if (mTransformParams.getTargetSet() != null) {
- mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
- }
- }
-
- @Override
- public void onBuildTargetParams(
- Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
- mMatrix.setTranslate(0, mProgress.value * mMaxTranslationY);
- builder.withMatrix(mMatrix);
- }
-
- @Override
- public void onConsumerAboutToBeSwitched() {
- mStateCallback.setState(STATE_HANDLER_INVALIDATED);
- }
-
- @Override
- public boolean allowInterceptByParent() {
- return !mThresholdCrossed;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
deleted file mode 100644
index 26df9c7..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.inputconsumers;
-
-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 android.view.MotionEvent.INVALID_POINTER_ID;
-
-import static com.android.launcher3.PagedView.ACTION_MOVE_ALLOW_EASY_FLING;
-import static com.android.launcher3.PagedView.DEBUG_FAILED_QUICKSWITCH;
-import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
-import static com.android.launcher3.Utilities.squaredHypot;
-import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
-import static com.android.quickstep.GestureState.STATE_OVERSCROLL_WINDOW_CREATED;
-import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
-
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.R;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.TraceHelper;
-import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.BaseSwipeUpHandler;
-import com.android.quickstep.BaseSwipeUpHandler.Factory;
-import com.android.quickstep.GestureState;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.RecentsAnimationCallbacks;
-import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.CachedEventDispatcher;
-import com.android.quickstep.util.MotionPauseDetector;
-import com.android.quickstep.util.NavBarPosition;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.InputMonitorCompat;
-
-import java.util.function.Consumer;
-
-/**
- * Input consumer for handling events originating from an activity other than Launcher
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer {
-
- public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN";
- private static final String UP_EVT = "OtherActivityInputConsumer.UP";
-
- // TODO: Move to quickstep contract
- public static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 9;
- public static final float QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL = 2;
-
- private final RecentsAnimationDeviceState mDeviceState;
- private final NavBarPosition mNavBarPosition;
- private final TaskAnimationManager mTaskAnimationManager;
- private final GestureState mGestureState;
- private RecentsAnimationCallbacks mActiveCallbacks;
- private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
- private final InputMonitorCompat mInputMonitorCompat;
- private final BaseActivityInterface mActivityInterface;
-
- private final BaseSwipeUpHandler.Factory mHandlerFactory;
-
- private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
- private final MotionPauseDetector mMotionPauseDetector;
- private final float mMotionPauseMinDisplacement;
-
- private VelocityTracker mVelocityTracker;
-
- private BaseSwipeUpHandler mInteractionHandler;
-
- private final boolean mIsDeferredDownTarget;
- private final PointF mDownPos = new PointF();
- private final PointF mLastPos = new PointF();
- private int mActivePointerId = INVALID_POINTER_ID;
-
- // Distance after which we start dragging the window.
- private final float mTouchSlop;
-
- private final float mSquaredTouchSlop;
- private final boolean mDisableHorizontalSwipe;
-
- // Slop used to check when we start moving window.
- private boolean mPassedWindowMoveSlop;
- // Slop used to determine when we say that the gesture has started.
- private boolean mPassedPilferInputSlop;
- // Same as mPassedPilferInputSlop, except when continuing a gesture mPassedPilferInputSlop is
- // initially true while this one is false.
- private boolean mPassedSlopOnThisGesture;
-
- // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
- private float mStartDisplacement;
-
- private Handler mMainThreadHandler;
- private Runnable mCancelRecentsAnimationRunnable = () -> {
- ActivityManagerWrapper.getInstance().cancelRecentsAnimation(
- true /* restoreHomeStackPosition */);
- };
-
- public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
- TaskAnimationManager taskAnimationManager, GestureState gestureState,
- boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
- InputMonitorCompat inputMonitorCompat, boolean disableHorizontalSwipe,
- Factory handlerFactory) {
- super(base);
- mDeviceState = deviceState;
- mNavBarPosition = mDeviceState.getNavBarPosition();
- mTaskAnimationManager = taskAnimationManager;
- mGestureState = gestureState;
- mMainThreadHandler = new Handler(Looper.getMainLooper());
- mHandlerFactory = handlerFactory;
- mActivityInterface = mGestureState.getActivityInterface();
-
- mMotionPauseDetector = new MotionPauseDetector(base, false,
- mNavBarPosition.isLeftEdge() || mNavBarPosition.isRightEdge()
- ? MotionEvent.AXIS_X : MotionEvent.AXIS_Y);
- mMotionPauseMinDisplacement = base.getResources().getDimension(
- R.dimen.motion_pause_detector_min_displacement_from_app);
- mOnCompleteCallback = onCompleteCallback;
- mVelocityTracker = VelocityTracker.obtain();
- mInputMonitorCompat = inputMonitorCompat;
-
- boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
- mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
-
- float slopMultiplier = mDeviceState.isFullyGesturalNavMode()
- ? QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL
- : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON;
- mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
- mSquaredTouchSlop = slopMultiplier * mTouchSlop * mTouchSlop;
-
- mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
- mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
- }
-
- @Override
- public int getType() {
- return TYPE_OTHER_ACTIVITY;
- }
-
- @Override
- public boolean isConsumerDetachedFromGesture() {
- return true;
- }
-
- private void forceCancelGesture(MotionEvent ev) {
- int action = ev.getAction();
- ev.setAction(ACTION_CANCEL);
- finishTouchTracking(ev);
- ev.setAction(action);
- }
-
- @Override
- public void onMotionEvent(MotionEvent ev) {
- if (mVelocityTracker == null) {
- return;
- }
-
- // Proxy events to recents view
- if (mPassedWindowMoveSlop && mInteractionHandler != null
- && !mRecentsViewDispatcher.hasConsumer()) {
- mRecentsViewDispatcher.setConsumer(mInteractionHandler
- .getRecentsViewDispatcher(mNavBarPosition.getRotation()));
- int action = ev.getAction();
- ev.setAction(ACTION_MOVE_ALLOW_EASY_FLING);
- mRecentsViewDispatcher.dispatchEvent(ev);
- ev.setAction(action);
- }
- int edgeFlags = ev.getEdgeFlags();
- ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
- mRecentsViewDispatcher.dispatchEvent(ev);
- ev.setEdgeFlags(edgeFlags);
-
- mVelocityTracker.addMovement(ev);
- if (ev.getActionMasked() == ACTION_POINTER_UP) {
- mVelocityTracker.clear();
- mMotionPauseDetector.clear();
- }
-
- switch (ev.getActionMasked()) {
- case ACTION_DOWN: {
- Object traceToken = TraceHelper.INSTANCE.beginSection(DOWN_EVT,
- FLAG_CHECK_FOR_RACE_CONDITIONS);
- mActivePointerId = ev.getPointerId(0);
- mDownPos.set(ev.getX(), ev.getY());
- mLastPos.set(mDownPos);
-
- // 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());
- }
-
- TraceHelper.INSTANCE.endSection(traceToken);
- break;
- }
- case ACTION_POINTER_DOWN: {
- if (!mPassedPilferInputSlop) {
- // Cancel interaction in case of multi-touch interaction
- int ptrIdx = ev.getActionIndex();
- if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
- forceCancelGesture(ev);
- }
- }
- 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);
- float displacementX = mLastPos.x - mDownPos.x;
- float displacementY = mLastPos.y - mDownPos.y;
-
- if (!mPassedWindowMoveSlop) {
- if (!mIsDeferredDownTarget) {
- // Normal gesture, ensure we pass the drag slop before we start tracking
- // the gesture
- if (Math.abs(displacement) > mTouchSlop) {
- mPassedWindowMoveSlop = true;
- mStartDisplacement = Math.min(displacement, -mTouchSlop);
- }
- }
- }
-
- float horizontalDist = Math.abs(displacementX);
- float upDist = -displacement;
- boolean passedSlop = squaredHypot(displacementX, displacementY)
- >= mSquaredTouchSlop;
- if (!mPassedSlopOnThisGesture && passedSlop) {
- mPassedSlopOnThisGesture = true;
- }
- // Until passing slop, we don't know what direction we're going, so assume
- // we're quick switching to avoid translating recents away when continuing
- // the gesture (in which case mPassedPilferInputSlop starts as true).
- boolean haveNotPassedSlopOnContinuedGesture =
- !mPassedSlopOnThisGesture && mPassedPilferInputSlop;
- boolean isLikelyToStartNewTask = haveNotPassedSlopOnContinuedGesture
- || horizontalDist > upDist;
-
- if (!mPassedPilferInputSlop) {
- if (passedSlop) {
- if (mDisableHorizontalSwipe
- && Math.abs(displacementX) > Math.abs(displacementY)) {
- // Horizontal gesture is not allowed in this region
- forceCancelGesture(ev);
- break;
- }
-
- mPassedPilferInputSlop = true;
-
- if (mIsDeferredDownTarget) {
- // Deferred gesture, start the animation and gesture tracking once
- // we pass the actual touch slop
- startTouchTrackingForWindowAnimation(ev.getEventTime());
- }
- if (!mPassedWindowMoveSlop) {
- mPassedWindowMoveSlop = true;
- mStartDisplacement = Math.min(displacement, -mTouchSlop);
-
- }
- notifyGestureStarted(isLikelyToStartNewTask);
- }
- }
-
- if (mInteractionHandler != null) {
- if (mPassedWindowMoveSlop) {
- // Move
- mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
- }
-
- if (mDeviceState.isFullyGesturalNavMode()) {
- mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
- || isLikelyToStartNewTask);
- mMotionPauseDetector.addPosition(ev);
- mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
- }
- }
- break;
- }
- case ACTION_CANCEL:
- case ACTION_UP: {
- if (DEBUG_FAILED_QUICKSWITCH && !mPassedWindowMoveSlop) {
- float displacementX = mLastPos.x - mDownPos.x;
- float displacementY = mLastPos.y - mDownPos.y;
- Log.d("Quickswitch", "mPassedWindowMoveSlop=false"
- + " disp=" + squaredHypot(displacementX, displacementY)
- + " slop=" + mSquaredTouchSlop);
- }
- finishTouchTracking(ev);
- break;
- }
- }
- }
-
- private void notifyGestureStarted(boolean isLikelyToStartNewTask) {
- ActiveGestureLog.INSTANCE.addLog("startQuickstep");
- if (mInteractionHandler == null) {
- return;
- }
- TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
- mInputMonitorCompat.pilferPointers();
-
- mActivityInterface.closeOverlay();
- ActivityManagerWrapper.getInstance().closeSystemWindows(
- CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-
- // Notify the handler that the gesture has actually started
- mInteractionHandler.onGestureStarted(isLikelyToStartNewTask);
- }
-
- private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
- ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
-
- mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs,
- mTaskAnimationManager.isRecentsAnimationRunning());
- mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
- mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
- Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
- mInteractionHandler.initWhenReady(intent);
-
- if (mTaskAnimationManager.isRecentsAnimationRunning()) {
- mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
- mActiveCallbacks.addListener(mInteractionHandler);
- mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
- notifyGestureStarted(true /*isLikelyToStartNewTask*/);
- } else {
- intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
- mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
- mInteractionHandler);
- }
- }
-
- /**
- * 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) {
- Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT,
- FLAG_CHECK_FOR_RACE_CONDITIONS);
-
- if (mPassedWindowMoveSlop && mInteractionHandler != null) {
- if (ev.getActionMasked() == ACTION_CANCEL) {
- mInteractionHandler.onGestureCancelled();
- } else {
- mVelocityTracker.computeCurrentVelocity(1000,
- ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
- float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
- float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
- float velocity = mNavBarPosition.isRightEdge()
- ? velocityX
- : mNavBarPosition.isLeftEdge()
- ? -velocityX
- : velocityY;
- mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
- mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY),
- mDownPos);
- }
- } else {
- // Since we start touch tracking on DOWN, we may reach this state without actually
- // starting the gesture. In that case, just cleanup immediately.
- onConsumerAboutToBeSwitched();
- onInteractionGestureFinished();
-
- // Cancel the recents animation if SysUI happens to handle UP before we have a chance
- // to start the recents animation. In addition, workaround for b/126336729 by delaying
- // the cancel of the animation for a period, in case SysUI is slow to handle UP and we
- // handle DOWN & UP and move the home stack before SysUI can start the activity
- mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable);
- mMainThreadHandler.postDelayed(mCancelRecentsAnimationRunnable, 100);
- }
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- mMotionPauseDetector.clear();
- TraceHelper.INSTANCE.endSection(traceToken);
- }
-
- @Override
- public void notifyOrientationSetup() {
- mDeviceState.onStartGesture();
- }
-
- @Override
- public void onConsumerAboutToBeSwitched() {
- Preconditions.assertUIThread();
- mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable);
- if (mInteractionHandler != null) {
- // The consumer is being switched while we are active. Set up the shared state to be
- // used by the next animation
- removeListener();
- mInteractionHandler.onConsumerAboutToBeSwitched();
- }
- }
-
- @UiThread
- private void onInteractionGestureFinished() {
- Preconditions.assertUIThread();
- removeListener();
- mInteractionHandler = null;
- mOnCompleteCallback.accept(this);
- }
-
- private void removeListener() {
- if (mActiveCallbacks != null) {
- mActiveCallbacks.removeListener(mInteractionHandler);
- }
- }
-
- private float getDisplacement(MotionEvent ev) {
- if (mNavBarPosition.isRightEdge()) {
- return ev.getX() - mDownPos.x;
- } else if (mNavBarPosition.isLeftEdge()) {
- return mDownPos.x - ev.getX();
- } else {
- return ev.getY() - mDownPos.y;
- }
- }
-
- @Override
- public boolean allowInterceptByParent() {
- return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
deleted file mode 100644
index 9bfe84f..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.inputconsumers;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.GestureState;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.InputMonitorCompat;
-
-/**
- * Input consumer for handling touch on the recents/Launcher activity.
- */
-public class OverviewInputConsumer<T extends StatefulActivity<?>>
- implements InputConsumer {
-
- private final T mActivity;
- private final BaseActivityInterface<?, T> mActivityInterface;
- private final BaseDragLayer mTarget;
- private final InputMonitorCompat mInputMonitor;
-
- private final int[] mLocationOnScreen = new int[2];
-
- private final boolean mStartingInActivityBounds;
- private boolean mTargetHandledTouch;
-
- public OverviewInputConsumer(GestureState gestureState, T activity,
- @Nullable InputMonitorCompat inputMonitor, boolean startingInActivityBounds) {
- mActivity = activity;
- mInputMonitor = inputMonitor;
- mStartingInActivityBounds = startingInActivityBounds;
- mActivityInterface = gestureState.getActivityInterface();
-
- mTarget = activity.getDragLayer();
- mTarget.getLocationOnScreen(mLocationOnScreen);
- }
-
- @Override
- public int getType() {
- return TYPE_OVERVIEW;
- }
-
- @Override
- public boolean allowInterceptByParent() {
- return !mTargetHandledTouch;
- }
-
- @Override
- public void onMotionEvent(MotionEvent ev) {
- int flags = ev.getEdgeFlags();
- if (!mStartingInActivityBounds) {
- ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR);
- }
- ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "OverviewInputConsumer");
- }
- boolean handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
- ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
- ev.setEdgeFlags(flags);
-
- if (!mTargetHandledTouch && handled) {
- mTargetHandledTouch = true;
- if (!mStartingInActivityBounds) {
- mActivityInterface.closeOverlay();
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
- ActiveGestureLog.INSTANCE.addLog("startQuickstep");
- }
- if (mInputMonitor != null) {
- TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
- mInputMonitor.pilferPointers();
- }
- }
- }
-
- @Override
- public void onKeyEvent(KeyEvent ev) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- mActivity.dispatchKeyEvent(ev);
- }
- }
-}
-
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
deleted file mode 100644
index 1c77a05..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.inputconsumers;
-
-import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.view.MotionEvent;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.logger.LauncherAtom;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.quickstep.GestureState;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
-import com.android.systemui.shared.system.InputMonitorCompat;
-
-public class OverviewWithoutFocusInputConsumer implements InputConsumer,
- TriggerSwipeUpTouchTracker.OnSwipeUpListener {
-
- private final Context mContext;
- private final InputMonitorCompat mInputMonitor;
- private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
- private final GestureState mGestureState;
-
- public OverviewWithoutFocusInputConsumer(Context context,
- RecentsAnimationDeviceState deviceState, GestureState gestureState,
- InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
- mContext = context;
- mGestureState = gestureState;
- mInputMonitor = inputMonitor;
- mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
- deviceState.getNavBarPosition(), this::onInterceptTouch, this);
- }
-
- @Override
- public int getType() {
- return TYPE_OVERVIEW_WITHOUT_FOCUS;
- }
-
- @Override
- public boolean allowInterceptByParent() {
- return !mTriggerSwipeUpTracker.interceptedTouch();
- }
-
- @Override
- public void onMotionEvent(MotionEvent ev) {
- mTriggerSwipeUpTracker.onMotionEvent(ev);
- }
-
- private void onInterceptTouch() {
- if (mInputMonitor != null) {
- TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
- mInputMonitor.pilferPointers();
- }
- }
-
- @Override
- public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
- mContext.startActivity(mGestureState.getHomeIntent());
- ActiveGestureLog.INSTANCE.addLog("startQuickstep");
- BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
- int pageIndex = -1; // This number doesn't reflect workspace page index.
- // It only indicates that launcher client screen was shown.
- int containerType = (mGestureState != null && mGestureState.getEndTarget() != null)
- ? mGestureState.getEndTarget().containerType
- : LauncherLogProto.ContainerType.WORKSPACE;
- activity.getUserEventDispatcher().logActionOnContainer(
- wasFling ? Touch.FLING : Touch.SWIPE, Direction.UP, containerType, pageIndex);
- activity.getUserEventDispatcher().setPreviousHomeGesture(true);
- activity.getStatsLogManager().logger()
- .withSrcState(LAUNCHER_STATE_HOME)
- .withDstState(LAUNCHER_STATE_HOME)
- .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
- .setWorkspace(
- LauncherAtom.WorkspaceContainer.newBuilder()
- .setPageIndex(-1))
- .build())
- .log(LAUNCHER_HOME_GESTURE);
- }
-
- @Override
- public void onSwipeUpCancelled() {}
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
deleted file mode 100644
index b9827ff..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.inputconsumers;
-
-import android.content.Context;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.R;
-import com.android.quickstep.GestureState;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.MotionPauseDetector;
-
-/**
- * An input consumer that detects swipe up and hold to exit screen pinning mode.
- */
-public class ScreenPinnedInputConsumer implements InputConsumer {
-
- private static final String TAG = "ScreenPinnedConsumer";
-
- private final float mMotionPauseMinDisplacement;
- private final MotionPauseDetector mMotionPauseDetector;
-
- private float mTouchDownY;
-
- public ScreenPinnedInputConsumer(Context context, GestureState gestureState) {
- mMotionPauseMinDisplacement = context.getResources().getDimension(
- R.dimen.motion_pause_detector_min_displacement_from_app);
- mMotionPauseDetector = new MotionPauseDetector(context, true /* makePauseHarderToTrigger*/);
- mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
- if (isPaused) {
- SystemUiProxy.INSTANCE.get(context).stopScreenPinning();
- BaseDraggingActivity launcherActivity = gestureState.getActivityInterface()
- .getCreatedActivity();
- if (launcherActivity != null) {
- launcherActivity.getRootView().performHapticFeedback(
- HapticFeedbackConstants.LONG_PRESS,
- HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
- }
- mMotionPauseDetector.clear();
- }
- });
- }
-
- @Override
- public int getType() {
- return TYPE_SCREEN_PINNED;
- }
-
- @Override
- public void onMotionEvent(MotionEvent ev) {
- float y = ev.getY();
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mTouchDownY = y;
- break;
- case MotionEvent.ACTION_MOVE:
- float displacement = mTouchDownY - y;
- mMotionPauseDetector.setDisallowPause(displacement < mMotionPauseMinDisplacement);
- mMotionPauseDetector.addPosition(ev);
- break;
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- mMotionPauseDetector.clear();
- break;
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
deleted file mode 100644
index e000803..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.logging;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-import java.util.ArrayList;
-
-/**
- * This class handles AOSP MetricsLogger function calls and logging around
- * quickstep interactions and app launches.
- */
-@SuppressWarnings("unused")
-public class UserEventDispatcherAppPredictionExtension extends UserEventDispatcherExtension {
-
- public static final int ALL_APPS_PREDICTION_TIPS = 2;
-
- private static final String TAG = "UserEventDispatcher";
-
- public UserEventDispatcherAppPredictionExtension(Context context) {
- super(context);
- }
-
- @Override
- protected void onFillInLogContainerData(
- @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target,
- @NonNull ArrayList<LauncherLogProto.Target> targets) {
- PredictionUiStateManager.fillInPredictedRank(itemInfo, target);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
deleted file mode 100644
index 552db1f..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
-
-import android.annotation.TargetApi;
-import android.app.TaskInfo;
-import android.content.Intent;
-import android.os.Build;
-
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.TaskInfoCompat;
-
-/**
- * Utility class for interacting with the Assistant.
- */
-@TargetApi(Build.VERSION_CODES.Q)
-public final class AssistantUtilities {
-
- /** Returns true if an Assistant activity that is excluded from recents is running. */
- public static boolean isExcludedAssistantRunning() {
- return isExcludedAssistant(ActivityManagerWrapper.getInstance().getRunningTask());
- }
-
- /** Returns true if the given task holds an Assistant activity that is excluded from recents. */
- public static boolean isExcludedAssistant(TaskInfo info) {
- return info != null
- && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
- && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
- }
-
- private AssistantUtilities() {}
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java
deleted file mode 100644
index 190763a..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ProtoTracer.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.util;
-
-import static com.android.launcher3.tracing.nano.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_H;
-import static com.android.launcher3.tracing.nano.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_L;
-
-import android.content.Context;
-import android.os.SystemClock;
-
-import com.android.launcher3.tracing.nano.LauncherTraceProto;
-import com.android.launcher3.tracing.nano.LauncherTraceEntryProto;
-import com.android.launcher3.tracing.nano.LauncherTraceFileProto;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.tracing.FrameProtoTracer;
-import com.android.systemui.shared.tracing.FrameProtoTracer.ProtoTraceParams;
-import com.android.systemui.shared.tracing.ProtoTraceable;
-import com.google.protobuf.nano.MessageNano;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Queue;
-
-
-/**
- * Controller for coordinating winscope proto tracing.
- */
-public class ProtoTracer implements ProtoTraceParams<MessageNano,
- LauncherTraceFileProto, LauncherTraceEntryProto, LauncherTraceProto> {
-
- public static final MainThreadInitializedObject<ProtoTracer> INSTANCE =
- new MainThreadInitializedObject<>(ProtoTracer::new);
-
- private static final String TAG = "ProtoTracer";
- private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
-
- private final Context mContext;
- private final FrameProtoTracer<MessageNano,
- LauncherTraceFileProto, LauncherTraceEntryProto, LauncherTraceProto> mProtoTracer;
-
- public ProtoTracer(Context context) {
- mContext = context;
- mProtoTracer = new FrameProtoTracer<>(this);
- }
-
- @Override
- public File getTraceFile() {
- return new File(mContext.getFilesDir(), "launcher_trace.pb");
- }
-
- @Override
- public LauncherTraceFileProto getEncapsulatingTraceProto() {
- return new LauncherTraceFileProto();
- }
-
- @Override
- public LauncherTraceEntryProto updateBufferProto(LauncherTraceEntryProto reuseObj,
- ArrayList<ProtoTraceable<LauncherTraceProto>> traceables) {
- LauncherTraceEntryProto proto = reuseObj != null
- ? reuseObj
- : new LauncherTraceEntryProto();
- proto.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
- proto.launcher = proto.launcher != null ? proto.launcher : new LauncherTraceProto();
- for (ProtoTraceable t : traceables) {
- t.writeToProto(proto.launcher);
- }
- return proto;
- }
-
- @Override
- public byte[] serializeEncapsulatingProto(LauncherTraceFileProto encapsulatingProto,
- Queue<LauncherTraceEntryProto> buffer) {
- encapsulatingProto.magicNumber = MAGIC_NUMBER_VALUE;
- encapsulatingProto.entry = buffer.toArray(new LauncherTraceEntryProto[0]);
- return MessageNano.toByteArray(encapsulatingProto);
- }
-
- @Override
- public byte[] getProtoBytes(MessageNano proto) {
- return MessageNano.toByteArray(proto);
- }
-
- @Override
- public int getProtoSize(MessageNano proto) {
- return proto.getCachedSize();
- }
-
- public void start() {
- mProtoTracer.start();
- }
-
- public void stop() {
- mProtoTracer.stop();
- }
-
- public void add(ProtoTraceable<LauncherTraceProto> traceable) {
- mProtoTracer.add(traceable);
- }
-
- public void remove(ProtoTraceable<LauncherTraceProto> traceable) {
- mProtoTracer.remove(traceable);
- }
-
- public void scheduleFrameUpdate() {
- mProtoTracer.scheduleFrameUpdate();
- }
-
- public void update() {
- mProtoTracer.update();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
deleted file mode 100644
index 5b0d503..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
-
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
-
-import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.quickstep.views.RecentsView;
-
-public class RecentsAtomicAnimationFactory<ACTIVITY_TYPE extends StatefulActivity, STATE_TYPE>
- extends AtomicAnimationFactory<STATE_TYPE> {
-
- public static final int INDEX_RECENTS_FADE_ANIM = AtomicAnimationFactory.NEXT_INDEX + 0;
- public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = AtomicAnimationFactory.NEXT_INDEX + 1;
-
- private static final int MY_ANIM_COUNT = 2;
- protected static final int NEXT_INDEX = AtomicAnimationFactory.NEXT_INDEX + MY_ANIM_COUNT;
-
- protected final ACTIVITY_TYPE mActivity;
-
- /**
- * @param extraAnims number of animations supported by the subclass. This should not include
- * the 2 animations supported by this class.
- */
- public RecentsAtomicAnimationFactory(ACTIVITY_TYPE activity, int extraAnims) {
- super(MY_ANIM_COUNT + extraAnims);
- mActivity = activity;
- }
-
- @Override
- public Animator createStateElementAnimation(int index, float... values) {
- switch (index) {
- case INDEX_RECENTS_FADE_ANIM:
- return ObjectAnimator.ofFloat(mActivity.getOverviewPanel(),
- RecentsView.CONTENT_ALPHA, values);
- case INDEX_RECENTS_TRANSLATE_X_ANIM: {
- RecentsView rv = mActivity.getOverviewPanel();
- return new SpringAnimationBuilder(mActivity)
- .setMinimumVisibleChange(1f / rv.getPageOffsetScale())
- .setDampingRatio(0.8f)
- .setStiffness(250)
- .setValues(values)
- .build(rv, ADJACENT_PAGE_OFFSET);
- }
- default:
- return super.createStateElementAnimation(index, values);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
deleted file mode 100644
index e5d2c53..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import android.animation.Animator;
-import android.content.Context;
-import android.graphics.PointF;
-import android.graphics.RectF;
-
-import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
-import androidx.dynamicanimation.animation.FloatPropertyCompat;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.FlingSpringAnim;
-import com.android.launcher3.util.DynamicResource;
-import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
-import com.android.systemui.plugins.ResourceProvider;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/**
- * Applies spring forces to animate from a starting rect to a target rect,
- * while providing update callbacks to the caller.
- */
-public class RectFSpringAnim extends ReleaseCheck {
-
- private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_X =
- new FloatPropertyCompat<RectFSpringAnim>("rectCenterXSpring") {
- @Override
- public float getValue(RectFSpringAnim anim) {
- return anim.mCurrentCenterX;
- }
-
- @Override
- public void setValue(RectFSpringAnim anim, float currentCenterX) {
- anim.mCurrentCenterX = currentCenterX;
- anim.onUpdate();
- }
- };
-
- private static final FloatPropertyCompat<RectFSpringAnim> RECT_Y =
- new FloatPropertyCompat<RectFSpringAnim>("rectYSpring") {
- @Override
- public float getValue(RectFSpringAnim anim) {
- return anim.mCurrentY;
- }
-
- @Override
- public void setValue(RectFSpringAnim anim, float y) {
- anim.mCurrentY = y;
- anim.onUpdate();
- }
- };
-
- private static final FloatPropertyCompat<RectFSpringAnim> RECT_SCALE_PROGRESS =
- new FloatPropertyCompat<RectFSpringAnim>("rectScaleProgress") {
- @Override
- public float getValue(RectFSpringAnim object) {
- return object.mCurrentScaleProgress;
- }
-
- @Override
- public void setValue(RectFSpringAnim object, float value) {
- object.mCurrentScaleProgress = value;
- object.onUpdate();
- }
- };
-
- private final RectF mStartRect;
- private final RectF mTargetRect;
- private final RectF mCurrentRect = new RectF();
- private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>();
- private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>();
-
- private float mCurrentCenterX;
- private float mCurrentY;
- // If true, tracking the bottom of the rects, else tracking the top.
- private boolean mTrackingBottomY;
- private float mCurrentScaleProgress;
- private FlingSpringAnim mRectXAnim;
- private FlingSpringAnim mRectYAnim;
- private SpringAnimation mRectScaleAnim;
- private boolean mAnimsStarted;
- private boolean mRectXAnimEnded;
- private boolean mRectYAnimEnded;
- private boolean mRectScaleAnimEnded;
-
- private float mMinVisChange;
- private float mYOvershoot;
-
- public RectFSpringAnim(RectF startRect, RectF targetRect, Context context) {
- mStartRect = startRect;
- mTargetRect = targetRect;
- mCurrentCenterX = mStartRect.centerX();
-
- mTrackingBottomY = startRect.bottom < targetRect.bottom;
- mCurrentY = mTrackingBottomY ? mStartRect.bottom : mStartRect.top;
-
- ResourceProvider rp = DynamicResource.provider(context);
- mMinVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change);
- mYOvershoot = rp.getDimension(R.dimen.swipe_up_y_overshoot);
- setCanRelease(true);
- }
-
- public void onTargetPositionChanged() {
- if (mRectXAnim != null && mRectXAnim.getTargetPosition() != mTargetRect.centerX()) {
- mRectXAnim.updatePosition(mCurrentCenterX, mTargetRect.centerX());
- }
-
- if (mRectYAnim != null) {
- if (mTrackingBottomY && mRectYAnim.getTargetPosition() != mTargetRect.bottom) {
- mRectYAnim.updatePosition(mCurrentY, mTargetRect.bottom);
- } else if (!mTrackingBottomY && mRectYAnim.getTargetPosition() != mTargetRect.top) {
- mRectYAnim.updatePosition(mCurrentY, mTargetRect.top);
- }
- }
- }
-
- public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
- mOnUpdateListeners.add(onUpdateListener);
- }
-
- public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
- mAnimatorListeners.add(animatorListener);
- }
-
- /**
- * Starts the fling/spring animation.
- * @param context The activity context.
- * @param velocityPxPerMs Velocity of swipe in px/ms.
- */
- public void start(Context context, PointF velocityPxPerMs) {
- // Only tell caller that we ended if both x and y animations have ended.
- OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
- mRectXAnimEnded = true;
- maybeOnEnd();
- });
- OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> {
- mRectYAnimEnded = true;
- maybeOnEnd();
- });
-
- float startX = mCurrentCenterX;
- float endX = mTargetRect.centerX();
- float minXValue = Math.min(startX, endX);
- float maxXValue = Math.max(startX, endX);
- mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX,
- velocityPxPerMs.x * 1000, mMinVisChange, minXValue, maxXValue, 1f, onXEndListener);
-
- float startVelocityY = velocityPxPerMs.y * 1000;
- // Scale the Y velocity based on the initial velocity to tune the curves.
- float springVelocityFactor = 0.1f + 0.9f * Math.abs(startVelocityY) / 20000.0f;
- float startY = mCurrentY;
- float endY = mTrackingBottomY ? mTargetRect.bottom : mTargetRect.top;
- float minYValue = Math.min(startY, endY - mYOvershoot);
- float maxYValue = Math.max(startY, endY);
- mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, startVelocityY,
- mMinVisChange, minYValue, maxYValue, springVelocityFactor, onYEndListener);
-
- float minVisibleChange = Math.abs(1f / mStartRect.height());
- ResourceProvider rp = DynamicResource.provider(context);
- float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio);
- float stiffness = rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness);
-
- mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS)
- .setSpring(new SpringForce(1f)
- .setDampingRatio(damping)
- .setStiffness(stiffness))
- .setStartVelocity(velocityPxPerMs.y * minVisibleChange)
- .setMaxValue(1f)
- .setMinimumVisibleChange(minVisibleChange)
- .addEndListener((animation, canceled, value, velocity) -> {
- mRectScaleAnimEnded = true;
- maybeOnEnd();
- });
-
- setCanRelease(false);
- mAnimsStarted = true;
-
- mRectXAnim.start();
- mRectYAnim.start();
- mRectScaleAnim.start();
- for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
- animatorListener.onAnimationStart(null);
- }
- }
-
- public void end() {
- if (mAnimsStarted) {
- mRectXAnim.end();
- mRectYAnim.end();
- if (mRectScaleAnim.canSkipToEnd()) {
- mRectScaleAnim.skipToEnd();
- }
- }
- mRectXAnimEnded = true;
- mRectYAnimEnded = true;
- mRectScaleAnimEnded = true;
- maybeOnEnd();
- }
-
- private boolean isEnded() {
- return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
- }
-
- private void onUpdate() {
- if (isEnded()) {
- // Prevent further updates from being called. This can happen between callbacks for
- // ending the x/y/scale animations.
- return;
- }
-
- if (!mOnUpdateListeners.isEmpty()) {
- float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
- mTargetRect.width());
- float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(),
- mTargetRect.height());
- if (mTrackingBottomY) {
- mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentY - currentHeight,
- mCurrentCenterX + currentWidth / 2, mCurrentY);
- } else {
- mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentY,
- mCurrentCenterX + currentWidth / 2, mCurrentY + currentHeight);
- }
- for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
- onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress);
- }
- }
- }
-
- private void maybeOnEnd() {
- if (mAnimsStarted && isEnded()) {
- mAnimsStarted = false;
- setCanRelease(true);
- for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
- animatorListener.onAnimationEnd(null);
- }
- }
- }
-
- public void cancel() {
- if (mAnimsStarted) {
- for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
- onUpdateListener.onCancel();
- }
- }
- end();
- }
-
- public interface OnUpdateListener {
- void onUpdate(RectF currentRect, float progress);
-
- default void onCancel() { }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
deleted file mode 100644
index 85006da..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_SHELF_ANIM;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.uioverrides.states.OverviewState;
-
-/**
- * Animates the shelf between states HIDE, PEEK, and OVERVIEW.
- */
-public class ShelfPeekAnim {
-
- public static final Interpolator INTERPOLATOR = OVERSHOOT_1_2;
- public static final long DURATION = 240;
-
- private final Launcher mLauncher;
-
- private ShelfAnimState mShelfState;
- private boolean mIsPeeking;
-
- public ShelfPeekAnim(Launcher launcher) {
- mLauncher = launcher;
- }
-
- /**
- * Animates to the given state, canceling the previous animation if it was still running.
- */
- public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
- if (mShelfState == shelfState || FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
- return;
- }
- mLauncher.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
- mShelfState = shelfState;
- mIsPeeking = mShelfState == ShelfAnimState.PEEK || mShelfState == ShelfAnimState.HIDE;
- if (mShelfState == ShelfAnimState.CANCEL) {
- return;
- }
- float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(mLauncher);
- float shelfOverviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
- // Peek based on default overview progress so we can see hotseat if we're showing
- // that instead of predictions in overview.
- float defaultOverviewProgress = OverviewState.getDefaultVerticalProgress(mLauncher);
- float shelfPeekingProgress = shelfHiddenProgress
- - (shelfHiddenProgress - defaultOverviewProgress) * 0.25f;
- float toProgress = mShelfState == ShelfAnimState.HIDE
- ? shelfHiddenProgress
- : mShelfState == ShelfAnimState.PEEK
- ? shelfPeekingProgress
- : shelfOverviewProgress;
- Animator shelfAnim = mLauncher.getStateManager()
- .createStateElementAnimation(INDEX_SHELF_ANIM, toProgress);
- shelfAnim.setInterpolator(interpolator);
- shelfAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(Animator animation) {
- mShelfState = ShelfAnimState.CANCEL;
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- mIsPeeking = mShelfState == ShelfAnimState.PEEK;
- }
- });
- shelfAnim.setDuration(duration).start();
- }
-
- /** @return Whether the shelf is currently peeking or animating to or from peeking. */
- public boolean isPeeking() {
- return mIsPeeking;
- }
-
- /** The various shelf states we can animate to. */
- public enum ShelfAnimState {
- HIDE(true), PEEK(true), OVERVIEW(false), CANCEL(false);
-
- ShelfAnimState(boolean shouldPreformHaptic) {
- this.shouldPreformHaptic = shouldPreformHaptic;
- }
-
- public final boolean shouldPreformHaptic;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
deleted file mode 100644
index 3cafd42..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
-import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.ShortcutAndWidgetContainer;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.util.DynamicResource;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.plugins.ResourceProvider;
-
-/**
- * Creates an animation where all the workspace items are moved into their final location,
- * staggered row by row from the bottom up.
- * This is used in conjunction with the swipe up to home animation.
- */
-public class StaggeredWorkspaceAnim {
-
- private static final int APP_CLOSE_ROW_START_DELAY_MS = 10;
- private static final int ALPHA_DURATION_MS = 250;
-
- private static final float MAX_VELOCITY_PX_PER_S = 22f;
-
- private final float mVelocity;
- private final float mSpringTransY;
-
- private final AnimatorSet mAnimators = new AnimatorSet();
-
- public StaggeredWorkspaceAnim(Launcher launcher, float velocity, boolean animateOverviewScrim) {
- prepareToAnimate(launcher, animateOverviewScrim);
-
- mVelocity = velocity;
-
- // Scale the translationY based on the initial velocity to better sync the workspace items
- // with the floating view.
- float transFactor = 0.2f + 0.9f * Math.abs(velocity) / MAX_VELOCITY_PX_PER_S;
- mSpringTransY = transFactor * launcher.getResources()
- .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y);
-
- DeviceProfile grid = launcher.getDeviceProfile();
- Workspace workspace = launcher.getWorkspace();
- CellLayout cellLayout = (CellLayout) workspace.getChildAt(workspace.getCurrentPage());
- ShortcutAndWidgetContainer currentPage = cellLayout.getShortcutsAndWidgets();
- ViewGroup hotseat = launcher.getHotseat();
-
- boolean workspaceClipChildren = workspace.getClipChildren();
- boolean workspaceClipToPadding = workspace.getClipToPadding();
- boolean cellLayoutClipChildren = cellLayout.getClipChildren();
- boolean cellLayoutClipToPadding = cellLayout.getClipToPadding();
- boolean hotseatClipChildren = hotseat.getClipChildren();
- boolean hotseatClipToPadding = hotseat.getClipToPadding();
-
- workspace.setClipChildren(false);
- workspace.setClipToPadding(false);
- cellLayout.setClipChildren(false);
- cellLayout.setClipToPadding(false);
- hotseat.setClipChildren(false);
- hotseat.setClipToPadding(false);
-
- // Hotseat and QSB takes up two additional rows.
- int totalRows = grid.inv.numRows + (grid.isVerticalBarLayout() ? 0 : 2);
-
- // Set up springs on workspace items.
- for (int i = currentPage.getChildCount() - 1; i >= 0; i--) {
- View child = currentPage.getChildAt(i);
- CellLayout.LayoutParams lp = ((CellLayout.LayoutParams) child.getLayoutParams());
- addStaggeredAnimationForView(child, lp.cellY + lp.cellVSpan, totalRows);
- }
-
- // Set up springs for the hotseat and qsb.
- ViewGroup hotseatChild = (ViewGroup) hotseat.getChildAt(0);
- if (grid.isVerticalBarLayout()) {
- for (int i = hotseatChild.getChildCount() - 1; i >= 0; i--) {
- View child = hotseatChild.getChildAt(i);
- CellLayout.LayoutParams lp = ((CellLayout.LayoutParams) child.getLayoutParams());
- addStaggeredAnimationForView(child, lp.cellY + 1, totalRows);
- }
- } else {
- for (int i = hotseatChild.getChildCount() - 1; i >= 0; i--) {
- View child = hotseatChild.getChildAt(i);
- addStaggeredAnimationForView(child, grid.inv.numRows + 1, totalRows);
- }
-
- if (launcher.getAppsView().getSearchUiManager()
- .isQsbVisible(NORMAL.getVisibleElements(launcher))) {
- addStaggeredAnimationForView(launcher.getAppsView().getSearchView(),
- grid.inv.numRows + 2, totalRows);
- }
- }
-
- if (animateOverviewScrim) {
- PendingAnimation pendingAnimation = new PendingAnimation(ALPHA_DURATION_MS);
- addScrimAnimationForState(launcher, NORMAL, pendingAnimation);
- mAnimators.play(pendingAnimation.buildAnim());
- }
-
- addDepthAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
-
- mAnimators.play(launcher.getDragLayer().getScrim().createSysuiMultiplierAnim(0f, 1f)
- .setDuration(ALPHA_DURATION_MS));
- mAnimators.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- workspace.setClipChildren(workspaceClipChildren);
- workspace.setClipToPadding(workspaceClipToPadding);
- cellLayout.setClipChildren(cellLayoutClipChildren);
- cellLayout.setClipToPadding(cellLayoutClipToPadding);
- hotseat.setClipChildren(hotseatClipChildren);
- hotseat.setClipToPadding(hotseatClipToPadding);
- }
- });
- }
-
- /**
- * Setup workspace with 0 duration to prepare for our staggered animation.
- */
- private void prepareToAnimate(Launcher launcher, boolean animateOverviewScrim) {
- StateAnimationConfig config = new StateAnimationConfig();
- config.animFlags = ANIM_ALL_COMPONENTS | SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER;
- config.duration = 0;
- // setRecentsAttachedToAppWindow() will animate recents out.
- launcher.getStateManager().createAtomicAnimation(BACKGROUND_APP, NORMAL, config).start();
-
- // Stop scrolling so that it doesn't interfere with the translation offscreen.
- launcher.<RecentsView>getOverviewPanel().getScroller().forceFinished(true);
-
- if (animateOverviewScrim) {
- addScrimAnimationForState(launcher, BACKGROUND_APP, NO_ANIM_PROPERTY_SETTER);
- }
- }
-
- public AnimatorSet getAnimators() {
- return mAnimators;
- }
-
- public StaggeredWorkspaceAnim addAnimatorListener(Animator.AnimatorListener listener) {
- mAnimators.addListener(listener);
- return this;
- }
-
- /**
- * Starts the animation.
- */
- public void start() {
- mAnimators.start();
- }
-
- /**
- * Adds an alpha/trans animator for {@param v}, with a start delay based on the view's row.
- *
- * @param v A view on the workspace.
- * @param row The bottom-most row that contains the view.
- * @param totalRows Total number of rows.
- */
- private void addStaggeredAnimationForView(View v, int row, int totalRows) {
- // Invert the rows, because we stagger starting from the bottom of the screen.
- int invertedRow = totalRows - row;
- // Add 1 to the inverted row so that the bottom most row has a start delay.
- long startDelay = (long) ((invertedRow + 1) * APP_CLOSE_ROW_START_DELAY_MS);
-
- v.setTranslationY(mSpringTransY);
-
- ResourceProvider rp = DynamicResource.provider(v.getContext());
- float stiffness = rp.getFloat(R.dimen.staggered_stiffness);
- float damping = rp.getFloat(R.dimen.staggered_damping_ratio);
- ValueAnimator springTransY = new SpringAnimationBuilder(v.getContext())
- .setStiffness(stiffness)
- .setDampingRatio(damping)
- .setMinimumVisibleChange(1f)
- .setStartValue(mSpringTransY)
- .setEndValue(0)
- .setStartVelocity(mVelocity)
- .build(v, VIEW_TRANSLATE_Y);
- springTransY.setStartDelay(startDelay);
- springTransY.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- v.setTranslationY(0f);
- }
- });
- mAnimators.play(springTransY);
-
- v.setAlpha(0);
- ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
- alpha.setInterpolator(LINEAR);
- alpha.setDuration(ALPHA_DURATION_MS);
- alpha.setStartDelay(startDelay);
- alpha.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- v.setAlpha(1f);
- }
- });
- mAnimators.play(alpha);
- }
-
- private void addScrimAnimationForState(Launcher launcher, LauncherState state,
- PropertySetter setter) {
- launcher.getWorkspace().getStateTransitionAnimation().setScrim(setter, state);
- setter.setFloat(
- launcher.getDragLayer().getOverviewScrim(),
- OverviewScrim.SCRIM_PROGRESS,
- state.getOverviewScrimAlpha(launcher),
- ACCEL_DEACCEL);
- }
-
- private void addDepthAnimationForState(Launcher launcher, LauncherState state, long duration) {
- if (!(launcher instanceof BaseQuickstepLauncher)) {
- return;
- }
- PendingAnimation builder = new PendingAnimation(duration);
- DepthController depthController = ((BaseQuickstepLauncher) launcher).getDepthController();
- depthController.setStateWithAnimation(state, new StateAnimationConfig(), builder);
- mAnimators.play(builder.buildAnim());
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java
deleted file mode 100644
index 0436a1c..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.systemui.shared.system.TransactionCompat.deferTransactionUntil;
-
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Message;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.View;
-
-import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-import com.android.systemui.shared.system.ViewRootImplCompat;
-
-import java.util.function.Consumer;
-
-
-/**
- * Helper class to apply surface transactions in sync with RenderThread similar to
- * android.view.SyncRtSurfaceTransactionApplier
- * with some Launcher specific utility methods
- */
-@TargetApi(Build.VERSION_CODES.R)
-public class SurfaceTransactionApplier extends ReleaseCheck {
-
- private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
-
- private final SurfaceControl mBarrierSurfaceControl;
- private final ViewRootImplCompat mTargetViewRootImpl;
- private final Handler mApplyHandler;
-
- private int mLastSequenceNumber = 0;
-
- /**
- * @param targetView The view in the surface that acts as synchronization anchor.
- */
- public SurfaceTransactionApplier(View targetView) {
- mTargetViewRootImpl = new ViewRootImplCompat(targetView);
- mBarrierSurfaceControl = mTargetViewRootImpl.getRenderSurfaceControl();
- mApplyHandler = new Handler(this::onApplyMessage);
- }
-
- protected boolean onApplyMessage(Message msg) {
- if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
- setCanRelease(msg.arg1 == mLastSequenceNumber);
- return true;
- }
- return false;
- }
-
- /**
- * Schedules applying surface parameters on the next frame.
- *
- * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
- * this method to avoid synchronization issues.
- */
- public void scheduleApply(final SurfaceParams... params) {
- View view = mTargetViewRootImpl.getView();
- if (view == null) {
- return;
- }
-
- mLastSequenceNumber++;
- final int toApplySeqNo = mLastSequenceNumber;
- setCanRelease(false);
- mTargetViewRootImpl.registerRtFrameCallback(frame -> {
- if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
- Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
- .sendToTarget();
- return;
- }
- Transaction t = new Transaction();
- for (int i = params.length - 1; i >= 0; i--) {
- SurfaceParams surfaceParams = params[i];
- if (surfaceParams.surface.isValid()) {
- deferTransactionUntil(t, surfaceParams.surface, mBarrierSurfaceControl, frame);
- surfaceParams.applyTo(t);
- }
- }
- t.apply();
- Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
- .sendToTarget();
- });
-
- // Make sure a frame gets scheduled.
- view.invalidate();
- }
-
- /**
- * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
- * attached if necessary.
- */
- public static void create(
- final View targetView, final Consumer<SurfaceTransactionApplier> callback) {
- if (targetView == null) {
- // No target view, no applier
- callback.accept(null);
- } else if (new ViewRootImplCompat(targetView).isValid()) {
- // Already attached, we're good to go
- callback.accept(new SurfaceTransactionApplier(targetView));
- } else {
- // Haven't been attached before we can get the view root
- targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- targetView.removeOnAttachStateChangeListener(this);
- callback.accept(new SurfaceTransactionApplier(targetView));
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- // Do nothing
- }
- });
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java
deleted file mode 100644
index 3ddf1b6..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
-
-import android.content.Context;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.Themes;
-
-public class TaskCornerRadius {
-
- public static float get(Context context) {
- return supportsRoundedCornersOnWindows(context.getResources()) ?
- Themes.getDialogCornerRadius(context):
- context.getResources().getDimension(R.dimen.task_corner_radius_small);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
deleted file mode 100644
index c9ed498..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ /dev/null
@@ -1,314 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.states.RotationHelper.deltaRotation;
-import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
-import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
-import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
-
-import android.animation.TimeInterpolator;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Matrix;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.util.IntProperty;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.quickstep.AnimatedFloat;
-import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.views.RecentsView.ScrollState;
-import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
-import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.FullscreenDrawParams;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
-
-/**
- * A utility class which emulates the layout behavior of TaskView and RecentsView
- */
-public class TaskViewSimulator implements TransformParams.BuilderProxy {
-
- public static final IntProperty<TaskViewSimulator> SCROLL =
- new IntProperty<TaskViewSimulator>("scroll") {
- @Override
- public void setValue(TaskViewSimulator simulator, int i) {
- simulator.setScroll(i);
- }
-
- @Override
- public Integer get(TaskViewSimulator simulator) {
- return simulator.mScrollState.scroll;
- }
- };
-
- private final Rect mTmpCropRect = new Rect();
- private final RectF mTempRectF = new RectF();
- private final float[] mTempPoint = new float[2];
-
- private final RecentsOrientedState mOrientationState;
- private final Context mContext;
- private final BaseActivityInterface mSizeStrategy;
-
- private final Rect mTaskRect = new Rect();
- private final PointF mPivot = new PointF();
- private DeviceProfile mDp;
-
- private final Matrix mMatrix = new Matrix();
- private final Point mRunningTargetWindowPosition = new Point();
-
- // Thumbnail view properties
- private final Rect mThumbnailPosition = new Rect();
- private final ThumbnailData mThumbnailData = new ThumbnailData();
- private final PreviewPositionHelper mPositionHelper = new PreviewPositionHelper();
- private final Matrix mInversePositionMatrix = new Matrix();
-
- // TaskView properties
- private final FullscreenDrawParams mCurrentFullscreenParams;
- private float mCurveScale = 1;
-
- // RecentsView properties
- public final AnimatedFloat recentsViewScale = new AnimatedFloat();
- public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
- private final ScrollState mScrollState = new ScrollState();
- private final int mPageSpacing;
-
- // Cached calculations
- private boolean mLayoutValid = false;
- private boolean mScrollValid = false;
-
- public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
- mContext = context;
- mSizeStrategy = sizeStrategy;
-
- mOrientationState = new RecentsOrientedState(context, sizeStrategy, i -> { });
- mOrientationState.setGestureActive(true);
-
- mCurrentFullscreenParams = new FullscreenDrawParams(context);
- mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
- }
-
- /**
- * Sets the device profile for the current state
- */
- public void setDp(DeviceProfile dp) {
- mDp = dp;
- mOrientationState.setMultiWindowMode(mDp.isMultiWindowMode);
- mLayoutValid = false;
- }
-
- /**
- * @see com.android.quickstep.views.RecentsView#setLayoutRotation(int, int)
- */
- public void setLayoutRotation(int touchRotation, int displayRotation) {
- mOrientationState.update(touchRotation, displayRotation);
- mLayoutValid = false;
- }
-
- /**
- * @see com.android.quickstep.views.RecentsView#onConfigurationChanged(Configuration)
- */
- public void setRecentsConfiguration(Configuration configuration) {
- mOrientationState.setActivityConfiguration(configuration);
- mLayoutValid = false;
- }
-
- /**
- * @see com.android.quickstep.views.RecentsView#FULLSCREEN_PROGRESS
- */
- public float getFullScreenScale() {
- if (mDp == null) {
- return 1;
- }
- mSizeStrategy.calculateTaskSize(mContext, mDp, mTaskRect,
- mOrientationState.getOrientationHandler());
- return mOrientationState.getFullScreenScaleAndPivot(mTaskRect, mDp, mPivot);
- }
-
- /**
- * Sets the targets which the simulator will control
- */
- public void setPreview(RemoteAnimationTargetCompat runningTarget) {
- setPreviewBounds(runningTarget.screenSpaceBounds, runningTarget.contentInsets);
- mRunningTargetWindowPosition.set(runningTarget.screenSpaceBounds.left,
- runningTarget.screenSpaceBounds.top);
- }
-
- /**
- * Sets the targets which the simulator will control
- */
- public void setPreviewBounds(Rect bounds, Rect insets) {
- mThumbnailData.insets.set(insets);
- // TODO: What is this?
- mThumbnailData.windowingMode = WINDOWING_MODE_FULLSCREEN;
-
- mThumbnailPosition.set(bounds);
- mLayoutValid = false;
- }
-
- /**
- * Updates the scroll for RecentsView
- */
- public void setScroll(int scroll) {
- if (mScrollState.scroll != scroll) {
- mScrollState.scroll = scroll;
- mScrollValid = false;
- }
- }
-
- /**
- * Adds animation for all the components corresponding to transition from an app to overview
- */
- public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
- pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
- pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, interpolator);
- }
-
- /**
- * Returns the current clipped/visible window bounds in the window coordinate space
- */
- public RectF getCurrentCropRect() {
- // Crop rect is the inverse of thumbnail matrix
- RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
- mTempRectF.set(-insets.left, -insets.top,
- mTaskRect.width() + insets.right, mTaskRect.height() + insets.bottom);
- mInversePositionMatrix.mapRect(mTempRectF);
- return mTempRectF;
- }
-
- public RecentsOrientedState getOrientationState() {
- return mOrientationState;
- }
-
- /**
- * Returns the current transform applied to the window
- */
- public Matrix getCurrentMatrix() {
- return mMatrix;
- }
-
- /**
- * Applies the rotation on the matrix to so that it maps from launcher coordinate space to
- * window coordinate space.
- */
- public void applyWindowToHomeRotation(Matrix matrix) {
- mMatrix.postTranslate(mDp.windowX, mDp.windowY);
- postDisplayRotation(deltaRotation(
- mOrientationState.getRecentsActivityRotation(),
- mOrientationState.getDisplayRotation()),
- mDp.widthPx, mDp.heightPx, matrix);
- matrix.postTranslate(-mRunningTargetWindowPosition.x, -mRunningTargetWindowPosition.y);
- }
-
- /**
- * Applies the target to the previously set parameters
- */
- public void apply(TransformParams params) {
- if (mDp == null || mThumbnailPosition.isEmpty()) {
- return;
- }
- if (!mLayoutValid) {
- mLayoutValid = true;
-
- getFullScreenScale();
- mThumbnailData.rotation = mOrientationState.getDisplayRotation();
-
- mPositionHelper.updateThumbnailMatrix(
- mThumbnailPosition, mThumbnailData,
- mTaskRect.width(), mTaskRect.height(),
- mDp, mOrientationState.getRecentsActivityRotation());
- mPositionHelper.getMatrix().invert(mInversePositionMatrix);
-
- PagedOrientationHandler poh = mOrientationState.getOrientationHandler();
- mScrollState.halfPageSize =
- poh.getPrimaryValue(mTaskRect.width(), mTaskRect.height()) / 2;
- mScrollState.halfScreenSize = poh.getPrimaryValue(mDp.widthPx, mDp.heightPx) / 2;
- mScrollValid = false;
- }
-
- if (!mScrollValid) {
- mScrollValid = true;
- int start = mOrientationState.getOrientationHandler()
- .getPrimaryValue(mTaskRect.left, mTaskRect.top);
- mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
- mScrollState.updateInterpolation(start, mPageSpacing);
- mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
- }
-
- float progress = Utilities.boundToRange(fullScreenProgress.value, 0, 1);
- mCurrentFullscreenParams.setProgress(
- progress, recentsViewScale.value, mTaskRect.width(), mDp, mPositionHelper);
-
- // Apply thumbnail matrix
- RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
- float scale = mCurrentFullscreenParams.mScale;
- float taskWidth = mTaskRect.width();
- float taskHeight = mTaskRect.height();
-
- mMatrix.set(mPositionHelper.getMatrix());
- mMatrix.postTranslate(insets.left, insets.top);
- mMatrix.postScale(scale, scale);
-
- // Apply TaskView matrix: translate, scale, scroll
- mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
- mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
- mOrientationState.getOrientationHandler().set(
- mMatrix, MATRIX_POST_TRANSLATE, mScrollState.scroll);
-
- // Apply recensView matrix
- mMatrix.postScale(recentsViewScale.value, recentsViewScale.value, mPivot.x, mPivot.y);
- applyWindowToHomeRotation(mMatrix);
-
- // Crop rect is the inverse of thumbnail matrix
- mTempRectF.set(-insets.left, -insets.top,
- taskWidth + insets.right, taskHeight + insets.bottom);
- mInversePositionMatrix.mapRect(mTempRectF);
- mTempRectF.roundOut(mTmpCropRect);
-
- params.applySurfaceParams(params.createSurfaceParams(this));
- }
-
- @Override
- public void onBuildTargetParams(
- Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
- builder.withMatrix(mMatrix)
- .withWindowCrop(mTmpCropRect)
- .withCornerRadius(getCurrentCornerRadius());
- }
-
- /**
- * Returns the corner radius that should be applied to the target so that it matches the
- * TaskView
- */
- public float getCurrentCornerRadius() {
- float visibleRadius = mCurrentFullscreenParams.mCurrentDrawnCornerRadius;
- mTempPoint[0] = visibleRadius;
- mTempPoint[1] = 0;
- mInversePositionMatrix.mapVectors(mTempPoint);
-
- // Ideally we should use square-root. This is an optimization as one of the dimension is 0.
- return Math.max(Math.abs(mTempPoint[0]), Math.abs(mTempPoint[1]));
- }
-
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
deleted file mode 100644
index 0135f74..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import android.util.FloatProperty;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
-import com.android.quickstep.RemoteAnimationTargets;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-import com.android.systemui.shared.system.TransactionCompat;
-
-public class TransformParams {
-
- public static FloatProperty<TransformParams> PROGRESS =
- new FloatProperty<TransformParams>("progress") {
- @Override
- public void setValue(TransformParams params, float v) {
- params.setProgress(v);
- }
-
- @Override
- public Float get(TransformParams params) {
- return params.getProgress();
- }
- };
-
- public static FloatProperty<TransformParams> TARGET_ALPHA =
- new FloatProperty<TransformParams>("targetAlpha") {
- @Override
- public void setValue(TransformParams params, float v) {
- params.setTargetAlpha(v);
- }
-
- @Override
- public Float get(TransformParams params) {
- return params.getTargetAlpha();
- }
- };
-
- private float mProgress;
- private float mTargetAlpha;
- private float mCornerRadius;
- private RemoteAnimationTargets mTargetSet;
- private SurfaceTransactionApplier mSyncTransactionApplier;
-
- private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
- private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
-
- public TransformParams() {
- mProgress = 0;
- mTargetAlpha = 1;
- mCornerRadius = -1;
- }
-
- /**
- * Sets the progress of the transformation, where 0 is the source and 1 is the target. We
- * automatically adjust properties such as currentRect and cornerRadius based on this
- * progress, unless they are manually overridden by setting them on this TransformParams.
- */
- public TransformParams setProgress(float progress) {
- mProgress = progress;
- return this;
- }
-
- /**
- * Sets the corner radius of the transformed window, in pixels. If unspecified (-1), we
- * simply interpolate between the window's corner radius to the task view's corner radius,
- * based on {@link #mProgress}.
- */
- public TransformParams setCornerRadius(float cornerRadius) {
- mCornerRadius = cornerRadius;
- return this;
- }
-
- /**
- * Specifies the alpha of the transformed window. Default is 1.
- */
- public TransformParams setTargetAlpha(float targetAlpha) {
- mTargetAlpha = targetAlpha;
- return this;
- }
-
- /**
- * Specifies the set of RemoteAnimationTargetCompats that are included in the transformation
- * that these TransformParams help compute. These TransformParams generally only apply to
- * the targetSet.apps which match the targetSet.targetMode (e.g. the MODE_CLOSING app when
- * swiping to home).
- */
- public TransformParams setTargetSet(RemoteAnimationTargets targetSet) {
- mTargetSet = targetSet;
- return this;
- }
-
- /**
- * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that
- * are computed based on these TransformParams.
- */
- public TransformParams setSyncTransactionApplier(
- SurfaceTransactionApplier applier) {
- mSyncTransactionApplier = applier;
- return this;
- }
-
- /**
- * Sets an alternate function to control transform for non-target apps. The default
- * implementation keeps the targets visible with alpha=1
- */
- public TransformParams setBaseBuilderProxy(BuilderProxy proxy) {
- mBaseBuilderProxy = proxy;
- return this;
- }
-
- /**
- * Sets an alternate function to control transform for home target. The default
- * implementation keeps the targets visible with alpha=1
- */
- public TransformParams setHomeBuilderProxy(BuilderProxy proxy) {
- mHomeBuilderProxy = proxy;
- return this;
- }
-
- public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
- RemoteAnimationTargets targets = mTargetSet;
- SurfaceParams[] surfaceParams = new SurfaceParams[targets.unfilteredApps.length];
- for (int i = 0; i < targets.unfilteredApps.length; i++) {
- RemoteAnimationTargetCompat app = targets.unfilteredApps[i];
- SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash);
-
- if (app.mode == targets.targetMode) {
- if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
- mHomeBuilderProxy.onBuildTargetParams(builder, app, this);
- } else {
- // Fade out Assistant overlay.
- if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
- && app.isNotInRecents) {
- float progress = Utilities.boundToRange(getProgress(), 0, 1);
- builder.withAlpha(1 - Interpolators.DEACCEL_2_5.getInterpolation(progress));
- } else {
- builder.withAlpha(getTargetAlpha());
- }
-
- proxy.onBuildTargetParams(builder, app, this);
- }
- } else {
- mBaseBuilderProxy.onBuildTargetParams(builder, app, this);
- }
- surfaceParams[i] = builder.build();
- }
- return surfaceParams;
- }
-
- // Pubic getters so outside packages can read the values.
-
- public float getProgress() {
- return mProgress;
- }
-
- public float getTargetAlpha() {
- return mTargetAlpha;
- }
-
- public float getCornerRadius() {
- return mCornerRadius;
- }
-
- public RemoteAnimationTargets getTargetSet() {
- return mTargetSet;
- }
-
- public void applySurfaceParams(SurfaceParams[] params) {
- if (mSyncTransactionApplier != null) {
- mSyncTransactionApplier.scheduleApply(params);
- } else {
- TransactionCompat t = new TransactionCompat();
- for (SurfaceParams param : params) {
- SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
- }
- t.apply();
- }
- }
-
- @FunctionalInterface
- public interface BuilderProxy {
-
- BuilderProxy NO_OP = (builder, app, params) -> { };
- BuilderProxy ALWAYS_VISIBLE = (builder, app, params) ->builder.withAlpha(1);
-
- void onBuildTargetParams(SurfaceParams.Builder builder,
- RemoteAnimationTargetCompat app, TransformParams params);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
deleted file mode 100644
index 29b9558..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-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_UP;
-
-import static com.android.launcher3.Utilities.squaredHypot;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.Utilities;
-
-/**
- * Tracks motion events to determine whether a gesture on the nav bar is a swipe up.
- */
-public class TriggerSwipeUpTouchTracker {
-
- private final PointF mDownPos = new PointF();
- private final float mSquaredTouchSlop;
- private final float mMinFlingVelocity;
- private final boolean mDisableHorizontalSwipe;
- private final NavBarPosition mNavBarPosition;
- private final Runnable mOnInterceptTouch;
- private final OnSwipeUpListener mOnSwipeUp;
-
- private boolean mInterceptedTouch;
- private VelocityTracker mVelocityTracker;
-
- public TriggerSwipeUpTouchTracker(Context context, boolean disableHorizontalSwipe,
- NavBarPosition navBarPosition, Runnable onInterceptTouch,
- OnSwipeUpListener onSwipeUp) {
- mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
- mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
- mNavBarPosition = navBarPosition;
- mDisableHorizontalSwipe = disableHorizontalSwipe;
- mOnInterceptTouch = onInterceptTouch;
- mOnSwipeUp = onSwipeUp;
-
- init();
- }
-
- /**
- * Reset some initial values to prepare for the next gesture.
- */
- public void init() {
- mInterceptedTouch = false;
- mVelocityTracker = VelocityTracker.obtain();
- }
-
- /**
- * @return Whether we have passed the touch slop and are still tracking the gesture.
- */
- public boolean interceptedTouch() {
- return mInterceptedTouch;
- }
-
- /**
- * Track motion events to determine whether an atomic swipe up has occurred.
- */
- public void onMotionEvent(MotionEvent ev) {
- if (mVelocityTracker == null) {
- return;
- }
-
- mVelocityTracker.addMovement(ev);
- switch (ev.getActionMasked()) {
- case ACTION_DOWN: {
- mDownPos.set(ev.getX(), ev.getY());
- break;
- }
- case ACTION_MOVE: {
- if (!mInterceptedTouch) {
- float displacementX = ev.getX() - mDownPos.x;
- float displacementY = ev.getY() - mDownPos.y;
- if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
- if (mDisableHorizontalSwipe
- && Math.abs(displacementX) > Math.abs(displacementY)) {
- // Horizontal gesture is not allowed in this region
- endTouchTracking();
- break;
- }
-
- mInterceptedTouch = true;
-
- if (mOnInterceptTouch != null) {
- mOnInterceptTouch.run();
- }
- }
- }
- break;
- }
-
- case ACTION_CANCEL:
- endTouchTracking();
- break;
-
- case ACTION_UP: {
- onGestureEnd(ev);
- endTouchTracking();
- break;
- }
- }
- }
-
- private void endTouchTracking() {
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- }
-
- private void onGestureEnd(MotionEvent ev) {
- mVelocityTracker.computeCurrentVelocity(1000);
- float velocityX = mVelocityTracker.getXVelocity();
- float velocityY = mVelocityTracker.getYVelocity();
- float velocity = mNavBarPosition.isRightEdge()
- ? -velocityX
- : mNavBarPosition.isLeftEdge()
- ? velocityX
- : -velocityY;
-
- final boolean wasFling = Math.abs(velocity) >= mMinFlingVelocity;
- final boolean isSwipeUp;
- if (wasFling) {
- isSwipeUp = velocity > 0;
- } else {
- float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
- float displacementY = ev.getY() - mDownPos.y;
- isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
- }
-
- if (mOnSwipeUp != null) {
- if (isSwipeUp) {
- mOnSwipeUp.onSwipeUp(wasFling, new PointF(velocityX, velocityY));
- } else {
- mOnSwipeUp.onSwipeUpCancelled();
- }
- }
- }
-
- /**
- * Callback when the gesture ends and was determined to be a swipe from the nav bar.
- */
- public interface OnSwipeUpListener {
- /**
- * Called on touch up if a swipe up was detected.
- * @param wasFling Whether the swipe was a fling, or just passed touch slop at low velocity.
- * @param finalVelocity The final velocity of the swipe.
- */
- void onSwipeUp(boolean wasFling, PointF finalVelocity);
-
- /** Called on touch up if a swipe up was not detected. */
- void onSwipeUpCancelled();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
deleted file mode 100644
index 0979c07..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_EDU_SHOWN;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.GradientDrawable;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.ViewGroup;
-
-import androidx.core.graphics.ColorUtils;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.util.Themes;
-import com.android.quickstep.util.MultiValueUpdateListener;
-
-/**
- * View used to educate the user on how to access All Apps when in No Nav Button navigation mode.
- */
-public class AllAppsEduView extends AbstractFloatingView {
-
- private Launcher mLauncher;
-
- private AnimatorSet mAnimation;
-
- private GradientDrawable mCircle;
- private GradientDrawable mGradient;
-
- private int mCircleSizePx;
- private int mPaddingPx;
- private int mWidthPx;
- private int mMaxHeightPx;
-
- public AllAppsEduView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle);
- mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size);
- mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding);
- mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width);
- mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height);
- setWillNotDraw(false);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- mGradient.draw(canvas);
- mCircle.draw(canvas);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mIsOpen = true;
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mIsOpen = false;
- }
-
- @Override
- protected void handleClose(boolean animate) {
- mLauncher.getDragLayer().removeView(this);
- }
-
- @Override
- public void logActionCommand(int command) {
- // TODO
- }
-
- @Override
- protected boolean isOfType(int type) {
- return (type & TYPE_ALL_APPS_EDU) != 0;
- }
-
- @Override
- public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- return mAnimation != null && mAnimation.isRunning();
- }
-
- private void playAnimation() {
- if (mAnimation != null) {
- return;
- }
- mAnimation = new AnimatorSet();
-
- final Rect circleBoundsOg = new Rect(mCircle.getBounds());
- final Rect gradientBoundsOg = new Rect(mGradient.getBounds());
- final Rect temp = new Rect();
- final float transY = mMaxHeightPx - mCircleSizePx - mPaddingPx;
-
- // 1st: Circle alpha/scale
- int firstPart = 600;
- // 2nd: Circle animates upwards, Gradient alpha fades in, Gradient grows, All Apps hint
- int secondPart = 1200;
- int introDuration = firstPart + secondPart;
-
- StateAnimationConfig config = new StateAnimationConfig();
- config.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
- 0, 0.08f));
- config.duration = secondPart;
- config.userControlled = false;
- AnimatorPlaybackController stateAnimationController =
- mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, config);
- float maxAllAppsProgress = 0.15f;
-
- ValueAnimator intro = ValueAnimator.ofFloat(0, 1f);
- intro.setInterpolator(LINEAR);
- intro.setDuration(introDuration);
- intro.addUpdateListener((new MultiValueUpdateListener() {
- FloatProp mCircleAlpha = new FloatProp(0, 255, 0, firstPart, LINEAR);
- FloatProp mCircleScale = new FloatProp(2f, 1f, 0, firstPart, OVERSHOOT_1_7);
- FloatProp mDeltaY = new FloatProp(0, transY, firstPart, secondPart, FAST_OUT_SLOW_IN);
- FloatProp mGradientAlpha = new FloatProp(0, 255, firstPart, secondPart * 0.3f, LINEAR);
-
- @Override
- public void onUpdate(float progress) {
- temp.set(circleBoundsOg);
- temp.offset(0, (int) -mDeltaY.value);
- Utilities.scaleRectAboutCenter(temp, mCircleScale.value);
- mCircle.setBounds(temp);
- mCircle.setAlpha((int) mCircleAlpha.value);
- mGradient.setAlpha((int) mGradientAlpha.value);
-
- temp.set(gradientBoundsOg);
- temp.top -= mDeltaY.value;
- mGradient.setBounds(temp);
- invalidate();
-
- float stateProgress = Utilities.mapToRange(mDeltaY.value, 0, transY, 0,
- maxAllAppsProgress, LINEAR);
- stateAnimationController.setPlayFraction(stateProgress);
- }
- }));
- intro.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mCircle.setAlpha(0);
- mGradient.setAlpha(0);
- }
- });
- mAnimation.play(intro);
-
- ValueAnimator closeAllApps = ValueAnimator.ofFloat(maxAllAppsProgress, 0f);
- closeAllApps.addUpdateListener(valueAnimator -> {
- stateAnimationController.setPlayFraction((float) valueAnimator.getAnimatedValue());
- });
- closeAllApps.setInterpolator(FAST_OUT_SLOW_IN);
- closeAllApps.setStartDelay(introDuration);
- closeAllApps.setDuration(250);
- mAnimation.play(closeAllApps);
-
- mAnimation.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mAnimation = null;
- stateAnimationController.dispatchOnCancel();
- handleClose(false);
- }
- });
- mAnimation.start();
- }
-
- private void init(Launcher launcher) {
- mLauncher = launcher;
-
- int accentColor = Themes.getColorAccent(launcher);
- mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
- Themes.getAttrBoolean(launcher, R.attr.isMainColorDark)
- ? new int[] {0xB3FFFFFF, 0x00FFFFFF}
- : new int[] {ColorUtils.setAlphaComponent(accentColor, 127),
- ColorUtils.setAlphaComponent(accentColor, 0)});
- float r = mWidthPx / 2f;
- mGradient.setCornerRadii(new float[] {r, r, r, r, 0, 0, 0, 0});
-
- int top = mMaxHeightPx - mCircleSizePx + mPaddingPx;
- mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx);
- mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx);
-
- DeviceProfile grid = launcher.getDeviceProfile();
- DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx);
- lp.ignoreInsets = true;
- lp.leftMargin = (grid.widthPx - mWidthPx) / 2;
- lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx;
- setLayoutParams(lp);
- }
-
- /**
- * Shows the All Apps education view and plays the animation.
- */
- public static void show(Launcher launcher) {
- final DragLayer dragLayer = launcher.getDragLayer();
- ViewGroup parent = (ViewGroup) dragLayer.getParent();
- AllAppsEduView view = launcher.getViewCache().getView(R.layout.all_apps_edu_view,
- launcher, parent);
- view.init(launcher);
- launcher.getDragLayer().addView(view);
- launcher.getStatsLogManager().logger().log(LAUNCHER_ALL_APPS_EDU_SHOWN);
-
- view.requestLayout();
- view.playAnimation();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
deleted file mode 100644
index fd74357..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.widget.Button;
-
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.quickstep.views.RecentsView.PageCallbacks;
-import com.android.quickstep.views.RecentsView.ScrollState;
-
-public class ClearAllButton extends Button implements PageCallbacks {
-
- public static final FloatProperty<ClearAllButton> VISIBILITY_ALPHA =
- new FloatProperty<ClearAllButton>("visibilityAlpha") {
- @Override
- public Float get(ClearAllButton view) {
- return view.mVisibilityAlpha;
- }
-
- @Override
- public void setValue(ClearAllButton view, float v) {
- view.setVisibilityAlpha(v);
- }
- };
-
- private float mScrollAlpha = 1;
- private float mContentAlpha = 1;
- private float mVisibilityAlpha = 1;
-
- private boolean mIsRtl;
-
- private int mScrollOffset;
-
- public ClearAllButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
- mScrollOffset = orientationHandler.getClearAllScrollOffset(getRecentsView(), mIsRtl);
- }
-
- private RecentsView getRecentsView() {
- return (RecentsView) getParent();
- }
-
- @Override
- public void onRtlPropertiesChanged(int layoutDirection) {
- super.onRtlPropertiesChanged(layoutDirection);
- mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- public void setContentAlpha(float alpha) {
- if (mContentAlpha != alpha) {
- mContentAlpha = alpha;
- updateAlpha();
- }
- }
-
- public void setVisibilityAlpha(float alpha) {
- if (mVisibilityAlpha != alpha) {
- mVisibilityAlpha = alpha;
- updateAlpha();
- }
- }
-
- @Override
- public void onPageScroll(ScrollState scrollState) {
- PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
- float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
- if (orientationSize == 0) {
- return;
- }
-
- float shift = Math.min(scrollState.scrollFromEdge, orientationSize);
- float translation = mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift);
- orientationHandler.setPrimaryAndResetSecondaryTranslate(this, translation);
- mScrollAlpha = 1 - shift / orientationSize;
- updateAlpha();
- }
-
- private void updateAlpha() {
- final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
- setAlpha(alpha);
- setClickable(alpha == 1);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
deleted file mode 100644
index b06d4bc..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
-
-import static com.android.launcher3.Utilities.prefixTextWithIcon;
-import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
-
-import android.annotation.TargetApi;
-import android.app.ActivityOptions;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.AppUsageLimit;
-import android.icu.text.MeasureFormat;
-import android.icu.text.MeasureFormat.FormatWidth;
-import android.icu.util.Measure;
-import android.icu.util.MeasureUnit;
-import android.os.Build;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.StringRes;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.R;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.time.Duration;
-import java.util.Locale;
-
-@TargetApi(Build.VERSION_CODES.Q)
-public final class DigitalWellBeingToast {
- static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
- static final int MINUTE_MS = 60000;
-
- private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
-
- private final BaseDraggingActivity mActivity;
- private final TaskView mTaskView;
- private final LauncherApps mLauncherApps;
-
- private Task mTask;
- private boolean mHasLimit;
- private long mAppRemainingTimeMs;
-
- public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
- mActivity = activity;
- mTaskView = taskView;
- mLauncherApps = activity.getSystemService(LauncherApps.class);
- }
-
- private void setTaskFooter(View view) {
- View oldFooter = mTaskView.setFooter(TaskView.INDEX_DIGITAL_WELLBEING_TOAST, view);
- if (oldFooter != null) {
- oldFooter.setOnClickListener(null);
- mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, oldFooter);
- }
- }
-
- private void setNoLimit() {
- mHasLimit = false;
- mTaskView.setContentDescription(mTask.titleDescription);
- setTaskFooter(null);
- mAppRemainingTimeMs = 0;
- }
-
- private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
- mAppRemainingTimeMs = appRemainingTimeMs;
- mHasLimit = true;
- TextView toast = mActivity.getViewCache().getView(R.layout.digital_wellbeing_toast,
- mActivity, mTaskView);
- toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
- toast.setOnClickListener(this::openAppUsageSettings);
- setTaskFooter(toast);
-
- mTaskView.setContentDescription(
- getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
- RecentsView rv = mTaskView.getRecentsView();
- if (rv != null) {
- rv.onDigitalWellbeingToastShown();
- }
- }
-
- public String getText() {
- return getText(mAppRemainingTimeMs);
- }
-
- public boolean hasLimit() {
- return mHasLimit;
- }
-
- public void initialize(Task task) {
- mTask = task;
-
- if (task.key.userId != UserHandle.myUserId()) {
- setNoLimit();
- return;
- }
-
- THREAD_POOL_EXECUTOR.execute(() -> {
- final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
- task.getTopComponent().getPackageName(),
- UserHandle.of(task.key.userId));
-
- final long appUsageLimitTimeMs =
- usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
- final long appRemainingTimeMs =
- usageLimit != null ? usageLimit.getUsageRemaining() : -1;
-
- mTaskView.post(() -> {
- if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
- setNoLimit();
- } else {
- setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
- }
- });
- });
- }
-
- private String getReadableDuration(
- Duration duration,
- FormatWidth formatWidthHourAndMinute,
- @StringRes int durationLessThanOneMinuteStringId,
- boolean forceFormatWidth) {
- int hours = Math.toIntExact(duration.toHours());
- int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
-
- // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero.
- if (hours > 0 && minutes > 0) {
- return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute)
- .formatMeasures(
- new Measure(hours, MeasureUnit.HOUR),
- new Measure(minutes, MeasureUnit.MINUTE));
- }
-
- // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced).
- if (hours > 0) {
- return MeasureFormat.getInstance(
- Locale.getDefault(),
- forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
- .formatMeasures(new Measure(hours, MeasureUnit.HOUR));
- }
-
- // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced).
- if (minutes > 0) {
- return MeasureFormat.getInstance(
- Locale.getDefault()
- , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
- .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE));
- }
-
- // Use a specific string for usage less than one minute but non-zero.
- if (duration.compareTo(Duration.ZERO) > 0) {
- return mActivity.getString(durationLessThanOneMinuteStringId);
- }
-
- // Otherwise, return 0-minute string.
- return MeasureFormat.getInstance(
- Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
- .formatMeasures(new Measure(0, MeasureUnit.MINUTE));
- }
-
- private String getReadableDuration(
- Duration duration,
- FormatWidth formatWidthHourAndMinute,
- @StringRes int durationLessThanOneMinuteStringId) {
- return getReadableDuration(
- duration,
- formatWidthHourAndMinute,
- durationLessThanOneMinuteStringId,
- /* forceFormatWidth= */ false);
- }
-
- private String getRoundedUpToMinuteReadableDuration(long remainingTime) {
- final Duration duration = Duration.ofMillis(
- remainingTime > MINUTE_MS ?
- (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
- remainingTime);
- return getReadableDuration(
- duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
- }
-
- private String getText(long remainingTime) {
- return mActivity.getString(
- R.string.time_left_for_app,
- getRoundedUpToMinuteReadableDuration(remainingTime));
- }
-
- public void openAppUsageSettings(View view) {
- final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
- .putExtra(Intent.EXTRA_PACKAGE_NAME,
- mTask.getTopComponent().getPackageName()).addFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- try {
- final BaseActivity activity = BaseActivity.fromContext(view.getContext());
- final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
- view, 0, 0,
- view.getWidth(), view.getHeight());
- activity.startActivity(intent, options.toBundle());
- activity.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
- LauncherLogProto.ControlType.APP_USAGE_SETTINGS, view);
- } catch (ActivityNotFoundException e) {
- Log.e(TAG, "Failed to open app usage settings for task "
- + mTask.getTopComponent().getPackageName(), e);
- }
- }
-
- private String getContentDescriptionForTask(
- Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
- return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
- mActivity.getString(
- R.string.task_contents_description_with_remaining_time,
- task.titleDescription,
- getText(appRemainingTimeMs)) :
- task.titleDescription;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
deleted file mode 100644
index 7cc00b7..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.FastBitmapDrawable;
-
-import java.util.ArrayList;
-
-/**
- * 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 {
-
- public interface OnScaleUpdateListener {
- public void onScaleUpdate(float scale);
- }
-
- private Drawable mDrawable;
-
- private ArrayList<OnScaleUpdateListener> mScaleListeners;
-
- 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();
- }
-
- public Drawable getDrawable() {
- return mDrawable;
- }
-
- @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
- public void invalidateDrawable(@NonNull Drawable drawable) {
- super.invalidateDrawable(drawable);
- if (drawable instanceof FastBitmapDrawable && mScaleListeners != null) {
- for (OnScaleUpdateListener listener : mScaleListeners) {
- listener.onScaleUpdate(((FastBitmapDrawable) drawable).getScale());
- }
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (mDrawable != null) {
- mDrawable.draw(canvas);
- }
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- public void addUpdateScaleListener(OnScaleUpdateListener listener) {
- if (mScaleListeners == null) {
- mScaleListeners = new ArrayList<>();
- }
- mScaleListeners.add(listener);
- if (mDrawable instanceof FastBitmapDrawable) {
- listener.onScaleUpdate(((FastBitmapDrawable) mDrawable).getScale());
- }
- }
-
- public void removeUpdateScaleListener(OnScaleUpdateListener listener) {
- if (mScaleListeners != null) {
- mScaleListeners.remove(listener);
- }
- }
-
- @Override
- public void setAlpha(float alpha) {
- super.setAlpha(alpha);
- if (alpha > 0) {
- setVisibility(VISIBLE);
- } else {
- setVisibility(INVISIBLE);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
deleted file mode 100644
index 846b944..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ /dev/null
@@ -1,367 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
-import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
-import static com.android.launcher3.LauncherState.SPRING_LOADED;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
-import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.UserHandle;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.Surface;
-import android.widget.FrameLayout;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.Hotseat;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.views.ScrimView;
-import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.TransformParams;
-import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.plugins.RecentsExtraCard;
-import com.android.systemui.shared.recents.model.Task;
-
-/**
- * {@link RecentsView} used in Launcher activity
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class LauncherRecentsView extends RecentsView<BaseQuickstepLauncher>
- implements StateListener<LauncherState> {
-
- private final TransformParams mTransformParams = new TransformParams();
-
- private RecentsExtraCard mRecentsExtraCardPlugin;
- private RecentsExtraViewContainer mRecentsExtraViewContainer;
- private PluginListener<RecentsExtraCard> mRecentsExtraCardPluginListener =
- new PluginListener<RecentsExtraCard>() {
- @Override
- public void onPluginConnected(RecentsExtraCard recentsExtraCard, Context context) {
- createRecentsExtraCard();
- mRecentsExtraCardPlugin = recentsExtraCard;
- mRecentsExtraCardPlugin.setupView(context, mRecentsExtraViewContainer, mActivity);
- }
-
- @Override
- public void onPluginDisconnected(RecentsExtraCard plugin) {
- removeView(mRecentsExtraViewContainer);
- mRecentsExtraCardPlugin = null;
- mRecentsExtraViewContainer = null;
- }
- };
-
- 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, LauncherActivityInterface.INSTANCE);
- mActivity.getStateManager().addStateListener(this);
- }
-
- @Override
- public void init(OverviewActionsView actionsView) {
- super.init(actionsView);
- setContentAlpha(0);
- }
-
- @Override
- public void startHome() {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- switchToScreenshot(null,
- () -> finishRecentsAnimation(true /* toRecents */,
- () -> mActivity.getStateManager().goToState(NORMAL)));
- } else {
- mActivity.getStateManager().goToState(NORMAL);
- }
- }
-
- @Override
- public void setTranslationY(float translationY) {
- super.setTranslationY(translationY);
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- LauncherState state = mActivity.getStateManager().getState();
- if (state == OVERVIEW || state == ALL_APPS) {
- redrawLiveTile(false);
- }
- }
- }
-
- /**
- * Animates adjacent tasks and translate hotseat off screen as well.
- */
- @Override
- public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
- AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv);
-
- if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
- // 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.getScrimView(), ScrimView.DRAG_HANDLE_ALPHA, 0);
- dragHandleAnim.setInterpolator(Interpolators.ACCEL_2);
- anim.play(dragHandleAnim);
-
- return anim;
- }
-
- @Override
- protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- if (tv.isRunningTask()) {
- mTransformParams.setProgress(1 - progress)
- .setSyncTransactionApplier(mSyncTransactionApplier);
- // TODO: Revisit live tiles
- } else {
- redrawLiveTile(true);
- }
- }
- }
-
- @Override
- protected void onTaskLaunchAnimationEnd(boolean success) {
- if (success) {
- mActivity.getStateManager().goToState(NORMAL, false /* animate */);
- } else {
- LauncherState state = mActivity.getStateManager().getState();
- mActivity.getAllAppsController().setState(state);
- }
- super.onTaskLaunchAnimationEnd(success);
- }
-
- @Override
- public void onTaskLaunched(Task task) {
- UserHandle user = UserHandle.of(task.key.userId);
- AppLaunchTracker.INSTANCE.get(getContext()).onStartApp(task.getTopComponent(), user,
- AppLaunchTracker.CONTAINER_OVERVIEW);
- }
-
- @Override
- public boolean shouldUseMultiWindowTaskSizeStrategy() {
- return TraceHelper.whitelistIpcs("isInMultiWindowMode", mActivity::isInMultiWindowMode);
- }
-
- @Override
- public void scrollTo(int x, int y) {
- super.scrollTo(x, y);
- if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
- redrawLiveTile(true);
- }
- }
-
- @Override
- public TransformParams getLiveTileParams(
- boolean mightNeedToRefill) {
- if (!mEnableDrawingLiveTile || mRecentsAnimationController == null
- || mRecentsAnimationTargets == null) {
- return null;
- }
- TaskView taskView = getRunningTaskView();
- if (taskView != null) {
- taskView.getThumbnail().getGlobalVisibleRect(mTempRect);
- int offsetX = (int) (mTaskWidth * taskView.getScaleX() * getScaleX()
- - mTempRect.width());
- int offsetY = (int) (mTaskHeight * taskView.getScaleY() * getScaleY()
- - mTempRect.height());
- if (((mCurrentPage != 0) || mightNeedToRefill) && offsetX > 0) {
- if (mTempRect.left - offsetX < 0) {
- mTempRect.left -= offsetX;
- } else {
- mTempRect.right += offsetX;
- }
- }
- if (mightNeedToRefill && offsetY > 0) {
- mTempRect.top -= offsetY;
- }
- mTransformParams.setProgress(1f)
- .setTargetAlpha(taskView.getAlpha())
- .setSyncTransactionApplier(mSyncTransactionApplier)
- .setTargetSet(mRecentsAnimationTargets);
- }
- return mTransformParams;
- }
-
- @Override
- public void reset() {
- super.reset();
-
- setLayoutRotation(Surface.ROTATION_0, Surface.ROTATION_0);
- // We are moving to home or some other UI with no recents. Switch back to the home client,
- // the home predictions should have been updated when the activity was resumed.
- PredictionUiStateManager.INSTANCE.get(getContext()).switchClient(Client.HOME);
- }
-
- @Override
- public void onStateTransitionStart(LauncherState toState) {
- setOverviewStateEnabled(toState.overviewUi);
- setFreezeViewVisibility(true);
- }
-
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- if (finalState == NORMAL || finalState == SPRING_LOADED) {
- // Clean-up logic that occurs when recents is no longer in use/visible.
- reset();
- }
- setOverlayEnabled(finalState == OVERVIEW || finalState == OVERVIEW_MODAL_TASK);
- setFreezeViewVisibility(false);
- }
-
- @Override
- public void setOverviewStateEnabled(boolean enabled) {
- super.setOverviewStateEnabled(enabled);
- if (enabled) {
- LauncherState state = mActivity.getStateManager().getState();
- boolean hasClearAllButton = (state.getVisibleElements(mActivity)
- & OVERVIEW_BUTTONS) != 0;
- setDisallowScrollToClearAll(!hasClearAllButton);
- }
- }
-
- @Override
- protected boolean shouldStealTouchFromSiblingsBelow(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- // Allow touches to go through to the hotseat.
- Hotseat hotseat = mActivity.getHotseat();
- boolean touchingHotseat = hotseat.isShown()
- && mActivity.getDragLayer().isEventOverView(hotseat, ev, this);
- return !touchingHotseat;
- }
- return super.shouldStealTouchFromSiblingsBelow(ev);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(
- mRecentsExtraCardPluginListener, RecentsExtraCard.class);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(
- mRecentsExtraCardPluginListener);
- }
-
- @Override
- protected int computeMinScroll() {
- if (canComputeScrollX() && !mIsRtl) {
- return computeScrollX();
- }
- return super.computeMinScroll();
- }
-
- @Override
- protected int computeMaxScroll() {
- if (canComputeScrollX() && mIsRtl) {
- return computeScrollX();
- }
- return super.computeMaxScroll();
- }
-
- private boolean canComputeScrollX() {
- return mRecentsExtraCardPlugin != null && getTaskViewCount() > 0
- && !mDisallowScrollToClearAll;
- }
-
- private int computeScrollX() {
- int scrollIndex = getTaskViewStartIndex() - 1;
- while (scrollIndex >= 0 && getChildAt(scrollIndex) instanceof RecentsExtraViewContainer
- && ((RecentsExtraViewContainer) getChildAt(scrollIndex)).isScrollable()) {
- scrollIndex--;
- }
- return getScrollForPage(scrollIndex + 1);
- }
-
- private void createRecentsExtraCard() {
- mRecentsExtraViewContainer = new RecentsExtraViewContainer(getContext());
- FrameLayout.LayoutParams helpCardParams =
- new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT);
- mRecentsExtraViewContainer.setLayoutParams(helpCardParams);
- mRecentsExtraViewContainer.setScrollable(true);
- addView(mRecentsExtraViewContainer, 0);
- }
-
- @Override
- public boolean hasRecentsExtraCard() {
- return mRecentsExtraViewContainer != null;
- }
-
- @Override
- public void setContentAlpha(float alpha) {
- super.setContentAlpha(alpha);
- if (mRecentsExtraViewContainer != null) {
- mRecentsExtraViewContainer.setAlpha(alpha);
- }
- }
-
- @Override
- protected DepthController getDepthController() {
- return mActivity.getDepthController();
- }
-
- @Override
- public void setModalStateEnabled(boolean isModalState) {
- super.setModalStateEnabled(isModalState);
- if (isModalState) {
- mActivity.getStateManager().goToState(LauncherState.OVERVIEW_MODAL_TASK);
- } else {
- if (mActivity.isInState(LauncherState.OVERVIEW_MODAL_TASK)) {
- mActivity.getStateManager().goToState(LauncherState.OVERVIEW);
- }
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
deleted file mode 100644
index 30c9f77..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
+++ /dev/null
@@ -1,152 +0,0 @@
-package com.android.quickstep.views;
-
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.util.FloatProperty;
-import android.view.ViewOverlay;
-
-import com.android.launcher3.anim.Interpolators;
-
-public class LiveTileOverlay extends Drawable {
-
- private static final long ICON_ANIM_DURATION = 120;
-
- private static final FloatProperty<LiveTileOverlay> PROGRESS =
- new FloatProperty<LiveTileOverlay>("progress") {
- @Override
- public void setValue(LiveTileOverlay liveTileOverlay, float progress) {
- liveTileOverlay.setIconAnimationProgress(progress);
- }
-
- @Override
- public Float get(LiveTileOverlay liveTileOverlay) {
- return liveTileOverlay.mIconAnimationProgress;
- }
- };
-
- public static final LiveTileOverlay INSTANCE = new LiveTileOverlay();
-
- private final Paint mPaint = new Paint();
- private final Rect mBoundsRect = new Rect();
-
- private RectF mCurrentRect;
- private float mCornerRadius;
- private Drawable mIcon;
- private Animator mIconAnimator;
-
- private boolean mDrawEnabled = true;
- private float mIconAnimationProgress = 0f;
- private boolean mIsAttached;
-
- private LiveTileOverlay() {
- mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
- }
-
- public void update(RectF currentRect, float cornerRadius) {
- invalidateSelf();
-
- mCurrentRect = currentRect;
- mCornerRadius = cornerRadius;
-
- mCurrentRect.roundOut(mBoundsRect);
- setBounds(mBoundsRect);
- invalidateSelf();
- }
-
- public void setIcon(Drawable icon) {
- mIcon = icon;
- }
-
- public void startIconAnimation() {
- if (mIconAnimator != null) {
- mIconAnimator.cancel();
- }
- // This animator must match the icon part of {@link TaskView#FOCUS_TRANSITION} animation.
- mIconAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 1);
- mIconAnimator.setDuration(ICON_ANIM_DURATION).setInterpolator(LINEAR);
- mIconAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mIconAnimator = null;
- }
- });
- mIconAnimator.start();
- }
-
- public float cancelIconAnimation() {
- if (mIconAnimator != null) {
- mIconAnimator.cancel();
- }
- return mIconAnimationProgress;
- }
-
- public void setDrawEnabled(boolean drawEnabled) {
- if (mDrawEnabled != drawEnabled) {
- mDrawEnabled = drawEnabled;
- invalidateSelf();
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mCurrentRect != null && mDrawEnabled) {
- canvas.drawRoundRect(mCurrentRect, mCornerRadius, mCornerRadius, mPaint);
- if (mIcon != null && mIconAnimationProgress > 0f) {
- canvas.save();
- float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, 0f,
- 1f).getInterpolation(mIconAnimationProgress);
- canvas.translate(mCurrentRect.centerX() - mIcon.getBounds().width() / 2 * scale,
- mCurrentRect.top - mIcon.getBounds().height() / 2 * scale);
- canvas.scale(scale, scale);
- mIcon.draw(canvas);
- canvas.restore();
- }
- }
- }
-
- @Override
- public void setAlpha(int i) { }
-
- @Override
- public void setColorFilter(ColorFilter colorFilter) { }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- public boolean attach(ViewOverlay overlay) {
- if (overlay != null && !mIsAttached) {
- overlay.add(this);
- mIsAttached = true;
- return true;
- }
-
- return false;
- }
-
- public void detach(ViewOverlay overlay) {
- if (overlay != null) {
- overlay.remove(this);
- mIsAttached = false;
- }
- }
-
- private void setIconAnimationProgress(float progress) {
- mIconAnimationProgress = progress;
- invalidateSelf();
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
deleted file mode 100644
index a2da398..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SHARE;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.FrameLayout;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.R;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
-import com.android.quickstep.util.LayoutUtils;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * View for showing action buttons in Overview
- */
-public class OverviewActionsView<T extends OverlayUICallbacks> extends FrameLayout
- implements OnClickListener, Insettable {
-
- private final Rect mInsets = new Rect();
-
- @IntDef(flag = true, value = {
- HIDDEN_UNSUPPORTED_NAVIGATION,
- HIDDEN_DISABLED_FEATURE,
- HIDDEN_NON_ZERO_ROTATION,
- HIDDEN_NO_TASKS,
- HIDDEN_GESTURE_RUNNING,
- HIDDEN_NO_RECENTS})
- @Retention(RetentionPolicy.SOURCE)
- public @interface ActionsHiddenFlags { }
-
- public static final int HIDDEN_UNSUPPORTED_NAVIGATION = 1 << 0;
- public static final int HIDDEN_DISABLED_FEATURE = 1 << 1;
- public static final int HIDDEN_NON_ZERO_ROTATION = 1 << 2;
- public static final int HIDDEN_NO_TASKS = 1 << 3;
- public static final int HIDDEN_GESTURE_RUNNING = 1 << 4;
- public static final int HIDDEN_NO_RECENTS = 1 << 5;
-
- @IntDef(flag = true, value = {
- DISABLED_SCROLLING,
- DISABLED_ROTATED})
- @Retention(RetentionPolicy.SOURCE)
- public @interface ActionsDisabledFlags { }
-
- public static final int DISABLED_SCROLLING = 1 << 0;
- public static final int DISABLED_ROTATED = 1 << 1;
-
- private static final int INDEX_CONTENT_ALPHA = 0;
- private static final int INDEX_VISIBILITY_ALPHA = 1;
- private static final int INDEX_FULLSCREEN_ALPHA = 2;
- private static final int INDEX_HIDDEN_FLAGS_ALPHA = 3;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- @ActionsHiddenFlags
- private int mHiddenFlags;
-
- @ActionsDisabledFlags
- protected int mDisabledFlags;
-
- protected T mCallbacks;
-
- public OverviewActionsView(Context context) {
- this(context, null);
- }
-
- public OverviewActionsView(Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr, 0);
- mMultiValueAlpha = new MultiValueAlpha(this, 4);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- View share = findViewById(R.id.action_share);
- share.setOnClickListener(this);
- findViewById(R.id.action_screenshot).setOnClickListener(this);
- if (ENABLE_OVERVIEW_SHARE.get()) {
- share.setVisibility(VISIBLE);
- findViewById(R.id.share_space).setVisibility(VISIBLE);
- }
- }
-
- /**
- * Set listener for callbacks on action button taps.
- *
- * @param callbacks for callbacks, or {@code null} to clear the listener.
- */
- public void setCallbacks(T callbacks) {
- mCallbacks = callbacks;
- }
-
- @Override
- public void onClick(View view) {
- if (mCallbacks == null) {
- return;
- }
- int id = view.getId();
- if (id == R.id.action_share) {
- mCallbacks.onShare();
- } else if (id == R.id.action_screenshot) {
- mCallbacks.onScreenshot();
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- updateHiddenFlags(HIDDEN_DISABLED_FEATURE, !ENABLE_OVERVIEW_ACTIONS.get());
- updateHiddenFlags(HIDDEN_UNSUPPORTED_NAVIGATION, !removeShelfFromOverview(getContext()));
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
- }
-
- @Override
- public void setInsets(Rect insets) {
- mInsets.set(insets);
- updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
- }
-
- public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean enable) {
- if (enable) {
- mHiddenFlags |= visibilityFlags;
- } else {
- mHiddenFlags &= ~visibilityFlags;
- }
- boolean isHidden = mHiddenFlags != 0;
- mMultiValueAlpha.getProperty(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
- setVisibility(isHidden ? INVISIBLE : VISIBLE);
- }
-
- /**
- * Updates the proper disabled flag to indicate whether OverviewActionsView should be enabled.
- * Ignores DISABLED_ROTATED flag for determining enabled. Flag is used to enable/disable
- * buttons individually, currently done for select button in subclass.
- *
- * @param disabledFlags The flag to update.
- * @param enable Whether to enable the disable flag: True will cause view to be disabled.
- */
- public void updateDisabledFlags(@ActionsDisabledFlags int disabledFlags, boolean enable) {
- if (enable) {
- mDisabledFlags |= disabledFlags;
- } else {
- mDisabledFlags &= ~disabledFlags;
- }
- //
- boolean isEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
- LayoutUtils.setViewEnabled(this, isEnabled);
- }
-
- public AlphaProperty getContentAlpha() {
- return mMultiValueAlpha.getProperty(INDEX_CONTENT_ALPHA);
- }
-
- public AlphaProperty getVisibilityAlpha() {
- return mMultiValueAlpha.getProperty(INDEX_VISIBILITY_ALPHA);
- }
-
- public AlphaProperty getFullscreenAlpha() {
- return mMultiValueAlpha.getProperty(INDEX_FULLSCREEN_ALPHA);
- }
-
- /** Updates vertical margins for different navigation mode or configuration changes. */
- public void updateVerticalMargin(Mode mode) {
- int bottomMargin;
- int orientation = getResources().getConfiguration().orientation;
- if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
- bottomMargin = 0;
- } else if (mode == Mode.THREE_BUTTONS) {
- bottomMargin = getResources()
- .getDimensionPixelSize(R.dimen.overview_actions_bottom_margin_three_button);
- } else {
- bottomMargin = getResources()
- .getDimensionPixelSize(R.dimen.overview_actions_bottom_margin_gesture);
- }
- bottomMargin += mInsets.bottom;
- LayoutParams params = (LayoutParams) getLayoutParams();
- params.setMargins(
- params.leftMargin, params.topMargin, params.rightMargin, bottomMargin);
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java
deleted file mode 100644
index 1ea6d4a..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.FrameLayout;
-
-/**
- * Empty view to house recents overview extra card
- */
-public class RecentsExtraViewContainer extends FrameLayout implements RecentsView.PageCallbacks {
-
- private boolean mScrollable = false;
-
- public RecentsExtraViewContainer(Context context) {
- super(context);
- }
-
- public RecentsExtraViewContainer(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public RecentsExtraViewContainer(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- /**
- * Determine whether the view should be scrolled to in the recents overview, similar to the
- * taskviews.
- * @return true if viewed should be scrolled to, false if not
- */
- public boolean isScrollable() {
- return mScrollable;
- }
-
- public void setScrollable(boolean scrollable) {
- this.mScrollable = scrollable;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
deleted file mode 100644
index 7b24b03..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ /dev/null
@@ -1,2316 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static android.view.Surface.ROTATION_0;
-import static android.view.View.MeasureSpec.EXACTLY;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-
-import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
-import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
-import static com.android.launcher3.Utilities.mapToRange;
-import static com.android.launcher3.Utilities.squaredHypot;
-import static com.android.launcher3.Utilities.squaredTouchSlop;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
-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.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
-import static com.android.launcher3.statehandlers.DepthController.DEPTH;
-import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
-import static com.android.quickstep.views.OverviewActionsView.HIDDEN_GESTURE_RUNNING;
-import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
-import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
-import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
-
-import android.animation.AnimatorSet;
-import android.animation.LayoutTransition;
-import android.animation.LayoutTransition.TransitionListener;
-import android.animation.ObjectAnimator;
-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.content.res.Configuration;
-import android.graphics.Canvas;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.text.Layout;
-import android.text.StaticLayout;
-import android.text.TextPaint;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.util.Property;
-import android.util.SparseBooleanArray;
-import android.view.HapticFeedbackConstants;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewDebug;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.animation.Interpolator;
-import android.widget.ListView;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.PagedView;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.anim.PendingAnimation.EndState;
-import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.anim.SpringProperty;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.DynamicResource;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.util.OverScroller;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.ViewPool;
-import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationTargets;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.TaskThumbnailCache;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.ViewUtils;
-import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.RecentsOrientedState;
-import com.android.quickstep.util.SplitScreenBounds;
-import com.android.quickstep.util.SurfaceTransactionApplier;
-import com.android.quickstep.util.TransformParams;
-import com.android.systemui.plugins.ResourceProvider;
-import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.LauncherEventUtil;
-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<T extends StatefulActivity> extends PagedView implements
- Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
- InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener,
- SplitScreenBounds.OnChangeListener {
-
- private static final String TAG = RecentsView.class.getSimpleName();
-
- public static final FloatProperty<RecentsView> CONTENT_ALPHA =
- new FloatProperty<RecentsView>("contentAlpha") {
- @Override
- public void setValue(RecentsView view, float v) {
- view.setContentAlpha(v);
- }
-
- @Override
- public Float get(RecentsView view) {
- return view.getContentAlpha();
- }
- };
-
- public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS =
- new FloatProperty<RecentsView>("fullscreenProgress") {
- @Override
- public void setValue(RecentsView recentsView, float v) {
- recentsView.setFullscreenProgress(v);
- }
-
- @Override
- public Float get(RecentsView recentsView) {
- return recentsView.mFullscreenProgress;
- }
- };
-
- public static final FloatProperty<RecentsView> TASK_MODALNESS =
- new FloatProperty<RecentsView>("taskModalness") {
- @Override
- public void setValue(RecentsView recentsView, float v) {
- recentsView.setTaskModalness(v);
- }
-
- @Override
- public Float get(RecentsView recentsView) {
- return recentsView.mTaskModalness;
- }
- };
-
- public static final FloatProperty<RecentsView> ADJACENT_PAGE_OFFSET =
- new FloatProperty<RecentsView>("adjacentPageOffset") {
- @Override
- public void setValue(RecentsView recentsView, float v) {
- if (recentsView.mAdjacentPageOffset != v) {
- recentsView.mAdjacentPageOffset = v;
- recentsView.updatePageOffsets();
- }
- }
-
- @Override
- public Float get(RecentsView recentsView) {
- return recentsView.mAdjacentPageOffset;
- }
- };
-
- protected RecentsOrientedState mOrientationState;
- protected final BaseActivityInterface mSizeStrategy;
- protected RecentsAnimationController mRecentsAnimationController;
- protected RecentsAnimationTargets mRecentsAnimationTargets;
- protected SurfaceTransactionApplier mSyncTransactionApplier;
- protected int mTaskWidth;
- protected int mTaskHeight;
- protected boolean mEnableDrawingLiveTile = false;
- protected final Rect mTempRect = new Rect();
- private final PointF mTempPointF = new PointF();
-
- private static final int DISMISS_TASK_DURATION = 300;
- private static final int ADDITION_TASK_DURATION = 200;
- // The threshold at which we update the SystemUI flags when animating from the task into the app
- public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f;
-
- protected final T mActivity;
- private final float mFastFlingVelocity;
- private final RecentsModel mModel;
- private final int mTaskTopMargin;
- private final ClearAllButton mClearAllButton;
- private final Rect mClearAllButtonDeadZoneRect = new Rect();
- private final Rect mTaskViewDeadZoneRect = new Rect();
-
- 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 final InvariantDeviceProfile mIdp;
-
- private final ViewPool<TaskView> mTaskViewPool;
-
- private boolean mDwbToastShown;
- protected boolean mDisallowScrollToClearAll;
- private boolean mOverlayEnabled;
- protected boolean mFreezeViewVisibility;
-
- private float mAdjacentPageOffset = 0;
-
- /**
- * TODO: Call reloadIdNeeded in onTaskStackChanged.
- */
- private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
- @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;
- }
-
- reloadIfNeeded();
- enableLayoutTransitions();
- }
-
- @Override
- public void onTaskRemoved(int taskId) {
- if (!mHandleTaskStackChanges) {
- return;
- }
-
- UI_HELPER_EXECUTOR.execute(() -> {
- 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 {
- mModel.findTaskWithId(taskKey.id, (key) -> {
- if (key == null) {
- // The task was removed from the recents list
- handler.post(() -> dismissTask(taskView, true /* animate */,
- false /* removeTask */));
- }
- });
- }
- });
- }
- };
-
- private final PinnedStackAnimationListener mIPinnedStackAnimationListener =
- new PinnedStackAnimationListener();
-
- // Used to keep track of the last requested task list id, so that we do not request to load the
- // tasks again if we have already requested it and the task list has not changed
- private int mTaskListChangeId = -1;
-
- // Only valid until the launcher state changes to NORMAL
- protected int mRunningTaskId = -1;
- protected boolean mRunningTaskTileHidden;
- private Task mTmpRunningTask;
-
- private boolean mRunningTaskIconScaledDown = false;
-
- private boolean mOverviewStateEnabled;
- private boolean mHandleTaskStackChanges;
- private boolean mSwipeDownShouldLaunchApp;
- private boolean mTouchDownToStartHome;
- private final float mSquaredTouchSlop;
- private int mDownX;
- private int mDownY;
-
- private PendingAnimation mPendingAnimation;
- private LayoutTransition mLayoutTransition;
-
- @ViewDebug.ExportedProperty(category = "launcher")
- protected float mContentAlpha = 1;
- @ViewDebug.ExportedProperty(category = "launcher")
- protected float mFullscreenProgress = 0;
- /**
- * How modal is the current task to be displayed, 1 means the task is fully modal and no other
- * tasks are show. 0 means the task is displays in context in the list with other tasks.
- */
- @ViewDebug.ExportedProperty(category = "launcher")
- protected float mTaskModalness = 0;
-
- // Keeps track of task id whose visual state should not be reset
- private int mIgnoreResetTaskId = -1;
-
- // 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 OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
- private Layout mEmptyTextLayout;
- private boolean mLiveTileOverlayAttached;
-
- // Keeps track of the index where the first TaskView should be
- private int mTaskViewStartIndex = 0;
- private OverviewActionsView mActionsView;
-
- private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
- (inMultiWindowMode) -> {
- if (mOrientationState != null) {
- mOrientationState.setMultiWindowMode(inMultiWindowMode);
- setLayoutRotation(mOrientationState.getTouchRotation(),
- mOrientationState.getDisplayRotation());
- rotateAllChildTasks();
- }
- if (!inMultiWindowMode && mOverviewStateEnabled) {
- // TODO: Re-enable layout transitions for addition of the unpinned task
- reloadIfNeeded();
- }
- };
-
- public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
- BaseActivityInterface sizeStrategy) {
- super(context, attrs, defStyleAttr);
- setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
- setEnableFreeScroll(true);
- mSizeStrategy = sizeStrategy;
- mActivity = BaseActivity.fromContext(context);
- mOrientationState = new RecentsOrientedState(
- context, mSizeStrategy, this::animateRecentsRotationInPlace);
- mOrientationState.setActivityConfiguration(context.getResources().getConfiguration());
-
- mFastFlingVelocity = getResources()
- .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
- mModel = RecentsModel.INSTANCE.get(context);
- mIdp = InvariantDeviceProfile.INSTANCE.get(context);
-
- mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
- .inflate(R.layout.overview_clear_all_button, this, false);
- mClearAllButton.setOnClickListener(this::dismissAllTasks);
- mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
- 10 /* initial size */);
-
- mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
- setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
- mTaskTopMargin = getResources()
- .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
- mSquaredTouchSlop = squaredTouchSlop(context);
-
- 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));
- mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
- Typeface.NORMAL));
- mEmptyMessagePadding = getResources()
- .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
- setWillNotDraw(false);
- updateEmptyMessage();
- mOrientationHandler = mOrientationState.getOrientationHandler();
-
- // Initialize quickstep specific cache params here, as this is constructed only once
- mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
- }
-
- public OverScroller getScroller() {
- return mScroller;
- }
-
- public boolean isRtl() {
- return mIsRtl;
- }
-
- @Override
- public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
- if (mHandleTaskStackChanges) {
- TaskView taskView = getTaskView(taskId);
- if (taskView != null) {
- Task task = taskView.getTask();
- taskView.getThumbnail().setThumbnail(task, thumbnailData);
- return task;
- }
- }
- return null;
- }
-
- @Override
- public void onTaskIconChanged(String pkg, UserHandle user) {
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView tv = getTaskViewAt(i);
- Task task = tv.getTask();
- if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
- && task.key.userId == user.getIdentifier()) {
- task.icon = null;
- if (tv.getIconView().getDrawable() != null) {
- tv.onTaskListVisibilityChanged(true /* visible */);
- }
- }
- }
- }
-
- /**
- * Update the thumbnail of the task.
- * @param refreshNow Refresh immediately if it's true.
- */
- public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) {
- TaskView taskView = getTaskView(taskId);
- if (taskView != null) {
- taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow);
- }
- return taskView;
- }
-
- /** See {@link #updateThumbnail(int, ThumbnailData, boolean)} */
- public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) {
- return updateThumbnail(taskId, thumbnailData, true /* refreshNow */);
- }
-
- @Override
- protected void onWindowVisibilityChanged(int visibility) {
- super.onWindowVisibilityChanged(visibility);
- updateTaskStackListenerState();
- }
-
- @Override
- public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
- if ((changeFlags & CHANGE_FLAG_ICON_PARAMS) == 0) {
- return;
- }
- mModel.getIconCache().clear();
- unloadVisibleTaskData();
- loadVisibleTaskData();
- }
-
- public void init(OverviewActionsView actionsView) {
- mActionsView = actionsView;
- mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- updateTaskStackListenerState();
- mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
- mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
- ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
- mSyncTransactionApplier = new SurfaceTransactionApplier(this);
- RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
- mIdp.addOnChangeListener(this);
- mIPinnedStackAnimationListener.setActivity(mActivity);
- SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
- mIPinnedStackAnimationListener);
- mOrientationState.initListeners();
- SplitScreenBounds.INSTANCE.addOnChangeListener(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- updateTaskStackListenerState();
- mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
- mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
- ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
- mSyncTransactionApplier = null;
- RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
- mIdp.removeOnChangeListener(this);
- SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
- SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
- mIPinnedStackAnimationListener.setActivity(null);
- mOrientationState.destroyListeners();
- }
-
- @Override
- public void onViewRemoved(View child) {
- super.onViewRemoved(child);
-
- // Clear the task data for the removed child if it was visible
- if (child instanceof TaskView) {
- TaskView taskView = (TaskView) child;
- mHasVisibleTaskData.delete(taskView.getTask().key.id);
- mTaskViewPool.recycle(taskView);
- mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
- }
- updateTaskStartIndex(child);
- }
-
- @Override
- public void onViewAdded(View child) {
- super.onViewAdded(child);
- child.setAlpha(mContentAlpha);
- // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
- // child direction back to match system settings.
- child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL);
- updateTaskStartIndex(child);
- mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false);
- updateEmptyMessage();
- }
-
- @Override
- public void draw(Canvas canvas) {
- maybeDrawEmptyMessage(canvas);
- super.draw(canvas);
- }
-
- private void updateTaskStartIndex(View affectingView) {
- if (!(affectingView instanceof TaskView) && !(affectingView instanceof ClearAllButton)) {
- int childCount = getChildCount();
-
- mTaskViewStartIndex = 0;
- while (mTaskViewStartIndex < childCount
- && !(getChildAt(mTaskViewStartIndex) instanceof TaskView)) {
- mTaskViewStartIndex++;
- }
- }
- }
-
- 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 < getTaskViewCount(); i++) {
- TaskView tv = getTaskViewAt(i);
- if (tv.getTask() != null && tv.getTask().key != null && tv.getTask().key.id == taskId) {
- return tv;
- }
- }
- return null;
- }
-
- public void setOverviewStateEnabled(boolean enabled) {
- mOverviewStateEnabled = enabled;
- updateTaskStackListenerState();
- mOrientationState.setRotationWatcherEnabled(enabled);
- if (!enabled) {
- // Reset the running task when leaving overview since it can still have a reference to
- // its thumbnail
- mTmpRunningTask = null;
- }
- }
-
- public void onDigitalWellbeingToastShown() {
- if (!mDwbToastShown) {
- mDwbToastShown = true;
- mActivity.getUserEventDispatcher().logActionTip(
- LauncherEventUtil.VISIBLE,
- LauncherLogProto.TipType.DWB_TOAST);
- }
- }
-
- /**
- * Whether the Clear All button is hidden or fully visible. Used to determine if center
- * displayed page is a task or the Clear All button.
- *
- * @return True = Clear All button not fully visible, center page is a task. False = Clear All
- * button fully visible, center page is Clear All button.
- */
- public boolean isClearAllHidden() {
- return mClearAllButton.getAlpha() != 1f;
- }
-
- @Override
- protected void onPageBeginTransition() {
- super.onPageBeginTransition();
- mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true);
- }
-
- @Override
- protected void onPageEndTransition() {
- super.onPageEndTransition();
- if (isClearAllHidden()) {
- mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
- }
- if (getNextPage() > 0) {
- setSwipeDownShouldLaunchApp(true);
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- super.onTouchEvent(ev);
- final int x = (int) ev.getX();
- final int y = (int) ev.getY();
- switch (ev.getAction()) {
- case MotionEvent.ACTION_UP:
- if (mTouchDownToStartHome) {
- startHome();
- }
- mTouchDownToStartHome = false;
- break;
- case MotionEvent.ACTION_CANCEL:
- mTouchDownToStartHome = false;
- break;
- case MotionEvent.ACTION_MOVE:
- // Passing the touch slop will not allow dismiss to home
- if (mTouchDownToStartHome &&
- (isHandlingTouch() ||
- squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) {
- mTouchDownToStartHome = false;
- }
- break;
- case MotionEvent.ACTION_DOWN:
- // Touch down anywhere but the deadzone around the visible clear all button and
- // between the task views will start home on touch up
- if (!isHandlingTouch() && !isModal()) {
- if (mShowEmptyMessage) {
- mTouchDownToStartHome = true;
- } else {
- updateDeadZoneRects();
- final boolean clearAllButtonDeadZoneConsumed =
- mClearAllButton.getAlpha() == 1
- && mClearAllButtonDeadZoneRect.contains(x, y);
- final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
- if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
- && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
- mTouchDownToStartHome = true;
- }
- }
- }
- mDownX = x;
- mDownY = y;
- break;
- }
-
-
- // Do not let touch escape to siblings below this view.
- return isHandlingTouch() || shouldStealTouchFromSiblingsBelow(ev);
- }
-
- @Override
- protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
- // Enables swiping to the left or right only if the task overlay is not modal.
- if (!isModal()) {
- super.determineScrollingStart(ev, touchSlopScale);
- }
- }
- protected boolean shouldStealTouchFromSiblingsBelow(MotionEvent ev) {
- return true;
- }
-
- protected void applyLoadPlan(ArrayList<Task> tasks) {
- if (mPendingAnimation != null) {
- mPendingAnimation.addEndListener((endState) -> applyLoadPlan(tasks));
- return;
- }
-
- if (tasks == null || tasks.isEmpty()) {
- removeTasksViewsAndClearAllButton();
- onTaskStackUpdated();
- return;
- }
-
- // Unload existing visible task data
- unloadVisibleTaskData();
-
- TaskView ignoreResetTaskView =
- mIgnoreResetTaskId == -1 ? null : getTaskView(mIgnoreResetTaskId);
-
- final int requiredTaskCount = tasks.size();
- if (getTaskViewCount() != requiredTaskCount) {
- if (indexOfChild(mClearAllButton) != -1) {
- removeView(mClearAllButton);
- }
- for (int i = getTaskViewCount(); i < requiredTaskCount; i++) {
- addView(mTaskViewPool.getView());
- }
- while (getTaskViewCount() > requiredTaskCount) {
- removeView(getChildAt(getChildCount() - 1));
- }
- if (requiredTaskCount > 0) {
- addView(mClearAllButton);
- }
- }
-
- // Rebind and reset all task views
- for (int i = requiredTaskCount - 1; i >= 0; i--) {
- final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
- final Task task = tasks.get(i);
- final TaskView taskView = (TaskView) getChildAt(pageIndex);
- taskView.bind(task, mOrientationState);
- }
-
- if (mNextPage == INVALID_PAGE) {
- // Set the current page to the running task, but not if settling on new task.
- TaskView runningTaskView = getRunningTaskView();
- if (runningTaskView != null) {
- setCurrentPage(indexOfChild(runningTaskView));
- } else if (getTaskViewCount() > 0) {
- setCurrentPage(indexOfChild(getTaskViewAt(0)));
- }
- }
-
- if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreResetTaskView) {
- // If the taskView mapping is changing, do not preserve the visuals. Since we are
- // mostly preserving the first task, and new taskViews are added to the end, it should
- // generally map to the same task.
- mIgnoreResetTaskId = -1;
- }
- resetTaskVisuals();
- onTaskStackUpdated();
- updateEnabledOverlays();
- }
-
- private boolean isModal() {
- return mTaskModalness > 0;
- }
-
- private void removeTasksViewsAndClearAllButton() {
- for (int i = getTaskViewCount() - 1; i >= 0; i--) {
- removeView(getTaskViewAt(i));
- }
- if (indexOfChild(mClearAllButton) != -1) {
- removeView(mClearAllButton);
- }
- }
-
- public int getTaskViewCount() {
- int taskViewCount = getChildCount() - mTaskViewStartIndex;
- if (indexOfChild(mClearAllButton) != -1) {
- taskViewCount--;
- }
- return taskViewCount;
- }
-
- protected void onTaskStackUpdated() {
- // Lazily update the empty message only when the task stack is reapplied
- updateEmptyMessage();
- }
-
- public void resetTaskVisuals() {
- for (int i = getTaskViewCount() - 1; i >= 0; i--) {
- TaskView taskView = getTaskViewAt(i);
- if (mIgnoreResetTaskId != taskView.getTask().key.id) {
- taskView.resetViewTransforms();
- taskView.setStableAlpha(mContentAlpha);
- taskView.setFullscreenProgress(mFullscreenProgress);
- taskView.setModalness(mTaskModalness);
- }
- }
- if (mRunningTaskTileHidden) {
- setRunningTaskHidden(mRunningTaskTileHidden);
- }
-
- // Force apply the scale.
- if (mIgnoreResetTaskId != mRunningTaskId) {
- applyRunningTaskIconScale();
- }
-
- updateCurveProperties();
- // Update the set of visible task's data
- loadVisibleTaskData();
- setTaskModalness(0);
- }
-
- public void setFullscreenProgress(float fullscreenProgress) {
- mFullscreenProgress = fullscreenProgress;
- int taskCount = getTaskViewCount();
- for (int i = 0; i < taskCount; i++) {
- getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
- }
- // Fade out the actions view quickly (0.1 range)
- mActionsView.getFullscreenAlpha().setValue(
- mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
- }
-
- 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);
- resetPaddingFromTaskSize();
- }
-
- private void resetPaddingFromTaskSize() {
- DeviceProfile dp = mActivity.getDeviceProfile();
- getTaskSize(mTempRect);
- mTaskWidth = mTempRect.width();
- mTaskHeight = mTempRect.height();
-
- mTempRect.top -= mTaskTopMargin;
- setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
- dp.widthPx - mInsets.right - mTempRect.right,
- dp.heightPx - mInsets.bottom - mTempRect.bottom);
- }
-
- public void getTaskSize(Rect outRect) {
- mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
- mOrientationHandler);
- }
-
- /** Gets the task size for modal state. */
- public void getModalTaskSize(Rect outRect) {
- mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
- }
-
- @Override
- protected boolean computeScrollHelper() {
- boolean scrolling = super.computeScrollHelper();
- boolean isFlingingFast = false;
- updateCurveProperties();
- if (scrolling || isHandlingTouch()) {
- 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 state
- mModel.getThumbnailCache().getHighResLoadingState().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;
- }
- mOrientationHandler.getCurveProperties(this, mInsets, mScrollState);
- mScrollState.scrollFromEdge =
- mIsRtl ? mScrollState.scroll : (mMaxScroll - mScrollState.scroll);
-
- final int pageCount = getPageCount();
- for (int i = 0; i < pageCount; i++) {
- View page = getPageAt(i);
- mScrollState.updateInterpolation(mOrientationHandler.getChildStartWithTranslation(page),
- mPageSpacing);
- ((PageCallbacks) page).onPageScroll(mScrollState);
- }
- }
-
- /**
- * Iterates through all the tasks, 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 || mTaskListChangeId == -1) {
- // Skip loading visible task data if we've already left the overview state, or if the
- // task list hasn't been loaded yet (the task views will not reflect the task list)
- return;
- }
-
- int centerPageIndex = getPageNearestToCenterOfScreen();
- int numChildren = getChildCount();
- int lower = Math.max(0, centerPageIndex - 2);
- int upper = Math.min(centerPageIndex + 2, numChildren - 1);
-
- // Update the task data for the in/visible children
- for (int i = 0; i < getTaskViewCount(); i++) {
- TaskView taskView = getTaskViewAt(i);
- Task task = taskView.getTask();
- int index = indexOfChild(taskView);
- boolean visible = lower <= index && index <= 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)) {
- taskView.onTaskListVisibilityChanged(true /* visible */);
- }
- mHasVisibleTaskData.put(task.key.id, visible);
- } else {
- if (mHasVisibleTaskData.get(task.key.id)) {
- taskView.onTaskListVisibilityChanged(false /* visible */);
- }
- mHasVisibleTaskData.delete(task.key.id);
- }
- }
- }
-
- /**
- * Unloads any associated data from the currently visible tasks
- */
- private void unloadVisibleTaskData() {
- for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
- if (mHasVisibleTaskData.valueAt(i)) {
- TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
- if (taskView != null) {
- taskView.onTaskListVisibilityChanged(false /* visible */);
- }
- }
- }
- mHasVisibleTaskData.clear();
- }
-
- @Override
- public void onHighResLoadingStateChanged(boolean enabled) {
- // Whenever the high res loading state changes, poke each of the visible tasks to see if
- // they want to updated their thumbnail state
- for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
- if (mHasVisibleTaskData.valueAt(i)) {
- TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
- if (taskView != null) {
- // Poke the view again, which will trigger it to load high res if the state
- // is enabled
- taskView.onTaskListVisibilityChanged(true /* visible */);
- }
- }
- }
- }
-
- public abstract void startHome();
-
- /** `true` if there is a +1 space available in overview. */
- public boolean hasRecentsExtraCard() {
- return false;
- }
-
- public void reset() {
- setCurrentTask(-1);
- mIgnoreResetTaskId = -1;
- mTaskListChangeId = -1;
-
- mRecentsAnimationController = null;
- mRecentsAnimationTargets = null;
-
- unloadVisibleTaskData();
- setCurrentPage(0);
- mDwbToastShown = false;
- mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
- LayoutUtils.setViewEnabled(mActionsView, true);
- if (mOrientationState.setGestureActive(false)) {
- updateOrientationHandler();
- }
- }
-
- public @Nullable TaskView getRunningTaskView() {
- return getTaskView(mRunningTaskId);
- }
-
- public int getRunningTaskIndex() {
- return getTaskIndexForId(mRunningTaskId);
- }
-
- /**
- * Get the index of the task view whose id matches {@param taskId}.
- * @return -1 if there is no task view for the task id, else the index of the task view.
- */
- public int getTaskIndexForId(int taskId) {
- TaskView tv = getTaskView(taskId);
- return tv == null ? -1 : indexOfChild(tv);
- }
-
- public int getTaskViewStartIndex() {
- return mTaskViewStartIndex;
- }
-
- /**
- * Reloads the view if anything in recents changed.
- */
- public void reloadIfNeeded() {
- if (!mModel.isTaskListValid(mTaskListChangeId)) {
- mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
- }
- }
-
- /**
- * Called when a gesture from an app is starting.
- */
- public void onGestureAnimationStart(int runningTaskId) {
- // This needs to be called before the other states are set since it can create the task view
- if (mOrientationState.setGestureActive(true)) {
- updateOrientationHandler();
- }
-
- showCurrentTask(runningTaskId);
- setEnableFreeScroll(false);
- setEnableDrawingLiveTile(false);
- setRunningTaskHidden(true);
- setRunningTaskIconScaledDown(true);
- mActionsView.updateHiddenFlags(HIDDEN_GESTURE_RUNNING, true);
- }
-
- /**
- * Called only when a swipe-up gesture from an app has completed. Only called after
- * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
- */
- public void onSwipeUpAnimationSuccess() {
- if (getRunningTaskView() != null) {
- float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlayAttached
- ? LiveTileOverlay.INSTANCE.cancelIconAnimation()
- : 0f;
- animateUpRunningTaskIconScale(startProgress);
- }
- setSwipeDownShouldLaunchApp(true);
- }
-
- private void animateRecentsRotationInPlace(int newRotation) {
- if (mOrientationState.canRecentsActivityRotate()) {
- // Let system take care of the rotation
- return;
- }
- AnimatorSet pa = setRecentsChangedOrientation(true);
- pa.addListener(AnimationSuccessListener.forRunnable(() -> {
- setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
- mActivity.getDragLayer().recreateControllers();
- rotateAllChildTasks();
- setRecentsChangedOrientation(false).start();
- }));
- pa.start();
- }
-
- public AnimatorSet setRecentsChangedOrientation(boolean fadeInChildren) {
- getRunningTaskIndex();
- int runningIndex = getCurrentPage();
- AnimatorSet as = new AnimatorSet();
- for (int i = 0; i < getTaskViewCount(); i++) {
- if (runningIndex == i) {
- continue;
- }
- View taskView = getTaskViewAt(i);
- as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1));
- }
- return as;
- }
-
-
- private void rotateAllChildTasks() {
- for (int i = 0; i < getTaskViewCount(); i++) {
- getTaskViewAt(i).setOrientationState(mOrientationState);
- }
- }
-
- /**
- * Called when a gesture from an app has finished.
- */
- public void onGestureAnimationEnd() {
- if (mOrientationState.setGestureActive(false)) {
- updateOrientationHandler();
- }
-
- setOnScrollChangeListener(null);
- setEnableFreeScroll(true);
- setEnableDrawingLiveTile(true);
- if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- setRunningTaskViewShowScreenshot(true);
- }
- setRunningTaskHidden(false);
- animateUpRunningTaskIconScale();
- animateActionsViewIn();
- }
-
- /**
- * Returns true if we should add a dummy taskView for the running task id
- */
- protected boolean shouldAddDummyTaskView(int runningTaskId) {
- return getTaskView(runningTaskId) == null;
- }
-
- /**
- * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
- *
- * 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 showCurrentTask(int runningTaskId) {
- if (shouldAddDummyTaskView(runningTaskId)) {
- boolean wasEmpty = getChildCount() == 0;
- // Add an empty view for now until the task plan is loaded and applied
- final TaskView taskView = mTaskViewPool.getView();
- addView(taskView, mTaskViewStartIndex);
- if (wasEmpty) {
- addView(mClearAllButton);
- }
- // 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(),
- new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0,
- false, true, false, false, new ActivityManager.TaskDescription(), 0,
- new ComponentName("", ""), false);
- taskView.bind(mTmpRunningTask, mOrientationState);
-
- // Measure and layout immediately so that the scroll values is updated instantly
- // as the user might be quick-switching
- measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
- makeMeasureSpec(getMeasuredHeight(), EXACTLY));
- layout(getLeft(), getTop(), getRight(), getBottom());
- }
-
- boolean runningTaskTileHidden = mRunningTaskTileHidden;
- setCurrentTask(runningTaskId);
- setCurrentPage(getRunningTaskIndex());
- setRunningTaskViewShowScreenshot(false);
- setRunningTaskHidden(runningTaskTileHidden);
-
- // Reload the task list
- mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
- }
-
- /**
- * Sets the running task id, cleaning up the old running task if necessary.
- * @param runningTaskId
- */
- public void setCurrentTask(int runningTaskId) {
- if (mRunningTaskId == runningTaskId) {
- return;
- }
-
- if (mRunningTaskId != -1) {
- // Reset the state on the old running task view
- setRunningTaskIconScaledDown(false);
- setRunningTaskViewShowScreenshot(true);
- setRunningTaskHidden(false);
- }
- mRunningTaskId = runningTaskId;
- }
-
- /**
- * Hides the tile associated with {@link #mRunningTaskId}
- */
- public void setRunningTaskHidden(boolean isHidden) {
- mRunningTaskTileHidden = isHidden;
- TaskView runningTask = getRunningTaskView();
- if (runningTask != null) {
- runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
- if (!isHidden) {
- AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
- AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
- }
- }
- }
-
- private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- TaskView runningTaskView = getRunningTaskView();
- if (runningTaskView != null) {
- runningTaskView.setShowScreenshot(showScreenshot);
- }
- }
- }
-
- public void showNextTask() {
- TaskView runningTaskView = getRunningTaskView();
- if (runningTaskView == null) {
- // Launch the first task
- if (getTaskViewCount() > 0) {
- getTaskViewAt(0).launchTask(true);
- }
- } else {
- if (getNextTaskView() != null) {
- getNextTaskView().launchTask(true);
- } else {
- runningTaskView.launchTask(true);
- }
- }
- }
-
- public void setRunningTaskIconScaledDown(boolean isScaledDown) {
- if (mRunningTaskIconScaledDown != isScaledDown) {
- mRunningTaskIconScaledDown = isScaledDown;
- applyRunningTaskIconScale();
- }
- }
-
- public boolean isTaskIconScaledDown(TaskView taskView) {
- return mRunningTaskIconScaledDown && getRunningTaskView() == taskView;
- }
-
- private void applyRunningTaskIconScale() {
- TaskView firstTask = getRunningTaskView();
- if (firstTask != null) {
- firstTask.setIconScaleAndDim(mRunningTaskIconScaledDown ? 0 : 1);
- }
- }
-
- private void animateActionsViewIn() {
- mActionsView.updateHiddenFlags(HIDDEN_GESTURE_RUNNING, false);
- ObjectAnimator anim = ObjectAnimator.ofFloat(
- mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 0, 1);
- anim.setDuration(TaskView.SCALE_ICON_DURATION);
- anim.start();
- }
-
- public void animateUpRunningTaskIconScale() {
- animateUpRunningTaskIconScale(0);
- }
-
- public void animateUpRunningTaskIconScale(float startProgress) {
- mRunningTaskIconScaledDown = false;
- TaskView firstTask = getRunningTaskView();
- if (firstTask != null) {
- firstTask.animateIconScaleAndDimIntoView();
- firstTask.setIconScaleAnimStartProgress(startProgress);
- }
- }
-
- private void enableLayoutTransitions() {
- if (mLayoutTransition == null) {
- mLayoutTransition = new LayoutTransition();
- mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING);
- mLayoutTransition.setDuration(ADDITION_TASK_DURATION);
- mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0);
-
- mLayoutTransition.addTransitionListener(new TransitionListener() {
- @Override
- public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
- View view, int i) {
- }
-
- @Override
- public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
- View view, int i) {
- // When the unpinned task is added, snap to first page and disable transitions
- if (view instanceof TaskView) {
- snapToPage(0);
- disableLayoutTransitions();
- }
-
- }
- });
- }
- setLayoutTransition(mLayoutTransition);
- }
-
- private void disableLayoutTransitions() {
- setLayoutTransition(null);
- }
-
- 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 extends CurveProperties {
-
- /**
- * 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;
-
- /**
- * The amount by which all the content is scrolled relative to the end of the list.
- */
- public float scrollFromEdge;
-
- /**
- * Updates linearInterpolation for the provided child position
- */
- public void updateInterpolation(float childStart, int pageSpacing) {
- float pageCenter = childStart + halfPageSize;
- float distanceFromScreenCenter = screenCenter - pageCenter;
- float distanceToReachEdge = halfScreenSize + halfPageSize + pageSpacing;
- linearInterpolation = Math.min(1,
- Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
- }
- }
-
- public void setIgnoreResetTask(int taskId) {
- mIgnoreResetTaskId = taskId;
- }
-
- public void clearIgnoreResetTask(int taskId) {
- if (mIgnoreResetTaskId == taskId) {
- mIgnoreResetTaskId = -1;
- }
- }
-
- private void addDismissedTaskAnimations(View taskView, long duration, PendingAnimation anim) {
- // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
- // alpha is set to 0 so that it can be recycled in the view pool properly
- anim.setFloat(taskView, VIEW_ALPHA, 0, ACCEL_2);
- FloatProperty<View> secondaryViewTranslate =
- mOrientationHandler.getSecondaryViewTranslate();
- int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
- int verticalFactor = mOrientationHandler.getTaskDismissDirectionFactor();
-
- ResourceProvider rp = DynamicResource.provider(mActivity);
- SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
- .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
- .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness));
-
- anim.add(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
- verticalFactor * secondaryTaskDimension).setDuration(duration), LINEAR, sp);
- }
-
- private void removeTask(TaskView taskView, int index, EndState endState) {
- if (taskView.getTask() != null) {
- ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id);
- ComponentKey compKey = TaskUtils.getLaunchComponentKeyForTask(taskView.getTask().key);
- mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
- endState.logAction, Direction.UP, index, compKey);
- mActivity.getStatsLogManager().logger().withItemInfo(taskView.getItemInfo())
- .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
- }
- }
-
- public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
- boolean shouldRemoveTask, long duration) {
- if (mPendingAnimation != null) {
- mPendingAnimation.finish(false, Touch.SWIPE);
- }
- PendingAnimation anim = new PendingAnimation(duration);
-
- int count = getPageCount();
- if (count == 0) {
- return anim;
- }
-
- int[] oldScroll = new int[count];
- int[] newScroll = new int[count];
- getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
- getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
- int taskCount = getTaskViewCount();
- int scrollDiffPerPage = 0;
- if (count > 1) {
- scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
- }
- 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, duration, anim);
- }
- } 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 = taskCount - 1;
- 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) {
- Property translationProperty = mOrientationHandler.getPrimaryViewTranslate();
-
- ResourceProvider rp = DynamicResource.provider(mActivity);
- SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
- .setDampingRatio(
- rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
- .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
- anim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
- .setDuration(duration), ACCEL, sp);
- needsCurveUpdates = true;
- }
- }
- }
-
- if (needsCurveUpdates) {
- anim.addOnFrameCallback(this::updateCurveProperties);
- }
-
- // Add a tiny bit of translation Z, so that it draws on top of other views
- if (animateTaskView) {
- taskView.setTranslationZ(0.1f);
- }
-
- mPendingAnimation = anim;
- mPendingAnimation.addEndListener(new Consumer<EndState>() {
- @Override
- public void accept(EndState endState) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
- taskView.isRunningTask() && endState.isSuccess) {
- finishRecentsAnimation(true /* toHome */, () -> onEnd(endState));
- } else {
- onEnd(endState);
- }
- }
-
- @SuppressWarnings("WrongCall")
- private void onEnd(EndState endState) {
- if (endState.isSuccess) {
- if (shouldRemoveTask) {
- removeTask(taskView, draggedIndex, endState);
- }
-
- int pageToSnapTo = mCurrentPage;
- if (draggedIndex < pageToSnapTo ||
- pageToSnapTo == (getTaskViewCount() - 1)) {
- pageToSnapTo -= 1;
- }
- removeViewInLayout(taskView);
-
- if (getTaskViewCount() == 0) {
- removeViewInLayout(mClearAllButton);
- startHome();
- } else {
- snapToPageImmediately(pageToSnapTo);
- }
- // Update the layout synchronously so that the position of next view is
- // immediately available.
- onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
- }
- resetTaskVisuals();
- mPendingAnimation = null;
- }
- });
- return anim;
- }
-
- public PendingAnimation createAllTasksDismissAnimation(long duration) {
- if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
- throw new IllegalStateException("Another pending animation is still running");
- }
- PendingAnimation anim = new PendingAnimation(duration);
-
- int count = getTaskViewCount();
- for (int i = 0; i < count; i++) {
- addDismissedTaskAnimations(getTaskViewAt(i), duration, anim);
- }
-
- mPendingAnimation = anim;
- mPendingAnimation.addEndListener((endState) -> {
- if (endState.isSuccess) {
- // Remove all the task views now
- ActivityManagerWrapper.getInstance().removeAllRecentTasks();
- removeTasksViewsAndClearAllButton();
- startHome();
- }
- mPendingAnimation = null;
- });
- return anim;
- }
-
- private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
- if (pageCount == 0) {
- return false;
- }
- final int newPageUnbound = getNextPage() + delta;
- if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) {
- return false;
- }
- snapToPage((newPageUnbound + pageCount) % pageCount);
- getChildAt(getNextPage()).requestFocus();
- return true;
- }
-
- protected void runDismissAnimation(PendingAnimation pendingAnim) {
- AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
- 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));
- }
-
- @SuppressWarnings("unused")
- private void dismissAllTasks(View view) {
- runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
- mActivity.getUserEventDispatcher().logActionOnControl(TAP, CLEAR_ALL_BUTTON);
- }
-
- private void dismissCurrentTask() {
- TaskView taskView = getNextPageTaskView();
- if (taskView != null) {
- dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
- }
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- switch (event.getKeyCode()) {
- case KeyEvent.KEYCODE_TAB:
- return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1,
- event.isAltPressed() /* cycle */);
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */);
- case KeyEvent.KEYCODE_DPAD_LEFT:
- return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */);
- case KeyEvent.KEYCODE_DEL:
- case KeyEvent.KEYCODE_FORWARD_DEL:
- dismissCurrentTask();
- return true;
- case KeyEvent.KEYCODE_NUMPAD_DOT:
- if (event.isAltPressed()) {
- // Numpad DEL pressed while holding Alt.
- dismissCurrentTask();
- 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) {
- if (alpha == mContentAlpha) {
- return;
- }
- alpha = Utilities.boundToRange(alpha, 0, 1);
- mContentAlpha = alpha;
- for (int i = getTaskViewCount() - 1; i >= 0; i--) {
- TaskView child = getTaskViewAt(i);
- if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) {
- child.setStableAlpha(alpha);
- }
- }
- mClearAllButton.setContentAlpha(mContentAlpha);
- int alphaInt = Math.round(alpha * 255);
- mEmptyMessagePaint.setAlpha(alphaInt);
- mEmptyIcon.setAlpha(alphaInt);
- mActionsView.getContentAlpha().setValue(mContentAlpha);
-
- if (alpha > 0) {
- setVisibility(VISIBLE);
- } else if (!mFreezeViewVisibility) {
- setVisibility(GONE);
- }
- }
-
- /**
- * Freezes the view visibility change. When frozen, the view will not change its visibility
- * to gone due to alpha changes.
- */
- public void setFreezeViewVisibility(boolean freezeViewVisibility) {
- if (mFreezeViewVisibility != freezeViewVisibility) {
- mFreezeViewVisibility = freezeViewVisibility;
- if (!mFreezeViewVisibility) {
- setVisibility(mContentAlpha > 0 ? VISIBLE : GONE);
- }
- }
- }
-
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
- if (mActionsView != null) {
- mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
- }
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- if (mOrientationState.setActivityConfiguration(newConfig)) {
- updateOrientationHandler();
- }
- }
-
- public void setLayoutRotation(int touchRotation, int displayRotation) {
- if (mOrientationState.update(touchRotation, displayRotation)) {
- updateOrientationHandler();
- }
- }
-
- private void updateOrientationHandler() {
- mOrientationHandler = mOrientationState.getOrientationHandler();
- mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
- setLayoutDirection(mIsRtl
- ? View.LAYOUT_DIRECTION_RTL
- : View.LAYOUT_DIRECTION_LTR);
- mClearAllButton.setLayoutDirection(mIsRtl
- ? View.LAYOUT_DIRECTION_LTR
- : View.LAYOUT_DIRECTION_RTL);
- mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
- mActivity.getDragLayer().recreateControllers();
- boolean isInLandscape = mOrientationState.getTouchRotation() != 0
- || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
- mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
- !mOrientationState.canRecentsActivityRotate() && isInLandscape);
- resetPaddingFromTaskSize();
- requestLayout();
- // Reapply the current page to update page scrolls.
- setCurrentPage(mCurrentPage);
- }
-
- public RecentsOrientedState getPagedViewOrientedState() {
- return mOrientationState;
- }
-
- public PagedOrientationHandler getPagedOrientationHandler() {
- return mOrientationHandler;
- }
-
- @Nullable
- public TaskView getNextTaskView() {
- return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() + 1);
- }
-
- @Nullable
- public TaskView getCurrentPageTaskView() {
- return getTaskViewAtByAbsoluteIndex(getCurrentPage());
- }
-
- @Nullable
- public TaskView getNextPageTaskView() {
- return getTaskViewAtByAbsoluteIndex(getNextPage());
- }
-
- @Nullable
- public TaskView getTaskViewNearestToCenterOfScreen() {
- return getTaskViewAtByAbsoluteIndex(getPageNearestToCenterOfScreen());
- }
-
- /**
- * Returns null instead of indexOutOfBoundsError when index is not in range
- */
- @Nullable
- public TaskView getTaskViewAt(int index) {
- return getTaskViewAtByAbsoluteIndex(index + mTaskViewStartIndex);
- }
-
- @Nullable
- private TaskView getTaskViewAtByAbsoluteIndex(int index) {
- if (index < getChildCount() && index >= 0) {
- View child = getChildAt(index);
- return child instanceof TaskView ? (TaskView) child : null;
- }
- return null;
- }
-
- public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
- mOnEmptyMessageUpdatedListener = listener;
- }
-
- public void updateEmptyMessage() {
- boolean isEmpty = getTaskViewCount() == 0;
- boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
- || mLastMeasureSize.y != getHeight();
- if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
- return;
- }
- setContentDescription(isEmpty ? mEmptyMessage : "");
- mShowEmptyMessage = isEmpty;
- updateEmptyStateUi(hasSizeChanged);
- invalidate();
-
- if (mOnEmptyMessageUpdatedListener != null) {
- mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- updateEmptyStateUi(changed);
-
- // Update the pivots such that when the task is scaled, it fills the full page
- getTaskSize(mTempRect);
- getPagedViewOrientedState().getFullScreenScaleAndPivot(
- mTempRect, mActivity.getDeviceProfile(), mTempPointF);
- setPivotX(mTempPointF.x);
- setPivotY(mTempPointF.y);
- setTaskModalness(mTaskModalness);
- updatePageOffsets();
- setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
- : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
-
- private void updatePageOffsets() {
- float offset = mAdjacentPageOffset * getWidth();
- float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness) * getWidth();
- if (mIsRtl) {
- offset = -offset;
- modalOffset = -modalOffset;
- }
- int count = getChildCount();
-
- TaskView runningTask = mRunningTaskId == -1 || !mRunningTaskTileHidden
- ? null : getTaskView(mRunningTaskId);
- int midPoint = runningTask == null ? -1 : indexOfChild(runningTask);
- int currentPage = getCurrentPage();
-
- for (int i = 0; i < count; i++) {
- float translation = i == midPoint ? 0 : (i < midPoint ? -offset : offset);
- float modalTranslation =
- i == currentPage ? 0 : (i < currentPage ? -modalOffset : modalOffset);
- getChildAt(i).setTranslationX(translation + modalTranslation);
- }
- updateCurveProperties();
- }
-
- /**
- * TODO: Do not assume motion across X axis for adjacent page
- */
- public float getPageOffsetScale() {
- return Math.max(getWidth(), 1);
- }
-
- /**
- * Resets the visuals when exit modal state.
- */
- public void resetModalVisuals() {
- TaskView taskView = getCurrentPageTaskView();
- if (taskView != null) {
- taskView.getThumbnail().getTaskOverlay().resetModalVisuals();
- }
- }
-
- private void updateDeadZoneRects() {
- // Get the deadzone rect surrounding the clear all button to not dismiss overview to home
- mClearAllButtonDeadZoneRect.setEmpty();
- if (mClearAllButton.getWidth() > 0) {
- int verticalMargin = getResources()
- .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin);
- mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect);
- mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin);
- }
-
- // Get the deadzone rect between the task views
- mTaskViewDeadZoneRect.setEmpty();
- int count = getTaskViewCount();
- if (count > 0) {
- final View taskView = getTaskViewAt(0);
- getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
- mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
- taskView.getBottom());
- }
- }
-
- private void updateEmptyStateUi(boolean sizeChanged) {
- boolean hasValidSize = getWidth() > 0 && getHeight() > 0;
- if (sizeChanged && hasValidSize) {
- mEmptyTextLayout = null;
- mLastMeasureSize.set(getWidth(), getHeight());
- }
-
- 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) {
- AnimatorSet anim = new AnimatorSet();
-
- int taskIndex = indexOfChild(tv);
- int centerTaskIndex = getCurrentPage();
- boolean launchingCenterTask = taskIndex == centerTaskIndex;
-
- float toScale = getMaxScaleForFullScreen();
- if (launchingCenterTask) {
- RecentsView recentsView = tv.getRecentsView();
- anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
- anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
- } 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(new PropertyListBuilder()
- .translationX(mIsRtl ? -displacementX : displacementX)
- .scale(1)
- .build(getPageAt(otherAdjacentTaskIndex)));
- }
- }
- return anim;
- }
-
- /**
- * Returns the scale up required on the view, so that it coves the screen completely
- */
- public float getMaxScaleForFullScreen() {
- getTaskSize(mTempRect);
- return getPagedViewOrientedState().getFullScreenScaleAndPivot(
- mTempRect, mActivity.getDeviceProfile(), mTempPointF);
- }
-
- public PendingAnimation createTaskLaunchAnimation(
- TaskView tv, long duration, Interpolator interpolator) {
- if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
- throw new IllegalStateException("Another pending animation is still running");
- }
-
- int count = getTaskViewCount();
- if (count == 0) {
- return new PendingAnimation(duration);
- }
-
- int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
- final boolean[] passedOverviewThreshold = new boolean[] {false};
- ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
- progressAnim.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);
-
- onTaskLaunchAnimationUpdate(animator.getAnimatedFraction(), tv);
-
- // Passing the threshold from taskview to fullscreen app will vibrate
- final boolean passed = animator.getAnimatedFraction() >=
- SUCCESS_TRANSITION_PROGRESS;
- if (passed != passedOverviewThreshold[0]) {
- passedOverviewThreshold[0] = passed;
- performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
- HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
- }
- });
-
- AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
-
- DepthController depthController = getDepthController();
- if (depthController != null) {
- ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController, DEPTH,
- BACKGROUND_APP.getDepth(mActivity));
- anim.play(depthAnimator);
- }
- anim.play(progressAnim);
- anim.setInterpolator(interpolator);
-
- mPendingAnimation = new PendingAnimation(duration);
- mPendingAnimation.add(anim);
- mPendingAnimation.addEndListener((endState) -> {
- if (endState.isSuccess) {
- Consumer<Boolean> onLaunchResult = (result) -> {
- onTaskLaunchAnimationEnd(result);
- if (!result) {
- tv.notifyTaskLaunchFailed(TAG);
- }
- };
- tv.launchTask(false, onLaunchResult, getHandler());
- Task task = tv.getTask();
- if (task != null) {
- mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
- endState.logAction, Direction.DOWN, indexOfChild(tv),
- TaskUtils.getLaunchComponentKeyForTask(task.key));
- mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo())
- .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
- }
- } else {
- onTaskLaunchAnimationEnd(false);
- }
- mPendingAnimation = null;
- });
- return mPendingAnimation;
- }
-
- protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
- }
-
- public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
-
- protected void onTaskLaunchAnimationEnd(boolean success) {
- if (success) {
- resetTaskVisuals();
- }
- }
-
- /**
- * Called when task activity is launched
- */
- public void onTaskLaunched(Task task){ }
-
- @Override
- protected void notifyPageSwitchListener(int prevPage) {
- super.notifyPageSwitchListener(prevPage);
- loadVisibleTaskData();
- updateEnabledOverlays();
- }
-
- @Override
- protected String getCurrentPageDescription() {
- return "";
- }
-
- @Override
- public void addChildrenForAccessibility(ArrayList<View> outChildren) {
- // Add children in reverse order
- for (int i = getChildCount() - 1; i >= 0; --i) {
- outChildren.add(getChildAt(i));
- }
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
- final AccessibilityNodeInfo.CollectionInfo
- collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
- 1, getTaskViewCount(), false,
- AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
- info.setCollectionInfo(collectionInfo);
- }
-
- @Override
- public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(event);
-
- final int taskViewCount = getTaskViewCount();
- event.setScrollable(taskViewCount > 0);
-
- if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
- final int[] visibleTasks = getVisibleChildrenRange();
- event.setFromIndex(taskViewCount - visibleTasks[1]);
- event.setToIndex(taskViewCount - visibleTasks[0]);
- event.setItemCount(taskViewCount);
- }
- }
-
- @Override
- public CharSequence getAccessibilityClassName() {
- // To hear position-in-list related feedback from Talkback.
- return ListView.class.getName();
- }
-
- @Override
- protected boolean isPageOrderFlipped() {
- return true;
- }
-
- public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) {
- mEnableDrawingLiveTile = enableDrawingLiveTile;
- }
-
- public void redrawLiveTile(boolean mightNeedToRefill) { }
-
- // TODO: To be removed in a follow up CL
- public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
- RecentsAnimationTargets recentsAnimationTargets) {
- mRecentsAnimationController = recentsAnimationController;
- mRecentsAnimationTargets = recentsAnimationTargets;
- }
-
- public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
- mLiveTileOverlayAttached = liveTileOverlayAttached;
- }
-
- public void updateLiveTileIcon(Drawable icon) {
- if (mLiveTileOverlayAttached) {
- LiveTileOverlay.INSTANCE.setIcon(icon);
- }
- }
-
- public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
- if (mRecentsAnimationController == null) {
- if (onFinishComplete != null) {
- onFinishComplete.run();
- }
- return;
- }
-
- mRecentsAnimationController.finish(toRecents, () -> {
- if (onFinishComplete != null) {
- onFinishComplete.run();
- // After we finish the recents animation, the current task id should be correctly
- // reset so that when the task is launched from Overview later, it goes through the
- // flow of starting a new task instead of finishing recents animation to app. A
- // typical example of this is (1) user swipes up from app to Overview (2) user
- // taps on QSB (3) user goes back to Overview and launch the most recent task.
- setCurrentTask(-1);
- }
- });
- }
-
- public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
- if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
- mDisallowScrollToClearAll = disallowScrollToClearAll;
- updateMinAndMaxScrollX();
- }
- }
-
- @Override
- protected int computeMinScroll() {
- if (getTaskViewCount() > 0) {
- if (mDisallowScrollToClearAll) {
- // We aren't showing the clear all button,
- // so use the leftmost task as the min scroll.
- if (mIsRtl) {
- return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)));
- }
- return getScrollForPage(mTaskViewStartIndex);
- }
- if (mIsRtl) {
- return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1);
- }
- return getScrollForPage(mTaskViewStartIndex);
- }
- return super.computeMinScroll();
- }
-
- @Override
- protected int computeMaxScroll() {
- if (getTaskViewCount() > 0) {
- if (mDisallowScrollToClearAll) {
- // We aren't showing the clear all button,
- // so use the rightmost task as the min scroll.
- if (mIsRtl) {
- return getScrollForPage(mTaskViewStartIndex);
- }
- return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)));
- }
- if (mIsRtl) {
- return getScrollForPage(mTaskViewStartIndex);
- }
- return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1);
- }
- return super.computeMaxScroll();
- }
-
- public ClearAllButton getClearAllButton() {
- return mClearAllButton;
- }
-
- @Override
- protected boolean onOverscroll(int amount) {
- // overscroll should only be accepted on -1 direction (for clear all button)
- if ((amount > 0 && !mIsRtl) || (amount < 0 && mIsRtl)) return false;
- return super.onOverscroll(amount);
- }
-
- /**
- * @return How many pixels the running task is offset on the currently laid out dominant axis.
- */
- public int getScrollOffset() {
- return getScrollOffset(getRunningTaskIndex());
- }
-
- /**
- * @return How many pixels the page is offset on the currently laid out dominant axis.
- */
- public int getScrollOffset(int pageIndex) {
- if (pageIndex == -1) {
- return 0;
- }
- return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this);
- }
-
- public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
- float degreesRotated;
- if (navbarRotation == 0) {
- degreesRotated = mOrientationHandler.getDegreesRotated();
- } else {
- degreesRotated = -navbarRotation;
- }
- if (degreesRotated == 0) {
- return super::onTouchEvent;
- }
-
- // At this point the event coordinates have already been transformed, so we need to
- // undo that transformation since PagedView also accommodates for the transformation via
- // PagedOrientationHandler
- return e -> {
- if (navbarRotation != 0
- && mOrientationState.isMultipleOrientationSupportedByDevice()
- && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) {
- mOrientationState.flipVertical(e);
- super.onTouchEvent(e);
- mOrientationState.flipVertical(e);
- return;
- }
- mOrientationState.transformEvent(-degreesRotated, e, true);
- super.onTouchEvent(e);
- mOrientationState.transformEvent(-degreesRotated, e, false);
- };
- }
-
- public TransformParams getLiveTileParams(
- boolean mightNeedToRefill) {
- return null;
- }
-
- private void updateEnabledOverlays() {
- int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
- int taskCount = getTaskViewCount();
- for (int i = mTaskViewStartIndex; i < mTaskViewStartIndex + taskCount; i++) {
- getTaskViewAtByAbsoluteIndex(i).setOverlayEnabled(i == overlayEnabledPage);
- }
- }
-
- public void setOverlayEnabled(boolean overlayEnabled) {
- if (mOverlayEnabled != overlayEnabled) {
- mOverlayEnabled = overlayEnabled;
- updateEnabledOverlays();
- }
- }
-
- /** If it's in the live tile mode, switch the running task into screenshot mode. */
- public void switchToScreenshot(ThumbnailData thumbnailData, Runnable onFinishRunnable) {
- TaskView taskView = getRunningTaskView();
- if (taskView != null) {
- taskView.setShowScreenshot(true);
- if (thumbnailData != null) {
- taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
- } else {
- taskView.getThumbnail().refresh();
- }
- ViewUtils.postDraw(taskView, onFinishRunnable);
- } else {
- onFinishRunnable.run();
- }
- }
-
- /**
- * The current task is fully modal (modalness = 1) when it is shown on its own in a modal
- * way. Modalness 0 means the task is shown in context with all the other tasks.
- */
- private void setTaskModalness(float modalness) {
- mTaskModalness = modalness;
- updatePageOffsets();
- if (getCurrentPageTaskView() != null) {
- getCurrentPageTaskView().setModalness(modalness);
- }
- // Only show actions view when it's modal for in-place landscape mode.
- boolean inPlaceLandscape = !mOrientationState.canRecentsActivityRotate()
- && mOrientationState.getTouchRotation() != ROTATION_0;
- mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape);
- }
-
- @Nullable
- protected DepthController getDepthController() {
- return null;
- }
-
- @Override
- public void onSecondaryWindowBoundsChanged() {
- // Invalidate the task view size
- setInsets(mInsets);
- requestLayout();
- }
-
- /**
- * Enables or disables modal state for RecentsView
- * @param isModalState
- */
- public void setModalStateEnabled(boolean isModalState) { }
-
- public BaseActivityInterface getSizeStrategy() {
- return mSizeStrategy;
- }
-
- /**
- * Used to register callbacks for when our empty message state changes.
- *
- * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener)
- * @see #updateEmptyMessage()
- */
- public interface OnEmptyMessageUpdatedListener {
- /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */
- void onEmptyMessageUpdated(boolean isEmpty);
- }
-
- private static class PinnedStackAnimationListener<T extends BaseActivity> extends
- IPinnedStackAnimationListener.Stub {
- private T mActivity;
-
- public void setActivity(T activity) {
- mActivity = activity;
- }
-
- @Override
- public void onPinnedStackAnimationStarted() {
- // Needed for activities that auto-enter PiP, which will not trigger a remote
- // animation to be created
- mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
deleted file mode 100644
index ef66b7a..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
+++ /dev/null
@@ -1,309 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.views.IconView.OnScaleUpdateListener;
-
-/**
- * Contains options for a recent task when long-pressing its icon.
- */
-public class TaskMenuView extends AbstractFloatingView {
-
- private static final Rect sTempRect = new Rect();
-
- private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() {
- @Override
- public void onScaleUpdate(float scale) {
- final Drawable drawable = mTaskIcon.getDrawable();
- if (drawable instanceof FastBitmapDrawable) {
- if (scale != ((FastBitmapDrawable) drawable).getScale()) {
- mMenuIconDrawable.setScale(scale);
- }
- }
- }
- };
-
- private final OnScaleUpdateListener mMenuIconScaleListener = new OnScaleUpdateListener() {
- @Override
- public void onScaleUpdate(float scale) {
- final Drawable taskViewDrawable = mTaskView.getIconView().getDrawable();
- if (taskViewDrawable instanceof FastBitmapDrawable) {
- final float currentScale = ((FastBitmapDrawable) taskViewDrawable).getScale();
- if (currentScale != scale) {
- ((FastBitmapDrawable) taskViewDrawable).setScale(scale);
- }
- }
- }
- };
-
- private static final int REVEAL_OPEN_DURATION = 150;
- private static final int REVEAL_CLOSE_DURATION = 100;
-
- private final float mThumbnailTopMargin;
- private BaseDraggingActivity mActivity;
- private TextView mTaskName;
- private IconView mTaskIcon;
- private AnimatorSet mOpenCloseAnimator;
- private TaskView mTaskView;
- private LinearLayout mOptionLayout;
- private FastBitmapDrawable mMenuIconDrawable;
-
- 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);
- mThumbnailTopMargin = getResources().getDimension(R.dimen.task_thumbnail_top_margin);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mTaskName = findViewById(R.id.task_name);
- mTaskIcon = findViewById(R.id.task_icon);
- mOptionLayout = findViewById(R.id.menu_option_layout);
- }
-
- @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 void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- // Remove all scale listeners when menu is removed
- mTaskView.getIconView().removeUpdateScaleListener(mTaskViewIconScaleListener);
- mTaskIcon.removeUpdateScaleListener(mMenuIconScaleListener);
- }
-
- @Override
- protected boolean isOfType(int type) {
- return (type & TYPE_TASK_MENU) != 0;
- }
-
- public void setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler) {
- float adjustedY = y + mThumbnailTopMargin;
- // Changing pivot to make computations easier
- // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
- // which would render the X and Y position set here incorrect
- setPivotX(0);
- setPivotY(0);
- setRotation(pagedOrientationHandler.getDegreesRotated());
- setX(pagedOrientationHandler.getTaskMenuX(x, mTaskView.getThumbnail()));
- setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail()));
- }
-
- public void onRotationChanged() {
- if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
- mOpenCloseAnimator.end();
- }
- if (mIsOpen) {
- mOptionLayout.removeAllViews();
- populateAndLayoutMenu();
- }
- }
-
- public static TaskMenuView 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) ? taskMenuView : null;
- }
-
- private boolean populateAndShowForTask(TaskView taskView) {
- if (isAttachedToWindow()) {
- return false;
- }
- mActivity.getDragLayer().addView(this);
- mTaskView = taskView;
- populateAndLayoutMenu();
- post(this::animateOpen);
- return true;
- }
-
- private void populateAndLayoutMenu() {
- addMenuOptions(mTaskView);
- orientAroundTaskView(mTaskView);
- }
-
- private void addMenuOptions(TaskView taskView) {
- Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
- mTaskIcon.setDrawable(icon);
- mTaskIcon.setOnClickListener(v -> close(true));
- mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
- mTaskName.setOnClickListener(v -> close(true));
-
- // Set the icons to match scale by listening to each other's changes
- mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null;
- taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener);
- mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener);
-
- // Move the icon and text up half an icon size to lay over the TaskView
- LinearLayout.LayoutParams params =
- (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams();
- params.topMargin = (int) -mThumbnailTopMargin;
- mTaskIcon.setLayoutParams(params);
-
- TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
- }
-
- private void addMenuOption(SystemShortcut menuOption) {
- ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate(
- R.layout.task_view_menu_option, this, false);
- menuOption.setIconAndLabelFor(
- menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
- LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
- mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
- menuOptionView.setOnClickListener(menuOption);
- mOptionLayout.addView(menuOptionView);
- }
-
- private void orientAroundTaskView(TaskView taskView) {
- PagedOrientationHandler orientationHandler = taskView.getPagedOrientationHandler();
- measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
- Rect insets = mActivity.getDragLayer().getInsets();
- BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
- params.width = orientationHandler.getTaskMenuWidth(taskView.getThumbnail());
- // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
- params.gravity = Gravity.LEFT;
- setLayoutParams(params);
- setScaleX(taskView.getScaleX());
- setScaleY(taskView.getScaleY());
- mOptionLayout.setOrientation(orientationHandler
- .getTaskMenuLayoutOrientation(mOptionLayout));
- setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top,
- taskView.getPagedOrientationHandler());
- }
-
- private void animateOpen() {
- animateOpenOrClosed(false);
- mIsOpen = true;
- }
-
- private void animateClose() {
- animateOpenOrClosed(true);
- }
-
- private void animateOpenOrClosed(boolean closing) {
- if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
- mOpenCloseAnimator.end();
- }
- mOpenCloseAnimator = new AnimatorSet();
-
- final Animator revealAnimator = createOpenCloseOutlineProvider()
- .createRevealAnimator(this, closing);
- revealAnimator.setInterpolator(Interpolators.DEACCEL);
- mOpenCloseAnimator.play(revealAnimator);
- mOpenCloseAnimator.play(ObjectAnimator.ofFloat(mTaskView.getThumbnail(), DIM_ALPHA,
- closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA));
- 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(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION);
- mOpenCloseAnimator.start();
- }
-
- private void closeComplete() {
- mIsOpen = false;
- mActivity.getDragLayer().removeView(this);
- }
-
- private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
- float radius = Themes.getDialogCornerRadius(getContext());
- Rect fromRect = new Rect(0, 0, getWidth(), 0);
- Rect toRect = new Rect(0, 0, getWidth(), getHeight());
- return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
- }
-
- public View findMenuItemByText(String text) {
- for (int i = mOptionLayout.getChildCount() - 1; i >= 0; --i) {
- final ViewGroup menuOptionView = (ViewGroup) mOptionLayout.getChildAt(i);
- if (text.equals(menuOptionView.<TextView>findViewById(R.id.text).getText())) {
- return menuOptionView;
- }
- }
- return null;
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
deleted file mode 100644
index b2f937f..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ /dev/null
@@ -1,622 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.graphics.Insets;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Shader;
-import android.os.Build;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.util.Property;
-import android.view.Surface;
-import android.view.View;
-
-import androidx.annotation.RequiresApi;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.util.MainThreadInitializedObject;
-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.quickstep.views.TaskView.FullscreenDrawParams;
-import com.android.systemui.plugins.OverviewScreenshotActions;
-import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ConfigurationCompat;
-
-/**
- * A task in the Recents view.
- */
-public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> {
-
- private static final ColorMatrix COLOR_MATRIX = new ColorMatrix();
- private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
-
- private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS =
- new MainThreadInitializedObject<>(FullscreenDrawParams::new);
-
- public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
- new FloatProperty<TaskThumbnailView>("dimAlpha") {
- @Override
- public void setValue(TaskThumbnailView thumbnail, float dimAlpha) {
- thumbnail.setDimAlpha(dimAlpha);
- }
-
- @Override
- public Float get(TaskThumbnailView thumbnailView) {
- return thumbnailView.mDimAlpha;
- }
- };
-
- private final BaseActivity mActivity;
- private final TaskOverlay mOverlay;
- private final boolean mIsDarkTextTheme;
- private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private final Paint mClearPaint = new Paint();
- private final Paint mDimmingPaintAfterClearing = new Paint();
-
- // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
- private final Rect mPreviewRect = new Rect();
- private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
- private TaskView.FullscreenDrawParams mFullscreenParams;
-
- private Task mTask;
- private ThumbnailData mThumbnailData;
- protected BitmapShader mBitmapShader;
-
- private float mDimAlpha = 1f;
- private float mDimAlphaMultiplier = 1f;
- private float mSaturation = 1f;
-
- private boolean mOverlayEnabled;
- private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
-
- 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);
- mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
- mPaint.setFilterBitmap(true);
- mBackgroundPaint.setColor(Color.WHITE);
- mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
- mDimmingPaintAfterClearing.setColor(Color.BLACK);
- mActivity = BaseActivity.fromContext(context);
- mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
- // Initialize with dummy value. It is overridden later by TaskView
- mFullscreenParams = TEMP_PARAMS.get(context);
- }
-
- /**
- * Updates the thumbnail to draw the provided task
- * @param task
- */
- public void bind(Task task) {
- mOverlay.reset();
- mTask = task;
- int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
- mPaint.setColor(color);
- mBackgroundPaint.setColor(color);
- }
-
- /**
- * Updates the thumbnail.
- * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
- * In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)}
- * version with {@code refreshNow} is true. The only exception is
- * in the live tile case that we grab a screenshot when user enters Overview
- * upon swipe up so that a usable screenshot is accessible immediately when
- * recents animation needs to be finished / cancelled.
- */
- public void setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow) {
- mTask = task;
- mThumbnailData =
- (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
- if (refreshNow) {
- refresh();
- }
- }
-
- /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */
- public void setThumbnail(Task task, ThumbnailData thumbnailData) {
- setThumbnail(task, thumbnailData, true /* refreshNow */);
- }
-
- /** Updates the shader, paint, matrix to redraw. */
- public void refresh() {
- if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
- Bitmap bm = mThumbnailData.thumbnail;
- bm.prepareToDraw();
- mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
- mPaint.setShader(mBitmapShader);
- updateThumbnailMatrix();
- } else {
- mBitmapShader = null;
- mThumbnailData = null;
- mPaint.setShader(null);
- mOverlay.reset();
- }
- if (mOverviewScreenshotActionsPlugin != null) {
- mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
- }
- updateThumbnailPaintFilter();
- }
-
- public void setDimAlphaMultipler(float dimAlphaMultipler) {
- mDimAlphaMultiplier = dimAlphaMultipler;
- setDimAlpha(mDimAlpha);
- }
-
- /**
- * Sets the alpha of the dim layer on top of this view.
- * <p>
- * 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 TaskOverlay getTaskOverlay() {
- return mOverlay;
- }
-
- public float getDimAlpha() {
- return mDimAlpha;
- }
-
- public Rect getInsets(Rect fallback) {
- if (mThumbnailData != null) {
- return mThumbnailData.insets;
- }
- return fallback;
- }
-
- /**
- * Get the scaled insets that are being used to draw the task view. This is a subsection of
- * the full snapshot.
- * @return the insets in snapshot bitmap coordinates.
- */
- @RequiresApi(api = Build.VERSION_CODES.Q)
- public Insets getScaledInsets() {
- if (mThumbnailData == null) {
- return Insets.NONE;
- }
-
- RectF bitmapRect = new RectF(
- 0, 0,
- mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
- RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
-
- // The position helper matrix tells us how to transform the bitmap to fit the view, the
- // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
- // difference between the bitmap bounds and the projected view bounds.
- Matrix boundsToBitmapSpace = new Matrix();
- mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
- RectF boundsInBitmapSpace = new RectF();
- boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
-
- return Insets.of(
- Math.round(boundsInBitmapSpace.left),
- Math.round(boundsInBitmapSpace.top),
- Math.round(bitmapRect.right - boundsInBitmapSpace.right),
- Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom));
- }
-
-
- 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) {
- RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets;
- canvas.save();
- canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale);
- canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
- // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
- drawOnCanvas(canvas,
- -currentDrawnInsets.left,
- -currentDrawnInsets.top,
- getMeasuredWidth() + currentDrawnInsets.right,
- getMeasuredHeight() + currentDrawnInsets.bottom,
- mFullscreenParams.mCurrentDrawnCornerRadius);
- canvas.restore();
- }
-
- @Override
- public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions,
- Context context) {
- mOverviewScreenshotActionsPlugin = overviewScreenshotActions;
- mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
- }
-
- @Override
- public void onPluginDisconnected(OverviewScreenshotActions plugin) {
- if (mOverviewScreenshotActionsPlugin != null) {
- mOverviewScreenshotActionsPlugin = null;
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- PluginManagerWrapper.INSTANCE.get(getContext())
- .addPluginListener(this, OverviewScreenshotActions.class);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
- }
-
- public PreviewPositionHelper getPreviewPositionHelper() {
- return mPreviewPositionHelper;
- }
-
- public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
- mFullscreenParams = fullscreenParams;
- invalidate();
- }
-
- public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
- float cornerRadius) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
- canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
- canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
- mDimmingPaintAfterClearing);
- return;
- }
- }
-
- // 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 || mPreviewPositionHelper.mClipBottom > 0
- || mThumbnailData.isTranslucent) {
- canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
- if (drawBackgroundOnly) {
- return;
- }
- }
-
- if (mPreviewPositionHelper.mClipBottom > 0) {
- canvas.save();
- canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom);
- canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
- canvas.restore();
- } else {
- canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
- }
- }
-
- public TaskView getTaskView() {
- return (TaskView) getParent();
- }
-
- public void setOverlayEnabled(boolean overlayEnabled) {
- if (mOverlayEnabled != overlayEnabled) {
- mOverlayEnabled = overlayEnabled;
- updateOverlay();
- }
- }
-
- private void updateOverlay() {
- if (mOverlayEnabled && mBitmapShader != null && mThumbnailData != null) {
- mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
- mPreviewPositionHelper.mIsOrientationChanged);
- } else {
- mOverlay.reset();
- }
- }
-
- private void updateThumbnailPaintFilter() {
- int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255);
- ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation);
- mBackgroundPaint.setColorFilter(filter);
- mDimmingPaintAfterClearing.setAlpha(255 - mul);
- if (mBitmapShader != null) {
- mPaint.setColorFilter(filter);
- } else {
- mPaint.setColorFilter(null);
- mPaint.setColor(Color.argb(255, mul, mul, mul));
- }
- invalidate();
- }
-
- private void updateThumbnailMatrix() {
- mPreviewPositionHelper.mClipBottom = -1;
- mPreviewPositionHelper.mIsOrientationChanged = false;
- if (mBitmapShader != null && mThumbnailData != null) {
- mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
- mThumbnailData.thumbnail.getHeight());
- int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
- mActivity.getResources().getConfiguration());
- mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
- getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
- currentRotation);
-
- mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
- mPaint.setShader(mBitmapShader);
- }
- getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper);
- invalidate();
-
- // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
- // as overlay could modify the views in the overlay as a side effect of its update.
- post(this::updateOverlay);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- updateThumbnailMatrix();
- }
-
- /**
- * @param intensity multiplier for color values. 0 - make black (white if shouldLighten), 255 -
- * leave unchanged.
- */
- private static ColorFilter getColorFilter(int intensity, boolean shouldLighten,
- float saturation) {
- intensity = Utilities.boundToRange(intensity, 0, 255);
-
- if (intensity == 255 && saturation == 1) {
- return null;
- }
-
- final float intensityScale = intensity / 255f;
- COLOR_MATRIX.setScale(intensityScale, intensityScale, intensityScale, 1);
-
- if (saturation != 1) {
- SATURATION_COLOR_MATRIX.setSaturation(saturation);
- COLOR_MATRIX.postConcat(SATURATION_COLOR_MATRIX);
- }
-
- if (shouldLighten) {
- final float[] colorArray = COLOR_MATRIX.getArray();
- final int colorAdd = 255 - intensity;
- colorArray[4] = colorAdd;
- colorArray[9] = colorAdd;
- colorArray[14] = colorAdd;
- }
-
- return new ColorMatrixColorFilter(COLOR_MATRIX);
- }
-
- public Bitmap getThumbnail() {
- if (mThumbnailData == null) {
- return null;
- }
- return mThumbnailData.thumbnail;
- }
-
- /**
- * Returns whether the snapshot is real.
- */
- public boolean isRealSnapshot() {
- if (mThumbnailData == null) {
- return false;
- }
- return mThumbnailData.isRealSnapshot;
- }
-
- /**
- * Utility class to position the thumbnail in the TaskView
- */
- public static class PreviewPositionHelper {
-
- // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
- private final RectF mClippedInsets = new RectF();
- private final Matrix mMatrix = new Matrix();
- private float mClipBottom = -1;
- private boolean mIsOrientationChanged;
-
- public Matrix getMatrix() {
- return mMatrix;
- }
-
- /**
- * Updates the matrix based on the provided parameters
- */
- public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
- int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) {
- boolean isRotated = false;
- boolean isOrientationDifferent;
- mClipBottom = -1;
-
- float scale = thumbnailData.scale;
- Rect activityInsets = dp.getInsets();
- Rect thumbnailInsets = getBoundedInsets(activityInsets, thumbnailData.insets);
- final float thumbnailWidth = thumbnailPosition.width()
- - (thumbnailInsets.left + thumbnailInsets.right) * scale;
- final float thumbnailHeight = thumbnailPosition.height()
- - (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
-
- final float thumbnailScale;
- int thumbnailRotation = thumbnailData.rotation;
- int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
-
- // Landscape vs portrait change
- boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
- && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
- isOrientationDifferent = isOrientationChange(deltaRotate)
- && windowingModeSupportsRotation;
- if (canvasWidth == 0) {
- // If we haven't measured , skip the thumbnail drawing and only draw the background
- // color
- thumbnailScale = 0f;
- } else {
- // Rotate the screenshot if not in multi-window mode
- isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
- // Scale the screenshot to always fit the width of the card.
- thumbnailScale = isOrientationDifferent
- ? canvasWidth / thumbnailHeight
- : canvasWidth / thumbnailWidth;
- }
-
- Rect splitScreenInsets = dp.getInsets();
- if (!isRotated) {
- // No Rotation
- if (dp.isMultiWindowMode) {
- mClippedInsets.offsetTo(splitScreenInsets.left * scale,
- splitScreenInsets.top * scale);
- } else {
- mClippedInsets.offsetTo(thumbnailInsets.left * scale,
- thumbnailInsets.top * scale);
- }
- mMatrix.setTranslate(
- -thumbnailInsets.left * scale,
- -thumbnailInsets.top * scale);
- } else {
- setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
- }
-
- final float widthWithInsets;
- final float heightWithInsets;
- if (isOrientationDifferent) {
- widthWithInsets = thumbnailPosition.height() * thumbnailScale;
- heightWithInsets = thumbnailPosition.width() * thumbnailScale;
- } else {
- widthWithInsets = thumbnailPosition.width() * thumbnailScale;
- heightWithInsets = thumbnailPosition.height() * thumbnailScale;
- }
- mClippedInsets.left *= thumbnailScale;
- mClippedInsets.top *= thumbnailScale;
-
- if (dp.isMultiWindowMode) {
- mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale;
- mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale;
- } else {
- mClippedInsets.right = Math.max(0,
- widthWithInsets - mClippedInsets.left - canvasWidth);
- mClippedInsets.bottom = Math.max(0,
- heightWithInsets - mClippedInsets.top - canvasHeight);
- }
-
- mMatrix.postScale(thumbnailScale, thumbnailScale);
-
- float bitmapHeight = Math.max(0,
- (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale);
- if (Math.round(bitmapHeight) < canvasHeight) {
- mClipBottom = bitmapHeight;
- }
- mIsOrientationChanged = isOrientationDifferent;
- }
-
- private Rect getBoundedInsets(Rect activityInsets, Rect insets) {
- return new Rect(Math.min(insets.left, activityInsets.left),
- Math.min(insets.top, activityInsets.top),
- Math.min(insets.right, activityInsets.right),
- Math.min(insets.bottom, activityInsets.bottom));
- }
-
- private int getRotationDelta(int oldRotation, int newRotation) {
- int delta = newRotation - oldRotation;
- if (delta < 0) delta += 4;
- return delta;
- }
-
- /**
- * @param deltaRotation the number of 90 degree turns from the current orientation
- * @return {@code true} if the change in rotation results in a shift from landscape to
- * portrait or vice versa, {@code false} otherwise
- */
- private boolean isOrientationChange(int deltaRotation) {
- return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
- }
-
- private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
- Rect thumbnailPosition) {
- int newLeftInset = 0;
- int newTopInset = 0;
- int translateX = 0;
- int translateY = 0;
-
- mMatrix.setRotate(90 * deltaRotate);
- switch (deltaRotate) { /* Counter-clockwise */
- case Surface.ROTATION_90:
- newLeftInset = thumbnailInsets.bottom;
- newTopInset = thumbnailInsets.left;
- translateX = thumbnailPosition.height();
- break;
- case Surface.ROTATION_270:
- newLeftInset = thumbnailInsets.top;
- newTopInset = thumbnailInsets.right;
- translateY = thumbnailPosition.width();
- break;
- case Surface.ROTATION_180:
- newLeftInset = -thumbnailInsets.top;
- newTopInset = -thumbnailInsets.left;
- translateX = thumbnailPosition.width();
- translateY = thumbnailPosition.height();
- break;
- }
- mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
- mMatrix.postTranslate(translateX - mClippedInsets.left,
- translateY - mClippedInsets.top);
- }
-
- /**
- * Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
- */
- public RectF getInsetsToDrawInFullscreen() {
- return mClippedInsets;
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
deleted file mode 100644
index 222f6e6..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ /dev/null
@@ -1,1050 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static android.view.Gravity.BOTTOM;
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.Gravity.CENTER_VERTICAL;
-import static android.view.Gravity.END;
-import static android.view.Gravity.START;
-import static android.view.Gravity.TOP;
-import static android.widget.Toast.LENGTH_SHORT;
-
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
-import static com.android.launcher3.Utilities.comp;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
- .LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.InsetDrawable;
-import android.os.Bundle;
-import android.os.Handler;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.util.Log;
-import android.view.Surface;
-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.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.ViewPool.Reusable;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.TaskIconCache;
-import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskThumbnailCache;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.util.RecentsOrientedState;
-import com.android.quickstep.util.TaskCornerRadius;
-import com.android.quickstep.views.RecentsView.PageCallbacks;
-import com.android.quickstep.views.RecentsView.ScrollState;
-import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.QuickStepContract;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * A task in the Recents view.
- */
-public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
-
- 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.
- */
- public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
-
- /**
- * How much to scale down pages near the edge of the screen.
- */
- public 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;
-
- private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
- Collections.singletonList(new Rect());
-
- private static final FloatProperty<TaskView> FOCUS_TRANSITION =
- new FloatProperty<TaskView>("focusTransition") {
- @Override
- public void setValue(TaskView taskView, float v) {
- taskView.setIconAndDimTransitionProgress(v, false /* invert */);
- }
-
- @Override
- public Float get(TaskView taskView) {
- return taskView.mFocusTransitionProgress;
- }
- };
-
- private final OnAttachStateChangeListener mTaskMenuStateListener =
- new OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View view) {
- }
-
- @Override
- public void onViewDetachedFromWindow(View view) {
- if (mMenuView != null) {
- mMenuView.removeOnAttachStateChangeListener(this);
- mMenuView = null;
- }
- }
- };
-
- private final TaskOutlineProvider mOutlineProvider;
-
- private Task mTask;
- private TaskThumbnailView mSnapshotView;
- private TaskMenuView mMenuView;
- private IconView mIconView;
- private final DigitalWellBeingToast mDigitalWellBeingToast;
- private float mCurveScale;
- private float mFullscreenProgress;
- private final FullscreenDrawParams mCurrentFullscreenParams;
- private final BaseDraggingActivity mActivity;
-
- private ObjectAnimator mIconAndDimAnimator;
- private float mIconScaleAnimStartProgress = 0;
- private float mFocusTransitionProgress = 1;
- private float mModalness = 0;
- private float mStableAlpha = 1;
-
- private boolean mShowScreenshot;
-
- // The current background requests to load the task thumbnail and icon
- private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest;
- private TaskIconCache.IconLoadRequest mIconLoadRequest;
-
- // Order in which the footers appear. Lower order appear below higher order.
- public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0;
- private final FooterWrapper[] mFooters = new FooterWrapper[2];
- private float mFooterVerticalOffset = 0;
- private float mFooterAlpha = 1;
- private int mStackHeight;
- private View mContextualChipWrapper;
- private View mContextualChip;
-
- 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);
- mActivity = BaseDraggingActivity.fromContext(context);
- setOnClickListener((view) -> {
- if (getTask() == null) {
- return;
- }
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- if (isRunningTask()) {
- createLaunchAnimationForRunningTask().start();
- } else {
- launchTask(true /* animate */);
- }
- } else {
- launchTask(true /* animate */);
- }
-
- mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
- Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
- TaskUtils.getLaunchComponentKeyForTask(getTask().key));
- mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
- .log(LAUNCHER_TASK_LAUNCH_TAP);
- });
-
- mCurrentFullscreenParams = new FullscreenDrawParams(context);
- mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
-
- mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams);
- setOutlineProvider(mOutlineProvider);
- }
-
- /**
- * Builds proto for logging
- */
- public WorkspaceItemInfo getItemInfo() {
- ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(getTask().key);
- WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
- dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK;
- dummyInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
- dummyInfo.user = componentKey.user;
- dummyInfo.intent = new Intent().setComponent(componentKey.componentName);
- dummyInfo.title = TaskUtils.getTitle(getContext(), getTask());
- dummyInfo.screenId = getRecentsView().indexOfChild(this);
- return dummyInfo;
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mSnapshotView = findViewById(R.id.snapshot);
- mIconView = findViewById(R.id.icon);
- }
-
- /**
- * The modalness of this view is how it should be displayed when it is shown on its own in the
- * modal state of overview.
- *
- * @param modalness [0, 1] 0 being in context with other tasks, 1 being shown on its own.
- */
- public void setModalness(float modalness) {
- mModalness = modalness;
- mIconView.setAlpha(comp(modalness));
- if (mContextualChip != null) {
- mContextualChip.setScaleX(comp(modalness));
- mContextualChip.setScaleY(comp(modalness));
- }
- if (mContextualChipWrapper != null) {
- mContextualChipWrapper.setAlpha(comp(modalness));
- }
-
- updateFooterVerticalOffset(mFooterVerticalOffset);
- }
-
- public TaskMenuView getMenuView() {
- return mMenuView;
- }
-
- public DigitalWellBeingToast getDigitalWellBeingToast() {
- return mDigitalWellBeingToast;
- }
-
- /**
- * Updates this task view to the given {@param task}.
- *
- * TODO(b/142282126) Re-evaluate if we need to pass in isMultiWindowMode after
- * that issue is fixed
- */
- public void bind(Task task, RecentsOrientedState orientedState) {
- cancelPendingLoadTasks();
- mTask = task;
- mSnapshotView.bind(task);
- setOrientationState(orientedState);
- }
-
- public Task getTask() {
- return mTask;
- }
-
- public TaskThumbnailView getThumbnail() {
- return mSnapshotView;
- }
-
- public IconView getIconView() {
- return mIconView;
- }
-
- public AnimatorPlaybackController createLaunchAnimationForRunningTask() {
- final PendingAnimation pendingAnimation = getRecentsView().createTaskLaunchAnimation(
- this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR);
- AnimatorPlaybackController currentAnimation = pendingAnimation.createPlaybackController();
- currentAnimation.setEndAction(() -> {
- pendingAnimation.finish(true, Touch.SWIPE);
- launchTask(false);
- });
- return currentAnimation;
- }
-
- public void launchTask(boolean animate) {
- launchTask(animate, false /* freezeTaskList */);
- }
-
- public void launchTask(boolean animate, boolean freezeTaskList) {
- launchTask(animate, freezeTaskList, (result) -> {
- if (!result) {
- notifyTaskLaunchFailed(TAG);
- }
- }, getHandler());
- }
-
- public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
- Handler resultCallbackHandler) {
- launchTask(animate, false /* freezeTaskList */, resultCallback, resultCallbackHandler);
- }
-
- public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback,
- Handler resultCallbackHandler) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- RecentsView recentsView = getRecentsView();
- if (isRunningTask()) {
- recentsView.finishRecentsAnimation(false /* toRecents */,
- () -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
- } else {
- // This is a workaround against the WM issue that app open is not correctly animated
- // when recents animation is being cleaned up (b/143774568). When that's possible,
- // we should rely on the framework side to cancel the recents animation, and we will
- // clean up the screenshot on the launcher side while we launch the next task.
- recentsView.switchToScreenshot(null,
- () -> recentsView.finishRecentsAnimation(true /* toRecents */,
- () -> launchTaskInternal(animate, freezeTaskList, resultCallback,
- resultCallbackHandler)));
- }
- } else {
- launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
- }
- }
-
- private void launchTaskInternal(boolean animate, boolean freezeTaskList,
- Consumer<Boolean> resultCallback, Handler resultCallbackHandler) {
- if (mTask != null) {
- final ActivityOptions opts;
- TestLogging.recordEvent(
- TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
- if (animate) {
- opts = mActivity.getActivityLaunchOptions(this);
- if (freezeTaskList) {
- ActivityOptionsCompat.setFreezeRecentTasksList(opts);
- }
- ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
- opts, resultCallback, resultCallbackHandler);
- } else {
- opts = ActivityOptionsCompat.makeCustomAnimation(getContext(), 0, 0, () -> {
- if (resultCallback != null) {
- // Only post the animation start after the system has indicated that the
- // transition has started
- resultCallbackHandler.post(() -> resultCallback.accept(true));
- }
- }, resultCallbackHandler);
- if (freezeTaskList) {
- ActivityOptionsCompat.setFreezeRecentTasksList(opts);
- }
- ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
- opts, (success) -> {
- if (resultCallback != null && !success) {
- // If the call to start activity failed, then post the result
- // immediately, otherwise, wait for the animation start callback
- // from the activity options above
- resultCallbackHandler.post(() -> resultCallback.accept(false));
- }
- }, resultCallbackHandler);
- }
- getRecentsView().onTaskLaunched(mTask);
- }
- }
-
- public void onTaskListVisibilityChanged(boolean visible) {
- if (mTask == null) {
- return;
- }
- cancelPendingLoadTasks();
- if (visible) {
- // These calls are no-ops if the data is already loaded, try and load the high
- // resolution thumbnail if the state permits
- RecentsModel model = RecentsModel.INSTANCE.get(getContext());
- TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
- TaskIconCache iconCache = model.getIconCache();
- mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground(
- mTask, thumbnail -> mSnapshotView.setThumbnail(mTask, thumbnail));
- mIconLoadRequest = iconCache.updateIconInBackground(mTask,
- (task) -> {
- setIcon(task.icon);
- if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
- getRecentsView().updateLiveTileIcon(task.icon);
- }
- mDigitalWellBeingToast.initialize(mTask);
- });
- } else {
- mSnapshotView.setThumbnail(null, null);
- setIcon(null);
- // Reset the task thumbnail reference as well (it will be fetched from the cache or
- // reloaded next time we need it)
- mTask.thumbnail = null;
- }
- }
-
- private void cancelPendingLoadTasks() {
- if (mThumbnailLoadRequest != null) {
- mThumbnailLoadRequest.cancel();
- mThumbnailLoadRequest = null;
- }
- if (mIconLoadRequest != null) {
- mIconLoadRequest.cancel();
- mIconLoadRequest = null;
- }
- }
-
- private boolean showTaskMenu(int action) {
- if (!getRecentsView().isClearAllHidden()) {
- getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
- } else {
- mMenuView = TaskMenuView.showForTask(this);
- mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
- .log(LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS);
- UserEventDispatcher.newInstance(getContext()).logActionOnItem(action, Direction.NONE,
- LauncherLogProto.ItemType.TASK_ICON);
- if (mMenuView != null) {
- mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener);
- }
- }
- return mMenuView != null;
- }
-
- private void setIcon(Drawable icon) {
- if (icon != null) {
- mIconView.setDrawable(icon);
- mIconView.setOnClickListener(v -> showTaskMenu(Touch.TAP));
- mIconView.setOnLongClickListener(v -> {
- requestDisallowInterceptTouchEvent(true);
- return showTaskMenu(Touch.LONGPRESS);
- });
- } else {
- mIconView.setDrawable(null);
- mIconView.setOnClickListener(null);
- mIconView.setOnLongClickListener(null);
- }
- }
-
- public void setOrientationState(RecentsOrientedState orientationState) {
- PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
- boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
- int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
- LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
- switch (orientationHandler.getRotation()) {
- case Surface.ROTATION_90:
- iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
- iconParams.rightMargin = -thumbnailPadding;
- iconParams.leftMargin = 0;
- iconParams.topMargin = snapshotParams.topMargin / 2;
- break;
- case Surface.ROTATION_180:
- iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
- iconParams.bottomMargin = -thumbnailPadding;
- iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
- break;
- case Surface.ROTATION_270:
- iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
- iconParams.leftMargin = -thumbnailPadding;
- iconParams.rightMargin = 0;
- iconParams.topMargin = snapshotParams.topMargin / 2;
- break;
- case Surface.ROTATION_0:
- default:
- iconParams.gravity = TOP | CENTER_HORIZONTAL;
- iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
- break;
- }
- mIconView.setLayoutParams(iconParams);
- mIconView.setRotation(orientationHandler.getDegreesRotated());
-
- if (mMenuView != null) {
- mMenuView.onRotationChanged();
- }
- }
-
- private void setIconAndDimTransitionProgress(float progress, boolean invert) {
- if (invert) {
- progress = 1 - progress;
- }
- mFocusTransitionProgress = progress;
- mSnapshotView.setDimAlphaMultipler(progress);
- float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION;
- float lowerClamp = invert ? 1f - iconScalePercentage : 0;
- float upperClamp = invert ? 1 : iconScalePercentage;
- float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp)
- .getInterpolation(progress);
- mIconView.setScaleX(scale);
- mIconView.setScaleY(scale);
-
- updateFooterVerticalOffset(1.0f - scale);
- }
-
- public void setIconScaleAnimStartProgress(float startProgress) {
- mIconScaleAnimStartProgress = startProgress;
- }
-
- public void animateIconScaleAndDimIntoView() {
- if (mIconAndDimAnimator != null) {
- mIconAndDimAnimator.cancel();
- }
- mIconAndDimAnimator = ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1);
- mIconAndDimAnimator.setCurrentFraction(mIconScaleAnimStartProgress);
- mIconAndDimAnimator.setDuration(DIM_ANIM_DURATION).setInterpolator(LINEAR);
- mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mIconAndDimAnimator = null;
- }
- });
- mIconAndDimAnimator.start();
- }
-
- protected void setIconScaleAndDim(float iconScale) {
- setIconScaleAndDim(iconScale, false);
- }
-
- private void setIconScaleAndDim(float iconScale, boolean invert) {
- if (mIconAndDimAnimator != null) {
- mIconAndDimAnimator.cancel();
- }
- setIconAndDimTransitionProgress(iconScale, invert);
- }
-
- protected void resetViewTransforms() {
- setCurveScale(1);
- setTranslationX(0f);
- setTranslationY(0f);
- setTranslationZ(0);
- setAlpha(mStableAlpha);
- setIconScaleAndDim(1);
- }
-
- public void setStableAlpha(float parentAlpha) {
- mStableAlpha = parentAlpha;
- setAlpha(mStableAlpha);
- }
-
- @Override
- public void onRecycle() {
- resetViewTransforms();
- // Clear any references to the thumbnail (it will be re-read either from the cache or the
- // system on next bind)
- mSnapshotView.setThumbnail(mTask, null);
- setOverlayEnabled(false);
- onTaskListVisibilityChanged(false);
- }
-
- @Override
- public void onPageScroll(ScrollState scrollState) {
- // Don't do anything if it's modal.
- if (mModalness > 0) {
- return;
- }
-
- float curveInterpolation =
- CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
- float curveScaleForCurveInterpolation = getCurveScaleForCurveInterpolation(
- curveInterpolation);
- mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
- setCurveScale(curveScaleForCurveInterpolation);
-
- mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f);
- for (FooterWrapper footer : mFooters) {
- if (footer != null) {
- footer.mView.setAlpha(mFooterAlpha);
- }
- }
-
- if (mMenuView != null) {
- PagedOrientationHandler pagedOrientationHandler = getPagedOrientationHandler();
- RecentsView recentsView = getRecentsView();
- mMenuView.setPosition(getX() - recentsView.getScrollX(),
- getY() - recentsView.getScrollY(), pagedOrientationHandler);
- mMenuView.setScaleX(getScaleX());
- mMenuView.setScaleY(getScaleY());
- }
- }
-
- /**
- * Sets the footer at the specific index and returns the previously set footer.
- */
- public View setFooter(int index, View view) {
- View oldFooter = null;
-
- // If the footer are is already collapsed, do not animate entry
- boolean shouldAnimateEntry = mFooterVerticalOffset <= 0;
-
- if (mFooters[index] != null) {
- oldFooter = mFooters[index].mView;
- mFooters[index].release();
- removeView(oldFooter);
-
- // If we are replacing an existing footer, do not animate entry
- shouldAnimateEntry = false;
- }
- if (view != null) {
- int indexToAdd = getChildCount();
- for (int i = index - 1; i >= 0; i--) {
- if (mFooters[i] != null) {
- indexToAdd = indexOfChild(mFooters[i].mView);
- break;
- }
- }
-
- addView(view, indexToAdd);
- LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
- layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
- layoutParams.bottomMargin =
- ((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin;
- view.setAlpha(mFooterAlpha);
- mFooters[index] = new FooterWrapper(view);
- if (shouldAnimateEntry) {
- mFooters[index].animateEntry();
- }
- } else {
- mFooters[index] = null;
- }
-
- mStackHeight = 0;
- for (FooterWrapper footer : mFooters) {
- if (footer != null) {
- footer.setVerticalShift(mStackHeight);
- mStackHeight += footer.mExpectedHeight;
- }
- }
-
- return oldFooter;
- }
-
- /**
- * Sets the contextual chip.
- *
- * @param view Wrapper view containing contextual chip.
- */
- public void setContextualChip(View view) {
- if (mContextualChipWrapper != null) {
- removeView(mContextualChipWrapper);
- }
- if (view != null) {
- mContextualChipWrapper = view;
- LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.WRAP_CONTENT);
- layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
- int expectedChipHeight = getExpectedViewHeight(view);
- float chipOffset = getResources().getDimension(R.dimen.chip_hint_vertical_offset);
- layoutParams.bottomMargin = (int)
- (((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin
- - expectedChipHeight + chipOffset);
- mContextualChip = ((FrameLayout) mContextualChipWrapper).getChildAt(0);
- mContextualChip.setScaleX(0f);
- mContextualChip.setScaleY(0f);
- GradientDrawable scrimDrawable = (GradientDrawable) getResources().getDrawable(
- R.drawable.chip_scrim_gradient, mActivity.getTheme());
- float cornerRadius = getTaskCornerRadius();
- scrimDrawable.setCornerRadii(
- new float[]{0, 0, 0, 0, cornerRadius, cornerRadius, cornerRadius,
- cornerRadius});
- InsetDrawable scrimDrawableInset = new InsetDrawable(scrimDrawable, 0, 0, 0,
- (int) (expectedChipHeight - chipOffset));
- mContextualChipWrapper.setBackground(scrimDrawableInset);
- mContextualChipWrapper.setPadding(0, 0, 0, 0);
- mContextualChipWrapper.setAlpha(0f);
- addView(view, getChildCount(), layoutParams);
- if (mContextualChip != null) {
- mContextualChip.animate().scaleX(1f).scaleY(1f).setDuration(50);
- }
- if (mContextualChipWrapper != null) {
- mContextualChipWrapper.animate().alpha(1f).setDuration(50);
- }
- }
- }
-
- public float getTaskCornerRadius() {
- return TaskCornerRadius.get(mActivity);
- }
-
- /**
- * Clears the contextual chip from TaskView.
- *
- * @return The contextual chip wrapper view to be recycled.
- */
- public View clearContextualChip() {
- if (mContextualChipWrapper != null) {
- removeView(mContextualChipWrapper);
- }
- View oldContextualChipWrapper = mContextualChipWrapper;
- mContextualChipWrapper = null;
- mContextualChip = null;
- return oldContextualChipWrapper;
- }
-
- @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);
- if (Utilities.ATLEAST_Q) {
- SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight());
- setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
- }
-
- mStackHeight = 0;
- for (FooterWrapper footer : mFooters) {
- if (footer != null) {
- mStackHeight += footer.mView.getHeight();
- }
- }
- updateFooterVerticalOffset(0);
- }
-
- private void updateFooterVerticalOffset(float offset) {
- mFooterVerticalOffset = offset;
-
- for (FooterWrapper footer : mFooters) {
- if (footer != null) {
- footer.updateFooterOffset();
- }
- }
- }
-
- 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;
- setScaleX(mCurveScale);
- setScaleY(mCurveScale);
- }
-
- public float getCurveScale() {
- return mCurveScale;
- }
-
- @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 FullscreenDrawParams mFullscreenParams;
-
- TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams) {
- mMarginTop = context.getResources().getDimensionPixelSize(
- R.dimen.task_thumbnail_top_margin);
- mFullscreenParams = fullscreenParams;
- }
-
- public void setFullscreenParams(FullscreenDrawParams params) {
- mFullscreenParams = params;
- }
-
- @Override
- public void getOutline(View view, Outline outline) {
- RectF insets = mFullscreenParams.mCurrentDrawnInsets;
- float scale = mFullscreenParams.mScale;
- outline.setRoundRect(0,
- (int) (mMarginTop * scale),
- (int) ((insets.left + view.getWidth() + insets.right) * scale),
- (int) ((insets.top + view.getHeight() + insets.bottom) * scale),
- mFullscreenParams.mCurrentDrawnCornerRadius);
- }
- }
-
- private class FooterWrapper extends ViewOutlineProvider {
-
- final View mView;
- final ViewOutlineProvider mOldOutlineProvider;
- final ViewOutlineProvider mDelegate;
-
- final int mExpectedHeight;
- final int mOldPaddingBottom;
-
- int mAnimationOffset = 0;
- int mEntryAnimationOffset = 0;
-
- public FooterWrapper(View view) {
- mView = view;
- mOldOutlineProvider = view.getOutlineProvider();
- mDelegate = mOldOutlineProvider == null
- ? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider;
-
- mExpectedHeight = getExpectedViewHeight(view);
- mOldPaddingBottom = view.getPaddingBottom();
-
- if (mOldOutlineProvider != null) {
- view.setOutlineProvider(this);
- view.setClipToOutline(true);
- }
- }
-
- public void setVerticalShift(int shift) {
- mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(),
- mView.getPaddingRight(), mOldPaddingBottom + shift);
- }
-
- @Override
- public void getOutline(View view, Outline outline) {
- mDelegate.getOutline(view, outline);
- outline.offset(0, -mAnimationOffset - mEntryAnimationOffset);
- }
-
- void updateFooterOffset() {
- float offset = Utilities.or(mFooterVerticalOffset, mModalness);
- mAnimationOffset = Math.round(mStackHeight * offset);
- mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset
- + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom
- + mCurrentFullscreenParams.mCurrentDrawnInsets.top);
- mView.invalidateOutline();
- }
-
- void release() {
- mView.setOutlineProvider(mOldOutlineProvider);
- setVerticalShift(0);
- }
-
- void animateEntry() {
- ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
- animator.addUpdateListener(anim -> {
- float factor = 1 - anim.getAnimatedFraction();
- int totalShift = mExpectedHeight + mView.getPaddingBottom() - mOldPaddingBottom;
- mEntryAnimationOffset = Math.round(factor * totalShift);
- updateFooterOffset();
- });
- animator.setDuration(100);
- animator.start();
- }
- }
-
- private int getExpectedViewHeight(View view) {
- int expectedHeight;
- int h = view.getLayoutParams().height;
- if (h > 0) {
- expectedHeight = h;
- } else {
- int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
- view.measure(m, m);
- expectedHeight = view.getMeasuredHeight();
- }
- return expectedHeight;
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
-
- info.addAction(
- new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close,
- getContext().getText(R.string.accessibility_close)));
-
- final Context context = getContext();
- for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
- info.addAction(s.createAccessibilityAction(context));
- }
-
- if (mDigitalWellBeingToast.hasLimit()) {
- info.addAction(
- new AccessibilityNodeInfo.AccessibilityAction(
- R.string.accessibility_app_usage_settings,
- getContext().getText(R.string.accessibility_app_usage_settings)));
- }
-
- final RecentsView recentsView = getRecentsView();
- final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
- AccessibilityNodeInfo.CollectionItemInfo.obtain(
- 0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1,
- 1, false);
- info.setCollectionItemInfo(itemInfo);
- }
-
- @Override
- public boolean performAccessibilityAction(int action, Bundle arguments) {
- if (action == R.string.accessibility_close) {
- getRecentsView().dismissTask(this, true /*animateTaskView*/,
- true /*removeTask*/);
- return true;
- }
-
- if (action == R.string.accessibility_app_usage_settings) {
- mDigitalWellBeingToast.openAppUsageSettings(this);
- return true;
- }
-
- for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
- if (s.hasHandlerForAction(action)) {
- s.onClick(this);
- return true;
- }
- }
-
- return super.performAccessibilityAction(action, arguments);
- }
-
- public RecentsView getRecentsView() {
- return (RecentsView) getParent();
- }
-
- PagedOrientationHandler getPagedOrientationHandler() {
- return getRecentsView().mOrientationState.getOrientationHandler();
- }
-
- 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();
- }
-
- /**
- * Hides the icon and shows insets when this TaskView is about to be shown fullscreen.
- *
- * @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets.
- */
- public void setFullscreenProgress(float progress) {
- progress = Utilities.boundToRange(progress, 0, 1);
- mFullscreenProgress = progress;
- boolean isFullscreen = mFullscreenProgress > 0;
- mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
- setClipChildren(!isFullscreen);
- setClipToPadding(!isFullscreen);
-
- TaskThumbnailView thumbnail = getThumbnail();
- updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
-
- if (!getRecentsView().isTaskIconScaledDown(this)) {
- // Some of the items in here are dependent on the current fullscreen params, but don't
- // update them if the icon is supposed to be scaled down.
- setIconScaleAndDim(progress, true /* invert */);
- }
-
- thumbnail.setFullscreenParams(mCurrentFullscreenParams);
- mOutlineProvider.setFullscreenParams(mCurrentFullscreenParams);
- invalidateOutline();
- }
-
- void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
- if (getRecentsView() == null) {
- return;
- }
- mCurrentFullscreenParams.setProgress(
- mFullscreenProgress,
- getRecentsView().getScaleX(),
- getWidth(), mActivity.getDeviceProfile(),
- previewPositionHelper);
- }
-
- public boolean isRunningTask() {
- if (getRecentsView() == null) {
- return false;
- }
- return this == getRecentsView().getRunningTaskView();
- }
-
- public void setShowScreenshot(boolean showScreenshot) {
- mShowScreenshot = showScreenshot;
- }
-
- public boolean showScreenshot() {
- if (!isRunningTask()) {
- return true;
- }
- return mShowScreenshot;
- }
-
- public void setOverlayEnabled(boolean overlayEnabled) {
- mSnapshotView.setOverlayEnabled(overlayEnabled);
- }
-
- /**
- * We update and subsequently draw these in {@link #setFullscreenProgress(float)}.
- */
- public static class FullscreenDrawParams {
-
- private final float mCornerRadius;
- private final float mWindowCornerRadius;
-
- public RectF mCurrentDrawnInsets = new RectF();
- public float mCurrentDrawnCornerRadius;
- /** The current scale we apply to the thumbnail to adjust for new left/right insets. */
- public float mScale = 1;
-
- public FullscreenDrawParams(Context context) {
- mCornerRadius = TaskCornerRadius.get(context);
- mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
-
- mCurrentDrawnCornerRadius = mCornerRadius;
- }
-
- /**
- * Sets the progress in range [0, 1]
- */
- public void setProgress(float fullscreenProgress, float parentScale, int previewWidth,
- DeviceProfile dp, PreviewPositionHelper pph) {
- RectF insets = pph.getInsetsToDrawInFullscreen();
-
- float currentInsetsLeft = insets.left * fullscreenProgress;
- float currentInsetsRight = insets.right * fullscreenProgress;
- mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress,
- currentInsetsRight, insets.bottom * fullscreenProgress);
- float fullscreenCornerRadius = dp.isMultiWindowMode ? 0 : mWindowCornerRadius;
-
- mCurrentDrawnCornerRadius =
- Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius)
- / parentScale;
-
- // We scaled the thumbnail to fit the content (excluding insets) within task view width.
- // Now that we are drawing left/right insets again, we need to scale down to fit them.
- if (previewWidth > 0) {
- mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight);
- }
- }
-
- }
-}
diff --git a/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml b/quickstep/res/drawable/all_apps_edu_circle.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml
rename to quickstep/res/drawable/all_apps_edu_circle.xml
diff --git a/quickstep/res/drawable/bg_circle.xml b/quickstep/res/drawable/bg_circle.xml
new file mode 100644
index 0000000..506177b
--- /dev/null
+++ b/quickstep/res/drawable/bg_circle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="#FFFFFFFF" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_sandbox_close_button.xml b/quickstep/res/drawable/bg_sandbox_close_button.xml
new file mode 100644
index 0000000..2811071
--- /dev/null
+++ b/quickstep/res/drawable/bg_sandbox_close_button.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="#303030"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_sandbox_feedback.xml b/quickstep/res/drawable/bg_sandbox_feedback.xml
new file mode 100644
index 0000000..83a3dea
--- /dev/null
+++ b/quickstep/res/drawable/bg_sandbox_feedback.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="28dp"/>
+ <solid android:color="?android:attr/colorBackgroundFloating"/>
+</shape>
diff --git a/quickstep/res/drawable/bg_wellbeing_toast.xml b/quickstep/res/drawable/bg_wellbeing_toast.xml
index 65730f6..4dc981f 100644
--- a/quickstep/res/drawable/bg_wellbeing_toast.xml
+++ b/quickstep/res/drawable/bg_wellbeing_toast.xml
@@ -13,7 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
- <solid android:color="#E61A73E8" />
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:priv-android="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?priv-android:attr/colorAccentPrimary" />
<corners android:radius="?android:attr/dialogCornerRadius" />
</shape>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/drawable/chip_hint_background_light.xml b/quickstep/res/drawable/chip_hint_background_light.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/drawable/chip_hint_background_light.xml
rename to quickstep/res/drawable/chip_hint_background_light.xml
diff --git a/quickstep/res/drawable/close_icon.xml b/quickstep/res/drawable/close_icon.xml
new file mode 100644
index 0000000..07f4336
--- /dev/null
+++ b/quickstep/res/drawable/close_icon.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="#909090"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
diff --git a/quickstep/res/drawable/default_sandbox_app_icon.xml b/quickstep/res/drawable/default_sandbox_app_icon.xml
new file mode 100644
index 0000000..e9c9701
--- /dev/null
+++ b/quickstep/res/drawable/default_sandbox_app_icon.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@color/gesture_tutorial_fake_task_view_color" />
+</shape>
diff --git a/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml b/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml
new file mode 100644
index 0000000..9c95497
--- /dev/null
+++ b/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/gesture_tutorial_fake_previous_task_view_color" />
+</shape>
diff --git a/quickstep/res/drawable/default_sandbox_app_task_thumbnail.xml b/quickstep/res/drawable/default_sandbox_app_task_thumbnail.xml
new file mode 100644
index 0000000..0d85c9a
--- /dev/null
+++ b/quickstep/res/drawable/default_sandbox_app_task_thumbnail.xml
@@ -0,0 +1,247 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportWidth="412"
+ android:viewportHeight="892">
+ <path
+ android:pathData="M0,101h412v791h-412z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M28,222L282,222A4,4 0,0 1,286 226L286,254A4,4 0,0 1,282 258L28,258A4,4 0,0 1,24 254L24,226A4,4 0,0 1,28 222z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,266L344,266A4,4 0,0 1,348 270L348,298A4,4 0,0 1,344 302L28,302A4,4 0,0 1,24 298L24,270A4,4 0,0 1,28 266z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,310L257,310A4,4 0,0 1,261 314L261,342A4,4 0,0 1,257 346L28,346A4,4 0,0 1,24 342L24,314A4,4 0,0 1,28 310z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,354L67,354A4,4 0,0 1,71 358L71,362A4,4 0,0 1,67 366L28,366A4,4 0,0 1,24 362L24,358A4,4 0,0 1,28 354z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M86,354L125,354A4,4 0,0 1,129 358L129,362A4,4 0,0 1,125 366L86,366A4,4 0,0 1,82 362L82,358A4,4 0,0 1,86 354z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,390L384,390A4,4 0,0 1,388 394L388,626A4,4 0,0 1,384 630L28,630A4,4 0,0 1,24 626L24,394A4,4 0,0 1,28 390z"
+ android:fillColor="#5F6368"/>
+ <path
+ android:pathData="M101.322,469.362L71.101,577.73H131.543L101.322,469.362Z"
+ android:strokeWidth="2"
+ android:fillColor="#5F6368"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M101.322,522.516V543.644V611.743"
+ android:fillColor="#5F6368"/>
+ <path
+ android:pathData="M101.322,522.516V543.644V611.743"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M109.577,532.822L101.322,548.135"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M109.577,558.221L101.322,574.196"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M94.54,524.945L101.322,534.442"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M90.929,549.239L101.322,561.166"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M62.772,412.307L30.855,563.669H94.688L62.772,412.307Z"
+ android:strokeWidth="2"
+ android:fillColor="#5F6368"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M62.772,486.516V516.037V611.154"
+ android:fillColor="#5F6368"/>
+ <path
+ android:pathData="M62.772,486.516V516.037V611.154"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M74.344,500.944L62.772,522.368"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M74.344,536.43L62.772,558.736"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M53.263,489.976L62.772,503.154"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M48.251,523.914L62.772,540.552"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#9ADCB2"/>
+ <path
+ android:pathData="M37,612H377"
+ android:strokeWidth="4"
+ android:fillColor="#00000000"
+ android:strokeColor="#9AA0A6"/>
+ <path
+ android:pathData="M81.24,441.187L84.066,443.127L87.333,441.98L86.273,445.244L88.393,447.978H84.949L83.006,450.8L81.947,447.537L78.679,446.655L81.417,444.626L81.24,441.187Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M172.561,469L175.387,471.029L178.654,469.794L177.594,473.057L179.714,475.791H176.27L174.327,478.702L173.267,475.438L170,474.468L172.737,472.44L172.561,469Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M278.073,481.229L280.81,483.257L284.078,482.022L283.018,485.286L285.137,488.02H281.693L279.839,490.93L278.691,487.667L275.424,486.697L278.161,484.668L278.073,481.229Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M325.561,459L328.387,460.94L331.654,459.794L330.594,463.057L332.714,465.791H329.27L327.327,468.613L326.267,465.35L323,464.468L325.737,462.44L325.561,459Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M209.649,517L212.475,519.029L215.654,517.794L214.682,521.057L216.802,523.791H213.358L211.415,526.702L210.356,523.438L207,522.468L209.737,520.44L209.649,517Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M128.836,482.816L129.455,485.11L131.662,485.815L129.808,487.314L129.896,489.784L128.042,488.285L125.922,489.078L126.629,486.785L125.304,484.757L127.512,484.845L128.836,482.816Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M37.532,450L38.239,452.381L40.358,453.087L38.504,454.498L38.592,456.968L36.737,455.556L34.618,456.35L35.325,453.969L34,452.029H36.208L37.532,450Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M284.077,516.947L284.784,519.329L286.903,520.034L285.048,521.445L285.137,523.915L283.282,522.415L281.163,523.297L281.869,520.916L280.545,518.976H282.753L284.077,516.947Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M232.771,444.893L233.478,447.274L235.597,447.979L233.743,449.391L233.831,451.86L231.977,450.449L229.857,451.243L230.564,448.861L229.239,446.921H231.447L232.771,444.893Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M152.384,521L152.914,522.676L154.415,523.117L153.091,524.175L153.179,525.851L151.854,524.881L150.442,525.41L150.883,523.734L150,522.411H151.501L152.384,521Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M136.473,515L136.914,516.676L138.415,517.117L137.179,518.175V519.851L135.943,518.881L134.442,519.41L134.971,517.822L134,516.411H135.589L136.473,515Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M314.454,410.406L314.896,412.082L316.397,412.523L315.16,413.493V415.257L313.924,414.199L312.423,414.816L312.953,413.14L311.981,411.817H313.571L314.454,410.406Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M344.212,488.195L344.742,489.871L346.243,490.4L344.919,491.37L345.007,493.046L343.683,492.076L342.27,492.605L342.711,491.018L341.828,489.606H343.329L344.212,488.195Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M47.067,420.814L47.508,422.402L49.009,422.931L47.773,423.901V425.665L46.537,424.607L45.036,425.136L45.566,423.549L44.594,422.137L46.184,422.226L47.067,420.814Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M265.887,448.42L266.328,450.007L267.829,450.537L266.593,451.507V453.271L265.357,452.212L263.856,452.83L264.385,451.154L263.414,449.831H265.004L265.887,448.42Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M110.912,413.759L111.441,415.435L112.854,415.876L111.618,416.934V418.61L110.382,417.551L108.969,418.169L109.41,416.493L108.439,415.17H110.028L110.912,413.759Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M149.412,418.344L152.238,420.372L155.505,419.138L154.445,422.489L156.565,425.135L153.121,425.223L151.178,428.045L150.118,424.782L146.851,423.812L149.589,421.783L149.412,418.344Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M37.532,450L38.239,452.381L40.358,453.087L38.504,454.498L38.592,456.968L36.737,455.556L34.618,456.35L35.325,453.969L34,452.029H36.208L37.532,450Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M141.906,455.74L142.612,458.122L144.731,458.827L142.877,460.238L142.965,462.708L141.111,461.208L139.08,462.09L139.786,459.709L138.374,457.769H140.669L141.906,455.74Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M201.777,459.267L202.395,461.56L204.603,462.265L202.748,463.765L202.836,466.146L200.982,464.735L198.863,465.529L199.569,463.235L198.245,461.207H200.452L201.777,459.267Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M370.175,504.248L370.793,506.629L373.001,507.335L371.146,508.746L371.234,511.216L369.38,509.716L367.261,510.598L367.967,508.217L366.643,506.277H368.85L370.175,504.248Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M300.532,437L301.239,439.381L303.358,440.087L301.592,441.498V443.968L299.826,442.468L297.706,443.35L298.413,440.969L297,439.029H299.296L300.532,437Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M193.384,420L193.914,421.676L195.415,422.117L194.091,423.087L194.179,424.851L192.854,423.792L191.442,424.41L191.883,422.734L191,421.411H192.501L193.384,420Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M248.313,469.323L248.843,470.911L250.256,471.44L249.02,472.41V474.174L247.784,473.116L246.371,473.645L246.812,472.057L245.841,470.646L247.43,470.734L248.313,469.323Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M169.988,496.751L170.518,498.427L172.019,498.956L170.694,499.926L170.782,501.602L169.458,500.632L168.045,501.161L168.487,499.573L167.604,498.162H169.105L169.988,496.751Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M222.441,400L222.882,401.676L224.384,402.117L223.059,403.175L223.147,404.851L221.911,403.792L220.41,404.41L220.851,402.734L219.968,401.411H221.469L222.441,400Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M211.754,492.341L212.284,493.928L213.785,494.458L212.461,495.428L212.549,497.192L211.225,496.133L209.812,496.662L210.253,495.075L209.37,493.664L210.871,493.752L211.754,492.341Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M254.054,420.814L254.584,422.402L256.085,422.931L254.761,423.901L254.849,425.665L253.524,424.607L252.111,425.136L252.553,423.549L251.67,422.137L253.171,422.226L254.054,420.814Z"
+ android:fillColor="#DADCE0"/>
+ <path
+ android:pathData="M307.162,602.392H205L255.565,502.27H322.273L374.238,602.392H307.162L255.565,502.785L307.162,602.392Z"
+ android:fillColor="#5F6368"/>
+ <path
+ android:pathData="M307.162,602.392H205L255.565,502.27H322.273L374.238,602.392H307.162ZM307.162,602.392L255.565,502.785"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#A8CBFE"/>
+ <path
+ android:pathData="M255.565,503.079V602.392"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#A8CBFE"/>
+ <path
+ android:pathData="M356.686,430.931C358.218,423.774 353.2,416.635 345.477,414.986C337.754,413.337 330.251,417.801 328.719,424.958C327.186,432.115 332.205,439.254 339.928,440.903C347.651,442.553 355.154,438.088 356.686,430.931Z"
+ android:fillColor="#FEEFC3"/>
+ <path
+ android:pathData="M344.708,427.026C345.789,421.976 342.244,416.939 336.79,415.774C331.336,414.609 326.038,417.759 324.957,422.808C323.876,427.858 327.421,432.895 332.875,434.06C338.329,435.225 343.627,432.076 344.708,427.026Z"
+ android:fillColor="#5F6368"/>
+ <path
+ android:pathData="M2,101L410,101A2,2 0,0 1,412 103L412,187A2,2 0,0 1,410 189L2,189A2,2 0,0 1,0 187L0,103A2,2 0,0 1,2 101z"
+ android:fillColor="#E8EAED"/>
+ <path
+ android:pathData="M99,129L313,129A2,2 0,0 1,315 131L315,159A2,2 0,0 1,313 161L99,161A2,2 0,0 1,97 159L97,131A2,2 0,0 1,99 129z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M32,123L60,123A8,8 0,0 1,68 131L68,159A8,8 0,0 1,60 167L32,167A8,8 0,0 1,24 159L24,131A8,8 0,0 1,32 123z"
+ android:fillColor="#80868B"/>
+ <path
+ android:pathData="M0,0h412v101h-412z"
+ android:fillColor="#202124"/>
+ <path
+ android:pathData="M34.5,48L377.5,48A18.5,18.5 0,0 1,396 66.5L396,66.5A18.5,18.5 0,0 1,377.5 85L34.5,85A18.5,18.5 0,0 1,16 66.5L16,66.5A18.5,18.5 0,0 1,34.5 48z"
+ android:fillColor="#3C4043"/>
+ <path
+ android:pathData="M28,654L356,654A4,4 0,0 1,360 658L360,672A4,4 0,0 1,356 676L28,676A4,4 0,0 1,24 672L24,658A4,4 0,0 1,28 654z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,684L367,684A4,4 0,0 1,371 688L371,702A4,4 0,0 1,367 706L28,706A4,4 0,0 1,24 702L24,688A4,4 0,0 1,28 684z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,714L337,714A4,4 0,0 1,341 718L341,732A4,4 0,0 1,337 736L28,736A4,4 0,0 1,24 732L24,718A4,4 0,0 1,28 714z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,744L210,744A4,4 0,0 1,214 748L214,762A4,4 0,0 1,210 766L28,766A4,4 0,0 1,24 762L24,748A4,4 0,0 1,28 744z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,790L344,790A4,4 0,0 1,348 794L348,808A4,4 0,0 1,344 812L28,812A4,4 0,0 1,24 808L24,794A4,4 0,0 1,28 790z"
+ android:fillColor="#BDC1C6"/>
+ <path
+ android:pathData="M28,820L337,820A4,4 0,0 1,341 824L341,838A4,4 0,0 1,337 842L28,842A4,4 0,0 1,24 838L24,824A4,4 0,0 1,28 820z"
+ android:fillColor="#BDC1C6"/>
+</vector>
diff --git a/quickstep/res/drawable/default_sandbox_mock_launcher.xml b/quickstep/res/drawable/default_sandbox_mock_launcher.xml
new file mode 100644
index 0000000..38fbcf0
--- /dev/null
+++ b/quickstep/res/drawable/default_sandbox_mock_launcher.xml
@@ -0,0 +1,21 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="360dp"
+ android:height="146dp"
+ android:viewportWidth="360"
+ android:viewportHeight="146">
+ <path
+ android:pathData="M25,96L335,96A25,25 0,0 1,360 121L360,121A25,25 0,0 1,335 146L25,146A25,25 0,0 1,0 121L0,121A25,25 0,0 1,25 96z"
+ android:fillColor="#3C4043"/>
+ <path
+ android:pathData="M30,30m-30,0a30,30 0,1 1,60 0a30,30 0,1 1,-60 0"
+ android:fillColor="#8AB4F8"/>
+ <path
+ android:pathData="M130,30m-30,0a30,30 0,1 1,60 0a30,30 0,1 1,-60 0"
+ android:fillColor="#F28B82"/>
+ <path
+ android:pathData="M230,30m-30,0a30,30 0,1 1,60 0a30,30 0,1 1,-60 0"
+ android:fillColor="#FDD663"/>
+ <path
+ android:pathData="M330,30m-30,0a30,30 0,1 1,60 0a30,30 0,1 1,-60 0"
+ android:fillColor="#81C995"/>
+</vector>
diff --git a/quickstep/res/drawable/default_sandbox_wallpaper.xml b/quickstep/res/drawable/default_sandbox_wallpaper.xml
new file mode 100644
index 0000000..816d1d6
--- /dev/null
+++ b/quickstep/res/drawable/default_sandbox_wallpaper.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground" />
+</shape>
diff --git a/quickstep/res/drawable/gesture_tutorial_action_button_background.xml b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
index 3f3b288..ac6a52a 100644
--- a/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
+++ b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
@@ -13,8 +13,19 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <corners android:radius="@dimen/default_dialog_corner_radius"/>
- <solid android:color="@color/gesture_tutorial_primary_color"/>
-</shape>
\ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:id="@android:id/mask"
+ android:shape="rectangle">
+ <corners android:radius="50dp"/>
+ </shape>
+ </item>
+ <item>
+ <shape
+ android:shape="rectangle">
+ <corners android:radius="50dp"/>
+ <solid android:color="@color/gesture_tutorial_primary_color"/>
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_cancel_button_background.xml b/quickstep/res/drawable/gesture_tutorial_cancel_button_background.xml
new file mode 100644
index 0000000..0a34af6
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_cancel_button_background.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="50dp"/>
+ <solid android:color="@android:color/transparent"/>
+ <stroke android:width="1dp" android:color="@color/gesture_tutorial_primary_color"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_loop_back.xml b/quickstep/res/drawable/gesture_tutorial_loop_back.xml
new file mode 100644
index 0000000..d2909ff
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_loop_back.xml
@@ -0,0 +1,96 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="783"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1000"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0.25"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1000"
+ android:propertyName="fillAlpha"
+ android:startOffset="1000"
+ android:valueFrom="0.75"
+ android:valueTo="0.25"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="2000"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:translateX="206"
+ android:translateY="446" />
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="12.5"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0.25"
+ android:fillColor="@color/gesture_tutorial_primary_color"
+ android:fillType="nonZero"
+ android:pathData=" M12.5 -446 C12.5,-446 12.5,446 12.5,446 C12.5,446 -12.5,446 -12.5,446 C-12.5,446 -12.5,-446 -12.5,-446 C-12.5,-446 12.5,-446 12.5,-446c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_loop_home.xml b/quickstep/res/drawable/gesture_tutorial_loop_home.xml
new file mode 100644
index 0000000..931f8c0
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_loop_home.xml
@@ -0,0 +1,96 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1000"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0.25"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1000"
+ android:propertyName="fillAlpha"
+ android:startOffset="1000"
+ android:valueFrom="0.75"
+ android:valueTo="0.25"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="850"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="2000"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:translateX="206"
+ android:translateY="879.5">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="0.25"
+ android:fillColor="@color/gesture_tutorial_primary_color"
+ android:fillType="nonZero"
+ android:pathData=" M206 -12.5 C206,-12.5 206,12.5 206,12.5 C206,12.5 -206,12.5 -206,12.5 C-206,12.5 -206,-12.5 -206,-12.5 C-206,-12.5 206,-12.5 206,-12.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="206"
+ android:translateY="446" />
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_loop_overview.xml b/quickstep/res/drawable/gesture_tutorial_loop_overview.xml
new file mode 100644
index 0000000..a4c532b
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_loop_overview.xml
@@ -0,0 +1,96 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1000"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0.25"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1000"
+ android:propertyName="fillAlpha"
+ android:startOffset="1000"
+ android:valueFrom="0.75"
+ android:valueTo="0.25"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="1500"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="2000"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:translateX="206"
+ android:translateY="879.5">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="0.25"
+ android:fillColor="@color/gesture_tutorial_primary_color"
+ android:fillType="nonZero"
+ android:pathData=" M206 -12.5 C206,-12.5 206,12.5 206,12.5 C206,12.5 -206,12.5 -206,12.5 C-206,12.5 -206,-12.5 -206,-12.5 C-206,-12.5 206,-12.5 206,-12.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="206"
+ android:translateY="446" />
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_motion_back.xml b/quickstep/res/drawable/gesture_tutorial_motion_back.xml
new file mode 100644
index 0000000..a6860fa
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_motion_back.xml
@@ -0,0 +1,1233 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_1_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_2_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_3_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_4_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_5_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_6_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_7_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_8_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_9_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_10_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_11_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_12_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G_D_13_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillAlpha"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="1367"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="283"
+ android:propertyName="scaleX"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0.88012"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,0.536 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="283"
+ android:propertyName="scaleY"
+ android:startOffset="1217"
+ android:valueFrom="1"
+ android:valueTo="0.88012"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,0.536 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2417"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_1_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_2_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_3_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_4_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_5_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_6_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_7_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_8_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_9_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_10_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_11_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_12_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_13_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_14_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_15_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_16_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_17_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_18_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_19_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_20_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_21_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_22_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_23_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G_D_24_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="1417"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="667"
+ android:pathData="M 123.282,129.757C 134.28199999999998,129.757 178.282,129.757 189.282,129.757"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="217">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="333"
+ android:pathData="M 189.282,129.757C 189.282,129.757 189.282,129.757 189.282,129.757"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="883">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="250"
+ android:pathData="M 189.282,129.757C 178.282,129.757 134.28199999999998,129.757 123.282,129.757"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="1217">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="217"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2383"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="967"
+ android:propertyName="fillAlpha"
+ android:startOffset="217"
+ android:valueFrom="0.75"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="1183"
+ android:valueFrom="0.75"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M-206.5 13.5 C-186.34,13.5 -170,29.84 -170,50 C-170,70.16 -186.34,86.5 -206.5,86.5 C-226.66,86.5 -243,70.16 -243,50 C-243,29.84 -226.66,13.5 -206.5,13.5c "
+ android:valueTo="M-206 0 C-178.39,0 -156,22.39 -156,50 C-156,77.61 -178.39,100 -206,100 C-233.61,100 -256,77.61 -256,50 C-256,22.39 -233.61,0 -206,0c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="583"
+ android:propertyName="pathData"
+ android:startOffset="217"
+ android:valueFrom="M-206 0 C-178.39,0 -156,22.39 -156,50 C-156,77.61 -178.39,100 -206,100 C-233.61,100 -256,77.61 -256,50 C-256,22.39 -233.61,0 -206,0c "
+ android:valueTo="M0 0 C27.61,0 50,22.39 50,50 C50,77.61 27.61,100 0,100 C-27.61,100 -50,77.61 -50,50 C-50,22.39 -27.61,0 0,0c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="383"
+ android:propertyName="pathData"
+ android:startOffset="800"
+ android:valueFrom="M0 0 C27.61,0 50,22.39 50,50 C50,77.61 27.61,100 0,100 C-27.61,100 -50,77.61 -50,50 C-50,22.39 -27.61,0 0,0c "
+ android:valueTo="M0 0 C27.61,0 50,22.39 50,50 C50,77.61 27.61,100 0,100 C-27.61,100 -50,77.61 -50,50 C-50,22.39 -27.61,0 0,0c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="pathData"
+ android:startOffset="1183"
+ android:valueFrom="M0 0 C27.61,0 50,22.39 50,50 C50,77.61 27.61,100 0,100 C-27.61,100 -50,77.61 -50,50 C-50,22.39 -27.61,0 0,0c "
+ android:valueTo="M0 13.5 C20.16,13.5 36.5,29.84 36.5,50 C36.5,70.16 20.16,86.5 0,86.5 C-20.16,86.5 -36.5,70.16 -36.5,50 C-36.5,29.84 -20.16,13.5 0,13.5c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1767"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_4_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -446 C206,-446 206,446 206,446 C206,446 -206,446 -206,446 C-206,446 -206,-446 -206,-446 C-206,-446 206,-446 206,-446c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G"
+ android:pivotX="206"
+ android:pivotY="446"
+ android:scaleX="1"
+ android:scaleY="1">
+ <group android:name="_R_G_L_3_G_L_0_G">
+ <group android:name="_R_G_L_3_G_L_0_G_L_0_G">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#f1f3f4"
+ android:fillType="nonZero"
+ android:pathData=" M412 101 C412,101 412,892 412,892 C412,892 0,892 0,892 C0,892 0,101 0,101 C0,101 412,101 412,101c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_1_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M412 0 C412,0 412,101 412,101 C412,101 0,101 0,101 C0,101 0,0 0,0 C0,0 412,0 412,0c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_2_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M383 804 C383,816.15 373.15,826 361,826 C361,826 51,826 51,826 C38.85,826 29,816.15 29,804 C29,791.85 38.85,782 51,782 C51,782 361,782 361,782 C373.15,782 383,791.85 383,804c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_3_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M201 47 C201,47 201,75 201,75 C201,77.21 199.21,79 197,79 C197,79 38,79 38,79 C35.79,79 34,77.21 34,75 C34,75 34,47 34,47 C34,44.79 35.79,43 38,43 C38,43 197,43 197,43 C199.21,43 201,44.79 201,47c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_4_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M388 47 C388,47 388,75 388,75 C388,77.21 386.21,79 384,79 C384,79 356,79 356,79 C353.79,79 352,77.21 352,75 C352,75 352,47 352,47 C352,44.79 353.79,43 356,43 C356,43 384,43 384,43 C386.21,43 388,44.79 388,47c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_5_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M336 47 C336,47 336,75 336,75 C336,77.21 334.21,79 332,79 C332,79 304,79 304,79 C301.79,79 300,77.21 300,75 C300,75 300,47 300,47 C300,44.79 301.79,43 304,43 C304,43 332,43 332,43 C334.21,43 336,44.79 336,47c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_6_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M70 618 C70,630.15 60.15,640 48,640 C35.85,640 26,630.15 26,618 C26,605.85 35.85,596 48,596 C60.15,596 70,605.85 70,618c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_7_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M70 396 C70,408.15 60.15,418 48,418 C35.85,418 26,408.15 26,396 C26,383.85 35.85,374 48,374 C60.15,374 70,383.85 70,396c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_8_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M394 248 C394,248 394,324 394,324 C394,333.94 385.94,342 376,342 C376,342 142,342 142,342 C132.06,342 124,333.94 124,324 C124,324 124,248 124,248 C124,238.06 132.06,230 142,230 C142,230 376,230 376,230 C385.94,230 394,238.06 394,248c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_9_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M393.94 468.38 C393.94,468.38 393.94,481.5 393.94,481.5 C393.94,483.71 392.15,485.5 389.94,485.5 C389.94,485.5 303.5,485.5 303.5,485.5 C301.29,485.5 299.5,483.71 299.5,481.5 C299.5,481.5 299.5,468.38 299.5,468.38 C299.5,466.17 301.29,464.38 303.5,464.38 C303.5,464.38 389.94,464.38 389.94,464.38 C392.15,464.38 393.94,466.17 393.94,468.38c M394 468 C394,477.67 386.17,485.5 376.5,485.5 C376.5,485.5 290,485.5 290,485.5 C280.33,485.5 272.5,477.67 272.5,468 C272.5,458.34 280.33,450.5 290,450.5 C290,450.5 376.5,450.5 376.5,450.5 C386.17,450.5 394,458.34 394,468c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_10_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M394 494 C394,494 394,547 394,547 C394,549.21 392.21,551 390,551 C390,551 164,551 164,551 C161.79,551 160,549.21 160,547 C160,547 160,494 160,494 C160,491.79 161.79,490 164,490 C164,490 390,490 390,490 C392.21,490 394,491.79 394,494c M394 508 C394,508 394,545 394,545 C394,554.94 385.94,563 376,563 C376,563 142,563 142,563 C132.06,563 124,554.94 124,545 C124,545 124,508 124,508 C124,498.06 132.06,490 142,490 C142,490 376,490 376,490 C385.94,490 394,498.06 394,508c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_11_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M394 690 C394,690 394,727 394,727 C394,736.94 385.94,745 376,745 C376,745 142,745 142,745 C132.06,745 124,736.94 124,727 C124,727 124,690 124,690 C124,680.06 132.06,672 142,672 C142,672 376,672 376,672 C385.94,672 394,680.06 394,690c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_12_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M267.5 617 C267.5,626.67 259.67,634.5 250,634.5 C250,634.5 104.5,634.5 104.5,634.5 C94.84,634.5 87,626.67 87,617 C87,607.34 94.84,599.5 104.5,599.5 C104.5,599.5 250,599.5 250,599.5 C259.67,599.5 267.5,607.34 267.5,617c " />
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_13_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M299 395.5 C299,405.17 291.16,413 281.5,413 C281.5,413 104.5,413 104.5,413 C94.84,413 87,405.17 87,395.5 C87,385.84 94.84,378 104.5,378 C104.5,378 281.5,378 281.5,378 C291.16,378 299,385.84 299,395.5c " />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_2_G"
+ android:scaleY="0">
+ <group
+ android:name="_R_G_L_2_G_L_0_G"
+ android:scaleY="0">
+ <group
+ android:name="_R_G_L_2_G_L_0_G_L_0_G"
+ android:scaleY="0">
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M412 0 C412,0 412,892 412,892 C412,892 0,892 0,892 C0,892 0,0 0,0 C0,0 412,0 412,0c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_1_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M412 0 C412,0 412,101 412,101 C412,101 0,101 0,101 C0,101 0,0 0,0 C0,0 412,0 412,0c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_2_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M195 143 C195,143 195,153 195,153 C195,155.21 193.21,157 191,157 C191,157 106,157 106,157 C103.79,157 102,155.21 102,153 C102,153 102,143 102,143 C102,140.79 103.79,139 106,139 C106,139 191,139 191,139 C193.21,139 195,140.79 195,143c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_3_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M270 165 C270,165 270,173 270,173 C270,175.21 268.21,177 266,177 C266,177 106,177 106,177 C103.79,177 102,175.21 102,173 C102,173 102,165 102,165 C102,162.79 103.79,161 106,161 C106,161 266,161 266,161 C268.21,161 270,162.79 270,165c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_4_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M222 231 C222,231 222,241 222,241 C222,243.21 220.21,245 218,245 C218,245 106,245 106,245 C103.79,245 102,243.21 102,241 C102,241 102,231 102,231 C102,228.79 103.79,227 106,227 C106,227 218,227 218,227 C220.21,227 222,228.79 222,231c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_5_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M241 253 C241,253 241,261 241,261 C241,263.21 239.21,265 237,265 C237,265 106,265 106,265 C103.79,265 102,263.21 102,261 C102,261 102,253 102,253 C102,250.79 103.79,249 106,249 C106,249 237,249 237,249 C239.21,249 241,250.79 241,253c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_6_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M214 319 C214,319 214,329 214,329 C214,331.21 212.21,333 210,333 C210,333 106,333 106,333 C103.79,333 102,331.21 102,329 C102,329 102,319 102,319 C102,316.79 103.79,315 106,315 C106,315 210,315 210,315 C212.21,315 214,316.79 214,319c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_7_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M333 341 C333,341 333,349 333,349 C333,351.21 331.21,353 329,353 C329,353 106,353 106,353 C103.79,353 102,351.21 102,349 C102,349 102,341 102,341 C102,338.79 103.79,337 106,337 C106,337 329,337 329,337 C331.21,337 333,338.79 333,341c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_8_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M238 407 C238,407 238,417 238,417 C238,419.21 236.21,421 234,421 C234,421 106,421 106,421 C103.79,421 102,419.21 102,417 C102,417 102,407 102,407 C102,404.79 103.79,403 106,403 C106,403 234,403 234,403 C236.21,403 238,404.79 238,407c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_9_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M295 429 C295,429 295,437 295,437 C295,439.21 293.21,441 291,441 C291,441 106,441 106,441 C103.79,441 102,439.21 102,437 C102,437 102,429 102,429 C102,426.79 103.79,425 106,425 C106,425 291,425 291,425 C293.21,425 295,426.79 295,429c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_10_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M168 495 C168,495 168,505 168,505 C168,507.21 166.21,509 164,509 C164,509 106,509 106,509 C103.79,509 102,507.21 102,505 C102,505 102,495 102,495 C102,492.79 103.79,491 106,491 C106,491 164,491 164,491 C166.21,491 168,492.79 168,495c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_11_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M269 517 C269,517 269,525 269,525 C269,527.21 267.21,529 265,529 C265,529 106,529 106,529 C103.79,529 102,527.21 102,525 C102,525 102,517 102,517 C102,514.79 103.79,513 106,513 C106,513 265,513 265,513 C267.21,513 269,514.79 269,517c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_12_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M235 583 C235,583 235,593 235,593 C235,595.21 233.21,597 231,597 C231,597 106,597 106,597 C103.79,597 102,595.21 102,593 C102,593 102,583 102,583 C102,580.79 103.79,579 106,579 C106,579 231,579 231,579 C233.21,579 235,580.79 235,583c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_13_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M295 605 C295,605 295,613 295,613 C295,615.21 293.21,617 291,617 C291,617 106,617 106,617 C103.79,617 102,615.21 102,613 C102,613 102,605 102,605 C102,602.79 103.79,601 106,601 C106,601 291,601 291,601 C293.21,601 295,602.79 295,605c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_14_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M223 671 C223,671 223,681 223,681 C223,683.21 221.21,685 219,685 C219,685 106,685 106,685 C103.79,685 102,683.21 102,681 C102,681 102,671 102,671 C102,668.79 103.79,667 106,667 C106,667 219,667 219,667 C221.21,667 223,668.79 223,671c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_15_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M246 693 C246,693 246,701 246,701 C246,703.21 244.21,705 242,705 C242,705 106,705 106,705 C103.79,705 102,703.21 102,701 C102,701 102,693 102,693 C102,690.79 103.79,689 106,689 C106,689 242,689 242,689 C244.21,689 246,690.79 246,693c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_16_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M388 798 C388,798 388,798 388,798 C388,813.45 375.45,826 360,826 C360,826 267,826 267,826 C251.55,826 239,813.45 239,798 C239,798 239,798 239,798 C239,782.55 251.55,770 267,770 C267,770 360,770 360,770 C375.45,770 388,782.55 388,798c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_17_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#f8f9fa"
+ android:fillType="nonZero"
+ android:pathData=" M377 47 C377,47 377,75 377,75 C377,77.21 375.21,79 373,79 C373,79 38,79 38,79 C35.79,79 34,77.21 34,75 C34,75 34,47 34,47 C34,44.79 35.79,43 38,43 C38,43 373,43 373,43 C375.21,43 377,44.79 377,47c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_18_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M82 157 C82,172.46 69.46,185 54,185 C38.54,185 26,172.46 26,157 C26,141.54 38.54,129 54,129 C69.46,129 82,141.54 82,157c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_19_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M82 245 C82,260.46 69.46,273 54,273 C38.54,273 26,260.46 26,245 C26,229.54 38.54,217 54,217 C69.46,217 82,229.54 82,245c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_20_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M82 333 C82,348.46 69.46,361 54,361 C38.54,361 26,348.46 26,333 C26,317.54 38.54,305 54,305 C69.46,305 82,317.54 82,333c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_21_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M82 421 C82,436.46 69.46,449 54,449 C38.54,449 26,436.46 26,421 C26,405.54 38.54,393 54,393 C69.46,393 82,405.54 82,421c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_22_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M82 509 C82,524.46 69.46,537 54,537 C38.54,537 26,524.46 26,509 C26,493.54 38.54,481 54,481 C69.46,481 82,493.54 82,509c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_23_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M82 597 C82,612.46 69.46,625 54,625 C38.54,625 26,612.46 26,597 C26,581.54 38.54,569 54,569 C69.46,569 82,581.54 82,597c " />
+ <path
+ android:name="_R_G_L_2_G_L_0_G_L_0_G_D_24_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M82 685 C82,700.46 69.46,713 54,713 C38.54,713 26,700.46 26,685 C26,669.54 38.54,657 54,657 C69.46,657 82,669.54 82,685c " />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G"
+ android:scaleY="0"
+ android:translateX="-17.875"
+ android:translateY="322.017">
+ <group
+ android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0"
+ android:translateX="123.282"
+ android:translateY="129.757">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#3b4043"
+ android:fillType="nonZero"
+ android:pathData=" M-109 27.43 C-109,27.43 -112.61,23.81 -112.61,23.81 C-112.61,23.81 -133.03,44.23 -133.03,44.23 C-133.03,44.23 -112.61,64.64 -112.61,64.64 C-112.61,64.64 -109,61.03 -109,61.03 C-109,61.03 -125.8,44.23 -125.8,44.23 C-125.8,44.23 -109,27.43 -109,27.43c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="@color/gesture_tutorial_primary_color"
+ android:fillType="nonZero"
+ android:pathData=" M-206.5 13.5 C-186.34,13.5 -170,29.84 -170,50 C-170,70.16 -186.34,86.5 -206.5,86.5 C-226.66,86.5 -243,70.16 -243,50 C-243,29.84 -226.66,13.5 -206.5,13.5c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_motion_home_dark_mode.xml b/quickstep/res/drawable/gesture_tutorial_motion_home_dark_mode.xml
new file mode 100644
index 0000000..aff35c1
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_motion_home_dark_mode.xml
@@ -0,0 +1,1254 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_2_G_L_4_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="50"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="650"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="50"
+ android:pathData="M 206,776C 206,776 206,776 206,776"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 206,776C 206,776 206,797 206,797"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="650">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="650"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_3_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 56,673C 56,673 56,706 56,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 156,673C 156,673 156,706 156,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 256,673C 256,673 256,706 256,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 356,673C 356,673 356,706 356,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_11_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#dadce0"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.215 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_10_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_9_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_8_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_7_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_6_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_5_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_4_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_3_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_3_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#e8eaed"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.232 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#80868b"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.674 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#80868b"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.674 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="150"
+ android:propertyName="fillColor"
+ android:startOffset="350"
+ android:valueFrom="#202124"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.69 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="150"
+ android:propertyName="fillColor"
+ android:startOffset="350"
+ android:valueFrom="#202124"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.69 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="150"
+ android:propertyName="fillColor"
+ android:startOffset="350"
+ android:valueFrom="#3c4043"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.69 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillColor"
+ android:startOffset="500"
+ android:valueFrom="#bac4d6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="50"
+ android:propertyName="fillColor"
+ android:startOffset="633"
+ android:valueFrom="#bac4d6"
+ android:valueTo="#8ab4f8"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,-0.214 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="283"
+ android:propertyName="fillAlpha"
+ android:startOffset="500"
+ android:valueFrom="1"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.999,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="283"
+ android:propertyName="pathData"
+ android:startOffset="500"
+ android:valueFrom="M206.06 -430.06 C206.06,-430.06 206,431 206,431 C206,446 189.75,446 189.79,446 C189.79,446 -189.98,446 -189.98,446 C-189.94,446 -206,446 -206,431 C-206,431 -206,-430 -206,-430 C-206,-446 -189.97,-446 -190.01,-446 C-190.01,-446 188.98,-446.06 188.98,-446.06 C188.94,-446.06 206,-446 206.06,-430.06c "
+ android:valueTo="M60 -0.06 C60,-0.06 60,0.06 60,0.06 C60,28 36,60.25 -0.02,60.25 C-0.02,60.25 0.02,60.25 0.02,60.25 C-32.5,60.25 -60,31.5 -60,0.06 C-60,0.06 -60,-0.06 -60,-0.06 C-60,-31.25 -34,-59.25 0.02,-59.25 C0.02,-59.25 -0.02,-59.25 -0.02,-59.25 C33.5,-59.25 60,-38 60,-0.06c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.999,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="500"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="850"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="283"
+ android:pathData="M 206,446C 201.417,411.133 195,385.297 195,385.297"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="217">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="333"
+ android:pathData="M 195,385.297C 195,385.297 105.5,148.09000000000003 56,691.5"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="500">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.443,0.093 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="467"
+ android:propertyName="scaleX"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.5"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="467"
+ android:propertyName="scaleY"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.5"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2167"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="233"
+ android:propertyName="fillAlpha"
+ android:startOffset="217"
+ android:valueFrom="0.75"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="450"
+ android:valueFrom="0.75"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M0 411 C19.33,411 35,426.67 35,446 C35,465.33 19.33,481 0,481 C-19.33,481 -35,465.33 -35,446 C-35,426.67 -19.33,411 0,411c "
+ android:valueTo="M0 396 C27.61,396 50,418.39 50,446 C50,473.61 27.61,496 0,496 C-27.61,496 -50,473.61 -50,446 C-50,418.39 -27.61,396 0,396c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="433"
+ android:propertyName="pathData"
+ android:startOffset="217"
+ android:valueFrom="M0 396 C27.61,396 50,418.39 50,446 C50,473.61 27.61,496 0,496 C-27.61,496 -50,473.61 -50,446 C-50,418.39 -27.61,396 0,396c "
+ android:valueTo="M0 68 C27.61,68 50,90.39 50,118 C50,145.61 27.61,168 0,168 C-27.61,168 -50,145.61 -50,118 C-50,90.39 -27.61,68 0,68c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2167"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1367"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_3_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="@color/fake_wallpaper_color_dark_mode"
+ android:fillType="nonZero"
+ android:pathData=" M206 -446 C206,-446 206,446 206,446 C206,446 -206,446 -206,446 C-206,446 -206,-446 -206,-446 C-206,-446 206,-446 206,-446c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G"
+ android:scaleY="0">
+ <group
+ android:name="_R_G_L_2_G_L_4_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="776">
+ <path
+ android:name="_R_G_L_2_G_L_4_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#3c4043"
+ android:fillType="nonZero"
+ android:pathData=" M180 0 C180,0 180,0 180,0 C180,13.8 168.8,25 155,25 C155,25 -155,25 -155,25 C-168.8,25 -180,13.8 -180,0 C-180,0 -180,0 -180,0 C-180,-13.8 -168.8,-25 -155,-25 C-155,-25 155,-25 155,-25 C168.8,-25 180,-13.8 180,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_3_G"
+ android:scaleY="0"
+ android:translateX="56"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_3_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#8ab4f8"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_2_G"
+ android:scaleY="0"
+ android:translateX="156"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_2_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#f28b82"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_1_G"
+ android:scaleY="0"
+ android:translateX="256"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_1_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#fdd663"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_0_G"
+ android:scaleY="0"
+ android:translateX="356"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#81c995"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_T_1"
+ android:scaleX="1"
+ android:scaleY="1"
+ android:translateX="206"
+ android:translateY="446">
+ <group
+ android:name="_R_G_L_1_G"
+ android:translateX="-206"
+ android:translateY="-446">
+ <group android:name="_R_G_L_1_G_L_4_G">
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_11_G"
+ android:scaleX="0.87473"
+ android:scaleY="0.98643"
+ android:translateX="206"
+ android:translateY="472.769">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M235.5 -407 C235.5,-407 235.5,407 235.5,407 C235.5,416.93 227.43,425 217.5,425 C217.5,425 -217.5,425 -217.5,425 C-227.43,425 -235.5,416.93 -235.5,407 C-235.5,407 -235.5,-407 -235.5,-407 C-235.5,-416.93 -227.43,-425 -217.5,-425 C-217.5,-425 217.5,-425 217.5,-425 C227.43,-425 235.5,-416.93 235.5,-407c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_10_G"
+ android:translateX="182.5"
+ android:translateY="831">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_9_G"
+ android:translateX="186"
+ android:translateY="801">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -3 C162,-3 162,3 162,3 C162,7.42 158.42,11 154,11 C154,11 -154,11 -154,11 C-158.42,11 -162,7.42 -162,3 C-162,3 -162,-3 -162,-3 C-162,-7.42 -158.42,-11 -154,-11 C-154,-11 154,-11 154,-11 C158.42,-11 162,-7.42 162,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_8_G"
+ android:translateX="119"
+ android:translateY="755">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M95 -3 C95,-3 95,3 95,3 C95,7.42 91.42,11 87,11 C87,11 -87,11 -87,11 C-91.42,11 -95,7.42 -95,3 C-95,3 -95,-3 -95,-3 C-95,-7.42 -91.42,-11 -87,-11 C-87,-11 87,-11 87,-11 C91.42,-11 95,-7.42 95,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_7_G"
+ android:translateX="182.5"
+ android:translateY="725">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_6_G"
+ android:translateX="197.5"
+ android:translateY="695">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M173.5 -3 C173.5,-3 173.5,3 173.5,3 C173.5,7.42 169.92,11 165.5,11 C165.5,11 -165.5,11 -165.5,11 C-169.92,11 -173.5,7.42 -173.5,3 C-173.5,3 -173.5,-3 -173.5,-3 C-173.5,-7.42 -169.92,-11 -165.5,-11 C-165.5,-11 165.5,-11 165.5,-11 C169.92,-11 173.5,-7.42 173.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_5_G"
+ android:translateX="192"
+ android:translateY="665">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M168 -3 C168,-3 168,3 168,3 C168,7.42 164.42,11 160,11 C160,11 -160,11 -160,11 C-164.42,11 -168,7.42 -168,3 C-168,3 -168,-3 -168,-3 C-168,-7.42 -164.42,-11 -160,-11 C-160,-11 160,-11 160,-11 C164.42,-11 168,-7.42 168,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_4_G"
+ android:translateX="105.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_3_G"
+ android:translateX="47.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_2_G"
+ android:translateX="142.5"
+ android:translateY="328">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M118.5 -10 C118.5,-10 118.5,10 118.5,10 C118.5,14.42 114.92,18 110.5,18 C110.5,18 -110.5,18 -110.5,18 C-114.92,18 -118.5,14.42 -118.5,10 C-118.5,10 -118.5,-10 -118.5,-10 C-118.5,-14.42 -114.92,-18 -110.5,-18 C-110.5,-18 110.5,-18 110.5,-18 C114.92,-18 118.5,-14.42 118.5,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_1_G"
+ android:translateX="186"
+ android:translateY="284">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -10 C162,-10 162,10 162,10 C162,14.42 158.42,18 154,18 C154,18 -154,18 -154,18 C-158.42,18 -162,14.42 -162,10 C-162,10 -162,-10 -162,-10 C-162,-14.42 -158.42,-18 -154,-18 C-154,-18 154,-18 154,-18 C158.42,-18 162,-14.42 162,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_0_G"
+ android:translateX="155"
+ android:translateY="240">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M131 -10 C131,-10 131,10 131,10 C131,14.42 127.42,18 123,18 C123,18 -123,18 -123,18 C-127.42,18 -131,14.42 -131,10 C-131,10 -131,-10 -131,-10 C-131,-14.42 -127.42,-18 -123,-18 C-123,-18 123,-18 123,-18 C127.42,-18 131,-14.42 131,-10c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_3_G"
+ android:translateX="24"
+ android:translateY="390">
+ <group
+ android:name="_R_G_L_1_G_L_3_G_L_0_G"
+ android:translateX="182"
+ android:translateY="120">
+ <path
+ android:name="_R_G_L_1_G_L_3_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M182 -98 C182,-98 182,98 182,98 C182,110.14 172.14,120 160,120 C160,120 -160,120 -160,120 C-172.14,120 -182,110.14 -182,98 C-182,98 -182,-98 -182,-98 C-182,-110.14 -172.14,-120 -160,-120 C-160,-120 160,-120 160,-120 C172.14,-120 182,-110.14 182,-98c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_1_G_L_2_G">
+ <group
+ android:name="_R_G_L_1_G_L_2_G_L_2_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_1_G_L_2_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -95.63 C206,-95.63 206,42.37 206,42.37 C206,43.47 205.1,44.37 204,44.37 C204,44.37 -204,44.37 -204,44.37 C-205.1,44.37 -206,43.47 -206,42.37 C-206,42.37 -206,-95.63 -206,-95.63 C-206,-96.73 -205.1,-97.63 -204,-97.63 C-204,-97.63 204,-97.63 204,-97.63 C205.1,-97.63 206,-96.73 206,-95.63c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_2_G_L_1_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_1_G_L_2_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M109 -14 C109,-14 109,14 109,14 C109,15.1 108.1,16 107,16 C107,16 -107,16 -107,16 C-108.1,16 -109,15.1 -109,14 C-109,14 -109,-14 -109,-14 C-109,-15.1 -108.1,-16 -107,-16 C-107,-16 107,-16 107,-16 C108.1,-16 109,-15.1 109,-14c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_2_G_L_0_G"
+ android:translateX="46"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_1_G_L_2_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M22 -14 C22,-14 22,14 22,14 C22,18.42 18.42,22 14,22 C14,22 -14,22 -14,22 C-18.42,22 -22,18.42 -22,14 C-22,14 -22,-14 -22,-14 C-22,-18.42 -18.42,-22 -14,-22 C-14,-22 14,-22 14,-22 C18.42,-22 22,-18.42 22,-14c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_1_G_L_1_G">
+ <group
+ android:name="_R_G_L_1_G_L_1_G_L_2_G"
+ android:translateX="206"
+ android:translateY="51">
+ <path
+ android:name="_R_G_L_1_G_L_1_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#202124"
+ android:fillType="nonZero"
+ android:pathData=" M206 -0.27 C206,-0.27 206,49.73 206,49.73 C206,49.73 -206,49.73 -206,49.73 C-206,49.73 -206,-0.27 -206,-0.27 C-206,-0.27 206,-0.27 206,-0.27c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_1_G_L_1_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_1_G_L_1_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#202124"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_1_G_L_0_G"
+ android:translateX="206"
+ android:translateY="66.5">
+ <path
+ android:name="_R_G_L_1_G_L_1_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#3c4043"
+ android:fillType="nonZero"
+ android:pathData=" M190 0 C190,0 190,0 190,0 C190,10.21 181.71,18.5 171.5,18.5 C171.5,18.5 -171.5,18.5 -171.5,18.5 C-181.71,18.5 -190,10.21 -190,0 C-190,0 -190,0 -190,0 C-190,-10.21 -181.71,-18.5 -171.5,-18.5 C-171.5,-18.5 171.5,-18.5 171.5,-18.5 C181.71,-18.5 190,-10.21 190,0c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_0_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_1_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bac4d6"
+ android:fillType="nonZero"
+ android:pathData=" M206.06 -430.06 C206.06,-430.06 206,431 206,431 C206,446 189.75,446 189.79,446 C189.79,446 -189.98,446 -189.98,446 C-189.94,446 -206,446 -206,431 C-206,431 -206,-430 -206,-430 C-206,-446 -189.97,-446 -190.01,-446 C-190.01,-446 188.98,-446.06 188.98,-446.06 C188.94,-446.06 206,-446 206.06,-430.06c " />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="@color/gesture_tutorial_primary_color"
+ android:fillType="nonZero"
+ android:pathData=" M0 411 C19.33,411 35,426.67 35,446 C35,465.33 19.33,481 0,481 C-19.33,481 -35,465.33 -35,446 C-35,426.67 -19.33,411 0,411c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_motion_home_light_mode.xml b/quickstep/res/drawable/gesture_tutorial_motion_home_light_mode.xml
new file mode 100644
index 0000000..98d97ad
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_motion_home_light_mode.xml
@@ -0,0 +1,1254 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_2_G_L_4_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="50"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="650"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="50"
+ android:pathData="M 206,776C 206,776 206,776 206,776"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 206,776C 206,776 206,797 206,797"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="650">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="650"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_3_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 56,673C 56,673 56,706 56,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 156,673C 156,673 156,706 156,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 256,673C 256,673 256,706 256,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="700"
+ android:pathData="M 356,673C 356,673 356,706 356,706"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.27,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="600"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_11_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#dadce0"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.215 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_10_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_9_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_8_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_7_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_6_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_5_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_4_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_3_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_3_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#bdc1c6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.92 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#e8eaed"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,1.232 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#80868b"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.674 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="fillColor"
+ android:startOffset="400"
+ android:valueFrom="#80868b"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.674 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G_L_2_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="150"
+ android:propertyName="fillColor"
+ android:startOffset="350"
+ android:valueFrom="#6e7175"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.674 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G_L_1_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="150"
+ android:propertyName="fillColor"
+ android:startOffset="350"
+ android:valueFrom="#6e7175"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.676 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="150"
+ android:propertyName="fillColor"
+ android:startOffset="350"
+ android:valueFrom="#9a9a9a"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.584 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="517"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="133"
+ android:propertyName="fillColor"
+ android:startOffset="500"
+ android:valueFrom="#bac4d6"
+ android:valueTo="#bac4d6"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="50"
+ android:propertyName="fillColor"
+ android:startOffset="633"
+ android:valueFrom="#bac4d6"
+ android:valueTo="#8ab4f8"
+ android:valueType="colorType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,-0.214 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="283"
+ android:propertyName="fillAlpha"
+ android:startOffset="500"
+ android:valueFrom="1"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.999,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="283"
+ android:propertyName="pathData"
+ android:startOffset="500"
+ android:valueFrom="M206.06 -430.06 C206.06,-430.06 206,431 206,431 C206,446 189.75,446 189.79,446 C189.79,446 -189.98,446 -189.98,446 C-189.94,446 -206,446 -206,431 C-206,431 -206,-430 -206,-430 C-206,-446 -189.97,-446 -190.01,-446 C-190.01,-446 188.98,-446.06 188.98,-446.06 C188.94,-446.06 206,-446 206.06,-430.06c "
+ android:valueTo="M60 -0.06 C60,-0.06 60,0.06 60,0.06 C60,28 36,60.25 -0.02,60.25 C-0.02,60.25 0.02,60.25 0.02,60.25 C-32.5,60.25 -60,31.5 -60,0.06 C-60,0.06 -60,-0.06 -60,-0.06 C-60,-31.25 -34,-59.25 0.02,-59.25 C0.02,-59.25 -0.02,-59.25 -0.02,-59.25 C33.5,-59.25 60,-38 60,-0.06c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.999,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="500"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="850"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="283"
+ android:pathData="M 206,446C 201.417,411.133 195,385.297 195,385.297"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="217">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="333"
+ android:pathData="M 195,385.297C 195,385.297 105.5,148.09000000000003 56,691.5"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="500">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.443,0.093 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="467"
+ android:propertyName="scaleX"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.5"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="467"
+ android:propertyName="scaleY"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.5"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2167"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="233"
+ android:propertyName="fillAlpha"
+ android:startOffset="217"
+ android:valueFrom="0.75"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="450"
+ android:valueFrom="0.75"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M0 411 C19.33,411 35,426.67 35,446 C35,465.33 19.33,481 0,481 C-19.33,481 -35,465.33 -35,446 C-35,426.67 -19.33,411 0,411c "
+ android:valueTo="M0 396 C27.61,396 50,418.39 50,446 C50,473.61 27.61,496 0,496 C-27.61,496 -50,473.61 -50,446 C-50,418.39 -27.61,396 0,396c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="433"
+ android:propertyName="pathData"
+ android:startOffset="217"
+ android:valueFrom="M0 396 C27.61,396 50,418.39 50,446 C50,473.61 27.61,496 0,496 C-27.61,496 -50,473.61 -50,446 C-50,418.39 -27.61,396 0,396c "
+ android:valueTo="M0 68 C27.61,68 50,90.39 50,118 C50,145.61 27.61,168 0,168 C-27.61,168 -50,145.61 -50,118 C-50,90.39 -27.61,68 0,68c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2167"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1367"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_3_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="@color/fake_wallpaper_color_light_mode"
+ android:fillType="nonZero"
+ android:pathData=" M206 -446 C206,-446 206,446 206,446 C206,446 -206,446 -206,446 C-206,446 -206,-446 -206,-446 C-206,-446 206,-446 206,-446c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G"
+ android:scaleY="0">
+ <group
+ android:name="_R_G_L_2_G_L_4_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="776">
+ <path
+ android:name="_R_G_L_2_G_L_4_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#d9d9da"
+ android:fillType="nonZero"
+ android:pathData=" M180 0 C180,0 180,0 180,0 C180,13.8 168.8,25 155,25 C155,25 -155,25 -155,25 C-168.8,25 -180,13.8 -180,0 C-180,0 -180,0 -180,0 C-180,-13.8 -168.8,-25 -155,-25 C-155,-25 155,-25 155,-25 C168.8,-25 180,-13.8 180,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_3_G"
+ android:scaleY="0"
+ android:translateX="56"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_3_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#8ab4f8"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_2_G"
+ android:scaleY="0"
+ android:translateX="156"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_2_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#f28b82"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_1_G"
+ android:scaleY="0"
+ android:translateX="256"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_1_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#fdd663"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ <group
+ android:name="_R_G_L_2_G_L_0_G"
+ android:scaleY="0"
+ android:translateX="356"
+ android:translateY="673">
+ <path
+ android:name="_R_G_L_2_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#81c995"
+ android:fillType="nonZero"
+ android:pathData=" M0 -30 C16.56,-30 30,-16.56 30,0 C30,16.56 16.56,30 0,30 C-16.56,30 -30,16.56 -30,0 C-30,-16.56 -16.56,-30 0,-30c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_T_1"
+ android:scaleX="1"
+ android:scaleY="1"
+ android:translateX="206"
+ android:translateY="446">
+ <group
+ android:name="_R_G_L_1_G"
+ android:translateX="-206"
+ android:translateY="-446">
+ <group android:name="_R_G_L_1_G_L_4_G">
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_11_G"
+ android:scaleX="0.87473"
+ android:scaleY="0.98643"
+ android:translateX="206"
+ android:translateY="472.769">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M235.5 -407 C235.5,-407 235.5,407 235.5,407 C235.5,416.93 227.43,425 217.5,425 C217.5,425 -217.5,425 -217.5,425 C-227.43,425 -235.5,416.93 -235.5,407 C-235.5,407 -235.5,-407 -235.5,-407 C-235.5,-416.93 -227.43,-425 -217.5,-425 C-217.5,-425 217.5,-425 217.5,-425 C227.43,-425 235.5,-416.93 235.5,-407c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_10_G"
+ android:translateX="182.5"
+ android:translateY="831">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_9_G"
+ android:translateX="186"
+ android:translateY="801">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -3 C162,-3 162,3 162,3 C162,7.42 158.42,11 154,11 C154,11 -154,11 -154,11 C-158.42,11 -162,7.42 -162,3 C-162,3 -162,-3 -162,-3 C-162,-7.42 -158.42,-11 -154,-11 C-154,-11 154,-11 154,-11 C158.42,-11 162,-7.42 162,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_8_G"
+ android:translateX="119"
+ android:translateY="755">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M95 -3 C95,-3 95,3 95,3 C95,7.42 91.42,11 87,11 C87,11 -87,11 -87,11 C-91.42,11 -95,7.42 -95,3 C-95,3 -95,-3 -95,-3 C-95,-7.42 -91.42,-11 -87,-11 C-87,-11 87,-11 87,-11 C91.42,-11 95,-7.42 95,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_7_G"
+ android:translateX="182.5"
+ android:translateY="725">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_6_G"
+ android:translateX="197.5"
+ android:translateY="695">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M173.5 -3 C173.5,-3 173.5,3 173.5,3 C173.5,7.42 169.92,11 165.5,11 C165.5,11 -165.5,11 -165.5,11 C-169.92,11 -173.5,7.42 -173.5,3 C-173.5,3 -173.5,-3 -173.5,-3 C-173.5,-7.42 -169.92,-11 -165.5,-11 C-165.5,-11 165.5,-11 165.5,-11 C169.92,-11 173.5,-7.42 173.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_5_G"
+ android:translateX="192"
+ android:translateY="665">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M168 -3 C168,-3 168,3 168,3 C168,7.42 164.42,11 160,11 C160,11 -160,11 -160,11 C-164.42,11 -168,7.42 -168,3 C-168,3 -168,-3 -168,-3 C-168,-7.42 -164.42,-11 -160,-11 C-160,-11 160,-11 160,-11 C164.42,-11 168,-7.42 168,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_4_G"
+ android:translateX="105.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_3_G"
+ android:translateX="47.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_2_G"
+ android:translateX="142.5"
+ android:translateY="328">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M118.5 -10 C118.5,-10 118.5,10 118.5,10 C118.5,14.42 114.92,18 110.5,18 C110.5,18 -110.5,18 -110.5,18 C-114.92,18 -118.5,14.42 -118.5,10 C-118.5,10 -118.5,-10 -118.5,-10 C-118.5,-14.42 -114.92,-18 -110.5,-18 C-110.5,-18 110.5,-18 110.5,-18 C114.92,-18 118.5,-14.42 118.5,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_1_G"
+ android:translateX="186"
+ android:translateY="284">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -10 C162,-10 162,10 162,10 C162,14.42 158.42,18 154,18 C154,18 -154,18 -154,18 C-158.42,18 -162,14.42 -162,10 C-162,10 -162,-10 -162,-10 C-162,-14.42 -158.42,-18 -154,-18 C-154,-18 154,-18 154,-18 C158.42,-18 162,-14.42 162,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_4_G_L_0_G"
+ android:translateX="155"
+ android:translateY="240">
+ <path
+ android:name="_R_G_L_1_G_L_4_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M131 -10 C131,-10 131,10 131,10 C131,14.42 127.42,18 123,18 C123,18 -123,18 -123,18 C-127.42,18 -131,14.42 -131,10 C-131,10 -131,-10 -131,-10 C-131,-14.42 -127.42,-18 -123,-18 C-123,-18 123,-18 123,-18 C127.42,-18 131,-14.42 131,-10c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_3_G"
+ android:translateX="24"
+ android:translateY="390">
+ <group
+ android:name="_R_G_L_1_G_L_3_G_L_0_G"
+ android:translateX="182"
+ android:translateY="120">
+ <path
+ android:name="_R_G_L_1_G_L_3_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M182 -98 C182,-98 182,98 182,98 C182,110.14 172.14,120 160,120 C160,120 -160,120 -160,120 C-172.14,120 -182,110.14 -182,98 C-182,98 -182,-98 -182,-98 C-182,-110.14 -172.14,-120 -160,-120 C-160,-120 160,-120 160,-120 C172.14,-120 182,-110.14 182,-98c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_1_G_L_2_G">
+ <group
+ android:name="_R_G_L_1_G_L_2_G_L_2_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_1_G_L_2_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -95.63 C206,-95.63 206,42.37 206,42.37 C206,43.47 205.1,44.37 204,44.37 C204,44.37 -204,44.37 -204,44.37 C-205.1,44.37 -206,43.47 -206,42.37 C-206,42.37 -206,-95.63 -206,-95.63 C-206,-96.73 -205.1,-97.63 -204,-97.63 C-204,-97.63 204,-97.63 204,-97.63 C205.1,-97.63 206,-96.73 206,-95.63c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_2_G_L_1_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_1_G_L_2_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M109 -14 C109,-14 109,14 109,14 C109,15.1 108.1,16 107,16 C107,16 -107,16 -107,16 C-108.1,16 -109,15.1 -109,14 C-109,14 -109,-14 -109,-14 C-109,-15.1 -108.1,-16 -107,-16 C-107,-16 107,-16 107,-16 C108.1,-16 109,-15.1 109,-14c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_2_G_L_0_G"
+ android:translateX="46"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_1_G_L_2_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M22 -14 C22,-14 22,14 22,14 C22,18.42 18.42,22 14,22 C14,22 -14,22 -14,22 C-18.42,22 -22,18.42 -22,14 C-22,14 -22,-14 -22,-14 C-22,-18.42 -18.42,-22 -14,-22 C-14,-22 14,-22 14,-22 C18.42,-22 22,-18.42 22,-14c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_1_G_L_1_G">
+ <group
+ android:name="_R_G_L_1_G_L_1_G_L_2_G"
+ android:translateX="206"
+ android:translateY="51">
+ <path
+ android:name="_R_G_L_1_G_L_1_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#6e7175"
+ android:fillType="nonZero"
+ android:pathData=" M206 -0.27 C206,-0.27 206,49.73 206,49.73 C206,49.73 -206,49.73 -206,49.73 C-206,49.73 -206,-0.27 -206,-0.27 C-206,-0.27 206,-0.27 206,-0.27c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_1_G_L_1_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_1_G_L_1_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#6e7175"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_1_G_L_0_G"
+ android:translateX="206"
+ android:translateY="66.5">
+ <path
+ android:name="_R_G_L_1_G_L_1_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9a9a9a"
+ android:fillType="nonZero"
+ android:pathData=" M190 0 C190,0 190,0 190,0 C190,10.21 181.71,18.5 171.5,18.5 C171.5,18.5 -171.5,18.5 -171.5,18.5 C-181.71,18.5 -190,10.21 -190,0 C-190,0 -190,0 -190,0 C-190,-10.21 -181.71,-18.5 -171.5,-18.5 C-171.5,-18.5 171.5,-18.5 171.5,-18.5 C181.71,-18.5 190,-10.21 190,0c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_L_0_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_1_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bac4d6"
+ android:fillType="nonZero"
+ android:pathData=" M206.06 -430.06 C206.06,-430.06 206,431 206,431 C206,446 189.75,446 189.79,446 C189.79,446 -189.98,446 -189.98,446 C-189.94,446 -206,446 -206,431 C-206,431 -206,-430 -206,-430 C-206,-446 -189.97,-446 -190.01,-446 C-190.01,-446 188.98,-446.06 188.98,-446.06 C188.94,-446.06 206,-446 206.06,-430.06c " />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#84ba69"
+ android:fillType="nonZero"
+ android:pathData=" M0 411 C19.33,411 35,426.67 35,446 C35,465.33 19.33,481 0,481 C-19.33,481 -35,465.33 -35,446 C-35,426.67 -19.33,411 0,411c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/gesture_tutorial_motion_overview_dark_mode.xml b/quickstep/res/drawable/gesture_tutorial_motion_overview_dark_mode.xml
new file mode 100644
index 0000000..b007d20
--- /dev/null
+++ b/quickstep/res/drawable/gesture_tutorial_motion_overview_dark_mode.xml
@@ -0,0 +1,1623 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_4_G_N_3_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1050"
+ android:pathData="M 206,446C 206,446 206,395 206,395"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="217">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_4_G_N_3_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1050"
+ android:propertyName="scaleX"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.6"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1050"
+ android:propertyName="scaleY"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.6"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_4_G_N_3_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="3400"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_28_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_27_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_26_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_25_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_24_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_23_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_22_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_21_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_20_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_19_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_18_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_17_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_16_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_15_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_14_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_13_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_12_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_11_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_10_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_9_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_8_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_7_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:pathData="M 206,395C 206,403.5 206,437.5 206,446"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="2083">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleX"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleX"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleY"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="0.6"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_D_0_P_0_G_0_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="scaleX"
+ android:startOffset="2567"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="scaleY"
+ android:startOffset="2567"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:pathData="M 206,395C 206,403.5 206,437.5 206,446"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="2083">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleX"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleX"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleY"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2567"
+ android:valueFrom="0"
+ android:valueTo="0.6"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="250"
+ android:pathData="M -556.176,-7.307C -556.176,-7.307 -421.176,-7.307 -421.176,-7.307"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="1350">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.272,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="417"
+ android:pathData="M -421.176,-7.307C -421.176,-7.307 -429.51,-7.307 -429.51,-7.307"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="1600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:pathData="M 206,395C 206,403.5 206,437.5 206,446"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="2083">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleX"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleX"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleY"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="1350"
+ android:valueFrom="0"
+ android:valueTo="0.6"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1833"
+ android:propertyName="fillAlpha"
+ android:startOffset="217"
+ android:valueFrom="0.75"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="2050"
+ android:valueFrom="0.75"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M0 406 C21.54,406 39,423.46 39,445 C39,466.54 21.54,484 0,484 C-21.54,484 -39,466.54 -39,445 C-39,423.46 -21.54,406 0,406c "
+ android:valueTo="M0 395 C27.61,395 50,417.39 50,445 C50,472.61 27.61,495 0,495 C-27.61,495 -50,472.61 -50,445 C-50,417.39 -27.61,395 0,395c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1050"
+ android:propertyName="pathData"
+ android:startOffset="217"
+ android:valueFrom="M0 395 C27.61,395 50,417.39 50,445 C50,472.61 27.61,495 0,495 C-27.61,495 -50,472.61 -50,445 C-50,417.39 -27.61,395 0,395c "
+ android:valueTo="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="783"
+ android:propertyName="pathData"
+ android:startOffset="1267"
+ android:valueFrom="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueTo="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="pathData"
+ android:startOffset="2050"
+ android:valueFrom="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueTo="M0 180 C19.88,180 36,196.12 36,216 C36,235.88 19.88,252 0,252 C-19.88,252 -36,235.88 -36,216 C-36,196.12 -19.88,180 0,180c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="2750"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_5_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="@color/fake_wallpaper_color_dark_mode"
+ android:fillType="nonZero"
+ android:pathData=" M206 -446 C206,-446 206,446 206,446 C206,446 -206,446 -206,446 C-206,446 -206,-446 -206,-446 C-206,-446 206,-446 206,-446c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_N_3_T_0"
+ android:scaleX="1"
+ android:scaleY="1"
+ android:translateX="206"
+ android:translateY="446">
+ <group
+ android:name="_R_G_L_4_G"
+ android:translateX="-206"
+ android:translateY="-446">
+ <group android:name="_R_G_L_4_G_L_0_G">
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_28_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_28_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#000000"
+ android:fillType="nonZero"
+ android:pathData=" M206 -422 C206,-422 206,422 206,422 C206,435.25 195.25,446 182,446 C182,446 -182,446 -182,446 C-195.25,446 -206,435.25 -206,422 C-206,422 -206,-422 -206,-422 C-206,-435.25 -195.25,-446 -182,-446 C-182,-446 182,-446 182,-446 C195.25,-446 206,-435.25 206,-422c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_27_G"
+ android:translateX="206"
+ android:translateY="422.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_27_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -395.5 C206,-395.5 206,395.5 206,395.5 C206,395.5 -206,395.5 -206,395.5 C-206,395.5 -206,-395.5 -206,-395.5 C-206,-395.5 206,-395.5 206,-395.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_26_G"
+ android:translateX="206"
+ android:translateY="496.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_26_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -377.5 C206,-377.5 206,377.5 206,377.5 C206,387.43 197.93,395.5 188,395.5 C188,395.5 -188,395.5 -188,395.5 C-197.93,395.5 -206,387.43 -206,377.5 C-206,377.5 -206,-377.5 -206,-377.5 C-206,-387.43 -197.93,-395.5 -188,-395.5 C-188,-395.5 188,-395.5 188,-395.5 C197.93,-395.5 206,-387.43 206,-377.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_25_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_25_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -23.5 C206,-23.5 206,50.5 206,50.5 C206,50.5 -206,50.5 -206,50.5 C-206,50.5 -206,-23.5 -206,-23.5 C-206,-23.5 206,-23.5 206,-23.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_24_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_24_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_23_G"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_23_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_22_G"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_22_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_21_G"
+ android:translateX="148.5"
+ android:translateY="148">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_21_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M46.5 -5 C46.5,-5 46.5,5 46.5,5 C46.5,7.21 44.71,9 42.5,9 C42.5,9 -42.5,9 -42.5,9 C-44.71,9 -46.5,7.21 -46.5,5 C-46.5,5 -46.5,-5 -46.5,-5 C-46.5,-7.21 -44.71,-9 -42.5,-9 C-42.5,-9 42.5,-9 42.5,-9 C44.71,-9 46.5,-7.21 46.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_20_G"
+ android:translateX="186"
+ android:translateY="169">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_20_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M84 -4 C84,-4 84,4 84,4 C84,6.21 82.21,8 80,8 C80,8 -80,8 -80,8 C-82.21,8 -84,6.21 -84,4 C-84,4 -84,-4 -84,-4 C-84,-6.21 -82.21,-8 -80,-8 C-80,-8 80,-8 80,-8 C82.21,-8 84,-6.21 84,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_19_G"
+ android:translateX="54"
+ android:translateY="245">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_19_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_18_G"
+ android:translateX="162"
+ android:translateY="236">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_18_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60 -5 C60,-5 60,5 60,5 C60,7.21 58.21,9 56,9 C56,9 -56,9 -56,9 C-58.21,9 -60,7.21 -60,5 C-60,5 -60,-5 -60,-5 C-60,-7.21 -58.21,-9 -56,-9 C-56,-9 56,-9 56,-9 C58.21,-9 60,-7.21 60,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_17_G"
+ android:translateX="171.5"
+ android:translateY="257">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_17_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M69.5 -4 C69.5,-4 69.5,4 69.5,4 C69.5,6.21 67.71,8 65.5,8 C65.5,8 -65.5,8 -65.5,8 C-67.71,8 -69.5,6.21 -69.5,4 C-69.5,4 -69.5,-4 -69.5,-4 C-69.5,-6.21 -67.71,-8 -65.5,-8 C-65.5,-8 65.5,-8 65.5,-8 C67.71,-8 69.5,-6.21 69.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_16_G"
+ android:translateX="54"
+ android:translateY="333">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_16_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_15_G"
+ android:translateX="158"
+ android:translateY="324">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_15_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M56 -5 C56,-5 56,5 56,5 C56,7.21 54.21,9 52,9 C52,9 -52,9 -52,9 C-54.21,9 -56,7.21 -56,5 C-56,5 -56,-5 -56,-5 C-56,-7.21 -54.21,-9 -52,-9 C-52,-9 52,-9 52,-9 C54.21,-9 56,-7.21 56,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_14_G"
+ android:translateX="217.5"
+ android:translateY="345">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_14_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M115.5 -4 C115.5,-4 115.5,4 115.5,4 C115.5,6.21 113.71,8 111.5,8 C111.5,8 -111.5,8 -111.5,8 C-113.71,8 -115.5,6.21 -115.5,4 C-115.5,4 -115.5,-4 -115.5,-4 C-115.5,-6.21 -113.71,-8 -111.5,-8 C-111.5,-8 111.5,-8 111.5,-8 C113.71,-8 115.5,-6.21 115.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_13_G"
+ android:translateX="54"
+ android:translateY="421">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_13_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_12_G"
+ android:translateX="170"
+ android:translateY="412">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_12_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M68 -5 C68,-5 68,5 68,5 C68,7.21 66.21,9 64,9 C64,9 -64,9 -64,9 C-66.21,9 -68,7.21 -68,5 C-68,5 -68,-5 -68,-5 C-68,-7.21 -66.21,-9 -64,-9 C-64,-9 64,-9 64,-9 C66.21,-9 68,-7.21 68,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_11_G"
+ android:translateX="198.5"
+ android:translateY="433">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_10_G"
+ android:translateX="54"
+ android:translateY="509">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_9_G"
+ android:translateX="135"
+ android:translateY="500">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M33 -5 C33,-5 33,5 33,5 C33,7.21 31.21,9 29,9 C29,9 -29,9 -29,9 C-31.21,9 -33,7.21 -33,5 C-33,5 -33,-5 -33,-5 C-33,-7.21 -31.21,-9 -29,-9 C-29,-9 29,-9 29,-9 C31.21,-9 33,-7.21 33,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_8_G"
+ android:translateX="185.5"
+ android:translateY="521">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M83.5 -4 C83.5,-4 83.5,4 83.5,4 C83.5,6.21 81.71,8 79.5,8 C79.5,8 -79.5,8 -79.5,8 C-81.71,8 -83.5,6.21 -83.5,4 C-83.5,4 -83.5,-4 -83.5,-4 C-83.5,-6.21 -81.71,-8 -79.5,-8 C-79.5,-8 79.5,-8 79.5,-8 C81.71,-8 83.5,-6.21 83.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_7_G"
+ android:translateX="54"
+ android:translateY="597">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_6_G"
+ android:translateX="168.5"
+ android:translateY="588">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M66.5 -5 C66.5,-5 66.5,5 66.5,5 C66.5,7.21 64.71,9 62.5,9 C62.5,9 -62.5,9 -62.5,9 C-64.71,9 -66.5,7.21 -66.5,5 C-66.5,5 -66.5,-5 -66.5,-5 C-66.5,-7.21 -64.71,-9 -62.5,-9 C-62.5,-9 62.5,-9 62.5,-9 C64.71,-9 66.5,-7.21 66.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_5_G"
+ android:translateX="198.5"
+ android:translateY="609">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_4_G"
+ android:translateX="54"
+ android:translateY="685">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_3_G"
+ android:translateX="162.5"
+ android:translateY="676">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60.5 -5 C60.5,-5 60.5,5 60.5,5 C60.5,7.21 58.71,9 56.5,9 C56.5,9 -56.5,9 -56.5,9 C-58.71,9 -60.5,7.21 -60.5,5 C-60.5,5 -60.5,-5 -60.5,-5 C-60.5,-7.21 -58.71,-9 -56.5,-9 C-56.5,-9 56.5,-9 56.5,-9 C58.71,-9 60.5,-7.21 60.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_2_G"
+ android:translateX="174"
+ android:translateY="697">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M72 -4 C72,-4 72,4 72,4 C72,6.21 70.21,8 68,8 C68,8 -68,8 -68,8 C-70.21,8 -72,6.21 -72,4 C-72,4 -72,-4 -72,-4 C-72,-6.21 -70.21,-8 -68,-8 C-68,-8 68,-8 68,-8 C70.21,-8 72,-6.21 72,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_1_G"
+ android:translateX="313.5"
+ android:translateY="798">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M74.5 0 C74.5,0 74.5,0 74.5,0 C74.5,15.45 61.95,28 46.5,28 C46.5,28 -46.5,28 -46.5,28 C-61.95,28 -74.5,15.45 -74.5,0 C-74.5,0 -74.5,0 -74.5,0 C-74.5,-15.45 -61.95,-28 -46.5,-28 C-46.5,-28 46.5,-28 46.5,-28 C61.95,-28 74.5,-15.45 74.5,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_0_G"
+ android:translateX="205.5"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#f8f9fa"
+ android:fillType="nonZero"
+ android:pathData=" M171.5 -14 C171.5,-14 171.5,14 171.5,14 C171.5,16.21 169.71,18 167.5,18 C167.5,18 -167.5,18 -167.5,18 C-169.71,18 -171.5,16.21 -171.5,14 C-171.5,14 -171.5,-14 -171.5,-14 C-171.5,-16.21 -169.71,-18 -167.5,-18 C-167.5,-18 167.5,-18 167.5,-18 C169.71,-18 171.5,-16.21 171.5,-14c " />
+ </group>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_3_G_N_2_T_0"
+ android:scaleX="0.6"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="395">
+ <group
+ android:name="_R_G_L_3_G"
+ android:translateX="-206"
+ android:translateY="-446">
+ <group
+ android:name="_R_G_L_3_G_L_0_G"
+ android:scaleY="0">
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_28_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_28_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#000000"
+ android:fillType="nonZero"
+ android:pathData=" M206 -422 C206,-422 206,422 206,422 C206,435.25 195.25,446 182,446 C182,446 -182,446 -182,446 C-195.25,446 -206,435.25 -206,422 C-206,422 -206,-422 -206,-422 C-206,-435.25 -195.25,-446 -182,-446 C-182,-446 182,-446 182,-446 C195.25,-446 206,-435.25 206,-422c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_27_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="422.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_27_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -395.5 C206,-395.5 206,395.5 206,395.5 C206,395.5 -206,395.5 -206,395.5 C-206,395.5 -206,-395.5 -206,-395.5 C-206,-395.5 206,-395.5 206,-395.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_26_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="496.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_26_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -377.5 C206,-377.5 206,377.5 206,377.5 C206,387.43 197.93,395.5 188,395.5 C188,395.5 -188,395.5 -188,395.5 C-197.93,395.5 -206,387.43 -206,377.5 C-206,377.5 -206,-377.5 -206,-377.5 C-206,-387.43 -197.93,-395.5 -188,-395.5 C-188,-395.5 188,-395.5 188,-395.5 C197.93,-395.5 206,-387.43 206,-377.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_25_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_25_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -23.5 C206,-23.5 206,50.5 206,50.5 C206,50.5 -206,50.5 -206,50.5 C-206,50.5 -206,-23.5 -206,-23.5 C-206,-23.5 206,-23.5 206,-23.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_24_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_24_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_23_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_23_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_22_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_22_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_21_G"
+ android:scaleY="0"
+ android:translateX="148.5"
+ android:translateY="148">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_21_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M46.5 -5 C46.5,-5 46.5,5 46.5,5 C46.5,7.21 44.71,9 42.5,9 C42.5,9 -42.5,9 -42.5,9 C-44.71,9 -46.5,7.21 -46.5,5 C-46.5,5 -46.5,-5 -46.5,-5 C-46.5,-7.21 -44.71,-9 -42.5,-9 C-42.5,-9 42.5,-9 42.5,-9 C44.71,-9 46.5,-7.21 46.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_20_G"
+ android:scaleY="0"
+ android:translateX="186"
+ android:translateY="169">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_20_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M84 -4 C84,-4 84,4 84,4 C84,6.21 82.21,8 80,8 C80,8 -80,8 -80,8 C-82.21,8 -84,6.21 -84,4 C-84,4 -84,-4 -84,-4 C-84,-6.21 -82.21,-8 -80,-8 C-80,-8 80,-8 80,-8 C82.21,-8 84,-6.21 84,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_19_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="245">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_19_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_18_G"
+ android:scaleY="0"
+ android:translateX="162"
+ android:translateY="236">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_18_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60 -5 C60,-5 60,5 60,5 C60,7.21 58.21,9 56,9 C56,9 -56,9 -56,9 C-58.21,9 -60,7.21 -60,5 C-60,5 -60,-5 -60,-5 C-60,-7.21 -58.21,-9 -56,-9 C-56,-9 56,-9 56,-9 C58.21,-9 60,-7.21 60,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_17_G"
+ android:scaleY="0"
+ android:translateX="171.5"
+ android:translateY="257">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_17_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M69.5 -4 C69.5,-4 69.5,4 69.5,4 C69.5,6.21 67.71,8 65.5,8 C65.5,8 -65.5,8 -65.5,8 C-67.71,8 -69.5,6.21 -69.5,4 C-69.5,4 -69.5,-4 -69.5,-4 C-69.5,-6.21 -67.71,-8 -65.5,-8 C-65.5,-8 65.5,-8 65.5,-8 C67.71,-8 69.5,-6.21 69.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_16_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="333">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_16_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_15_G"
+ android:scaleY="0"
+ android:translateX="158"
+ android:translateY="324">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_15_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M56 -5 C56,-5 56,5 56,5 C56,7.21 54.21,9 52,9 C52,9 -52,9 -52,9 C-54.21,9 -56,7.21 -56,5 C-56,5 -56,-5 -56,-5 C-56,-7.21 -54.21,-9 -52,-9 C-52,-9 52,-9 52,-9 C54.21,-9 56,-7.21 56,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_14_G"
+ android:scaleY="0"
+ android:translateX="217.5"
+ android:translateY="345">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_14_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M115.5 -4 C115.5,-4 115.5,4 115.5,4 C115.5,6.21 113.71,8 111.5,8 C111.5,8 -111.5,8 -111.5,8 C-113.71,8 -115.5,6.21 -115.5,4 C-115.5,4 -115.5,-4 -115.5,-4 C-115.5,-6.21 -113.71,-8 -111.5,-8 C-111.5,-8 111.5,-8 111.5,-8 C113.71,-8 115.5,-6.21 115.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_13_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="421">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_13_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_12_G"
+ android:scaleY="0"
+ android:translateX="170"
+ android:translateY="412">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_12_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M68 -5 C68,-5 68,5 68,5 C68,7.21 66.21,9 64,9 C64,9 -64,9 -64,9 C-66.21,9 -68,7.21 -68,5 C-68,5 -68,-5 -68,-5 C-68,-7.21 -66.21,-9 -64,-9 C-64,-9 64,-9 64,-9 C66.21,-9 68,-7.21 68,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_11_G"
+ android:scaleY="0"
+ android:translateX="198.5"
+ android:translateY="433">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_10_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="509">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_9_G"
+ android:scaleY="0"
+ android:translateX="135"
+ android:translateY="500">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M33 -5 C33,-5 33,5 33,5 C33,7.21 31.21,9 29,9 C29,9 -29,9 -29,9 C-31.21,9 -33,7.21 -33,5 C-33,5 -33,-5 -33,-5 C-33,-7.21 -31.21,-9 -29,-9 C-29,-9 29,-9 29,-9 C31.21,-9 33,-7.21 33,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_8_G"
+ android:scaleY="0"
+ android:translateX="185.5"
+ android:translateY="521">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M83.5 -4 C83.5,-4 83.5,4 83.5,4 C83.5,6.21 81.71,8 79.5,8 C79.5,8 -79.5,8 -79.5,8 C-81.71,8 -83.5,6.21 -83.5,4 C-83.5,4 -83.5,-4 -83.5,-4 C-83.5,-6.21 -81.71,-8 -79.5,-8 C-79.5,-8 79.5,-8 79.5,-8 C81.71,-8 83.5,-6.21 83.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_7_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="597">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_6_G"
+ android:scaleY="0"
+ android:translateX="168.5"
+ android:translateY="588">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M66.5 -5 C66.5,-5 66.5,5 66.5,5 C66.5,7.21 64.71,9 62.5,9 C62.5,9 -62.5,9 -62.5,9 C-64.71,9 -66.5,7.21 -66.5,5 C-66.5,5 -66.5,-5 -66.5,-5 C-66.5,-7.21 -64.71,-9 -62.5,-9 C-62.5,-9 62.5,-9 62.5,-9 C64.71,-9 66.5,-7.21 66.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_5_G"
+ android:scaleY="0"
+ android:translateX="198.5"
+ android:translateY="609">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_4_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="685">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_3_G"
+ android:scaleY="0"
+ android:translateX="162.5"
+ android:translateY="676">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60.5 -5 C60.5,-5 60.5,5 60.5,5 C60.5,7.21 58.71,9 56.5,9 C56.5,9 -56.5,9 -56.5,9 C-58.71,9 -60.5,7.21 -60.5,5 C-60.5,5 -60.5,-5 -60.5,-5 C-60.5,-7.21 -58.71,-9 -56.5,-9 C-56.5,-9 56.5,-9 56.5,-9 C58.71,-9 60.5,-7.21 60.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_2_G"
+ android:scaleY="0"
+ android:translateX="174"
+ android:translateY="697">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M72 -4 C72,-4 72,4 72,4 C72,6.21 70.21,8 68,8 C68,8 -68,8 -68,8 C-70.21,8 -72,6.21 -72,4 C-72,4 -72,-4 -72,-4 C-72,-6.21 -70.21,-8 -68,-8 C-68,-8 68,-8 68,-8 C70.21,-8 72,-6.21 72,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_1_G"
+ android:scaleY="0"
+ android:translateX="313.5"
+ android:translateY="798">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M74.5 0 C74.5,0 74.5,0 74.5,0 C74.5,15.45 61.95,28 46.5,28 C46.5,28 -46.5,28 -46.5,28 C-61.95,28 -74.5,15.45 -74.5,0 C-74.5,0 -74.5,0 -74.5,0 C-74.5,-15.45 -61.95,-28 -46.5,-28 C-46.5,-28 46.5,-28 46.5,-28 C61.95,-28 74.5,-15.45 74.5,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_0_G"
+ android:scaleY="0"
+ android:translateX="205.5"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#f8f9fa"
+ android:fillType="nonZero"
+ android:pathData=" M171.5 -14 C171.5,-14 171.5,14 171.5,14 C171.5,16.21 169.71,18 167.5,18 C167.5,18 -167.5,18 -167.5,18 C-169.71,18 -171.5,16.21 -171.5,14 C-171.5,14 -171.5,-14 -171.5,-14 C-171.5,-16.21 -169.71,-18 -167.5,-18 C-167.5,-18 167.5,-18 167.5,-18 C169.71,-18 171.5,-16.21 171.5,-14c " />
+ </group>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_2_G_N_2_T_0"
+ android:scaleX="0.6"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="395">
+ <group
+ android:name="_R_G_L_2_G"
+ android:scaleX="1.3767699999999998"
+ android:scaleY="1.3767699999999998"
+ android:translateY="-508.163">
+ <group
+ android:name="_R_G_L_2_G_D_0_P_0_G_0_T_0"
+ android:scaleX="0"
+ android:scaleY="0">
+ <path
+ android:name="_R_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M0 25 C13.81,25 25,13.81 25,0 C25,-13.81 13.81,-25 0,-25 C-13.81,-25 -25,-13.81 -25,0 C-25,13.81 -13.81,25 0,25c " />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_N_2_T_0"
+ android:scaleX="0.6"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="395">
+ <group
+ android:name="_R_G_L_1_G"
+ android:scaleX="1.39"
+ android:scaleY="1.39"
+ android:translateX="-556.176"
+ android:translateY="-7.307">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="@color/gesture_tutorial_fake_previous_task_view_color"
+ android:fillType="nonZero"
+ android:pathData=" M135 -301 C135,-301 135,311 135,311 C135,319.28 128.28,326 120,326 C120,326 -120,326 -120,326 C-128.28,326 -135,319.28 -135,311 C-135,311 -135,-301 -135,-301 C-135,-309.28 -128.28,-316 -120,-316 C-120,-316 120,-316 120,-316 C128.28,-316 135,-309.28 135,-301c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="@color/gesture_tutorial_primary_color"
+ android:fillType="nonZero"
+ android:pathData=" M0 406 C21.54,406 39,423.46 39,445 C39,466.54 21.54,484 0,484 C-21.54,484 -39,466.54 -39,445 C-39,423.46 -21.54,406 0,406c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml b/quickstep/res/drawable/hotseat_edu_notification_icon.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml
rename to quickstep/res/drawable/hotseat_edu_notification_icon.xml
diff --git a/quickstep/res/drawable/ic_all_set.xml b/quickstep/res/drawable/ic_all_set.xml
new file mode 100644
index 0000000..f718b8b
--- /dev/null
+++ b/quickstep/res/drawable/ic_all_set.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/quickstep/res/drawable/ic_ime_switcher.xml b/quickstep/res/drawable/ic_ime_switcher.xml
new file mode 100644
index 0000000..a86d390
--- /dev/null
+++ b/quickstep/res/drawable/ic_ime_switcher.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M19,7h2v2h-2V7zM15,7h2v2h-2V7zM3,7h2v2H3V7zM7,7h2v2H7V7zM11,7h2v2h-2V7zM19,11h2v2h-2V11zM15,11h2v2h-2V11zM3,11h2v2H3V11zM7,11h2v2H7V11zM11,11h2v2h-2V11zM7,15h10v2H7V15z"
+ android:fillColor="@android:color/white" />
+</vector>
diff --git a/quickstep/res/drawable/ic_sysbar_back.xml b/quickstep/res/drawable/ic_sysbar_back.xml
new file mode 100644
index 0000000..1eea677
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_back.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="28dp"
+ android:height="28dp"
+ android:autoMirrored="true"
+ android:viewportWidth="28"
+ android:viewportHeight="28">
+
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M6.49,14.86c-0.66-0.39-0.66-1.34,0-1.73l6.02-3.53l5.89-3.46C19.11,5.73,20,6.26,20,7.1V14v6.9 c0,0.84-0.89,1.37-1.6,0.95l-5.89-3.46L6.49,14.86z" />
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_home.xml b/quickstep/res/drawable/ic_sysbar_home.xml
new file mode 100644
index 0000000..b4b397b
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_home.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28"
+ android:viewportHeight="28">
+
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M 14 7 C 17.8659932488 7 21 10.1340067512 21 14 C 21 17.8659932488 17.8659932488 21 14 21 C 10.1340067512 21 7 17.8659932488 7 14 C 7 10.1340067512 10.1340067512 7 14 7 Z" />
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_recent.xml b/quickstep/res/drawable/ic_sysbar_recent.xml
new file mode 100644
index 0000000..f8c4778
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_recent.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28"
+ android:viewportHeight="28">
+
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19.9,21.5H8.1c-0.88,0-1.6-0.72-1.6-1.6V8.1c0-0.88,0.72-1.6,1.6-1.6h11.8c0.88,0,1.6,0.72,1.6,1.6v11.8 C21.5,20.78,20.78,21.5,19.9,21.5z" />
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/mock_conversation.xml b/quickstep/res/drawable/mock_conversation.xml
new file mode 100644
index 0000000..272d9ed
--- /dev/null
+++ b/quickstep/res/drawable/mock_conversation.xml
@@ -0,0 +1,212 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -446 C206,-446 206,446 206,446 C206,446 -206,446 -206,446 C-206,446 -206,-446 -206,-446 C-206,-446 206,-446 206,-446c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:pivotX="206"
+ android:pivotY="446"
+ android:scaleX="1"
+ android:scaleY="1">
+ <group android:name="_R_G_L_0_G_L_0_G">
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_14_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_14_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#000000"
+ android:fillType="nonZero"
+ android:pathData=" M206 -422 C206,-422 206,422 206,422 C206,435.25 195.25,446 182,446 C182,446 -182,446 -182,446 C-195.25,446 -206,435.25 -206,422 C-206,422 -206,-422 -206,-422 C-206,-435.25 -195.25,-446 -182,-446 C-182,-446 182,-446 182,-446 C195.25,-446 206,-435.25 206,-422c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_13_G"
+ android:translateX="206"
+ android:translateY="496.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_13_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#f1f3f4"
+ android:fillType="nonZero"
+ android:pathData=" M206 -395.5 C206,-395.5 206,395.5 206,395.5 C206,395.5 -206,395.5 -206,395.5 C-206,395.5 -206,-395.5 -206,-395.5 C-206,-395.5 206,-395.5 206,-395.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_12_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_12_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -50.5 C206,-50.5 206,50.5 206,50.5 C206,50.5 -206,50.5 -206,50.5 C-206,50.5 -206,-50.5 -206,-50.5 C-206,-50.5 206,-50.5 206,-50.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_11_G"
+ android:translateX="206"
+ android:translateY="804">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M177 0 C177,12.15 167.15,22 155,22 C155,22 -155,22 -155,22 C-167.15,22 -177,12.15 -177,0 C-177,-12.15 -167.15,-22 -155,-22 C-155,-22 155,-22 155,-22 C167.15,-22 177,-12.15 177,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_10_G"
+ android:translateX="117.5"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M83.5 -14 C83.5,-14 83.5,14 83.5,14 C83.5,16.21 81.71,18 79.5,18 C79.5,18 -79.5,18 -79.5,18 C-81.71,18 -83.5,16.21 -83.5,14 C-83.5,14 -83.5,-14 -83.5,-14 C-83.5,-16.21 -81.71,-18 -79.5,-18 C-79.5,-18 79.5,-18 79.5,-18 C81.71,-18 83.5,-16.21 83.5,-14c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_9_G"
+ android:translateX="370"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M18 -14 C18,-14 18,14 18,14 C18,16.21 16.21,18 14,18 C14,18 -14,18 -14,18 C-16.21,18 -18,16.21 -18,14 C-18,14 -18,-14 -18,-14 C-18,-16.21 -16.21,-18 -14,-18 C-14,-18 14,-18 14,-18 C16.21,-18 18,-16.21 18,-14c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_8_G"
+ android:translateX="318"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M18 -14 C18,-14 18,14 18,14 C18,16.21 16.21,18 14,18 C14,18 -14,18 -14,18 C-16.21,18 -18,16.21 -18,14 C-18,14 -18,-14 -18,-14 C-18,-16.21 -16.21,-18 -14,-18 C-14,-18 14,-18 14,-18 C16.21,-18 18,-16.21 18,-14c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_7_G"
+ android:translateX="48"
+ android:translateY="618">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M22 0 C22,12.15 12.15,22 0,22 C-12.15,22 -22,12.15 -22,0 C-22,-12.15 -12.15,-22 0,-22 C12.15,-22 22,-12.15 22,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_6_G"
+ android:translateX="48"
+ android:translateY="396">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M22 0 C22,12.15 12.15,22 0,22 C-12.15,22 -22,12.15 -22,0 C-22,-12.15 -12.15,-22 0,-22 C12.15,-22 22,-12.15 22,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_5_G"
+ android:translateX="259"
+ android:translateY="286">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M135 -38 C135,-38 135,38 135,38 C135,47.94 126.94,56 117,56 C117,56 -117,56 -117,56 C-126.94,56 -135,47.94 -135,38 C-135,38 -135,-38 -135,-38 C-135,-47.94 -126.94,-56 -117,-56 C-117,-56 117,-56 117,-56 C126.94,-56 135,-47.94 135,-38c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_4_G"
+ android:translateX="259"
+ android:translateY="468">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M134.5 4 C134.5,4 134.5,14 134.5,14 C134.5,16.21 132.71,18 130.5,18 C130.5,18 44.5,18 44.5,18 C42.29,18 40.5,16.21 40.5,14 C40.5,14 40.5,4 40.5,4 C40.5,1.79 42.29,0 44.5,0 C44.5,0 130.5,0 130.5,0 C132.71,0 134.5,1.79 134.5,4c M135 0 C135,9.66 127.17,17.5 117.5,17.5 C117.5,17.5 31,17.5 31,17.5 C21.34,17.5 13.5,9.66 13.5,0 C13.5,-9.66 21.34,-17.5 31,-17.5 C31,-17.5 117.5,-17.5 117.5,-17.5 C127.17,-17.5 135,-9.66 135,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_3_G"
+ android:translateX="259"
+ android:translateY="526.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M135 -32.5 C135,-32.5 135,20.5 135,20.5 C135,22.71 133.21,24.5 131,24.5 C131,24.5 -95,24.5 -95,24.5 C-97.21,24.5 -99,22.71 -99,20.5 C-99,20.5 -99,-32.5 -99,-32.5 C-99,-34.71 -97.21,-36.5 -95,-36.5 C-95,-36.5 131,-36.5 131,-36.5 C133.21,-36.5 135,-34.71 135,-32.5c M135 -18.5 C135,-18.5 135,18.5 135,18.5 C135,28.44 126.94,36.5 117,36.5 C117,36.5 -117,36.5 -117,36.5 C-126.94,36.5 -135,28.44 -135,18.5 C-135,18.5 -135,-18.5 -135,-18.5 C-135,-28.44 -126.94,-36.5 -117,-36.5 C-117,-36.5 117,-36.5 117,-36.5 C126.94,-36.5 135,-28.44 135,-18.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_2_G"
+ android:translateX="259"
+ android:translateY="708.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M135 -18.5 C135,-18.5 135,18.5 135,18.5 C135,28.44 126.94,36.5 117,36.5 C117,36.5 -117,36.5 -117,36.5 C-126.94,36.5 -135,28.44 -135,18.5 C-135,18.5 -135,-18.5 -135,-18.5 C-135,-28.44 -126.94,-36.5 -117,-36.5 C-117,-36.5 117,-36.5 117,-36.5 C126.94,-36.5 135,-28.44 135,-18.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_1_G"
+ android:translateX="222"
+ android:translateY="617">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M45.5 0 C45.5,9.66 37.67,17.5 28,17.5 C28,17.5 -117.5,17.5 -117.5,17.5 C-127.16,17.5 -135,9.66 -135,0 C-135,-9.66 -127.16,-17.5 -117.5,-17.5 C-117.5,-17.5 28,-17.5 28,-17.5 C37.67,-17.5 45.5,-9.66 45.5,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_0_G"
+ android:translateX="222"
+ android:translateY="395.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M77 0 C77,9.66 69.16,17.5 59.5,17.5 C59.5,17.5 -117.5,17.5 -117.5,17.5 C-127.16,17.5 -135,9.66 -135,0 C-135,-9.66 -127.16,-17.5 -117.5,-17.5 C-117.5,-17.5 59.5,-17.5 59.5,-17.5 C69.16,-17.5 77,-9.66 77,0c " />
+ </group>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/mock_conversations_list.xml b/quickstep/res/drawable/mock_conversations_list.xml
new file mode 100644
index 0000000..2dbc88f
--- /dev/null
+++ b/quickstep/res/drawable/mock_conversations_list.xml
@@ -0,0 +1,361 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_1_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -446 C206,-446 206,446 206,446 C206,446 -206,446 -206,446 C-206,446 -206,-446 -206,-446 C-206,-446 206,-446 206,-446c " />
+ </group>
+ <group android:name="_R_G_L_0_G">
+ <group android:name="_R_G_L_0_G_L_0_G">
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_28_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_28_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#000000"
+ android:fillType="nonZero"
+ android:pathData=" M206 -422 C206,-422 206,422 206,422 C206,435.25 195.25,446 182,446 C182,446 -182,446 -182,446 C-195.25,446 -206,435.25 -206,422 C-206,422 -206,-422 -206,-422 C-206,-435.25 -195.25,-446 -182,-446 C-182,-446 182,-446 182,-446 C195.25,-446 206,-435.25 206,-422c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_27_G"
+ android:translateX="206"
+ android:translateY="422.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_27_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -395.5 C206,-395.5 206,395.5 206,395.5 C206,395.5 -206,395.5 -206,395.5 C-206,395.5 -206,-395.5 -206,-395.5 C-206,-395.5 206,-395.5 206,-395.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_26_G"
+ android:translateX="206"
+ android:translateY="496.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_26_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -377.5 C206,-377.5 206,377.5 206,377.5 C206,387.43 197.93,395.5 188,395.5 C188,395.5 -188,395.5 -188,395.5 C-197.93,395.5 -206,387.43 -206,377.5 C-206,377.5 -206,-377.5 -206,-377.5 C-206,-387.43 -197.93,-395.5 -188,-395.5 C-188,-395.5 188,-395.5 188,-395.5 C197.93,-395.5 206,-387.43 206,-377.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_25_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_25_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -23.5 C206,-23.5 206,50.5 206,50.5 C206,50.5 -206,50.5 -206,50.5 C-206,50.5 -206,-23.5 -206,-23.5 C-206,-23.5 206,-23.5 206,-23.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_24_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_24_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_23_G"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_23_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_22_G"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_22_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_21_G"
+ android:translateX="148.5"
+ android:translateY="148">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_21_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M46.5 -5 C46.5,-5 46.5,5 46.5,5 C46.5,7.21 44.71,9 42.5,9 C42.5,9 -42.5,9 -42.5,9 C-44.71,9 -46.5,7.21 -46.5,5 C-46.5,5 -46.5,-5 -46.5,-5 C-46.5,-7.21 -44.71,-9 -42.5,-9 C-42.5,-9 42.5,-9 42.5,-9 C44.71,-9 46.5,-7.21 46.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_20_G"
+ android:translateX="186"
+ android:translateY="169">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_20_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M84 -4 C84,-4 84,4 84,4 C84,6.21 82.21,8 80,8 C80,8 -80,8 -80,8 C-82.21,8 -84,6.21 -84,4 C-84,4 -84,-4 -84,-4 C-84,-6.21 -82.21,-8 -80,-8 C-80,-8 80,-8 80,-8 C82.21,-8 84,-6.21 84,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_19_G"
+ android:translateX="54"
+ android:translateY="245">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_19_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_18_G"
+ android:translateX="162"
+ android:translateY="236">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_18_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60 -5 C60,-5 60,5 60,5 C60,7.21 58.21,9 56,9 C56,9 -56,9 -56,9 C-58.21,9 -60,7.21 -60,5 C-60,5 -60,-5 -60,-5 C-60,-7.21 -58.21,-9 -56,-9 C-56,-9 56,-9 56,-9 C58.21,-9 60,-7.21 60,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_17_G"
+ android:translateX="171.5"
+ android:translateY="257">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_17_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M69.5 -4 C69.5,-4 69.5,4 69.5,4 C69.5,6.21 67.71,8 65.5,8 C65.5,8 -65.5,8 -65.5,8 C-67.71,8 -69.5,6.21 -69.5,4 C-69.5,4 -69.5,-4 -69.5,-4 C-69.5,-6.21 -67.71,-8 -65.5,-8 C-65.5,-8 65.5,-8 65.5,-8 C67.71,-8 69.5,-6.21 69.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_16_G"
+ android:translateX="54"
+ android:translateY="333">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_16_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_15_G"
+ android:translateX="158"
+ android:translateY="324">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_15_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M56 -5 C56,-5 56,5 56,5 C56,7.21 54.21,9 52,9 C52,9 -52,9 -52,9 C-54.21,9 -56,7.21 -56,5 C-56,5 -56,-5 -56,-5 C-56,-7.21 -54.21,-9 -52,-9 C-52,-9 52,-9 52,-9 C54.21,-9 56,-7.21 56,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_14_G"
+ android:translateX="217.5"
+ android:translateY="345">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_14_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M115.5 -4 C115.5,-4 115.5,4 115.5,4 C115.5,6.21 113.71,8 111.5,8 C111.5,8 -111.5,8 -111.5,8 C-113.71,8 -115.5,6.21 -115.5,4 C-115.5,4 -115.5,-4 -115.5,-4 C-115.5,-6.21 -113.71,-8 -111.5,-8 C-111.5,-8 111.5,-8 111.5,-8 C113.71,-8 115.5,-6.21 115.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_13_G"
+ android:translateX="54"
+ android:translateY="421">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_13_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_12_G"
+ android:translateX="170"
+ android:translateY="412">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_12_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M68 -5 C68,-5 68,5 68,5 C68,7.21 66.21,9 64,9 C64,9 -64,9 -64,9 C-66.21,9 -68,7.21 -68,5 C-68,5 -68,-5 -68,-5 C-68,-7.21 -66.21,-9 -64,-9 C-64,-9 64,-9 64,-9 C66.21,-9 68,-7.21 68,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_11_G"
+ android:translateX="198.5"
+ android:translateY="433">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_10_G"
+ android:translateX="54"
+ android:translateY="509">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_9_G"
+ android:translateX="135"
+ android:translateY="500">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M33 -5 C33,-5 33,5 33,5 C33,7.21 31.21,9 29,9 C29,9 -29,9 -29,9 C-31.21,9 -33,7.21 -33,5 C-33,5 -33,-5 -33,-5 C-33,-7.21 -31.21,-9 -29,-9 C-29,-9 29,-9 29,-9 C31.21,-9 33,-7.21 33,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_8_G"
+ android:translateX="185.5"
+ android:translateY="521">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M83.5 -4 C83.5,-4 83.5,4 83.5,4 C83.5,6.21 81.71,8 79.5,8 C79.5,8 -79.5,8 -79.5,8 C-81.71,8 -83.5,6.21 -83.5,4 C-83.5,4 -83.5,-4 -83.5,-4 C-83.5,-6.21 -81.71,-8 -79.5,-8 C-79.5,-8 79.5,-8 79.5,-8 C81.71,-8 83.5,-6.21 83.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_7_G"
+ android:translateX="54"
+ android:translateY="597">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_6_G"
+ android:translateX="168.5"
+ android:translateY="588">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M66.5 -5 C66.5,-5 66.5,5 66.5,5 C66.5,7.21 64.71,9 62.5,9 C62.5,9 -62.5,9 -62.5,9 C-64.71,9 -66.5,7.21 -66.5,5 C-66.5,5 -66.5,-5 -66.5,-5 C-66.5,-7.21 -64.71,-9 -62.5,-9 C-62.5,-9 62.5,-9 62.5,-9 C64.71,-9 66.5,-7.21 66.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_5_G"
+ android:translateX="198.5"
+ android:translateY="609">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_4_G"
+ android:translateX="54"
+ android:translateY="685">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_3_G"
+ android:translateX="162.5"
+ android:translateY="676">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60.5 -5 C60.5,-5 60.5,5 60.5,5 C60.5,7.21 58.71,9 56.5,9 C56.5,9 -56.5,9 -56.5,9 C-58.71,9 -60.5,7.21 -60.5,5 C-60.5,5 -60.5,-5 -60.5,-5 C-60.5,-7.21 -58.71,-9 -56.5,-9 C-56.5,-9 56.5,-9 56.5,-9 C58.71,-9 60.5,-7.21 60.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_2_G"
+ android:translateX="174"
+ android:translateY="697">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M72 -4 C72,-4 72,4 72,4 C72,6.21 70.21,8 68,8 C68,8 -68,8 -68,8 C-70.21,8 -72,6.21 -72,4 C-72,4 -72,-4 -72,-4 C-72,-6.21 -70.21,-8 -68,-8 C-68,-8 68,-8 68,-8 C70.21,-8 72,-6.21 72,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_1_G"
+ android:translateX="313.5"
+ android:translateY="798">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M74.5 0 C74.5,0 74.5,0 74.5,0 C74.5,15.45 61.95,28 46.5,28 C46.5,28 -46.5,28 -46.5,28 C-61.95,28 -74.5,15.45 -74.5,0 C-74.5,0 -74.5,0 -74.5,0 C-74.5,-15.45 -61.95,-28 -46.5,-28 C-46.5,-28 46.5,-28 46.5,-28 C61.95,-28 74.5,-15.45 74.5,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_0_G"
+ android:translateX="205.5"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#f8f9fa"
+ android:fillType="nonZero"
+ android:pathData=" M171.5 -14 C171.5,-14 171.5,14 171.5,14 C171.5,16.21 169.71,18 167.5,18 C167.5,18 -167.5,18 -167.5,18 C-169.71,18 -171.5,16.21 -171.5,14 C-171.5,14 -171.5,-14 -171.5,-14 C-171.5,-16.21 -169.71,-18 -167.5,-18 C-167.5,-18 167.5,-18 167.5,-18 C169.71,-18 171.5,-16.21 171.5,-14c " />
+ </group>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/mock_webpage_dark_mode.xml b/quickstep/res/drawable/mock_webpage_dark_mode.xml
new file mode 100644
index 0000000..93b22b7
--- /dev/null
+++ b/quickstep/res/drawable/mock_webpage_dark_mode.xml
@@ -0,0 +1,251 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_0_G">
+ <group android:name="_R_G_L_0_G_L_3_G">
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_11_G"
+ android:scaleX="0.87473"
+ android:scaleY="0.98643"
+ android:translateX="206"
+ android:translateY="472.769">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M235.5 -407 C235.5,-407 235.5,407 235.5,407 C235.5,416.93 227.43,425 217.5,425 C217.5,425 -217.5,425 -217.5,425 C-227.43,425 -235.5,416.93 -235.5,407 C-235.5,407 -235.5,-407 -235.5,-407 C-235.5,-416.93 -227.43,-425 -217.5,-425 C-217.5,-425 217.5,-425 217.5,-425 C227.43,-425 235.5,-416.93 235.5,-407c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_10_G"
+ android:translateX="182.5"
+ android:translateY="831">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_9_G"
+ android:translateX="186"
+ android:translateY="801">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -3 C162,-3 162,3 162,3 C162,7.42 158.42,11 154,11 C154,11 -154,11 -154,11 C-158.42,11 -162,7.42 -162,3 C-162,3 -162,-3 -162,-3 C-162,-7.42 -158.42,-11 -154,-11 C-154,-11 154,-11 154,-11 C158.42,-11 162,-7.42 162,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_8_G"
+ android:translateX="119"
+ android:translateY="755">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M95 -3 C95,-3 95,3 95,3 C95,7.42 91.42,11 87,11 C87,11 -87,11 -87,11 C-91.42,11 -95,7.42 -95,3 C-95,3 -95,-3 -95,-3 C-95,-7.42 -91.42,-11 -87,-11 C-87,-11 87,-11 87,-11 C91.42,-11 95,-7.42 95,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_7_G"
+ android:translateX="182.5"
+ android:translateY="725">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_6_G"
+ android:translateX="197.5"
+ android:translateY="695">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M173.5 -3 C173.5,-3 173.5,3 173.5,3 C173.5,7.42 169.92,11 165.5,11 C165.5,11 -165.5,11 -165.5,11 C-169.92,11 -173.5,7.42 -173.5,3 C-173.5,3 -173.5,-3 -173.5,-3 C-173.5,-7.42 -169.92,-11 -165.5,-11 C-165.5,-11 165.5,-11 165.5,-11 C169.92,-11 173.5,-7.42 173.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_5_G"
+ android:translateX="192"
+ android:translateY="665">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M168 -3 C168,-3 168,3 168,3 C168,7.42 164.42,11 160,11 C160,11 -160,11 -160,11 C-164.42,11 -168,7.42 -168,3 C-168,3 -168,-3 -168,-3 C-168,-7.42 -164.42,-11 -160,-11 C-160,-11 160,-11 160,-11 C164.42,-11 168,-7.42 168,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_4_G"
+ android:translateX="105.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_3_G"
+ android:translateX="47.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_2_G"
+ android:translateX="142.5"
+ android:translateY="328">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M118.5 -10 C118.5,-10 118.5,10 118.5,10 C118.5,14.42 114.92,18 110.5,18 C110.5,18 -110.5,18 -110.5,18 C-114.92,18 -118.5,14.42 -118.5,10 C-118.5,10 -118.5,-10 -118.5,-10 C-118.5,-14.42 -114.92,-18 -110.5,-18 C-110.5,-18 110.5,-18 110.5,-18 C114.92,-18 118.5,-14.42 118.5,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_1_G"
+ android:translateX="186"
+ android:translateY="284">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -10 C162,-10 162,10 162,10 C162,14.42 158.42,18 154,18 C154,18 -154,18 -154,18 C-158.42,18 -162,14.42 -162,10 C-162,10 -162,-10 -162,-10 C-162,-14.42 -158.42,-18 -154,-18 C-154,-18 154,-18 154,-18 C158.42,-18 162,-14.42 162,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_0_G"
+ android:translateX="155"
+ android:translateY="240">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M131 -10 C131,-10 131,10 131,10 C131,14.42 127.42,18 123,18 C123,18 -123,18 -123,18 C-127.42,18 -131,14.42 -131,10 C-131,10 -131,-10 -131,-10 C-131,-14.42 -127.42,-18 -123,-18 C-123,-18 123,-18 123,-18 C127.42,-18 131,-14.42 131,-10c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_2_G"
+ android:translateX="24"
+ android:translateY="390">
+ <group
+ android:name="_R_G_L_0_G_L_2_G_L_0_G"
+ android:translateX="182"
+ android:translateY="120">
+ <path
+ android:name="_R_G_L_0_G_L_2_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M182 -98 C182,-98 182,98 182,98 C182,110.14 172.14,120 160,120 C160,120 -160,120 -160,120 C-172.14,120 -182,110.14 -182,98 C-182,98 -182,-98 -182,-98 C-182,-110.14 -172.14,-120 -160,-120 C-160,-120 160,-120 160,-120 C172.14,-120 182,-110.14 182,-98c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_0_G_L_1_G">
+ <group
+ android:name="_R_G_L_0_G_L_1_G_L_2_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_0_G_L_1_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -95.63 C206,-95.63 206,42.37 206,42.37 C206,43.47 205.1,44.37 204,44.37 C204,44.37 -204,44.37 -204,44.37 C-205.1,44.37 -206,43.47 -206,42.37 C-206,42.37 -206,-95.63 -206,-95.63 C-206,-96.73 -205.1,-97.63 -204,-97.63 C-204,-97.63 204,-97.63 204,-97.63 C205.1,-97.63 206,-96.73 206,-95.63c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_1_G_L_1_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_0_G_L_1_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M109 -14 C109,-14 109,14 109,14 C109,15.1 108.1,16 107,16 C107,16 -107,16 -107,16 C-108.1,16 -109,15.1 -109,14 C-109,14 -109,-14 -109,-14 C-109,-15.1 -108.1,-16 -107,-16 C-107,-16 107,-16 107,-16 C108.1,-16 109,-15.1 109,-14c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_1_G_L_0_G"
+ android:translateX="46"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_0_G_L_1_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M22 -14 C22,-14 22,14 22,14 C22,18.42 18.42,22 14,22 C14,22 -14,22 -14,22 C-18.42,22 -22,18.42 -22,14 C-22,14 -22,-14 -22,-14 C-22,-18.42 -18.42,-22 -14,-22 C-14,-22 14,-22 14,-22 C18.42,-22 22,-18.42 22,-14c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_0_G_L_0_G">
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_2_G"
+ android:translateX="206"
+ android:translateY="51">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#202124"
+ android:fillType="nonZero"
+ android:pathData=" M206 -0.27 C206,-0.27 206,49.73 206,49.73 C206,49.73 -206,49.73 -206,49.73 C-206,49.73 -206,-0.27 -206,-0.27 C-206,-0.27 206,-0.27 206,-0.27c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_1_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#202124"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G_L_0_G"
+ android:translateX="206"
+ android:translateY="66.5">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#3c4043"
+ android:fillType="nonZero"
+ android:pathData=" M190 0 C190,0 190,0 190,0 C190,10.21 181.71,18.5 171.5,18.5 C171.5,18.5 -171.5,18.5 -171.5,18.5 C-181.71,18.5 -190,10.21 -190,0 C-190,0 -190,0 -190,0 C-190,-10.21 -181.71,-18.5 -171.5,-18.5 C-171.5,-18.5 171.5,-18.5 171.5,-18.5 C181.71,-18.5 190,-10.21 190,0c " />
+ </group>
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/mock_webpage_light_mode.xml b/quickstep/res/drawable/mock_webpage_light_mode.xml
new file mode 100644
index 0000000..98abb92
--- /dev/null
+++ b/quickstep/res/drawable/mock_webpage_light_mode.xml
@@ -0,0 +1,263 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group android:name="_R_G_L_0_G">
+ <group android:name="_R_G_L_0_G_L_4_G">
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_11_G"
+ android:scaleX="0.87473"
+ android:scaleY="0.98643"
+ android:translateX="206"
+ android:translateY="472.769">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M235.5 -407 C235.5,-407 235.5,407 235.5,407 C235.5,416.93 227.43,425 217.5,425 C217.5,425 -217.5,425 -217.5,425 C-227.43,425 -235.5,416.93 -235.5,407 C-235.5,407 -235.5,-407 -235.5,-407 C-235.5,-416.93 -227.43,-425 -217.5,-425 C-217.5,-425 217.5,-425 217.5,-425 C227.43,-425 235.5,-416.93 235.5,-407c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_10_G"
+ android:translateX="182.5"
+ android:translateY="831">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_9_G"
+ android:translateX="186"
+ android:translateY="801">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -3 C162,-3 162,3 162,3 C162,7.42 158.42,11 154,11 C154,11 -154,11 -154,11 C-158.42,11 -162,7.42 -162,3 C-162,3 -162,-3 -162,-3 C-162,-7.42 -158.42,-11 -154,-11 C-154,-11 154,-11 154,-11 C158.42,-11 162,-7.42 162,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_8_G"
+ android:translateX="119"
+ android:translateY="755">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M95 -3 C95,-3 95,3 95,3 C95,7.42 91.42,11 87,11 C87,11 -87,11 -87,11 C-91.42,11 -95,7.42 -95,3 C-95,3 -95,-3 -95,-3 C-95,-7.42 -91.42,-11 -87,-11 C-87,-11 87,-11 87,-11 C91.42,-11 95,-7.42 95,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_7_G"
+ android:translateX="182.5"
+ android:translateY="725">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M158.5 -3 C158.5,-3 158.5,3 158.5,3 C158.5,7.42 154.92,11 150.5,11 C150.5,11 -150.5,11 -150.5,11 C-154.92,11 -158.5,7.42 -158.5,3 C-158.5,3 -158.5,-3 -158.5,-3 C-158.5,-7.42 -154.92,-11 -150.5,-11 C-150.5,-11 150.5,-11 150.5,-11 C154.92,-11 158.5,-7.42 158.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_6_G"
+ android:translateX="197.5"
+ android:translateY="695">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M173.5 -3 C173.5,-3 173.5,3 173.5,3 C173.5,7.42 169.92,11 165.5,11 C165.5,11 -165.5,11 -165.5,11 C-169.92,11 -173.5,7.42 -173.5,3 C-173.5,3 -173.5,-3 -173.5,-3 C-173.5,-7.42 -169.92,-11 -165.5,-11 C-165.5,-11 165.5,-11 165.5,-11 C169.92,-11 173.5,-7.42 173.5,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_5_G"
+ android:translateX="192"
+ android:translateY="665">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M168 -3 C168,-3 168,3 168,3 C168,7.42 164.42,11 160,11 C160,11 -160,11 -160,11 C-164.42,11 -168,7.42 -168,3 C-168,3 -168,-3 -168,-3 C-168,-7.42 -164.42,-11 -160,-11 C-160,-11 160,-11 160,-11 C164.42,-11 168,-7.42 168,-3c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_4_G"
+ android:translateX="105.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_3_G"
+ android:translateX="47.5"
+ android:translateY="360">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M23.5 -2 C23.5,-2 23.5,2 23.5,2 C23.5,4.21 21.71,6 19.5,6 C19.5,6 -19.5,6 -19.5,6 C-21.71,6 -23.5,4.21 -23.5,2 C-23.5,2 -23.5,-2 -23.5,-2 C-23.5,-4.21 -21.71,-6 -19.5,-6 C-19.5,-6 19.5,-6 19.5,-6 C21.71,-6 23.5,-4.21 23.5,-2c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_2_G"
+ android:translateX="142.5"
+ android:translateY="328">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M118.5 -10 C118.5,-10 118.5,10 118.5,10 C118.5,14.42 114.92,18 110.5,18 C110.5,18 -110.5,18 -110.5,18 C-114.92,18 -118.5,14.42 -118.5,10 C-118.5,10 -118.5,-10 -118.5,-10 C-118.5,-14.42 -114.92,-18 -110.5,-18 C-110.5,-18 110.5,-18 110.5,-18 C114.92,-18 118.5,-14.42 118.5,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_1_G"
+ android:translateX="186"
+ android:translateY="284">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M162 -10 C162,-10 162,10 162,10 C162,14.42 158.42,18 154,18 C154,18 -154,18 -154,18 C-158.42,18 -162,14.42 -162,10 C-162,10 -162,-10 -162,-10 C-162,-14.42 -158.42,-18 -154,-18 C-154,-18 154,-18 154,-18 C158.42,-18 162,-14.42 162,-10c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_4_G_L_0_G"
+ android:translateX="155"
+ android:translateY="240">
+ <path
+ android:name="_R_G_L_0_G_L_4_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M131 -10 C131,-10 131,10 131,10 C131,14.42 127.42,18 123,18 C123,18 -123,18 -123,18 C-127.42,18 -131,14.42 -131,10 C-131,10 -131,-10 -131,-10 C-131,-14.42 -127.42,-18 -123,-18 C-123,-18 123,-18 123,-18 C127.42,-18 131,-14.42 131,-10c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_3_G"
+ android:translateX="24"
+ android:translateY="390">
+ <group
+ android:name="_R_G_L_0_G_L_3_G_L_0_G"
+ android:translateX="182"
+ android:translateY="120">
+ <path
+ android:name="_R_G_L_0_G_L_3_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M182 -98 C182,-98 182,98 182,98 C182,110.14 172.14,120 160,120 C160,120 -160,120 -160,120 C-172.14,120 -182,110.14 -182,98 C-182,98 -182,-98 -182,-98 C-182,-110.14 -172.14,-120 -160,-120 C-160,-120 160,-120 160,-120 C172.14,-120 182,-110.14 182,-98c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_0_G_L_2_G">
+ <group
+ android:name="_R_G_L_0_G_L_2_G_L_2_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_0_G_L_2_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -95.63 C206,-95.63 206,42.37 206,42.37 C206,43.47 205.1,44.37 204,44.37 C204,44.37 -204,44.37 -204,44.37 C-205.1,44.37 -206,43.47 -206,42.37 C-206,42.37 -206,-95.63 -206,-95.63 C-206,-96.73 -205.1,-97.63 -204,-97.63 C-204,-97.63 204,-97.63 204,-97.63 C205.1,-97.63 206,-96.73 206,-95.63c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_2_G_L_1_G"
+ android:translateX="206"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_0_G_L_2_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M109 -14 C109,-14 109,14 109,14 C109,15.1 108.1,16 107,16 C107,16 -107,16 -107,16 C-108.1,16 -109,15.1 -109,14 C-109,14 -109,-14 -109,-14 C-109,-15.1 -108.1,-16 -107,-16 C-107,-16 107,-16 107,-16 C108.1,-16 109,-15.1 109,-14c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_2_G_L_0_G"
+ android:translateX="46"
+ android:translateY="145">
+ <path
+ android:name="_R_G_L_0_G_L_2_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#80868b"
+ android:fillType="nonZero"
+ android:pathData=" M22 -14 C22,-14 22,14 22,14 C22,18.42 18.42,22 14,22 C14,22 -14,22 -14,22 C-18.42,22 -22,18.42 -22,14 C-22,14 -22,-14 -22,-14 C-22,-18.42 -18.42,-22 -14,-22 C-14,-22 14,-22 14,-22 C18.42,-22 22,-18.42 22,-14c " />
+ </group>
+ </group>
+ <group android:name="_R_G_L_0_G_L_1_G">
+ <group
+ android:name="_R_G_L_0_G_L_1_G_L_2_G"
+ android:translateX="206"
+ android:translateY="51">
+ <path
+ android:name="_R_G_L_0_G_L_1_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#6e7175"
+ android:fillType="nonZero"
+ android:pathData=" M206 -0.27 C206,-0.27 206,49.73 206,49.73 C206,49.73 -206,49.73 -206,49.73 C-206,49.73 -206,-0.27 -206,-0.27 C-206,-0.27 206,-0.27 206,-0.27c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_1_G_L_1_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_0_G_L_1_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#6e7175"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_1_G_L_0_G"
+ android:translateX="206"
+ android:translateY="66.5">
+ <path
+ android:name="_R_G_L_0_G_L_1_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9a9a9a"
+ android:fillType="nonZero"
+ android:pathData=" M190 0 C190,0 190,0 190,0 C190,10.21 181.71,18.5 171.5,18.5 C171.5,18.5 -171.5,18.5 -171.5,18.5 C-181.71,18.5 -190,10.21 -190,0 C-190,0 -190,0 -190,0 C-190,-10.21 -181.71,-18.5 -171.5,-18.5 C-171.5,-18.5 171.5,-18.5 171.5,-18.5 C181.71,-18.5 190,-10.21 190,0c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G_L_0_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bac4d6"
+ android:fillType="nonZero"
+ android:pathData=" M206.06 -430.06 C206.06,-430.06 206,431 206,431 C206,446 189.75,446 189.79,446 C189.79,446 -189.98,446 -189.98,446 C-189.94,446 -206,446 -206,431 C-206,431 -206,-430 -206,-430 C-206,-446 -189.97,-446 -190.01,-446 C-190.01,-446 188.98,-446.06 188.98,-446.06 C188.94,-446.06 206,-446 206.06,-430.06c " />
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/task_menu_bg.xml b/quickstep/res/drawable/task_menu_bg.xml
index 7334d98..be1b387 100644
--- a/quickstep/res/drawable/task_menu_bg.xml
+++ b/quickstep/res/drawable/task_menu_bg.xml
@@ -14,25 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:gravity="bottom">
- <!-- Shadow -->
- <shape>
- <gradient android:angle="270"
- android:endColor="@android:color/transparent"
- android:startColor="#26000000" />
- <size android:height="@dimen/task_card_menu_shadow_height" />
- </shape>
- </item>
- <item android:bottom="@dimen/task_card_menu_shadow_height">
- <!-- Background -->
- <shape>
- <corners
- android:topLeftRadius="?android:attr/dialogCornerRadius"
- android:topRightRadius="?android:attr/dialogCornerRadius"
- android:bottomLeftRadius="0dp"
- android:bottomRightRadius="0dp" />
- <solid android:color="?attr/popupColorPrimary" />
- </shape>
- </item>
-</layer-list>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@android:color/transparent"/>
+ <corners android:radius="@dimen/task_menu_corner_radius"/>
+</shape>
diff --git a/quickstep/res/drawable/task_menu_item_bg.xml b/quickstep/res/drawable/task_menu_item_bg.xml
new file mode 100644
index 0000000..b6a8b90
--- /dev/null
+++ b/quickstep/res/drawable/task_menu_item_bg.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="?android:attr/colorPrimary"/>
+ <corners android:radius="@dimen/task_menu_item_corner_radius"/>
+</shape>
diff --git a/quickstep/res/drawable/taskbar_icon_click_feedback_roundrect.xml b/quickstep/res/drawable/taskbar_icon_click_feedback_roundrect.xml
new file mode 100644
index 0000000..d6160de
--- /dev/null
+++ b/quickstep/res/drawable/taskbar_icon_click_feedback_roundrect.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/taskbar_icon_selection_ripple">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/white" />
+ <corners android:radius="8dp" />
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/quickstep/res/drawable/tutorial_step_indicator_pill.xml b/quickstep/res/drawable/tutorial_step_indicator_pill.xml
new file mode 100644
index 0000000..d4e8f84
--- /dev/null
+++ b/quickstep/res/drawable/tutorial_step_indicator_pill.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="3dp"/>
+ <size android:width="16dp" android:height="6dp"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/layout/activity_allset.xml b/quickstep/res/layout/activity_allset.xml
new file mode 100644
index 0000000..e79e57e
--- /dev/null
+++ b/quickstep/res/layout/activity_allset.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="@dimen/allset_page_margin_horizontal"
+ android:paddingEnd="@dimen/allset_page_margin_horizontal"
+ android:layoutDirection="locale"
+ android:textDirection="locale">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_gravity="start"
+ android:gravity="start"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/allset_title_icon_margin_top"
+ android:src="@drawable/ic_all_set"/>
+
+ <TextView
+ android:id="@+id/title"
+ style="@style/TextAppearance.GestureTutorial.Feedback.Title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/allset_title_margin_top"
+ android:gravity="start"
+ android:text="@string/allset_title"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ style="@style/TextAppearance.GestureTutorial.Feedback.Subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/allset_subtitle_margin_top"
+ android:gravity="start"
+ android:text="@string/allset_description"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/navigation_settings"
+ style="@style/TextAppearance.GestureTutorial.LinkText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/hint"
+ android:gravity="center"
+ android:layout_marginBottom="72dp"
+ android:minHeight="48dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/allset_navigation_settings" />
+
+ <TextView
+ android:id="@id/hint"
+ style="@style/TextAppearance.GestureTutorial.Feedback.Subtitle"
+ android:textSize="14sp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/allset_hint_margin_bottom"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:text="@string/allset_hint"/>
+</RelativeLayout>
diff --git a/quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml b/quickstep/res/layout/all_apps_edu_view.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml
rename to quickstep/res/layout/all_apps_edu_view.xml
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index 83594e4..c4642e4 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -16,12 +16,13 @@
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:priv-android="http://schemas.android.com/apk/prv/res/android"
+ style="@style/TextTitle"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/bg_wellbeing_toast"
- android:fontFamily="sans-serif"
android:forceHasOverlappingRendering="false"
android:gravity="center"
android:importantForAccessibility="noHideDescendants"
- android:textColor="@android:color/white"
+ android:textColor="?priv-android:attr/textColorOnAccent"
android:textSize="14sp"/>
\ 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 0000000..a43296f
--- /dev/null
+++ b/quickstep/res/layout/fallback_recents_activity.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+-->
+<com.android.launcher3.LauncherRootView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <com.android.quickstep.views.SplitPlaceholderView
+ android:id="@+id/split_placeholder"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/split_placeholder_size"
+ android:background="@android:color/white"
+ android:alpha=".8"
+ android:visibility="gone" />
+
+ <com.android.quickstep.fallback.RecentsDragLayer
+ android:id="@+id/drag_layer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false">
+
+ <com.android.launcher3.views.ScrimView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/scrim_view"
+ android:background="@android:color/transparent" />
+
+ <com.android.quickstep.fallback.FallbackRecentsView
+ android:id="@id/overview_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:outlineProvider="none"
+ android:theme="@style/HomeScreenElementTheme" />
+
+ <include
+ android:id="@+id/overview_actions_view"
+ layout="@layout/overview_actions_container" />
+
+ </com.android.quickstep.fallback.RecentsDragLayer>
+</com.android.launcher3.LauncherRootView>
diff --git a/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml b/quickstep/res/layout/floating_header_content.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/layout/floating_header_content.xml
rename to quickstep/res/layout/floating_header_content.xml
diff --git a/quickstep/res/layout/gesture_tutorial_dialog.xml b/quickstep/res/layout/gesture_tutorial_dialog.xml
new file mode 100644
index 0000000..59bf7b9
--- /dev/null
+++ b/quickstep/res/layout/gesture_tutorial_dialog.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/bg_sandbox_feedback"
+ android:paddingTop="24dp"
+ android:paddingBottom="24dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp">
+
+ <TextView
+ android:id="@+id/gesture_tutorial_dialog_title"
+ style="@style/TextAppearance.GestureTutorial.Dialog.Title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/skip_tutorial_dialog_title"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <TextView
+ android:id="@+id/gesture_tutorial_dialog_subtitle"
+ style="@style/TextAppearance.GestureTutorial.Dialog.Subtitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/skip_tutorial_dialog_subtitle"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_dialog_title"/>
+
+ <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
+ of elevation and shadow) which is replaced by ripple effect in android:foreground -->
+ <Button
+ android:id="@+id/gesture_tutorial_dialog_cancel_button"
+ style="@style/TextAppearance.GestureTutorial.CancelButtonLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="46dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:background="@drawable/gesture_tutorial_cancel_button_background"
+ android:foreground="?android:attr/selectableItemBackgroundBorderless"
+ android:stateListAnimator="@null"
+ android:text="@string/gesture_tutorial_action_button_label_cancel"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_dialog_subtitle"/>
+
+ <Button
+ android:id="@+id/gesture_tutorial_dialog_confirm_button"
+ style="@style/TextAppearance.GestureTutorial.ButtonLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="46dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:background="@drawable/gesture_tutorial_action_button_background"
+ android:foreground="?android:attr/selectableItemBackgroundBorderless"
+ android:stateListAnimator="@null"
+ android:text="@string/gesture_tutorial_action_button_label_skip"
+
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_dialog_subtitle"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
index 459d65f..cdda43c 100644
--- a/quickstep/res/layout/gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -13,10 +13,47 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.quickstep.interaction.RootSandboxLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?android:attr/colorBackground">
+ android:clipChildren="false">
+
+ <RelativeLayout
+ android:id="@+id/gesture_tutorial_fake_launcher_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/gesture_tutorial_fake_hotseat_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="70dp"/>
+
+ </RelativeLayout>
+
+ <com.android.launcher3.views.ClipIconView
+ android:id="@+id/gesture_tutorial_fake_icon_view"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:visibility="invisible" />
+
+ <View
+ android:id="@+id/gesture_tutorial_fake_previous_task_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleX="0.98"
+ android:scaleY="0.98"
+ android:visibility="invisible" />
+
+ <View
+ android:id="@+id/gesture_tutorial_fake_task_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="visible" />
<View
android:id="@+id/gesture_tutorial_ripple_view"
@@ -24,98 +61,112 @@
android:layout_height="match_parent"
android:background="@drawable/gesture_tutorial_ripple"/>
- <View
- android:id="@+id/gesture_tutorial_fake_task_view"
+ <ImageView
+ android:id="@+id/gesture_tutorial_feedback_video"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/gesture_tutorial_fake_task_view_color"
- android:visibility="invisible" />
+ android:layout_alignParentTop="true"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentEnd="true"
+ android:scaleType="fitXY"
+ android:visibility="gone"/>
<ImageView
- android:id="@+id/gesture_tutorial_fragment_hand_coaching"
+ android:id="@+id/gesture_tutorial_gesture_video"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:scaleType="centerCrop"/>
-
- <ImageButton
- android:id="@+id/gesture_tutorial_fragment_close_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="18dp"
- android:layout_marginTop="30dp"
- android:layout_marginStart="4dp"
- android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
- android:background="@android:color/transparent"
- android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_titles_container"
- android:contentDescription="@string/gesture_tutorial_close_button_content_description"
- android:tint="?android:attr/textColorPrimary"
- android:src="@drawable/gesture_tutorial_close_button"/>
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentEnd="true"
+ android:scaleType="fitXY"
+ android:visibility="gone"/>
- <LinearLayout
- android:id="@+id/gesture_tutorial_fragment_titles_container"
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/gesture_tutorial_fragment_feedback_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="70dp"
android:layout_alignParentTop="true"
- android:focusable="true"
- android:gravity="center_horizontal"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/gesture_tutorial_fragment_title_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/gesture_tutorial_title_margin_start_end"
- android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"
- style="@style/TextAppearance.GestureTutorial.Title"/>
-
- <TextView
- android:id="@+id/gesture_tutorial_fragment_subtitle_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
- android:layout_marginStart="@dimen/gesture_tutorial_subtitle_margin_start_end"
- android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"
- style="@style/TextAppearance.GestureTutorial.Subtitle"/>
- </LinearLayout>
-
- <TextView
- android:id="@+id/gesture_tutorial_fragment_feedback_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
android:layout_centerHorizontal="true"
- android:layout_above="@id/gesture_tutorial_fragment_action_button"
android:layout_marginStart="@dimen/gesture_tutorial_feedback_margin_start_end"
android:layout_marginEnd="@dimen/gesture_tutorial_feedback_margin_start_end"
- style="@style/TextAppearance.GestureTutorial.Feedback"/>
+ android:layout_marginTop="24dp"
+ android:paddingTop="24dp"
+ android:paddingBottom="16dp"
+ android:background="@drawable/bg_sandbox_feedback">
- <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
- of elevation and shadow) which is replaced by ripple effect in android:foreground -->
- <Button
- android:id="@+id/gesture_tutorial_fragment_action_button"
- android:layout_width="142dp"
- android:layout_height="49dp"
- android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
- android:layout_marginBottom="48dp"
- android:layout_alignParentEnd="true"
- android:layout_alignParentBottom="true"
- android:stateListAnimator="@null"
- android:background="@drawable/gesture_tutorial_action_button_background"
- android:foreground="?android:attr/selectableItemBackgroundBorderless"
- style="@style/TextAppearance.GestureTutorial.ButtonLabel"/>
+ <TextView
+ android:id="@+id/gesture_tutorial_fragment_feedback_title"
+ style="@style/TextAppearance.GestureTutorial.Feedback.Title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
- <Button
- android:id="@+id/gesture_tutorial_fragment_action_text_button"
- android:layout_width="142dp"
- android:layout_height="49dp"
- android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
- android:layout_marginBottom="48dp"
- android:layout_alignParentStart="true"
- android:layout_alignParentBottom="true"
- android:stateListAnimator="@null"
- android:background="@null"
- android:foreground="?android:attr/selectableItemBackgroundBorderless"
- style="@style/TextAppearance.GestureTutorial.TextButtonLabel"/>
-</RelativeLayout>
\ No newline at end of file
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <TextView
+ android:id="@+id/gesture_tutorial_fragment_feedback_subtitle"
+ style="@style/TextAppearance.GestureTutorial.Feedback.Subtitle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_title"/>
+
+ <com.android.quickstep.interaction.TutorialStepIndicator
+ android:id="@+id/gesture_tutorial_fragment_feedback_tutorial_step"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/gesture_tutorial_fragment_action_button"
+ app:layout_constraintBottom_toBottomOf="@id/gesture_tutorial_fragment_action_button"/>
+
+ <Button
+ android:id="@+id/gesture_tutorial_fragment_action_button"
+ style="@style/TextAppearance.GestureTutorial.ButtonLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:paddingStart="26dp"
+ android:paddingEnd="26dp"
+ android:text="@string/gesture_tutorial_action_button_label"
+ android:background="@drawable/gesture_tutorial_action_button_background"
+ android:stateListAnimator="@null"
+ android:visibility="invisible"
+
+ app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_subtitle"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <Button
+ style="@style/TextAppearance.GestureTutorial.Feedback.Subtext"
+ android:id="@+id/gesture_tutorial_fragment_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="16dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:paddingStart="26dp"
+ android:paddingEnd="26dp"
+ android:text="@string/gesture_tutorial_action_button_label_skip"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+
+ app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_subtitle"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.android.quickstep.interaction.RootSandboxLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index e05688e..68680d3 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -14,18 +14,17 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<!-- NOTE! don't add dimensions for margins / gravity to root view in this file, they need to be
+ loaded at runtime. -->
<com.android.quickstep.views.OverviewActionsView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="@dimen/overview_actions_height"
- android:layout_gravity="center_horizontal|bottom"
- android:layout_marginLeft="@dimen/overview_actions_horizontal_margin"
- android:layout_marginRight="@dimen/overview_actions_horizontal_margin">
+ android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/action_buttons"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
+ android:layout_height="@dimen/overview_actions_height"
+ android:layout_gravity="bottom|center_horizontal"
android:orientation="horizontal">
<Space
@@ -40,7 +39,8 @@
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_screenshot"
android:text="@string/action_screenshot"
- android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+ android:theme="@style/ThemeControlHighlightWorkspaceColor"
+ android:visibility="gone" />
<Space
android:layout_width="0dp"
@@ -58,7 +58,7 @@
android:visibility="gone" />
<Space
- android:id="@+id/share_space"
+ android:id="@+id/oav_three_button_space"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index fc06ba0..1ee726e 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -21,7 +21,5 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recents_clear_all"
- android:textColor="?attr/workspaceTextColor"
- android:textSize="14sp"
- android:translationY="@dimen/task_thumbnail_half_top_margin"
- />
\ No newline at end of file
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="14sp" />
\ 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 0000000..f303f31
--- /dev/null
+++ b/quickstep/res/layout/overview_panel.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+
+ <com.android.quickstep.views.LauncherRecentsView
+ android:id="@+id/overview_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:accessibilityPaneTitle="@string/accessibility_recent_apps"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:visibility="invisible" />
+
+ <com.android.quickstep.views.SplitPlaceholderView
+ android:id="@+id/split_placeholder"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/split_placeholder_size"
+ android:background="@android:color/darker_gray"
+ android:visibility="gone" />
+
+ <include
+ android:id="@+id/overview_actions_view"
+ layout="@layout/overview_actions_container" />
+
+</merge>
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml b/quickstep/res/layout/predicted_app_icon.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
rename to quickstep/res/layout/predicted_app_icon.xml
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml b/quickstep/res/layout/predicted_hotseat_edu.xml
similarity index 100%
rename from quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
rename to quickstep/res/layout/predicted_hotseat_edu.xml
diff --git a/quickstep/res/layout/scrim_view.xml b/quickstep/res/layout/scrim_view.xml
deleted file mode 100644
index 2cc37f9..0000000
--- a/quickstep/res/layout/scrim_view.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<com.android.quickstep.views.ShelfScrimView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- 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
index f9bb2f2..7e5b85c 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -13,19 +13,20 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<!-- NOTE! don't add dimensions for margins / paddings / sizes that change per orientation to this
+ file, they need to be loaded at runtime. -->
<com.android.quickstep.views.TaskView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
- android:elevation="4dp"
android:focusable="true">
<com.android.quickstep.views.TaskThumbnailView
android:id="@+id/snapshot"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginTop="@dimen/task_thumbnail_top_margin"/>
+ android:layout_height="match_parent"/>
<com.android.quickstep.views.IconView
android:id="@+id/icon"
diff --git a/quickstep/res/layout/task_menu.xml b/quickstep/res/layout/task_menu.xml
index 098b34f..a5c9445 100644
--- a/quickstep/res/layout/task_menu.xml
+++ b/quickstep/res/layout/task_menu.xml
@@ -16,39 +16,30 @@
-->
<com.android.quickstep.views.TaskMenuView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="@drawable/task_menu_bg"
- android:paddingBottom="@dimen/task_card_menu_shadow_height"
android:orientation="vertical"
android:visibility="invisible">
- <com.android.quickstep.views.IconView
- android:id="@+id/task_icon"
- android:layout_width="@dimen/task_thumbnail_icon_size"
- android:layout_height="@dimen/task_thumbnail_icon_size"
- android:layout_gravity="top|center_horizontal"
- android:layout_marginBottom="@dimen/deep_shortcut_drawable_padding"
- android:focusable="false"
- android:importantForAccessibility="no" />
-
<TextView
android:id="@+id/task_name"
+ android:background="@drawable/task_menu_item_bg"
+ android:textColor="?android:attr/textColorPrimary"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:layout_marginBottom="16dp"
- android:textSize="12sp"/>
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:layout_marginBottom="2dp"
+ android:paddingTop="@dimen/task_menu_vertical_padding"
+ android:paddingBottom="@dimen/task_menu_vertical_padding"
+ android:textSize="16sp"/>
<LinearLayout
android:id="@+id/menu_option_layout"
- style="@style/TaskMenu"
- android:divider="@drawable/all_apps_divider"
- android:showDividers="beginning"
- android:paddingStart="@dimen/task_card_menu_horizontal_padding"
- android:paddingEnd="@dimen/task_card_menu_horizontal_padding"
android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:showDividers="middle" />
</com.android.quickstep.views.TaskMenuView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_view_menu_option.xml b/quickstep/res/layout/task_view_menu_option.xml
index 102ae9b..5978b97 100644
--- a/quickstep/res/layout/task_view_menu_option.xml
+++ b/quickstep/res/layout/task_view_menu_option.xml
@@ -16,35 +16,30 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- style="@style/TaskMenu.Option"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center"
android:orientation="vertical"
android:paddingTop="@dimen/task_card_menu_option_vertical_padding"
android:paddingBottom="@dimen/task_card_menu_option_vertical_padding"
- android:background="?android:attr/selectableItemBackground"
+ android:background="@drawable/task_menu_item_bg"
android:theme="@style/PopupItem" >
<View
android:id="@+id/icon"
android:layout_width="@dimen/system_shortcut_icon_size"
android:layout_height="@dimen/system_shortcut_icon_size"
- android:layout_marginTop="@dimen/system_shortcut_header_icon_padding"
- android:layout_marginBottom="@dimen/deep_shortcut_drawable_padding"
+ android:layout_marginStart="@dimen/task_menu_option_start_margin"
android:layout_gravity="center_horizontal"
- android:backgroundTint="?android:attr/textColorTertiary"/>
+ android:backgroundTint="?android:attr/textColorPrimary"/>
<TextView
style="@style/BaseIcon"
android:id="@+id/text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingBottom="@dimen/popup_padding_end"
- android:textSize="12sp"
+ android:layout_marginStart="@dimen/task_menu_option_start_margin"
+ android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary"
- android:fontFamily="sans-serif"
- android:gravity="center_horizontal"
- android:layout_gravity="center_horizontal"
android:focusable="false" />
</LinearLayout>
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
new file mode 100644
index 0000000..e680233
--- /dev/null
+++ b/quickstep/res/layout/taskbar.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.launcher3.taskbar.TaskbarDragLayer
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/taskbar_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <com.android.launcher3.taskbar.TaskbarView
+ android:id="@+id/taskbar_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:forceHasOverlappingRendering="false"
+ android:layout_gravity="bottom" >
+
+ <LinearLayout
+ android:id="@+id/system_button_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
+ android:paddingRight="@dimen/taskbar_nav_buttons_spacing"
+ android:forceHasOverlappingRendering="false"
+ android:gravity="center" />
+
+ <LinearLayout
+ android:id="@+id/hotseat_icons_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:forceHasOverlappingRendering="false"
+ android:gravity="center" />
+
+ </com.android.launcher3.taskbar.TaskbarView>
+
+ <com.android.launcher3.taskbar.ImeBarView
+ android:id="@+id/ime_bar_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"/>
+
+</com.android.launcher3.taskbar.TaskbarDragLayer>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar_app_icon.xml b/quickstep/res/layout/taskbar_app_icon.xml
new file mode 100644
index 0000000..6fefdb6
--- /dev/null
+++ b/quickstep/res/layout/taskbar_app_icon.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.launcher3.views.DoubleShadowBubbleTextView style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/layout/taskbar_predicted_app_icon.xml b/quickstep/res/layout/taskbar_predicted_app_icon.xml
new file mode 100644
index 0000000..211ebc8
--- /dev/null
+++ b/quickstep/res/layout/taskbar_predicted_app_icon.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/values-land/dimens.xml b/quickstep/res/values-land/dimens.xml
index c03eaa2..668aea2 100644
--- a/quickstep/res/values-land/dimens.xml
+++ b/quickstep/res/values-land/dimens.xml
@@ -15,5 +15,5 @@
limitations under the License.
-->
<resources>
- <dimen name="task_card_menu_horizontal_padding">24dp</dimen>
+ <dimen name="overview_task_margin">8dp</dimen>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values-land/styles.xml b/quickstep/res/values-land/styles.xml
deleted file mode 100644
index 0824b4f..0000000
--- a/quickstep/res/values-land/styles.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-<resources>
- <!-- Task Menu layout styles. -->
- <style name="TaskMenu">
- <item name="android:orientation">horizontal</item>
- </style>
-
- <!-- Task Menu Option layout styles. -->
- <style name="TaskMenu.Option">
- <item name="android:layout_width">0dp</item>
- <item name="android:layout_weight">1</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/quickstep/res/values-night/styles.xml b/quickstep/res/values-night/styles.xml
new file mode 100644
index 0000000..1bd3f5d
--- /dev/null
+++ b/quickstep/res/values-night/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+
+ <style name="AllSetTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:enforceNavigationBarContrast">false</item>
+ <item name="android:windowLightStatusBar">false</item>
+ <item name="android:windowBackground">#FF000000</item>
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml
new file mode 100644
index 0000000..336fb57
--- /dev/null
+++ b/quickstep/res/values/attrs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <declare-styleable name="AllSetLinkSpan">
+ <attr name="android:textSize"/>
+ <attr name="android:fontFamily"/>
+ </declare-styleable>
+</resources>
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 40da136..167c7c3 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -16,4 +16,16 @@
<resources>
<color name="back_arrow_color_light">#FFFFFFFF</color>
<color name="back_arrow_color_dark">#99000000</color>
+
+ <color name="chip_hint_foreground_color">#fff</color>
+ <color name="chip_scrim_start_color">#39000000</color>
+
+ <color name="all_apps_label_text">#61000000</color>
+ <color name="all_apps_label_text_dark">#61FFFFFF</color>
+ <color name="all_apps_prediction_row_separator">#3c000000</color>
+ <color name="all_apps_prediction_row_separator_dark">#3cffffff</color>
+
+ <!-- Taskbar -->
+ <color name="taskbar_background">#101010</color>
+ <color name="taskbar_icon_selection_ripple">#E0E0E0</color>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 0f2955b..d67b23b 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -14,6 +14,7 @@
limitations under the License.
-->
<resources>
+ <string name="overscroll_plugin_factory_class" translatable="false" />
<string name="task_overlay_factory_class" translatable="false"/>
<!-- Activities which block home gesture -->
@@ -35,4 +36,6 @@
<integer name="assistant_gesture_corner_deg_threshold">20</integer>
<string name="wellbeing_provider_pkg" translatable="false"/>
+
+ <integer name="max_depth_blur_radius">23</integer>
</resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9d70316..6cc64e0 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -15,57 +15,73 @@
-->
<resources>
-
- <dimen name="task_thumbnail_top_margin">24dp</dimen>
- <dimen name="task_thumbnail_half_top_margin">12dp</dimen>
<dimen name="task_thumbnail_icon_size">48dp</dimen>
+ <dimen name="task_thumbnail_icon_size_grid">32dp</dimen>
<!-- For screens without rounded corners -->
<dimen name="task_corner_radius_small">2dp</dimen>
+ <!-- For Launchers that want to override the default dialog corner radius -->
+ <dimen name="task_corner_radius_override">-1dp</dimen>
+
+ <!-- Task Menu View -->
+ <dimen name="task_menu_corner_radius">22dp</dimen>
+ <dimen name="task_menu_item_corner_radius">4dp</dimen>
+ <dimen name="task_menu_spacing">2dp</dimen>
+ <dimen name="overview_proactive_row_height">48dp</dimen>
+ <dimen name="overview_proactive_row_bottom_margin">16dp</dimen>
+
+ <dimen name="overview_minimum_next_prev_size">50dp</dimen>
+ <dimen name="overview_task_margin">16dp</dimen>
<!-- Overrideable in overlay that provides the Overview Actions. -->
- <dimen name="overview_actions_height">66dp</dimen>
- <dimen name="overview_actions_bottom_margin_gesture">16dp</dimen>
+ <dimen name="overview_actions_height">48dp</dimen>
+ <dimen name="overview_actions_bottom_margin_gesture">28dp</dimen>
<dimen name="overview_actions_bottom_margin_three_button">8dp</dimen>
<dimen name="overview_actions_horizontal_margin">16dp</dimen>
- <dimen name="recents_page_spacing">10dp</dimen>
+ <dimen name="overview_grid_top_margin">77dp</dimen>
+ <dimen name="overview_grid_bottom_margin">90dp</dimen>
+ <dimen name="overview_grid_side_margin">54dp</dimen>
+ <dimen name="overview_grid_row_spacing">42dp</dimen>
+ <dimen name="overview_grid_focus_vertical_margin">90dp</dimen>
+ <dimen name="split_placeholder_size">110dp</dimen>
+
+ <!-- These speeds are in dp/s -->
+ <dimen name="max_task_dismiss_drag_velocity">2.25dp</dimen>
+ <dimen name="default_task_dismiss_drag_velocity">1.5dp</dimen>
+ <dimen name="default_task_dismiss_drag_velocity_grid">1dp</dimen>
+ <dimen name="default_task_dismiss_drag_velocity_grid_focus_task">5dp</dimen>
+
+ <dimen name="recents_page_spacing">16dp</dimen>
<dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
- <dimen name="overview_peek_distance">96dp</dimen>
<!-- The speed in dp/s at which the user needs to be scrolling in recents such that we start
loading full resolution screenshots. -->
<dimen name="recents_fast_fling_velocity">600dp</dimen>
- <!-- These velocities are in dp / s -->
- <dimen name="quickstep_fling_threshold_velocity">500dp</dimen>
- <dimen name="quickstep_fling_min_velocity">250dp</dimen>
-
<!-- These speeds are in dp / ms -->
<dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
<dimen name="motion_pause_detector_speed_slow">0.15dp</dimen>
<dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
<dimen name="motion_pause_detector_speed_fast">1.4dp</dimen>
<dimen name="motion_pause_detector_min_displacement_from_app">36dp</dimen>
+ <dimen name="quickstep_fling_threshold_speed">0.5dp</dimen>
<!-- Launcher app transition -->
- <dimen name="content_trans_y">50dp</dimen>
- <dimen name="workspace_trans_y">50dp</dimen>
+ <item name="content_scale" format="float" type="dimen">0.97</item>
<dimen name="closing_window_trans_y">115dp</dimen>
<dimen name="recents_empty_message_text_size">16sp</dimen>
<dimen name="recents_empty_message_text_padding">16dp</dimen>
+ <dimen name="max_shadow_radius">0dp</dimen>
+
<!-- Total space (start + end) between the task card and the edge of the screen
in various configurations -->
- <dimen name="task_card_vert_space">40dp</dimen>
- <dimen name="task_card_menu_option_vertical_padding">8dp</dimen>
+ <dimen name="task_card_menu_option_vertical_padding">16dp</dimen>
+ <dimen name="task_menu_vertical_padding">8dp</dimen>
+ <dimen name="task_card_margin">8dp</dimen>
<dimen name="task_card_menu_shadow_height">3dp</dimen>
- <dimen name="task_card_menu_horizontal_padding">0dp</dimen>
- <dimen name="portrait_task_card_horz_space">136dp</dimen>
- <dimen name="portrait_task_card_horz_space_big_overview">96dp</dimen>
- <dimen name="portrait_modal_task_card_horz_space">60dp</dimen>
- <dimen name="landscape_task_card_horz_space">200dp</dimen>
- <dimen name="multi_window_task_card_horz_space">100dp</dimen>
+ <dimen name="task_menu_option_start_margin">12dp</dimen>
<!-- Copied from framework resource:
docked_stack_divider_thickness - 2 * docked_stack_divider_insets -->
<dimen name="multi_window_task_divider_size">10dp</dimen>
@@ -76,6 +92,10 @@
<dimen name="gestures_assistant_drag_threshold">55dp</dimen>
<dimen name="gestures_assistant_fling_threshold">55dp</dimen>
+ <!-- One-Handed Mode -->
+ <!-- Threshold for draging distance to enable one-handed mode -->
+ <dimen name="gestures_onehanded_drag_threshold">20dp</dimen>
+
<!-- Distance to move elements when swiping up to go home from launcher -->
<dimen name="home_pullback_distance">28dp</dimen>
@@ -93,9 +113,48 @@
<dimen name="gesture_tutorial_feedback_margin_start_end">24dp</dimen>
<dimen name="gesture_tutorial_button_margin_start_end">18dp</dimen>
+ <!-- All Set page -->
+ <dimen name="allset_page_margin_horizontal">40dp</dimen>
+ <dimen name="allset_title_margin_top">24dp</dimen>
+ <dimen name="allset_title_icon_margin_top">32dp</dimen>
+ <dimen name="allset_hint_margin_bottom">52dp</dimen>
+ <dimen name="allset_subtitle_margin_top">24dp</dimen>
+
<!-- All Apps Education tutorial -->
<dimen name="swipe_edu_padding">8dp</dimen>
<dimen name="swipe_edu_circle_size">64dp</dimen>
<dimen name="swipe_edu_width">80dp</dimen>
<dimen name="swipe_edu_max_height">184dp</dimen>
+
+ <dimen name="chip_hint_border_width">1dp</dimen>
+ <dimen name="chip_hint_corner_radius">20dp</dimen>
+ <dimen name="chip_hint_outer_padding">20dp</dimen>
+ <dimen name="chip_hint_start_padding">10dp</dimen>
+ <dimen name="chip_hint_end_padding">12dp</dimen>
+ <dimen name="chip_hint_horizontal_margin">20dp</dimen>
+ <dimen name="chip_hint_vertical_offset">16dp</dimen>
+ <dimen name="chip_hint_elevation">2dp</dimen>
+ <dimen name="chip_icon_size">16dp</dimen>
+ <dimen name="chip_text_height">26dp</dimen>
+ <dimen name="chip_text_top_padding">4dp</dimen>
+ <dimen name="chip_text_start_padding">10dp</dimen>
+ <dimen name="chip_text_size">14sp</dimen>
+
+ <dimen name="all_apps_prediction_row_divider_height">17dp</dimen>
+ <dimen name="all_apps_label_top_padding">16dp</dimen>
+ <dimen name="all_apps_label_bottom_padding">8dp</dimen>
+ <dimen name="all_apps_label_text_size">14sp</dimen>
+
+ <!-- Minimum distance to swipe to trigger accessibility gesture -->
+ <dimen name="accessibility_gesture_min_swipe_distance">80dp</dimen>
+
+ <!-- Taskbar -->
+ <dimen name="taskbar_size">60dp</dimen>
+ <dimen name="taskbar_icon_size">44dp</dimen>
+ <dimen name="taskbar_icon_touch_size">48dp</dimen>
+ <dimen name="taskbar_icon_drag_icon_size">54dp</dimen>
+ <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
+ <dimen name="taskbar_icon_spacing">8dp</dimen>
+ <dimen name="taskbar_folder_margin">16dp</dimen>
+ <dimen name="taskbar_nav_buttons_spacing">16dp</dimen>
</resources>
diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml
new file mode 100644
index 0000000..705ec9d
--- /dev/null
+++ b/quickstep/res/values/override.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Class overrides for launcher with quickstep. -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
+
+ <string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
+
+ <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
+
+ <string name="model_delegate_class" translatable="false">com.android.launcher3.model.QuickstepModelDelegate</string>
+
+</resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 86120e3..4aee2a9 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -22,8 +22,6 @@
<string name="derived_app_name" translatable="false">Quickstep</string>
<!-- Options for recent tasks -->
- <!-- Title for an option to enter split screen mode for a given app -->
- <string name="recent_task_option_split_screen">Split screen</string>
<!-- Title for an option to keep an app pinned to the screen until it is unpinned -->
<string name="recent_task_option_pin">Pin</string>
<!-- Title for an option to enter freeform mode for a given app -->
@@ -61,7 +59,7 @@
<string name="all_apps_prediction_tip">Your predicted apps</string>
<!-- Content description for a close button. [CHAR LIMIT=NONE] -->
- <string name="gesture_tutorial_close_button_content_description" translatable="false">Close</string>
+ <string name="gesture_tutorial_close_button_content_description" translatable="false">Close</string>
<!-- Hotseat educational strings for users who don't qualify for migration -->
<string name="hotseat_edu_title_migrate">Get app suggestions on the bottom row of your Home screen</string>
@@ -93,68 +91,100 @@
<!-- content description for hotseat items -->
<string name="hotseat_prediction_content_description">Predicted app: <xliff:g id="title" example="Chrome">%1$s</xliff:g></string>
- <!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
- <string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
- <!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
- <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge" translatable="false">Start at the right edge and swipe toward the middle</string>
<!-- Feedback shown during interactive parts of Back gesture tutorial for right edge when the gesture is too far from the edge. [CHAR LIMIT=100] -->
- <string name="back_gesture_feedback_swipe_too_far_from_right_edge" translatable="false">Make sure you swipe from the far right edge</string>
+ <string name="back_gesture_feedback_swipe_too_far_from_edge">Make sure you swipe from the far-right or far-left edge.</string>
<!-- Feedback shown during interactive parts of Back gesture tutorial for right edge when the gesture is cancelled. [CHAR LIMIT=100] -->
- <string name="back_gesture_feedback_cancelled_right_edge" translatable="false">Make sure you swipe straight to the left and let go</string>
-
- <!-- Title shown during interactive part of Back gesture tutorial for left edge. [CHAR LIMIT=30] -->
- <string name="back_gesture_tutorial_playground_title_swipe_inward_left_edge" translatable="false">Try the other side</string>
- <!-- Subtitle shown during interactive parts of Back gesture tutorial for left edge. [CHAR LIMIT=60] -->
- <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge" translatable="false">That\'s it! Now try swiping from the left edge.</string>
- <!-- Feedback shown during interactive parts of Back gesture tutorial for left edge when the gesture is too far from the edge. [CHAR LIMIT=100] -->
- <string name="back_gesture_feedback_swipe_too_far_from_left_edge" translatable="false">Make sure you swipe from the far left edge</string>
- <!-- Feedback shown during interactive parts of Back gesture tutorial for left edge when the gesture is cancelled. [CHAR LIMIT=100] -->
- <string name="back_gesture_feedback_cancelled_left_edge" translatable="false">Make sure you swipe straight to the right and let go</string>
-
+ <string name="back_gesture_feedback_cancelled">Make sure you swipe from the right or left edge to the middle of the screen and let go.</string>
+ <!-- Feedback shown after completing the back gesture step if the user is following the full gesture tutorial flow. [CHAR LIMIT=100] -->
+ <string name="back_gesture_feedback_complete_with_overview_follow_up">You learned how to swipe from the right to go back. Next up, learn how to switch apps.</string>
+ <!-- Feedback shown after completing the back gesture step if the user started this tutorial individually. [CHAR LIMIT=100] -->
+ <string name="back_gesture_feedback_complete_without_follow_up">You completed the go back gesture.</string>
<!-- Feedback shown during interactive parts of Back gesture tutorial when the gesture is within the nav bar region. [CHAR LIMIT=100] -->
- <string name="back_gesture_feedback_swipe_in_nav_bar" translatable="false">Make sure you don\'t swipe too close to the bottom of the screen</string>
+ <string name="back_gesture_feedback_swipe_in_nav_bar">Make sure you don\'t swipe too close to the bottom of the screen.</string>
<!-- Subtitle shown on the confirmation screen after successful gesture. [CHAR LIMIT=60] -->
- <string name="back_gesture_tutorial_confirm_subtitle" translatable="false">To change the sensitivity of the back gesture, go to Settings</string>
-
- <!-- Title shown during interactive part of Home gesture tutorial. [CHAR LIMIT=30] -->
- <string name="home_gesture_tutorial_playground_title" translatable="false">Tutorial: Go Home</string>
- <!-- Subtitle shown during interactive parts of Home gesture tutorial. [CHAR LIMIT=60] -->
- <string name="home_gesture_tutorial_playground_subtitle" translatable="false">Try swiping upward from the bottom edge of the screen</string>
+ <string name="back_gesture_tutorial_confirm_subtitle">To change the sensitivity of the back gesture, go to Settings</string>
<!-- Feedback shown during interactive parts of Home gesture tutorial when the gesture is started too far from the edge. [CHAR LIMIT=100] -->
- <string name="home_gesture_feedback_swipe_too_far_from_edge" translatable="false">Make sure you swipe from the bottom edge of the screen</string>
- <!-- Feedback shown during interactive parts of Home gesture tutorial when the Overview gesture is detected. [CHAR LIMIT=100] -->
- <string name="home_gesture_feedback_overview_detected" translatable="false">Make sure you don\'t pause before letting go</string>
- <!-- Feedback shown during interactive parts of Home gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] -->
- <string name="home_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up</string>
+ <!-- Introduction title for the Back gesture tutorial. [CHAR LIMIT=100] -->
+ <string name="back_gesture_intro_title">Swipe to go back</string>
+ <!-- Introduction subtitle for the Back gesture tutorial. [CHAR LIMIT=200] -->
+ <string name="back_gesture_intro_subtitle">To go back to the last screen, swipe from the left or right edge to the middle of the screen.</string>
- <!-- Title shown during interactive part of Overview gesture tutorial. [CHAR LIMIT=30] -->
- <string name="overview_gesture_tutorial_playground_title" translatable="false">Tutorial: Switch Apps</string>
- <!-- Subtitle shown during interactive parts of Overview gesture tutorial. [CHAR LIMIT=60] -->
- <string name="overview_gesture_tutorial_playground_subtitle" translatable="false">Swipe up from the bottom of the screen and hold</string>
+ <string name="home_gesture_feedback_swipe_too_far_from_edge">Make sure you swipe up from the bottom edge of the screen.</string>
+ <!-- Feedback shown during interactive parts of Home gesture tutorial when the Overview gesture is detected. [CHAR LIMIT=100] -->
+ <string name="home_gesture_feedback_overview_detected">Make sure you don\'t pause before letting go.</string>
+ <!-- Feedback shown during interactive parts of Home gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] -->
+ <string name="home_gesture_feedback_wrong_swipe_direction">Make sure you swipe straight up.</string>
+ <!-- Feedback shown after completing the home gesture step if the user is following the full gesture tutorial flow. [CHAR LIMIT=100] -->
+ <string name="home_gesture_feedback_complete_with_follow_up">You completed the go Home gesture. Next up, learn how to go back.</string>
+ <!-- Feedback shown after completing the home gesture step if the user started this tutorial individually. [CHAR LIMIT=100] -->
+ <string name="home_gesture_feedback_complete_without_follow_up">You completed the go Home gesture.</string>
+ <!-- Introduction title for the Home gesture tutorial. [CHAR LIMIT=100] -->
+ <string name="home_gesture_intro_title">Swipe to go home</string>
+ <!-- Introduction subtitle for the Home gesture tutorial. [CHAR LIMIT=100] -->
+ <string name="home_gesture_intro_subtitle">Swipe up from the bottom of your screen. This gesture always takes you to the Home screen.</string>
+
<!-- Feedback shown during interactive parts of Overview gesture tutorial when the gesture is started too far from the edge. [CHAR LIMIT=100] -->
- <string name="overview_gesture_feedback_swipe_too_far_from_edge" translatable="false">Make sure you swipe from the bottom edge of the screen</string>
- <!-- Feedback shown during interactive parts of Overview gesture tutorial when the Home gesture is detected. [CHAR LIMIT=100] -->
- <string name="overview_gesture_feedback_home_detected" translatable="false">Try holding the window for longer before releasing</string>
+ <string name="overview_gesture_feedback_swipe_too_far_from_edge">Make sure you swipe up from the bottom edge of the screen.</string>
+ <!-- Feedback shown during interactive parts of Overview gesture tutorial when the Home gesture is detected. The window refers to the current app's window during the gesture. [CHAR LIMIT=100] -->
+ <string name="overview_gesture_feedback_home_detected">Try holding the window for longer before releasing.</string>
<!-- Feedback shown during interactive parts of Overview gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] -->
- <string name="overview_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up and pause</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction">Make sure you swipe straight up, then pause.</string>
+ <!-- Feedback shown after completing the overview gesture step if the user is following the full gesture tutorial flow. [CHAR LIMIT=100] -->
+ <string name="overview_gesture_feedback_complete_with_follow_up">You learned how to use gestures. To turn off gestures, go to Settings.</string>
+ <!-- Feedback shown after completing the overview gesture step if the user started this tutorial individually. [CHAR LIMIT=100] -->
+ <string name="overview_gesture_feedback_complete_without_follow_up">You completed the switch apps gesture.</string>
+ <!-- Introduction title for the Overview gesture tutorial. [CHAR LIMIT=100] -->
+ <string name="overview_gesture_intro_title">Swipe to switch apps</string>
+ <!-- Introduction subtitle for the Overview gesture tutorial. [CHAR LIMIT=100] -->
+ <string name="overview_gesture_intro_subtitle">To switch between apps, swipe up from the bottom of your screen, hold, then release.</string>
<!-- Title shown during interactive part of Assistant gesture tutorial. [CHAR LIMIT=30] -->
<string name="assistant_gesture_tutorial_playground_title" translatable="false">Tutorial: Assistant</string>
<!-- Subtitle shown during interactive parts of Assistant gesture tutorial. [CHAR LIMIT=60] -->
<string name="assistant_gesture_tutorial_playground_subtitle" translatable="false">Try swiping diagonally from a bottom corner of the screen</string>
<!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture is started too far from the corner. [CHAR LIMIT=100] -->
- <string name="assistant_gesture_feedback_swipe_too_far_from_corner" translatable="false">Make sure you swipe from a bottom corner of the screen</string>
+ <string name="assistant_gesture_feedback_swipe_too_far_from_corner" translatable="false">Make sure you swipe from a bottom corner of the screen.</string>
<!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go diagonally enough. [CHAR LIMIT=100] -->
- <string name="assistant_gesture_feedback_swipe_not_diagonal" translatable="false">Make sure you swipe diagonally</string>
+ <string name="assistant_gesture_feedback_swipe_not_diagonal" translatable="false">Make sure you swipe diagonally.</string>
<!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go far enough. [CHAR LIMIT=100] -->
- <string name="assistant_gesture_feedback_swipe_not_long_enough" translatable="false">Try swiping further</string>
+ <string name="assistant_gesture_feedback_swipe_not_long_enough" translatable="false">Try swiping further.</string>
+
+ <!-- Title shown in sandbox mode part of gesture tutorial. [CHAR LIMIT=30] -->
+ <string name="sandbox_mode_title" translatable="false">Sandbox Mode</string>
+ <!-- Subtitle shown in sandbox mode part of gesture tutorial. [CHAR LIMIT=60] -->
+ <string name="sandbox_mode_subtitle" translatable="false">Try any navigation gesture</string>
+ <!-- Feedback shown in sandbox mode when the back gesture is successfully issued. [CHAR LIMIT=60] -->
+ <string name="sandbox_mode_back_gesture_feedback_successful" translatable="false">Back gesture successful</string>
+ <!-- Feedback shown in sandbox mode when the assistant gesture is a successfully issued. [CHAR LIMIT=60] -->
+ <string name="sandbox_mode_assistant_gesture_feedback_successful" translatable="false">Assistant gesture successful</string>
+ <!-- Feedback shown in sandbox mode when the home gesture is a successfully issued. [CHAR LIMIT=60] -->
+ <string name="sandbox_mode_home_gesture_feedback_successful" translatable="false">Home gesture successful</string>
+ <!-- Feedback shown in sandbox mode when the overview gesture is a successfully issued. [CHAR LIMIT=60] -->
+ <string name="sandbox_mode_overview_gesture_feedback_successful" translatable="false">Overview gesture successful</string>
+ <!-- Feedback shown in sandbox mode when the back gesture swipe is too far from the edge. [CHAR LIMIT=60] -->
+ <string name="sandbox_mode_back_gesture_feedback_swipe_too_far_from_edge" translatable="false">Make sure you swipe from the left/right edge of the screen</string>
<!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] -->
- <string name="gesture_tutorial_confirm_title" translatable="false">All set</string>
- <!-- Button text shown on a button on the confirm screen to leave the tutorial. [CHAR LIMIT=14] -->
- <string name="gesture_tutorial_action_button_label_done" translatable="false">Done</string>
+ <string name="gesture_tutorial_confirm_title">All set</string>
+ <!-- Button text shown on a button on the feedback popup to complete the gesture tutorial. [CHAR LIMIT=14] -->
+ <string name="gesture_tutorial_action_button_label">Done</string>
<!-- Button text shown on a button to go to Settings. [CHAR LIMIT=14] -->
- <string name="gesture_tutorial_action_button_label_settings" translatable="false">Settings</string>
+ <string name="gesture_tutorial_action_button_label_settings">Settings</string>
+ <!-- Feedback title to try again. [CHAR LIMIT=30] -->
+ <string name="gesture_tutorial_try_again">Try again</string>
+ <!-- Feedback title for a successful gesture. [CHAR LIMIT=30] -->
+ <string name="gesture_tutorial_nice">Nice!</string>
+ <!-- Feedback subtext displaying the current step and the total number of steps for the tutorial. [CHAR LIMIT=30] -->
+ <string name="gesture_tutorial_step">Tutorial <xliff:g id="current">%1$d</xliff:g>/<xliff:g id="total">%2$d</xliff:g></string>
+
+ <!-- Title of "All Set" page [CHAR LIMIT=NONE] -->
+ <string name="allset_title">All set!</string>
+ <!-- Hint string at the bottom of "All Set" page [CHAR LIMIT=NONE] -->
+ <string name="allset_hint">Swipe up to go Home</string>
+ <!-- Description of "All Set" page [CHAR LIMIT=NONE] -->
+ <string name="allset_description">You\u2019re ready to start using your phone</string>
+ <!-- String linking to navigation settings on "All Set" page [CHAR LIMIT=NONE] -->
+ <string name="allset_navigation_settings"><annotation id="link">System navigation settings</annotation></string>
<!-- ******* Overview ******* -->
<!-- Label for a button that causes the current overview app to be shared. [CHAR_LIMIT=40] -->
@@ -163,4 +193,14 @@
<string name="action_screenshot">Screenshot</string>
<!-- Message shown when an action is blocked by a policy enforced by the app or the organization managing the device. [CHAR_LIMIT=NONE] -->
<string name="blocked_by_policy">This action isn\'t allowed by the app or your organization</string>
+
+ <!-- ******* Skip tutorial dialog ******* -->
+ <!-- Title for the dialog that allows the user to skip the gesture navigation tutorial. [CHAR_LIMIT=40] -->
+ <string name="skip_tutorial_dialog_title">Skip navigation tutorial?</string>
+ <!-- Subtitle for the dialog that allows the user to skip the gesture navigation tutorial. This directs the user to where they can find the gesture tutorial again. [CHAR_LIMIT=100] -->
+ <string name="skip_tutorial_dialog_subtitle">You can find this later in the <xliff:g id="name">%1$s</xliff:g> app</string>
+ <!-- Button text shown on a button on the tutorial skip dialog to return to the tutorial. [CHAR LIMIT=14] -->
+ <string name="gesture_tutorial_action_button_label_cancel">Cancel</string>
+ <!-- Button text shown on a button on the tutorial skip dialog to exit the tutorial. [CHAR LIMIT=14] -->
+ <string name="gesture_tutorial_action_button_label_skip">Skip</string>
</resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 8d054b4..07c448d 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -15,16 +15,6 @@
limitations under the License.
-->
<resources>
- <!-- Task Menu layout styles. -->
- <style name="TaskMenu">
- <item name="android:orientation">vertical</item>
- </style>
-
- <!-- Task Menu Option layout styles. -->
- <style name="TaskMenu.Option">
- <item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">wrap_content</item>
- </style>
<style name="TextAppearance.GestureTutorial"
parent="android:TextAppearance.Material.Body1" />
@@ -47,12 +37,53 @@
<item name="android:textSize">21sp</item>
</style>
- <style name="TextAppearance.GestureTutorial.Feedback"
+ <style name="TextAppearance.GestureTutorial.Feedback.Title"
parent="TextAppearance.GestureTutorial">
- <item name="android:gravity">center</item>
+ <item name="android:gravity">start</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:fontFamily">google-sans-regular</item>
<item name="android:letterSpacing">0.03</item>
- <item name="android:textSize">21sp</item>
+ <item name="android:textSize">36sp</item>
+ <item name="android:lineHeight">44sp</item>
+ </style>
+
+ <style name="TextAppearance.GestureTutorial.Dialog.Title"
+ parent="TextAppearance.GestureTutorial.Feedback.Title">
+ <item name="android:gravity">center_horizontal</item>
+ <item name="android:fontFamily">google-sans</item>
+ <item name="android:lineHeight">32sp</item>
+ <item name="android:textSize">24sp</item>
+ </style>
+
+ <style name="TextAppearance.GestureTutorial.Feedback.Subtitle"
+ parent="TextAppearance.GestureTutorial">
+ <item name="android:gravity">start</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:fontFamily">google-sans-text</item>
+ <item name="android:letterSpacing">0.03</item>
+ <item name="android:textSize">18sp</item>
+ <item name="android:lineHeight">24sp</item>
+ </style>
+
+ <style name="TextAppearance.GestureTutorial.Dialog.Subtitle"
+ parent="TextAppearance.GestureTutorial.Feedback.Subtitle">
+ <item name="android:gravity">center_horizontal</item>
+ <item name="android:fontFamily">google-sans-text</item>
+ <item name="android:letterSpacing">0.025</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="TextAppearance.GestureTutorial.Feedback.Subtext"
+ parent="TextAppearance.GestureTutorial.Feedback.Subtitle">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">@color/gesture_tutorial_primary_color</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="TextAppearance.GestureTutorial.Feedback.Subtext.Dark"
+ parent="TextAppearance.GestureTutorial.Feedback.Subtext">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="TextAppearance.GestureTutorial.ButtonLabel"
@@ -64,11 +95,29 @@
<item name="android:textAllCaps">false</item>
</style>
+ <style name="TextAppearance.GestureTutorial.CancelButtonLabel"
+ parent="TextAppearance.GestureTutorial.ButtonLabel">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
<style name="TextAppearance.GestureTutorial.TextButtonLabel"
parent="TextAppearance.GestureTutorial.ButtonLabel">
<item name="android:textColor">@color/gesture_tutorial_primary_color</item>
</style>
+ <style name="TextAppearance.GestureTutorial.LinkText"
+ parent="TextAppearance.GestureTutorial.Feedback.Subtitle">
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="AllSetTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:enforceNavigationBarContrast">false</item>
+ <item name="android:windowLightStatusBar">true</item>
+ <item name="android:windowBackground">#FFFFFFFF</item>
+ </style>
+
<!--
Can be applied to views to color things like ripples and list highlights the workspace text
color.
@@ -81,8 +130,13 @@
parent="@android:style/Widget.DeviceDefault.Button.Borderless">
<item name="android:textColor">@color/overview_button</item>
<item name="android:drawableTint">@color/overview_button</item>
- <item name="android:tint">?attr/workspaceTextColor</item>
+ <item name="android:tint">?android:attr/textColorPrimary</item>
<item name="android:drawablePadding">8dp</item>
<item name="android:textAllCaps">false</item>
</style>
+
+ <!-- Icon displayed on the taskbar -->
+ <style name="BaseIcon.Workspace.Taskbar" >
+ <item name="iconDisplay">taskbar</item>
+ </style>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/xml/indexable_launcher_prefs.xml b/quickstep/res/xml/indexable_launcher_prefs.xml
index c7e864f..b4740e5 100644
--- a/quickstep/res/xml/indexable_launcher_prefs.xml
+++ b/quickstep/res/xml/indexable_launcher_prefs.xml
@@ -26,7 +26,7 @@
android:key="pref_allowRotation"
android:title="@string/allow_rotation_title"
android:summary="@string/allow_rotation_desc"
- android:defaultValue="@bool/allow_rotation"
+ android:defaultValue="false"
android:persistent="true" />
</PreferenceScreen>
diff --git a/quickstep/res/xml/overview_file_provider_paths.xml b/quickstep/res/xml/overview_file_provider_paths.xml
index 14d7459..07e3236 100644
--- a/quickstep/res/xml/overview_file_provider_paths.xml
+++ b/quickstep/res/xml/overview_file_provider_paths.xml
@@ -2,4 +2,5 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="shared_images" path="/" />
<files-path name="log_files" path="/" />
+ <root-path name="apps" path="/" />
</paths>
\ No newline at end of file
diff --git a/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
new file mode 100644
index 0000000..7c97b93
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetId;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Process;
+import android.os.UserHandle;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shadows.ShadowDeviceFlag;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowAppWidgetManager;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsPredicationUpdateTaskTest {
+
+ private AppWidgetProviderInfo mApp1Provider1 = new AppWidgetProviderInfo();
+ private AppWidgetProviderInfo mApp1Provider2 = new AppWidgetProviderInfo();
+ private AppWidgetProviderInfo mApp2Provider1 = new AppWidgetProviderInfo();
+ private AppWidgetProviderInfo mApp4Provider1 = new AppWidgetProviderInfo();
+ private AppWidgetProviderInfo mApp4Provider2 = new AppWidgetProviderInfo();
+ private AppWidgetProviderInfo mApp5Provider1 = new AppWidgetProviderInfo();
+
+ private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
+ private Context mContext;
+ private LauncherModelHelper mModelHelper;
+ private UserHandle mUserHandle;
+ private InvariantDeviceProfile mTestProfile;
+
+ @Mock
+ private IconCache mIconCache;
+
+ @Before
+ public void setup() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = invocation.getArgument(0);
+ return componentWithLabel.getComponent().getShortClassName();
+ }).when(mIconCache).getTitleNoCache(any());
+
+ mContext = RuntimeEnvironment.application;
+ mModelHelper = new LauncherModelHelper();
+ mUserHandle = Process.myUserHandle();
+ mTestProfile = new InvariantDeviceProfile();
+ // 2 widgets, app4/provider1 & app5/provider1, have already been added to the workspace.
+ mModelHelper.initializeData("/widgets_predication_update_task_data.txt");
+
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ mApp1Provider1.provider = ComponentName.createRelative("app1", "provider1");
+ ReflectionHelpers.setField(mApp1Provider1, "providerInfo",
+ packageManager.addReceiverIfNotPresent(mApp1Provider1.provider));
+ mApp1Provider2.provider = ComponentName.createRelative("app1", "provider2");
+ ReflectionHelpers.setField(mApp1Provider2, "providerInfo",
+ packageManager.addReceiverIfNotPresent(mApp1Provider2.provider));
+ mApp2Provider1.provider = ComponentName.createRelative("app2", "provider1");
+ ReflectionHelpers.setField(mApp2Provider1, "providerInfo",
+ packageManager.addReceiverIfNotPresent(mApp2Provider1.provider));
+ mApp4Provider1.provider = ComponentName.createRelative("app4", "provider1");
+ ReflectionHelpers.setField(mApp4Provider1, "providerInfo",
+ packageManager.addReceiverIfNotPresent(mApp4Provider1.provider));
+ mApp4Provider2.provider = ComponentName.createRelative("app4", ".provider2");
+ ReflectionHelpers.setField(mApp4Provider2, "providerInfo",
+ packageManager.addReceiverIfNotPresent(mApp4Provider2.provider));
+ mApp5Provider1.provider = ComponentName.createRelative("app5", "provider1");
+ ReflectionHelpers.setField(mApp5Provider1, "providerInfo",
+ packageManager.addReceiverIfNotPresent(mApp5Provider1.provider));
+
+ ShadowAppWidgetManager shadowAppWidgetManager =
+ shadowOf(mContext.getSystemService(AppWidgetManager.class));
+ shadowAppWidgetManager.addInstalledProvider(mApp1Provider1);
+ shadowAppWidgetManager.addInstalledProvider(mApp1Provider2);
+ shadowAppWidgetManager.addInstalledProvider(mApp2Provider1);
+ shadowAppWidgetManager.addInstalledProvider(mApp4Provider1);
+ shadowAppWidgetManager.addInstalledProvider(mApp4Provider2);
+ shadowAppWidgetManager.addInstalledProvider(mApp5Provider1);
+
+ mModelHelper.getModel().addCallbacks(mCallback);
+
+ MODEL_EXECUTOR.post(() -> mModelHelper.getBgDataModel().widgetsModel.update(
+ LauncherAppState.getInstance(mContext), /* packageUser= */ null));
+ waitUntilIdle();
+ }
+
+
+ @Test
+ public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()
+ throws Exception {
+ // WHEN newPredicationTask is executed with app predication of 5 apps.
+ AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className",
+ mUserHandle);
+ AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className",
+ mUserHandle);
+ AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
+ mUserHandle);
+ AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className",
+ mUserHandle);
+ AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className",
+ mUserHandle);
+ mModelHelper.executeTaskForTest(
+ newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)))
+ .forEach(Runnable::run);
+
+ // THEN only 3 widgets are returned because
+ // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
+ // excluded from the result.
+ // 2. app3 doesn't have a widget.
+ // 3. only 1 widget is picked from app1 because we only want to promote one widget per app.
+ List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+ .stream()
+ .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+ .collect(Collectors.toList());
+ assertThat(recommendedWidgets).hasSize(3);
+ assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
+ assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2);
+ assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
+ }
+
+ @Test
+ public void widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder()
+ throws Exception {
+ ShadowDeviceFlag shadowDeviceFlag = Shadow.extract(
+ FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER);
+ shadowDeviceFlag.setValue(false);
+
+ // WHEN newPredicationTask is executed with 5 predicated widgets.
+ AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
+ mUserHandle);
+ AppTarget widget2 = new AppTarget(new AppTargetId("app1"), "app1", "provider2",
+ mUserHandle);
+ // Not installed app
+ AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1",
+ mUserHandle);
+ // Not installed widget
+ AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider3",
+ mUserHandle);
+ AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
+ mUserHandle);
+ mModelHelper.executeTaskForTest(
+ newWidgetsPredicationTask(List.of(widget5, widget3, widget2, widget4, widget1)))
+ .forEach(Runnable::run);
+
+ // THEN only 3 widgets are returned because the launcher only filters out non-exist widgets.
+ List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+ .stream()
+ .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+ .collect(Collectors.toList());
+ assertThat(recommendedWidgets).hasSize(3);
+ assertWidgetInfo(recommendedWidgets.get(0).info, mApp5Provider1);
+ assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider2);
+ assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
+ }
+
+ private void assertWidgetInfo(
+ LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
+ assertThat(actual.provider).isEqualTo(expected.provider);
+ assertThat(actual.getUser()).isEqualTo(expected.getProfile());
+ }
+
+ private void waitUntilIdle() {
+ shadowOf(MODEL_EXECUTOR.getLooper()).idle();
+ shadowOf(MAIN_EXECUTOR.getLooper()).idle();
+ }
+
+ private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List<AppTarget> appTargets) {
+ return new WidgetsPredictionUpdateTask(
+ new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"),
+ appTargets);
+ }
+
+ private final class FakeBgDataModelCallback implements BgDataModel.Callbacks {
+
+ private FixedContainerItems mRecommendedWidgets = null;
+
+ @Override
+ public void bindExtraContainerItems(FixedContainerItems item) {
+ mRecommendedWidgets = item;
+ }
+
+ @Override
+ public int getPageToBindSynchronously() {
+ return 0;
+ }
+
+ @Override
+ public void clearPendingBinds() { }
+
+ @Override
+ public void startBinding() { }
+
+ @Override
+ public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
+
+ @Override
+ public void bindScreens(IntArray orderedScreenIds) { }
+
+ @Override
+ public void finishFirstPageBind(ViewOnDrawExecutor executor) { }
+
+ @Override
+ public void finishBindingItems(int pageBoundFirst) { }
+
+ @Override
+ public void preAddApps() { }
+
+ @Override
+ public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
+ ArrayList<ItemInfo> addAnimated) { }
+
+ @Override
+ public void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
+
+ @Override
+ public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
+
+ @Override
+ public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
+
+ @Override
+ public void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
+
+ @Override
+ public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
+
+ @Override
+ public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
+
+ @Override
+ public void onPageBoundSynchronously(int page) { }
+
+ @Override
+ public void executeOnNextDraw(ViewOnDrawExecutor executor) { }
+
+ @Override
+ public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
+
+ @Override
+ public void bindAllApplications(AppInfo[] apps, int flags) { }
+ }
+}
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index 22d205a..2b1b57c 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -17,40 +17,59 @@
package com.android.quickstep;
+import static android.view.Display.DEFAULT_DISPLAY;
+
import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
+import android.hardware.display.DisplayManager;
import android.util.DisplayMetrics;
+import android.view.Display;
import android.view.MotionEvent;
import android.view.Surface;
import com.android.launcher3.ResourceUtils;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public class OrientationTouchTransformerTest {
- private static final int SIZE_WIDTH = 1080;
- private static final int SIZE_HEIGHT = 2280;
+ static class ScreenSize {
+ int mHeight;
+ int mWidth;
+
+ ScreenSize(int height, int width) {
+ mHeight = height;
+ mWidth = width;
+ }
+ }
+
+ private static final ScreenSize NORMAL_SCREEN_SIZE = new ScreenSize(2280, 1080);
+ private static final ScreenSize LARGE_SCREEN_SIZE = new ScreenSize(3280, 1080);
private static final float DENSITY_DISPLAY_METRICS = 3.0f;
private OrientationTouchTransformer mTouchTransformer;
Resources mResources;
- private DefaultDisplay.Info mInfo;
+ private DisplayController.Info mInfo;
@Before
@@ -63,14 +82,16 @@
DisplayMetrics mockDisplayMetrics = new DisplayMetrics();
mockDisplayMetrics.density = DENSITY_DISPLAY_METRICS;
when(mResources.getDisplayMetrics()).thenReturn(mockDisplayMetrics);
- mInfo = createDisplayInfo(Surface.ROTATION_0);
+ mInfo = createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_0);
mTouchTransformer = new OrientationTouchTransformer(mResources, NO_BUTTON, () -> 0);
}
@Test
public void disabledMultipleRegions_shouldOverrideFirstRegion() {
- float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
- float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+ float portraitRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+ float landscapeRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
mTouchTransformer.createOrAddTouchRegion(mInfo);
tapAndAssertTrue(100, portraitRegionY,
@@ -83,7 +104,8 @@
event -> mTouchTransformer.touchInAssistantRegion(event));
// Override region
- mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+ mTouchTransformer
+ .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
tapAndAssertFalse(100, portraitRegionY,
event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
tapAndAssertTrue(100, landscapeRegionY,
@@ -107,10 +129,13 @@
@Test
public void enableMultipleRegions_shouldOverrideFirstRegion() {
- float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
- float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+ float portraitRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+ float landscapeRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
- mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+ mTouchTransformer
+ .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
tapAndAssertFalse(100, portraitRegionY,
event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
tapAndAssertTrue(100, landscapeRegionY,
@@ -136,11 +161,14 @@
@Test
public void enableMultipleRegions_assistantTriggersInMostRecent() {
- float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
- float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+ float portraitRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+ float landscapeRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
mTouchTransformer.enableMultipleRegions(true, mInfo);
- mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+ mTouchTransformer
+ .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
mTouchTransformer.createOrAddTouchRegion(mInfo);
tapAndAssertTrue(0, portraitRegionY,
event -> mTouchTransformer.touchInAssistantRegion(event));
@@ -150,12 +178,15 @@
@Test
public void enableMultipleRegions_assistantTriggersInCurrentOrientationAfterDisable() {
- float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
- float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+ float portraitRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+ float landscapeRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
mTouchTransformer.enableMultipleRegions(true, mInfo);
mTouchTransformer.createOrAddTouchRegion(mInfo);
- mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+ mTouchTransformer
+ .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
mTouchTransformer.enableMultipleRegions(false, mInfo);
tapAndAssertTrue(0, portraitRegionY,
event -> mTouchTransformer.touchInAssistantRegion(event));
@@ -164,6 +195,26 @@
}
@Test
+ public void assistantTriggersInCurrentScreenAfterScreenSizeChange() {
+ float smallerScreenPortraitRegionY =
+ generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+ float largerScreenPortraitRegionY =
+ generateTouchRegionHeight(LARGE_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+
+ mTouchTransformer.enableMultipleRegions(false,
+ createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_0));
+ tapAndAssertTrue(0, smallerScreenPortraitRegionY,
+ event -> mTouchTransformer.touchInAssistantRegion(event));
+
+ mTouchTransformer
+ .enableMultipleRegions(false, createDisplayInfo(LARGE_SCREEN_SIZE, Surface.ROTATION_0));
+ tapAndAssertTrue(0, largerScreenPortraitRegionY,
+ event -> mTouchTransformer.touchInAssistantRegion(event));
+ tapAndAssertFalse(0, smallerScreenPortraitRegionY,
+ event -> mTouchTransformer.touchInAssistantRegion(event));
+ }
+
+ @Test
public void applyTransform_taskNotFrozen_notInRegion() {
mTouchTransformer.createOrAddTouchRegion(mInfo);
tapAndAssertFalse(100, 100,
@@ -182,7 +233,7 @@
public void applyTransform_taskFrozen_noRotate_inRegion() {
mTouchTransformer.createOrAddTouchRegion(mInfo);
mTouchTransformer.enableMultipleRegions(true, mInfo);
- float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+ float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
tapAndAssertTrue(100, y,
event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
}
@@ -190,34 +241,34 @@
@Test
public void applyTransform_taskNotFrozen_noRotate_inDefaultRegion() {
mTouchTransformer.createOrAddTouchRegion(mInfo);
- float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+ float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
tapAndAssertTrue(100, y,
event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
}
@Test
public void applyTransform_taskNotFrozen_90Rotate_inRegion() {
- mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
- float y = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+ mTouchTransformer
+ .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+ float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
tapAndAssertTrue(100, y,
event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
}
@Test
- @Ignore("There's too much that goes into needing to mock a real motion event so the "
- + "transforms in native code get applied correctly. Once that happens then maybe we can"
- + " write slightly more complex unit tests")
- public void applyTransform_taskNotFrozen_90Rotate_inTwoRegions() {
+ public void applyTransform_taskNotFrozen_90Rotate_withTwoRegions() {
mTouchTransformer.createOrAddTouchRegion(mInfo);
mTouchTransformer.enableMultipleRegions(true, mInfo);
- mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+ mTouchTransformer
+ .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
// Landscape point
- float y1 = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+ float y1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
MotionEvent inRegion1_down = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, y1);
MotionEvent inRegion1_up = generateMotionEvent(MotionEvent.ACTION_UP, 10, y1);
// Portrait point in landscape orientation axis
MotionEvent inRegion2 = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, 10);
mTouchTransformer.transform(inRegion1_down);
+ // no-op
mTouchTransformer.transform(inRegion2);
assertTrue(mTouchTransformer.touchInValidSwipeRegions(
inRegion1_down.getX(), inRegion1_down.getY()));
@@ -225,24 +276,49 @@
assertFalse(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
mTouchTransformer.transform(inRegion1_up);
+ }
- // Set the new region with this MotionEvent.ACTION_DOWN
- inRegion2 = generateAndTransformMotionEvent(MotionEvent.ACTION_DOWN, 10, 370);
+ @Test
+ public void applyTransform_90Rotate_inRotatedRegion() {
+ // Create regions for both 0 Rotation and 90 Rotation
+ mTouchTransformer.createOrAddTouchRegion(mInfo);
+ mTouchTransformer.enableMultipleRegions(true, mInfo);
+ mTouchTransformer
+ .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+ // Portrait point in landscape orientation axis
+ float x1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0);
+ // bottom of screen, from landscape perspective right side of screen
+ MotionEvent inRegion2 = generateAndTransformMotionEvent(MotionEvent.ACTION_DOWN, x1, 370);
assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
}
- private DefaultDisplay.Info createDisplayInfo(int rotation) {
- Point p = new Point(SIZE_WIDTH, SIZE_HEIGHT);
+ private DisplayController.Info createDisplayInfo(ScreenSize screenSize, int rotation) {
+ Context context = RuntimeEnvironment.application;
+ Display display = spy(context.getSystemService(DisplayManager.class)
+ .getDisplay(DEFAULT_DISPLAY));
+
+ Point p = new Point(screenSize.mWidth, screenSize.mHeight);
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
- p = new Point(SIZE_HEIGHT, SIZE_WIDTH);
+ p.set(screenSize.mHeight, screenSize.mWidth);
}
- return new DefaultDisplay.Info(0, rotation, 0, p, p, p, null);
+
+ doReturn(rotation).when(display).getRotation();
+ doAnswer(i -> {
+ ((Point) i.getArgument(0)).set(p.x, p.y);
+ return null;
+ }).when(display).getRealSize(any(Point.class));
+ doAnswer(i -> {
+ ((Point) i.getArgument(0)).set(p.x, p.y);
+ ((Point) i.getArgument(1)).set(p.x, p.y);
+ return null;
+ }).when(display).getCurrentSizeRange(any(Point.class), any(Point.class));
+ return new DisplayController.Info(context, display);
}
- private float generateTouchRegionHeight(int rotation) {
- float height = SIZE_HEIGHT;
+ private float generateTouchRegionHeight(ScreenSize screenSize, int rotation) {
+ float height = screenSize.mHeight;
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
- height = SIZE_WIDTH;
+ height = screenSize.mWidth;
}
return height - ResourceUtils.DEFAULT_NAVBAR_VALUE * DENSITY_DISPLAY_METRICS;
}
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
index 93b64e6..9df9ab1 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
@@ -17,6 +17,7 @@
import static com.android.launcher3.util.LauncherUIHelper.doLayout;
+import android.app.ActivityManager.RunningTaskInfo;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
@@ -36,6 +37,7 @@
@RunWith(RobolectricTestRunner.class)
@LooperMode(Mode.PAUSED)
+@org.junit.Ignore
public class RecentsActivityTest {
@Test
@@ -50,7 +52,7 @@
}
@Test
- public void testRecets_showCurrentTask() {
+ public void testRecents_showCurrentTask() {
ActivityController<RecentsActivity> controller =
Robolectric.buildActivity(RecentsActivity.class);
@@ -58,7 +60,10 @@
doLayout(activity);
FallbackRecentsView frv = activity.getOverviewPanel();
- frv.showCurrentTask(22);
+
+ RunningTaskInfo placeholderTask = new RunningTaskInfo();
+ placeholderTask.taskId = 22;
+ frv.showCurrentTask(placeholderTask);
doLayout(activity);
ThumbnailData thumbnailData = new ThumbnailData();
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/util/RecentsOrientedStateTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/util/RecentsOrientedStateTest.java
new file mode 100644
index 0000000..656379f
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/util/RecentsOrientedStateTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_90;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.content.Context;
+
+import com.android.quickstep.FallbackActivityInterface;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+
+/**
+ * Tests for {@link RecentsOrientedState}
+ */
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class RecentsOrientedStateTest {
+
+ private RecentsOrientedState mR1, mR2;
+
+ @Before
+ public void setup() {
+ Context context = RuntimeEnvironment.application;
+ mR1 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { });
+ mR2 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { });
+ assertEquals(mR1.getStateId(), mR2.getStateId());
+ }
+
+ @Test
+ public void stateId_changesWithFlags() {
+ mR1.setGestureActive(true);
+ mR2.setGestureActive(false);
+ assertNotEquals(mR1.getStateId(), mR2.getStateId());
+
+ mR2.setGestureActive(true);
+ assertEquals(mR1.getStateId(), mR2.getStateId());
+ }
+
+ @Test
+ public void stateId_changesWithRecentsRotation() {
+ mR1.setRecentsRotation(ROTATION_90);
+ mR2.setRecentsRotation(ROTATION_180);
+ assertNotEquals(mR1.getStateId(), mR2.getStateId());
+
+ mR2.setRecentsRotation(ROTATION_90);
+ assertEquals(mR1.getStateId(), mR2.getStateId());
+ }
+
+ @Test
+ public void stateId_changesWithDisplayRotation() {
+ mR1.update(ROTATION_0, ROTATION_90);
+ mR2.update(ROTATION_0, ROTATION_180);
+ assertNotEquals(mR1.getStateId(), mR2.getStateId());
+
+ mR2.update(ROTATION_90, ROTATION_90);
+ assertNotEquals(mR1.getStateId(), mR2.getStateId());
+
+ mR2.update(ROTATION_90, ROTATION_0);
+ assertNotEquals(mR1.getStateId(), mR2.getStateId());
+
+ mR2.update(ROTATION_0, ROTATION_90);
+ assertEquals(mR1.getStateId(), mR2.getStateId());
+ }
+}
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
index a31ba21..fd93d98 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -17,6 +17,8 @@
import static android.view.Display.DEFAULT_DISPLAY;
+import static org.mockito.Mockito.mock;
+
import android.content.Context;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -27,8 +29,9 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.shadows.LShadowDisplay;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController;
import com.android.quickstep.LauncherActivityInterface;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
import org.hamcrest.Description;
@@ -144,11 +147,11 @@
LauncherActivityInterface.INSTANCE);
tvs.setDp(mDeviceProfile);
- int launcherRotation = DefaultDisplay.INSTANCE.get(mContext).getInfo().rotation;
+ int launcherRotation = DisplayController.INSTANCE.get(mContext).getInfo().rotation;
if (mAppRotation < 0) {
mAppRotation = launcherRotation;
}
- tvs.setLayoutRotation(launcherRotation, mAppRotation);
+ tvs.getOrientationState().update(launcherRotation, mAppRotation);
if (mAppInsets == null) {
mAppInsets = new Rect(mLauncherInsets);
}
@@ -162,7 +165,7 @@
@Override
public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
SurfaceParams.Builder builder = new SurfaceParams.Builder((SurfaceControl) null);
- proxy.onBuildTargetParams(builder, null, this);
+ proxy.onBuildTargetParams(builder, mock(RemoteAnimationTargetCompat.class), this);
return new SurfaceParams[] {builder.build()};
}
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 47ce320..a44de79 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -19,43 +19,65 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
import static com.android.launcher3.LauncherState.FLAG_HIDE_BACK_BUTTON;
import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.launcher3.LauncherState.NO_OFFSET;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentSender;
-import android.content.SharedPreferences;
+import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.view.View;
+import android.window.SplashScreen;
+
+import androidx.annotation.Nullable;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
+import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.model.WellbeingModel;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter;
import com.android.launcher3.proxy.StartActivityParams;
import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.taskbar.LauncherTaskbarUIController;
+import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarStateHandler;
import com.android.launcher3.uioverrides.RecentsViewStateController;
-import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.ObjectWrapper;
import com.android.launcher3.util.UiThreadHelper;
+import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.SysUINavigationMode.Mode;
import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.QuickstepOnboardingPrefs;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.TouchInteractionService.TISBinder;
import com.android.quickstep.util.RemoteAnimationProvider;
import com.android.quickstep.util.RemoteFadeOutAnimationListener;
-import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.SplitPlaceholderView;
import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import java.util.List;
import java.util.stream.Stream;
/**
@@ -65,37 +87,76 @@
implements NavigationModeChangeListener {
private DepthController mDepthController = new DepthController(this);
+ private QuickstepTransitionManager mAppTransitionManager;
/**
* Reusable command for applying the back button alpha on the background thread.
*/
public static final UiThreadHelper.AsyncCommand SET_BACK_BUTTON_ALPHA =
- (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setBackButtonAlpha(
+ (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setNavBarButtonAlpha(
Float.intBitsToFloat(arg1), arg2 != 0);
- private final ShelfPeekAnim mShelfPeekAnim = new ShelfPeekAnim(this);
-
private OverviewActionsView mActionsView;
- protected HotseatPredictionController mHotseatPredictionController;
+
+ private @Nullable TaskbarManager mTaskbarManager;
+ private @Nullable OverviewCommandHelper mOverviewCommandHelper;
+ private @Nullable LauncherTaskbarUIController mTaskbarUIController;
+ private final ServiceConnection mTisBinderConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ mTaskbarManager = ((TISBinder) iBinder).getTaskbarManager();
+ mTaskbarManager.setLauncher(BaseQuickstepLauncher.this);
+
+ mOverviewCommandHelper = ((TISBinder) iBinder).getOverviewCommandHelper();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) { }
+ };
+ private final TaskbarStateHandler mTaskbarStateHandler = new TaskbarStateHandler(this);
+
+ // Will be updated when dragging from taskbar.
+ private @Nullable DragOptions mNextWorkspaceDragOptions = null;
+ private SplitPlaceholderView mSplitPlaceholderView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this);
addMultiWindowModeChangedListener(mDepthController);
}
@Override
public void onDestroy() {
+ mAppTransitionManager.onActivityDestroyed();
+
SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
+
+
+ unbindService(mTisBinderConnection);
+ if (mTaskbarManager != null) {
+ mTaskbarManager.setLauncher(null);
+ }
super.onDestroy();
}
@Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ if (mOverviewCommandHelper != null) {
+ mOverviewCommandHelper.clearPendingCommands();
+ }
+ }
+
+ public QuickstepTransitionManager getAppTransitionManager() {
+ return mAppTransitionManager;
+ }
+
+ @Override
public void onNavigationModeChanged(Mode newMode) {
getDragLayer().recreateControllers();
- if (mActionsView != null && isOverviewActionsEnabled()) {
+ if (mActionsView != null) {
mActionsView.updateVerticalMargin(newMode);
}
}
@@ -111,15 +172,32 @@
}
@Override
+ protected void handleGestureContract(Intent intent) {
+ if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) {
+ super.handleGestureContract(intent);
+ }
+ }
+
+ @Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
RecentsModel.INSTANCE.get(this).onTrimMemory(level);
}
@Override
- protected void onUiChangedWhileSleeping() {
+ public void onUiChangedWhileSleeping() {
// Remove the snapshot because the content view may have obvious changes.
- ActivityManagerWrapper.getInstance().invalidateHomeTaskSnapshot(this);
+ UI_HELPER_EXECUTOR.execute(
+ () -> ActivityManagerWrapper.getInstance().invalidateHomeTaskSnapshot(this));
+ }
+
+ @Override
+ protected void onScreenOff() {
+ super.onScreenOff();
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ RecentsView recentsView = getOverviewPanel();
+ recentsView.finishRecentsAnimation(true /* toRecents */, null);
+ }
}
@Override
@@ -157,7 +235,18 @@
@Override
protected void onDeferredResumed() {
super.onDeferredResumed();
- if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
+ handlePendingActivityRequest();
+ }
+
+ @Override
+ public void onStateSetEnd(LauncherState state) {
+ super.onStateSetEnd(state);
+ handlePendingActivityRequest();
+ }
+
+ private void handlePendingActivityRequest() {
+ if (mPendingActivityRequestCode != -1 && isInState(NORMAL)
+ && ((getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
// Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher.
onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null);
// ProxyActivityStarter is started with clear task to reset the task after which it
@@ -172,54 +261,84 @@
SysUINavigationMode.INSTANCE.get(this).updateMode();
mActionsView = findViewById(R.id.overview_actions_view);
- ((RecentsView) getOverviewPanel()).init(mActionsView);
+ mSplitPlaceholderView = findViewById(R.id.split_placeholder);
+ RecentsView overviewPanel = (RecentsView) getOverviewPanel();
+ mSplitPlaceholderView.init(
+ new SplitSelectStateController(mHandler, SystemUiProxy.INSTANCE.get(this))
+ );
+ overviewPanel.init(mActionsView, mSplitPlaceholderView);
+ mActionsView.setDp(getDeviceProfile());
+ mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
- if (isOverviewActionsEnabled()) {
- // Overview is above all other launcher elements, including qsb, so move it to the top.
- getOverviewPanel().bringToFront();
- mActionsView.bringToFront();
- mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
- }
+ mAppTransitionManager = new QuickstepTransitionManager(this);
+ mAppTransitionManager.registerRemoteAnimations();
+
+ bindService(new Intent(this, TouchInteractionService.class), mTisBinderConnection, 0);
+
}
- private boolean isOverviewActionsEnabled() {
- return FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(this);
+ public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
+ mTaskbarUIController = taskbarUIController;
}
public <T extends OverviewActionsView> T getActionsView() {
return (T) mActionsView;
}
- @Override
- protected void closeOpenViews(boolean animate) {
- super.closeOpenViews(animate);
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY);
+ public SplitPlaceholderView getSplitPlaceholderView() {
+ return mSplitPlaceholderView;
}
@Override
- protected StateHandler<LauncherState>[] createStateHandlers() {
- return new StateHandler[] {
- getAllAppsController(),
- getWorkspace(),
- getDepthController(),
- new RecentsViewStateController(this),
- new BackButtonAlphaHandler(this)};
+ protected void closeOpenViews(boolean animate) {
+ super.closeOpenViews(animate);
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY);
+ }
+
+ @Override
+ protected void collectStateHandlers(List<StateHandler> out) {
+ super.collectStateHandlers(out);
+ out.add(getDepthController());
+ out.add(new RecentsViewStateController(this));
+ out.add(new BackButtonAlphaHandler(this));
+ out.add(getTaskbarStateHandler());
}
public DepthController getDepthController() {
return mDepthController;
}
+ public @Nullable LauncherTaskbarUIController getTaskbarUIController() {
+ return mTaskbarUIController;
+ }
+
+ public TaskbarStateHandler getTaskbarStateHandler() {
+ return mTaskbarStateHandler;
+ }
+
@Override
- protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) {
- return new QuickstepOnboardingPrefs(this, sharedPrefs);
+ public boolean supportsAdaptiveIconAnimation(View clickedView) {
+ return mAppTransitionManager.hasControlRemoteAppTransitionPermission()
+ && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get();
+ }
+
+ @Override
+ public DragOptions getDefaultWorkspaceDragOptions() {
+ if (mNextWorkspaceDragOptions != null) {
+ DragOptions options = mNextWorkspaceDragOptions;
+ mNextWorkspaceDragOptions = null;
+ return options;
+ }
+ return super.getDefaultWorkspaceDragOptions();
+ }
+
+ public void setNextWorkspaceDragOptions(DragOptions dragOptions) {
+ mNextWorkspaceDragOptions = dragOptions;
}
@Override
public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
- QuickstepAppTransitionManagerImpl appTransitionManager =
- (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
+ QuickstepTransitionManager appTransitionManager = getAppTransitionManager();
appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
@Override
public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
@@ -240,8 +359,16 @@
@Override
public float[] getNormalOverviewScaleAndOffset() {
- return SysUINavigationMode.getMode(this) == Mode.NO_BUTTON
- ? new float[] {1, 1} : new float[] {1.1f, 0};
+ return SysUINavigationMode.getMode(this).hasGestures
+ ? new float[] {1, 1} : new float[] {1.1f, NO_OFFSET};
+ }
+
+ @Override
+ public float getNormalTaskbarScale() {
+ if (mTaskbarUIController != null) {
+ return mTaskbarUIController.getTaskbarScaleOnHome();
+ }
+ return super.getNormalTaskbarScale();
}
@Override
@@ -260,6 +387,12 @@
mDepthController.setActivityStarted(isStarted());
}
+ if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) {
+ if (mTaskbarUIController != null) {
+ mTaskbarUIController.onLauncherResumedOrPaused(hasBeenResumed());
+ }
+ }
+
super.onActivityFlagsChanged(changeBits);
}
@@ -282,8 +415,10 @@
*/
private void onLauncherStateOrFocusChanged() {
boolean shouldBackButtonBeHidden = shouldBackButtonBeHidden(getStateManager().getState());
- UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
- shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+ if (SysUINavigationMode.getMode(this) == TWO_BUTTONS) {
+ UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
+ shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+ }
if (getDragLayer() != null) {
getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
}
@@ -300,19 +435,60 @@
@Override
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
- return Stream.concat(super.getSupportedShortcuts(),
- Stream.of(WellbeingModel.SHORTCUT_FACTORY));
+ return Stream.concat(Stream.of(WellbeingModel.SHORTCUT_FACTORY),
+ super.getSupportedShortcuts());
}
- public ShelfPeekAnim getShelfPeekAnim() {
- return mShelfPeekAnim;
+ @Override
+ public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
+ ActivityOptionsWrapper activityOptions =
+ mAppTransitionManager.hasControlRemoteAppTransitionPermission()
+ ? mAppTransitionManager.getActivityLaunchOptions(v)
+ : super.getActivityLaunchOptions(v, item);
+ if (mLastTouchUpTime > 0) {
+ ActivityOptionsCompat.setLauncherSourceInfo(
+ activityOptions.options, mLastTouchUpTime);
+ }
+ activityOptions.options.setSplashscreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
+ addLaunchCookie(item, activityOptions.options);
+ return activityOptions;
}
/**
- * Returns Prediction controller for hybrid hotseat
+ * Adds a new launch cookie for the activity launch of the given {@param info} if supported.
*/
- public HotseatPredictionController getHotseatPredictionController() {
- return mHotseatPredictionController;
+ public void addLaunchCookie(ItemInfo info, ActivityOptions opts) {
+ if (info == null) {
+ return;
+ }
+ switch (info.container) {
+ case LauncherSettings.Favorites.CONTAINER_DESKTOP:
+ case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
+ // Fall through and continue it's on the workspace (we don't support swiping back
+ // to other containers like all apps or the hotseat predictions (which can change)
+ break;
+ default:
+ if (info.container >= 0) {
+ // Also allow swiping to folders
+ break;
+ }
+ // Reset any existing launch cookies associated with the cookie
+ opts.setLaunchCookie(ObjectWrapper.wrap(NO_MATCHING_ID));
+ return;
+ }
+ switch (info.itemType) {
+ case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+ case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+ // Fall through and continue if it's an app, shortcut, or widget
+ break;
+ default:
+ // Reset any existing launch cookies associated with the cookie
+ opts.setLaunchCookie(ObjectWrapper.wrap(NO_MATCHING_ID));
+ return;
+ }
+ opts.setLaunchCookie(ObjectWrapper.wrap(new Integer(info.id)));
}
public void setHintUserWillBeActive() {
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 02769c6..661053a 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -16,7 +16,9 @@
package com.android.launcher3;
import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
import android.animation.Animator;
@@ -28,45 +30,67 @@
import android.os.Handler;
import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
import androidx.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,
- WrappedAnimationRunnerImpl {
+import java.lang.ref.WeakReference;
- private static final String TAG = "LauncherAnimationRunner";
+/**
+ * This class is needed to wrap any animation runner that is a part of the
+ * RemoteAnimationDefinition:
+ * - Launcher creates a new instance of the LauncherAppTransitionManagerImpl whenever it is
+ * created, which in turn registers a new definition
+ * - When the definition is registered, window manager retains a strong binder reference to the
+ * runner passed in
+ * - If the Launcher activity is recreated, the new definition registered will replace the old
+ * reference in the system's activity record, but until the system server is GC'd, the binder
+ * reference will still exist, which references the runner in the Launcher process, which
+ * references the (old) Launcher activity through this class
+ *
+ * Instead we make the runner provided to the definition static only holding a weak reference to
+ * the runner implementation. When this animation manager is destroyed, we remove the Launcher
+ * reference to the runner, leaving only the weak ref from the runner.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
+
+ private static final RemoteAnimationFactory DEFAULT_FACTORY =
+ (transit, appTargets, wallpaperTargets, nonAppTargets, result) ->
+ result.setAnimation(null, null);
private final Handler mHandler;
private final boolean mStartAtFrontOfQueue;
+ private final WeakReference<RemoteAnimationFactory> mFactory;
+
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) {
+ public LauncherAnimationRunner(Handler handler, RemoteAnimationFactory factory,
+ boolean startAtFrontOfQueue) {
mHandler = handler;
+ mFactory = new WeakReference<>(factory);
mStartAtFrontOfQueue = startAtFrontOfQueue;
}
- public Handler getHandler() {
- return mHandler;
- }
-
- // Called only in R+ platform
+ // Called only in S+ platform
@BinderThread
- public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, Runnable runnable) {
+ public void onAnimationStart(
+ int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ Runnable runnable) {
Runnable r = () -> {
finishExistingAnimation();
- mAnimationResult = new AnimationResult(() -> {
- runnable.run();
- mAnimationResult = null;
- });
- onCreateAnimation(appTargets, wallpaperTargets, mAnimationResult);
+ mAnimationResult = new AnimationResult(() -> mAnimationResult = null, runnable);
+ getFactory().onCreateAnimation(transit, appTargets, wallpaperTargets, nonAppTargets,
+ mAnimationResult);
};
if (mStartAtFrontOfQueue) {
postAtFrontOfQueueAsynchronously(mHandler, r);
@@ -75,6 +99,14 @@
}
}
+ // Called only in R platform
+ @BinderThread
+ public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets, Runnable runnable) {
+ onAnimationStart(0 /* transit */, appTargets, wallpaperTargets,
+ new RemoteAnimationTargetCompat[0], runnable);
+ }
+
// Called only in Q platform
@BinderThread
@Deprecated
@@ -82,14 +114,11 @@
onAnimationStart(appTargets, new RemoteAnimationTargetCompat[0], runnable);
}
- /**
- * Called on the UI thread when the animation targets are received. The implementation must
- * call {@link AnimationResult#setAnimation} with the target animation to be run.
- */
- @UiThread
- public abstract void onCreateAnimation(
- RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result);
+
+ private RemoteAnimationFactory getFactory() {
+ RemoteAnimationFactory factory = mFactory.get();
+ return factory != null ? factory : DEFAULT_FACTORY;
+ }
@UiThread
private void finishExistingAnimation() {
@@ -105,42 +134,69 @@
@BinderThread
@Override
public void onAnimationCancelled() {
- postAsyncCallback(mHandler, this::finishExistingAnimation);
+ postAsyncCallback(mHandler, () -> {
+ finishExistingAnimation();
+ getFactory().onAnimationCancelled();
+ });
}
public static final class AnimationResult {
- private final Runnable mFinishRunnable;
+ private final Runnable mSyncFinishRunnable;
+ private final Runnable mASyncFinishRunnable;
private AnimatorSet mAnimator;
+ private Runnable mOnCompleteCallback;
private boolean mFinished = false;
private boolean mInitialized = false;
- private AnimationResult(Runnable finishRunnable) {
- mFinishRunnable = finishRunnable;
+ private AnimationResult(Runnable syncFinishRunnable, Runnable asyncFinishRunnable) {
+ mSyncFinishRunnable = syncFinishRunnable;
+ mASyncFinishRunnable = asyncFinishRunnable;
}
@UiThread
private void finish() {
if (!mFinished) {
- mFinishRunnable.run();
+ mSyncFinishRunnable.run();
+ UI_HELPER_EXECUTOR.execute(() -> {
+ mASyncFinishRunnable.run();
+ if (mOnCompleteCallback != null) {
+ MAIN_EXECUTOR.execute(mOnCompleteCallback);
+ }
+ });
mFinished = true;
}
}
@UiThread
public void setAnimation(AnimatorSet animation, Context context) {
+ setAnimation(animation, context, null, true);
+ }
+
+ /**
+ * Sets the animation to play for this app launch
+ * @param skipFirstFrame Iff true, we skip the first frame of the animation.
+ * We set to false when skipping first frame causes jank.
+ */
+ @UiThread
+ public void setAnimation(AnimatorSet animation, Context context,
+ @Nullable Runnable onCompleteCallback, boolean skipFirstFrame) {
if (mInitialized) {
throw new IllegalStateException("Animation already initialized");
}
mInitialized = true;
mAnimator = animation;
+ mOnCompleteCallback = onCompleteCallback;
if (mAnimator == null) {
finish();
} else if (mFinished) {
// Animation callback was already finished, skip the animation.
mAnimator.start();
mAnimator.end();
+ if (mOnCompleteCallback != null) {
+ mOnCompleteCallback.run();
+ }
} else {
// Start the animation
mAnimator.addListener(new AnimatorListenerAdapter() {
@@ -151,11 +207,37 @@
});
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(
- Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration()));
+ if (skipFirstFrame) {
+ // Because t=0 has the app icon in its original spot, we can skip the
+ // first frame and have the same movement one frame earlier.
+ mAnimator.setCurrentPlayTime(
+ Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration()));
+ }
}
}
}
+
+ /**
+ * Used with LauncherAnimationRunner as an interface for the runner to call back to the
+ * implementation.
+ */
+ @FunctionalInterface
+ public interface RemoteAnimationFactory {
+
+ /**
+ * Called on the UI thread when the animation targets are received. The implementation must
+ * call {@link AnimationResult#setAnimation} with the target animation to be run.
+ */
+ void onCreateAnimation(int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ LauncherAnimationRunner.AnimationResult result);
+
+ /**
+ * Called when the animation is cancelled. This can happen with or without
+ * the create being called.
+ */
+ default void onAnimationCancelled() { }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 7fb0d43..5fc79f0 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -46,8 +46,8 @@
@Override
public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
if (mRemoteAnimationProvider != null) {
- QuickstepAppTransitionManagerImpl appTransitionManager =
- (QuickstepAppTransitionManagerImpl) launcher.getAppTransitionManager();
+ QuickstepTransitionManager appTransitionManager =
+ ((BaseQuickstepLauncher) launcher).getAppTransitionManager();
// Set a one-time animation provider. After the first call, this will get cleared.
// TODO: Probably also check the intended target id.
diff --git a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
new file mode 100644
index 0000000..96559cb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+
+import java.util.List;
+
+public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate {
+
+ public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) {
+ super(launcher);
+ mActions.put(PIN_PREDICTION, new LauncherAction(
+ PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P));
+ }
+
+ @Override
+ protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
+ if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) {
+ out.add(new LauncherAction(PIN_PREDICTION, R.string.pin_prediction,
+ KeyEvent.KEYCODE_P));
+ }
+ super.getSupportedActions(host, item, out);
+ }
+
+ @Override
+ protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
+ QuickstepLauncher launcher = (QuickstepLauncher) mLauncher;
+ if (action == PIN_PREDICTION) {
+ if (launcher.getHotseatPredictionController() == null) {
+ return false;
+ }
+ launcher.getHotseatPredictionController().pinPrediction(item);
+ return true;
+ }
+ return super.performAction(host, item, action, fromKeyboard);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
deleted file mode 100644
index 10f789d..0000000
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ /dev/null
@@ -1,939 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
-
-import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
-import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
-import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
-import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-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.EXAGGERATED_EASE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
-import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
-import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
-import static com.android.launcher3.statehandlers.DepthController.DEPTH;
-import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
-import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
-import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
-import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.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.Point;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Pair;
-import android.util.TypedValue;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
-import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.util.DynamicResource;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
-import com.android.launcher3.views.FloatingIconView;
-import com.android.quickstep.RemoteAnimationTargets;
-import com.android.quickstep.util.MultiValueUpdateListener;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
-import com.android.quickstep.util.SurfaceTransactionApplier;
-import com.android.systemui.shared.system.ActivityCompat;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
-import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat;
-import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-import com.android.systemui.shared.system.WindowManagerWrapper;
-
-/**
- * {@link LauncherAppTransitionManager} with Quickstep-specific app transitions for launching from
- * home and/or all-apps. Not used for 3p launchers.
- */
-@TargetApi(Build.VERSION_CODES.O)
-@SuppressWarnings("unused")
-public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTransitionManager
- implements OnDeviceProfileChangeListener {
-
- private static final String TAG = "QuickstepTransition";
-
- /** Duration of status bar animations. */
- public static final int STATUS_BAR_TRANSITION_DURATION = 120;
-
- /**
- * Since our animations decelerate heavily when finishing, we want to start status bar animations
- * x ms before the ending.
- */
- public static final int STATUS_BAR_TRANSITION_PRE_DELAY = 96;
-
- private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
- "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
-
- private static final long APP_LAUNCH_DURATION = 450;
- // Use a shorter duration for x or y translation to create a curve effect
- private static final long APP_LAUNCH_CURVED_DURATION = 250;
- private static final long APP_LAUNCH_ALPHA_DURATION = 50;
- private static final long APP_LAUNCH_ALPHA_START_DELAY = 25;
-
- // 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 long APP_LAUNCH_DOWN_DURATION =
- (long) (APP_LAUNCH_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
- private static final long APP_LAUNCH_DOWN_CURVED_DURATION =
- (long) (APP_LAUNCH_CURVED_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
- private static final long APP_LAUNCH_ALPHA_DOWN_DURATION =
- (long) (APP_LAUNCH_ALPHA_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
-
- private static final long CROP_DURATION = 375;
- private static final long RADIUS_DURATION = 375;
-
- public static final int RECENTS_LAUNCH_DURATION = 336;
- private static final int LAUNCHER_RESUME_START_DELAY = 100;
- private static final int CLOSING_TRANSITION_DURATION_MS = 250;
-
- protected static final int CONTENT_ALPHA_DURATION = 217;
- protected static final int CONTENT_TRANSLATION_DURATION = 350;
-
- // 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;
-
- protected final BaseQuickstepLauncher mLauncher;
-
- private final DragLayer mDragLayer;
- private final AlphaProperty mDragLayerAlpha;
-
- final Handler mHandler;
- private final boolean mIsRtl;
-
- private final float mContentTransY;
- private final float mWorkspaceTransY;
- private final float mClosingWindowTransY;
-
- private DeviceProfile mDeviceProfile;
-
- private RemoteAnimationProvider mRemoteAnimationProvider;
- // Strong refs to runners which are cleared when the launcher activity is destroyed
- private WrappedAnimationRunnerImpl mWallpaperOpenRunner;
- private WrappedAnimationRunnerImpl mAppLaunchRunner;
- private WrappedAnimationRunnerImpl mKeyguardGoingAwayRunner;
-
- 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 QuickstepAppTransitionManagerImpl(Context context) {
- mLauncher = Launcher.cast(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);
- }
-
- @Override
- public void onDeviceProfileChanged(DeviceProfile dp) {
- mDeviceProfile = dp;
- }
-
- @Override
- public boolean supportsAdaptiveIconAnimation() {
- return hasControlRemoteAppTransitionPermission()
- && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get();
- }
-
- /**
- * @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()) {
- boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
- mAppLaunchRunner = new AppLaunchAnimationRunner(mHandler, v);
- RemoteAnimationRunnerCompat runner = new WrappedLauncherAnimationRunner<>(
- mAppLaunchRunner, true /* startAtFrontOfQueue */);
-
- // Note that this duration is a guess as we do not know if the animation will be a
- // recents launch or not for sure until we know the opening app targets.
- long duration = fromRecents
- ? RECENTS_LAUNCH_DURATION
- : APP_LAUNCH_DURATION;
-
- long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
- - STATUS_BAR_TRANSITION_PRE_DELAY;
- return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
- runner, duration, statusBarTransitionDelay));
- }
- return super.getActivityLaunchOptions(launcher, v);
- }
-
- /**
- * Whether the launch is a recents app transition and we should do a launch animation
- * from the recents view. Note that if the remote animation targets are not provided, this
- * may not always be correct as we may resolve the opening app to a task when the animation
- * starts.
- *
- * @param v the view to launch from
- * @param targets apps that are opening/closing
- * @return true if the app is launching from recents, false if it most likely is not
- */
- protected abstract boolean isLaunchingFromRecents(@NonNull View v,
- @Nullable RemoteAnimationTargetCompat[] targets);
-
- /**
- * Composes the animations for a launch from the recents list.
- *
- * @param anim the animator set to add to
- * @param v the launching view
- * @param appTargets the apps that are opening/closing
- * @param launcherClosing true if the launcher app is closing
- */
- protected abstract void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
- @NonNull RemoteAnimationTargetCompat[] appTargets,
- @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing);
-
- /**
- * Compose the animations for a launch from the app icon.
- *
- * @param anim the animation to add to
- * @param v the launching view with the icon
- * @param appTargets the list of opening/closing apps
- * @param launcherClosing true if launcher is closing
- */
- private void composeIconLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
- @NonNull RemoteAnimationTargetCompat[] appTargets,
- @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
- boolean launcherClosing) {
- // Set the state animation first so that any state listeners are called
- // before our internal listeners.
- mLauncher.getStateManager().setCurrentAnimation(anim);
-
- Rect windowTargetBounds = getWindowTargetBounds(appTargets);
- boolean isAllOpeningTargetTrs = true;
- for (int i = 0; i < appTargets.length; i++) {
- RemoteAnimationTargetCompat target = appTargets[i];
- if (target.mode == MODE_OPENING) {
- isAllOpeningTargetTrs &= target.isTranslucent;
- }
- if (!isAllOpeningTargetTrs) break;
- }
- anim.play(getOpeningWindowAnimators(v, appTargets, wallpaperTargets, windowTargetBounds,
- !isAllOpeningTargetTrs));
- if (launcherClosing) {
- Pair<AnimatorSet, Runnable> launcherContentAnimator =
- getLauncherContentAnimator(true /* isAppOpening */,
- new float[] {0, -mContentTransY});
- anim.play(launcherContentAnimator.first);
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- launcherContentAnimator.second.run();
- }
- });
- } else {
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mLauncher.addOnResumeCallback(() ->
- ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
- mLauncher.getStateManager().getState().getDepth(mLauncher)).start());
- }
- });
- }
- }
-
- /**
- * 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[] appTargets) {
- Rect bounds = new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx);
- if (mLauncher.isInMultiWindowMode()) {
- for (RemoteAnimationTargetCompat target : appTargets) {
- if (target.mode == MODE_OPENING) {
- bounds.set(target.screenSpaceBounds);
- if (target.localBounds != null) {
- bounds.set(target.localBounds);
- } else {
- 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;
- }
- });
- }
-
- /**
- * Content is everything on screen except the background and the floating view (if any).
- *
- * @param isAppOpening True when this is called when an app is opening.
- * False when this is called when an app is closing.
- * @param trans Array that contains the start and end translation values for the content.
- */
- private Pair<AnimatorSet, Runnable> getLauncherContentAnimator(boolean isAppOpening,
- float[] trans) {
- AnimatorSet launcherAnimator = new AnimatorSet();
- Runnable endListener;
-
- float[] alphas = isAppOpening
- ? new float[] {1, 0}
- : new float[] {0, 1};
-
- 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(CONTENT_ALPHA_DURATION);
- 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(CONTENT_TRANSLATION_DURATION);
-
- 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));
- endListener = composeViewContentAnimator(launcherAnimator, alphas, trans);
- } else {
- mDragLayerAlpha.setValue(alphas[0]);
- ObjectAnimator alpha =
- ObjectAnimator.ofFloat(mDragLayerAlpha, MultiValueAlpha.VALUE, alphas);
- alpha.setDuration(CONTENT_ALPHA_DURATION);
- alpha.setInterpolator(LINEAR);
- launcherAnimator.play(alpha);
-
- Workspace workspace = mLauncher.getWorkspace();
- View currentPage = ((CellLayout) workspace.getChildAt(workspace.getCurrentPage()))
- .getShortcutsAndWidgets();
- View hotseat = mLauncher.getHotseat();
- View qsb = mLauncher.findViewById(R.id.search_container_all_apps);
-
- currentPage.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- qsb.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-
- launcherAnimator.play(ObjectAnimator.ofFloat(currentPage, View.TRANSLATION_Y, trans));
- launcherAnimator.play(ObjectAnimator.ofFloat(hotseat, View.TRANSLATION_Y, trans));
- launcherAnimator.play(ObjectAnimator.ofFloat(qsb, View.TRANSLATION_Y, trans));
-
- // Pause page indicator animations as they lead to layer trashing.
- mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
-
- endListener = () -> {
- currentPage.setTranslationY(0);
- hotseat.setTranslationY(0);
- qsb.setTranslationY(0);
-
- currentPage.setLayerType(View.LAYER_TYPE_NONE, null);
- hotseat.setLayerType(View.LAYER_TYPE_NONE, null);
- qsb.setLayerType(View.LAYER_TYPE_NONE, null);
-
- mDragLayerAlpha.setValue(1f);
- mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
- };
- }
- return new Pair<>(launcherAnimator, endListener);
- }
-
- /**
- * Compose recents view alpha and translation Y animation when launcher opens/closes apps.
- *
- * @param anim the animator set to add to
- * @param alphas the alphas to animate to over time
- * @param trans the translation Y values to animator to over time
- * @return listener to run when the animation ends
- */
- protected abstract Runnable composeViewContentAnimator(@NonNull AnimatorSet anim,
- float[] alphas, float[] trans);
-
- /**
- * @return Animator that controls the window of the opening targets from app icons.
- */
- private Animator getOpeningWindowAnimators(View v,
- RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets,
- Rect windowTargetBounds, boolean toggleVisibility) {
- RectF bounds = new RectF();
- FloatingIconView floatingView = FloatingIconView.getFloatingIconView(mLauncher, v,
- toggleVisibility, bounds, true /* isOpening */);
- Rect crop = new Rect();
- Matrix matrix = new Matrix();
-
- RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
- wallpaperTargets, MODE_OPENING);
- SurfaceTransactionApplier surfaceApplier =
- new SurfaceTransactionApplier(floatingView);
- openingTargets.addReleaseCheck(surfaceApplier);
-
- // Scale the app icon to take up the entire screen. This simplifies the math when
- // animating the app window position / scale.
- float smallestSize = Math.min(windowTargetBounds.height(), windowTargetBounds.width());
- float maxScaleX = smallestSize / bounds.width();
- float maxScaleY = smallestSize / bounds.height();
- float scale = Math.max(maxScaleX, maxScaleY);
- float startScale = 1f;
- if (v instanceof BubbleTextView && !(v.getParent() instanceof DeepShortcutView)) {
- Drawable dr = ((BubbleTextView) v).getIcon();
- if (dr instanceof FastBitmapDrawable) {
- startScale = ((FastBitmapDrawable) dr).getAnimatedScale();
- }
- }
- final float initialStartScale = startScale;
-
- 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 dX = centerX - bounds.centerX();
- float dY = centerY - bounds.centerY();
-
- boolean useUpwardAnimation = bounds.top > centerY
- || Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx;
- final long xDuration = useUpwardAnimation ? APP_LAUNCH_CURVED_DURATION
- : APP_LAUNCH_DOWN_DURATION;
- final long yDuration = useUpwardAnimation ? APP_LAUNCH_DURATION
- : APP_LAUNCH_DOWN_CURVED_DURATION;
- final long alphaDuration = useUpwardAnimation ? APP_LAUNCH_ALPHA_DURATION
- : APP_LAUNCH_ALPHA_DOWN_DURATION;
-
- RectF targetBounds = new RectF(windowTargetBounds);
- RectF iconBounds = new RectF();
- RectF temp = new RectF();
- Point tmpPos = new Point();
-
- AnimatorSet animatorSet = new AnimatorSet();
- ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
- appAnimator.setDuration(APP_LAUNCH_DURATION);
- appAnimator.setInterpolator(LINEAR);
- appAnimator.addListener(floatingView);
- appAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- if (v instanceof BubbleTextView) {
- ((BubbleTextView) v).setStayPressed(false);
- }
- openingTargets.release();
- }
- });
-
- float shapeRevealDuration = APP_LAUNCH_DURATION * SHAPE_PROGRESS_DURATION;
-
- final float startCrop;
- final float endCrop;
- if (mDeviceProfile.isVerticalBarLayout()) {
- startCrop = windowTargetBounds.height();
- endCrop = windowTargetBounds.width();
- } else {
- startCrop = windowTargetBounds.width();
- endCrop = windowTargetBounds.height();
- }
-
- final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources())
- ? startCrop / 2f : 0f;
- final float windowRadius = mDeviceProfile.isMultiWindowMode
- ? 0 : getWindowCornerRadius(mLauncher.getResources());
- appAnimator.addUpdateListener(new MultiValueUpdateListener() {
- FloatProp mDx = new FloatProp(0, dX, 0, xDuration, AGGRESSIVE_EASE);
- FloatProp mDy = new FloatProp(0, dY, 0, yDuration, AGGRESSIVE_EASE);
- FloatProp mScale = new FloatProp(initialStartScale, scale, 0, APP_LAUNCH_DURATION,
- EXAGGERATED_EASE);
- FloatProp mIconAlpha = new FloatProp(1f, 0f, APP_LAUNCH_ALPHA_START_DELAY,
- alphaDuration, LINEAR);
- FloatProp mCroppedSize = new FloatProp(startCrop, endCrop, 0, CROP_DURATION,
- EXAGGERATED_EASE);
- FloatProp mWindowRadius = new FloatProp(initialWindowRadius, windowRadius, 0,
- RADIUS_DURATION, EXAGGERATED_EASE);
-
- @Override
- public void onUpdate(float percent) {
- // Calculate the size.
- float width = bounds.width() * mScale.value;
- float height = bounds.height() * mScale.value;
-
- // Animate the crop so that it starts off as a square.
- final int cropWidth;
- final int cropHeight;
- if (mDeviceProfile.isVerticalBarLayout()) {
- cropWidth = (int) mCroppedSize.value;
- cropHeight = windowTargetBounds.height();
- } else {
- cropWidth = windowTargetBounds.width();
- cropHeight = (int) mCroppedSize.value;
- }
- crop.set(0, 0, cropWidth, cropHeight);
-
- // Scale the size to match the crop.
- float scaleX = width / cropWidth;
- float scaleY = height / cropHeight;
- float scale = Math.min(1f, Math.max(scaleX, scaleY));
-
- float scaledCropWidth = cropWidth * scale;
- float scaledCropHeight = cropHeight * scale;
- float offsetX = (scaledCropWidth - width) / 2;
- float offsetY = (scaledCropHeight - height) / 2;
-
- // Calculate the window position.
- temp.set(bounds);
- temp.offset(dragLayerBounds[0], dragLayerBounds[1]);
- temp.offset(mDx.value, mDy.value);
- Utilities.scaleRectFAboutCenter(temp, mScale.value);
- float windowTransX0 = temp.left - offsetX;
- float windowTransY0 = temp.top - offsetY;
-
- // Calculate the icon position.
- iconBounds.set(bounds);
- iconBounds.offset(mDx.value, mDy.value);
- Utilities.scaleRectFAboutCenter(iconBounds, mScale.value);
- iconBounds.left -= offsetX;
- iconBounds.top -= offsetY;
- iconBounds.right += offsetX;
- iconBounds.bottom += offsetY;
-
- float croppedHeight = (windowTargetBounds.height() - crop.height()) * scale;
- float croppedWidth = (windowTargetBounds.width() - crop.width()) * scale;
- SurfaceParams[] params = new SurfaceParams[appTargets.length];
- for (int i = appTargets.length - 1; i >= 0; i--) {
- RemoteAnimationTargetCompat target = appTargets[i];
- SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
-
- if (target.mode == MODE_OPENING) {
- matrix.setScale(scale, scale);
- matrix.postTranslate(windowTransX0, windowTransY0);
-
- floatingView.update(iconBounds, mIconAlpha.value, percent, 0f,
- mWindowRadius.value * scale, true /* isOpening */);
- builder.withMatrix(matrix)
- .withWindowCrop(crop)
- .withAlpha(1f - mIconAlpha.value)
- .withCornerRadius(mWindowRadius.value);
- } else {
- tmpPos.set(target.position.x, target.position.y);
- if (target.localBounds != null) {
- final Rect localBounds = target.localBounds;
- tmpPos.set(target.localBounds.left, target.localBounds.top);
- }
-
- matrix.setTranslate(tmpPos.x, tmpPos.y);
- final Rect crop = new Rect(target.screenSpaceBounds);
- crop.offsetTo(0, 0);
- builder.withMatrix(matrix)
- .withWindowCrop(crop)
- .withAlpha(1f);
- }
- params[i] = builder.build();
- }
- surfaceApplier.scheduleApply(params);
- }
- });
-
- // When launching an app from overview that doesn't map to a task, we still want to just
- // blur the wallpaper instead of the launcher surface as well
- boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW;
- DepthController depthController = mLauncher.getDepthController();
- ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController, DEPTH,
- BACKGROUND_APP.getDepth(mLauncher))
- .setDuration(APP_LAUNCH_DURATION);
- if (allowBlurringLauncher) {
- depthController.setSurfaceToApp(RemoteAnimationProvider.findLowestOpaqueLayerTarget(
- appTargets, MODE_OPENING));
- backgroundRadiusAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- depthController.setSurfaceToApp(null);
- }
- });
- }
-
- animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
- return animatorSet;
- }
-
- /**
- * Registers remote animations used when closing apps to home screen.
- */
- @Override
- public void registerRemoteAnimations() {
- if (SEPARATE_RECENTS_ACTIVITY.get()) {
- return;
- }
- if (hasControlRemoteAppTransitionPermission()) {
- mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
-
- RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
- definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN,
- WindowManagerWrapper.ACTIVITY_TYPE_STANDARD,
- new RemoteAnimationAdapterCompat(
- new WrappedLauncherAnimationRunner<>(mWallpaperOpenRunner,
- false /* startAtFrontOfQueue */),
- CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
-
- if (KEYGUARD_ANIMATION.get()) {
- mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */);
- definition.addRemoteAnimation(
- WindowManagerWrapper.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
- new RemoteAnimationAdapterCompat(
- new WrappedLauncherAnimationRunner<>(mKeyguardGoingAwayRunner,
- true /* startAtFrontOfQueue */),
- CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
- }
-
- new ActivityCompat(mLauncher).registerRemoteAnimations(definition);
- }
- }
-
- /**
- * Unregisters all remote animations.
- */
- @Override
- public void unregisterRemoteAnimations() {
- if (SEPARATE_RECENTS_ACTIVITY.get()) {
- return;
- }
- if (hasControlRemoteAppTransitionPermission()) {
- new ActivityCompat(mLauncher).unregisterRemoteAnimations();
-
- // Also clear strong references to the runners registered with the remote animation
- // definition so we don't have to wait for the system gc
- mWallpaperOpenRunner = null;
- mAppLaunchRunner = null;
- mKeyguardGoingAwayRunner = null;
- }
- }
-
- 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.
- */
- WrappedAnimationRunnerImpl createWallpaperOpenRunner(boolean fromUnlock) {
- return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock);
- }
-
- /**
- * Animator that controls the transformations of the windows when unlocking the device.
- */
- private Animator getUnlockWindowAnimator(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets) {
- SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
- ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1);
- unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
- float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 :
- QuickStepContract.getWindowCornerRadius(mLauncher.getResources());
- unlockAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- SurfaceParams[] params = new SurfaceParams[appTargets.length];
- for (int i = appTargets.length - 1; i >= 0; i--) {
- RemoteAnimationTargetCompat target = appTargets[i];
- params[i] = new SurfaceParams.Builder(target.leash)
- .withAlpha(1f)
- .withWindowCrop(target.screenSpaceBounds)
- .withCornerRadius(cornerRadius)
- .build();
- }
- surfaceApplier.scheduleApply(params);
- }
- });
- return unlockAnimator;
- }
-
- /**
- * Animator that controls the transformations of the windows the targets that are closing.
- */
- private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets) {
- SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
- Matrix matrix = new Matrix();
- Point tmpPos = new Point();
- ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1);
- int duration = CLOSING_TRANSITION_DURATION_MS;
- float windowCornerRadius = mDeviceProfile.isMultiWindowMode
- ? 0 : getWindowCornerRadius(mLauncher.getResources());
- 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);
-
- @Override
- public void onUpdate(float percent) {
- SurfaceParams[] params = new SurfaceParams[appTargets.length];
- for (int i = appTargets.length - 1; i >= 0; i--) {
- RemoteAnimationTargetCompat target = appTargets[i];
- SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
-
- tmpPos.set(target.position.x, target.position.y);
- if (target.localBounds != null) {
- tmpPos.set(target.localBounds.left, target.localBounds.top);
- }
-
- if (target.mode == MODE_CLOSING) {
- matrix.setScale(mScale.value, mScale.value,
- target.screenSpaceBounds.centerX(),
- target.screenSpaceBounds.centerY());
- matrix.postTranslate(0, mDy.value);
- matrix.postTranslate(tmpPos.x, tmpPos.y);
- builder.withMatrix(matrix)
- .withAlpha(mAlpha.value)
- .withCornerRadius(windowCornerRadius);
- } else {
- matrix.setTranslate(tmpPos.x, tmpPos.y);
- builder.withMatrix(matrix)
- .withAlpha(1f);
- }
- final Rect crop = new Rect(target.screenSpaceBounds);
- crop.offsetTo(0, 0);
- params[i] = builder
- .withWindowCrop(crop)
- .build();
- }
- surfaceApplier.scheduleApply(params);
- }
- });
-
- return closingAnimator;
- }
-
- private boolean hasControlRemoteAppTransitionPermission() {
- return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
- == PackageManager.PERMISSION_GRANTED;
- }
-
- /**
- * Remote animation runner for animation from the app to Launcher, including recents.
- */
- protected class WallpaperOpenLauncherAnimationRunner implements WrappedAnimationRunnerImpl {
-
- private final Handler mHandler;
- private final boolean mFromUnlock;
-
- public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean fromUnlock) {
- mHandler = handler;
- mFromUnlock = fromUnlock;
- }
-
- @Override
- public Handler getHandler() {
- return mHandler;
- }
-
- @Override
- public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets,
- LauncherAnimationRunner.AnimationResult result) {
- if (mLauncher.isDestroyed()) {
- AnimatorSet anim = new AnimatorSet();
- anim.play(getClosingWindowAnimators(appTargets, wallpaperTargets));
- result.setAnimation(anim, mLauncher.getApplicationContext());
- return;
- }
-
- if (!mLauncher.hasBeenResumed()) {
- // If launcher is not resumed, wait until new async-frame after resume
- mLauncher.addOnResumeCallback(() ->
- postAsyncCallback(mHandler, () ->
- onCreateAnimation(appTargets, wallpaperTargets, result)));
- return;
- }
-
- if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) {
- mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS);
- mLauncher.getStateManager().moveToRestState();
- }
-
- AnimatorSet anim = null;
- RemoteAnimationProvider provider = mRemoteAnimationProvider;
- if (provider != null) {
- anim = provider.createWindowAnimation(appTargets, wallpaperTargets);
- }
-
- if (anim == null) {
- anim = new AnimatorSet();
- anim.play(mFromUnlock
- ? getUnlockWindowAnimator(appTargets, wallpaperTargets)
- : getClosingWindowAnimators(appTargets, wallpaperTargets));
-
- // 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(appTargets, MODE_OPENING)
- || mLauncher.isForceInvisible()) {
- // Only register the content animation for cancellation when state changes
- mLauncher.getStateManager().setCurrentAnimation(anim);
-
- if (mLauncher.isInState(LauncherState.ALL_APPS)) {
- Pair<AnimatorSet, Runnable> contentAnimator =
- getLauncherContentAnimator(false /* isAppOpening */,
- new float[] {-mContentTransY, 0});
- 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 {
- float velocityDpPerS = DynamicResource.provider(mLauncher)
- .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
- float velocityPxPerS = TypedValue.applyDimension(COMPLEX_UNIT_DIP,
- velocityDpPerS, mLauncher.getResources().getDisplayMetrics());
- anim.play(new StaggeredWorkspaceAnim(mLauncher, velocityPxPerS, false)
- .getAnimators());
- }
- }
- }
-
- mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
- result.setAnimation(anim, mLauncher);
- }
- }
-
- /**
- * Remote animation runner for animation to launch an app.
- */
- private class AppLaunchAnimationRunner implements WrappedAnimationRunnerImpl {
-
- private final Handler mHandler;
- private final View mV;
-
- AppLaunchAnimationRunner(Handler handler, View v) {
- mHandler = handler;
- mV = v;
- }
-
- @Override
- public Handler getHandler() {
- return mHandler;
- }
-
- @Override
- public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets,
- LauncherAnimationRunner.AnimationResult result) {
- AnimatorSet anim = new AnimatorSet();
-
- boolean launcherClosing =
- launcherIsATargetWithMode(appTargets, MODE_CLOSING);
-
- if (isLaunchingFromRecents(mV, appTargets)) {
- composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
- launcherClosing);
- } else {
- composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
- launcherClosing);
- }
-
- if (launcherClosing) {
- anim.addListener(mForceInvisibleListener);
- }
-
- result.setAnimation(anim, mLauncher);
- }
- }
-}
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
new file mode 100644
index 0000000..5bb76d6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -0,0 +1,1501 @@
+/*
+ * 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 android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE;
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+
+import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
+import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
+import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
+import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
+import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SCRIM_FOR_APP_LAUNCH;
+import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
+import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
+import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
+import static com.android.launcher3.statehandlers.DepthController.DEPTH;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
+import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
+import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
+import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
+import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.util.Pair;
+import android.util.Size;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewTreeObserver;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.icons.FastBitmapDrawable;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.views.ScrimView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.quickstep.RemoteAnimationTargets;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.util.MultiValueUpdateListener;
+import com.android.quickstep.util.RemoteAnimationProvider;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.WorkspaceRevealAnim;
+import com.android.quickstep.views.FloatingWidgetView;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.ActivityCompat;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.BlurUtils;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
+import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat;
+import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.RemoteTransitionCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+import com.android.wm.shell.startingsurface.IStartingWindowListener;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+/**
+ * Manages the opening and closing app transitions from Launcher
+ */
+public class QuickstepTransitionManager implements OnDeviceProfileChangeListener {
+
+ private static final String TAG = "QuickstepTransition";
+
+ private static final boolean ENABLE_SHELL_STARTING_SURFACE =
+ SystemProperties.getBoolean("persist.debug.shell_starting_surface", true);
+
+ /** Duration of status bar animations. */
+ public static final int STATUS_BAR_TRANSITION_DURATION = 120;
+
+ /**
+ * Since our animations decelerate heavily when finishing, we want to start status bar
+ * animations x ms before the ending.
+ */
+ public static final int STATUS_BAR_TRANSITION_PRE_DELAY = 96;
+
+ private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
+ "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
+
+ private static final long APP_LAUNCH_DURATION = 450;
+ // Use a shorter duration for x or y translation to create a curve effect
+ private static final long APP_LAUNCH_CURVED_DURATION = 250;
+ private static final long APP_LAUNCH_ALPHA_DURATION = 50;
+ private static final long APP_LAUNCH_ALPHA_START_DELAY = 25;
+
+ // 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 long APP_LAUNCH_DOWN_DURATION =
+ (long) (APP_LAUNCH_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
+ private static final long APP_LAUNCH_DOWN_CURVED_DURATION =
+ (long) (APP_LAUNCH_CURVED_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
+ private static final long APP_LAUNCH_ALPHA_DOWN_DURATION =
+ (long) (APP_LAUNCH_ALPHA_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
+
+ public static final int ANIMATION_NAV_FADE_IN_DURATION = 266;
+ public static final int ANIMATION_NAV_FADE_OUT_DURATION = 133;
+ public static final long ANIMATION_DELAY_NAV_FADE_IN =
+ APP_LAUNCH_DURATION - ANIMATION_NAV_FADE_IN_DURATION;
+ public static final Interpolator NAV_FADE_IN_INTERPOLATOR =
+ new PathInterpolator(0f, 0f, 0f, 1f);
+ public static final Interpolator NAV_FADE_OUT_INTERPOLATOR =
+ new PathInterpolator(0.2f, 0f, 1f, 1f);
+
+ private static final long CROP_DURATION = 375;
+ private static final long RADIUS_DURATION = 375;
+
+ public static final int RECENTS_LAUNCH_DURATION = 336;
+ private static final int LAUNCHER_RESUME_START_DELAY = 100;
+ private static final int CLOSING_TRANSITION_DURATION_MS = 250;
+
+ public static final int CONTENT_ALPHA_DURATION = 217;
+ protected static final int CONTENT_SCALE_DURATION = 350;
+ protected static final int CONTENT_SCRIM_DURATION = 350;
+
+ private static final int MAX_NUM_TASKS = 5;
+
+ // Cross-fade duration between App Widget and App
+ private static final int WIDGET_CROSSFADE_DURATION_MILLIS = 125;
+
+ protected final BaseQuickstepLauncher mLauncher;
+
+ private final DragLayer mDragLayer;
+ private final AlphaProperty mDragLayerAlpha;
+
+ final Handler mHandler;
+
+ private final float mContentScale;
+ private final float mClosingWindowTransY;
+ private final float mMaxShadowRadius;
+
+ private final StartingWindowListener mStartingWindowListener = new StartingWindowListener();
+
+ private DeviceProfile mDeviceProfile;
+
+ private RemoteAnimationProvider mRemoteAnimationProvider;
+ // Strong refs to runners which are cleared when the launcher activity is destroyed
+ private RemoteAnimationFactory mWallpaperOpenRunner;
+ private RemoteAnimationFactory mAppLaunchRunner;
+ private RemoteAnimationFactory mKeyguardGoingAwayRunner;
+
+ private RemoteAnimationFactory mWallpaperOpenTransitionRunner;
+ private RemoteTransitionCompat mLauncherOpenTransition;
+
+ private 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);
+ }
+ };
+
+ // Pairs of window starting type and starting window background color for starting tasks
+ // Will never be larger than MAX_NUM_TASKS
+ private LinkedHashMap<Integer, Pair<Integer, Integer>> mTaskStartParams;
+
+ public QuickstepTransitionManager(Context context) {
+ mLauncher = Launcher.cast(Launcher.getLauncher(context));
+ mDragLayer = mLauncher.getDragLayer();
+ mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS);
+ mHandler = new Handler(Looper.getMainLooper());
+ mDeviceProfile = mLauncher.getDeviceProfile();
+
+ Resources res = mLauncher.getResources();
+ mContentScale = res.getFloat(R.dimen.content_scale);
+ mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
+ mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius);
+
+ mLauncher.addOnDeviceProfileChangeListener(this);
+
+ if (supportsSSplashScreen()) {
+ mTaskStartParams = new LinkedHashMap<Integer, Pair<Integer, Integer>>(MAX_NUM_TASKS) {
+ @Override
+ protected boolean removeEldestEntry(Entry<Integer, Pair<Integer, Integer>> entry) {
+ return size() > MAX_NUM_TASKS;
+ }
+ };
+
+ mStartingWindowListener.setTransitionManager(this);
+ SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(
+ mStartingWindowListener);
+ }
+ }
+
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile dp) {
+ mDeviceProfile = dp;
+ }
+
+ /**
+ * @return ActivityOptions with remote animations that controls how the window of the opening
+ * targets are displayed.
+ */
+ public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
+ boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
+ RunnableList onEndCallback = new RunnableList();
+ mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback);
+ RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(
+ mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */);
+
+ // Note that this duration is a guess as we do not know if the animation will be a
+ // recents launch or not for sure until we know the opening app targets.
+ long duration = fromRecents
+ ? RECENTS_LAUNCH_DURATION
+ : APP_LAUNCH_DURATION;
+
+ long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
+ - STATUS_BAR_TRANSITION_PRE_DELAY;
+ RemoteAnimationAdapterCompat adapterCompat =
+ new RemoteAnimationAdapterCompat(runner, duration, statusBarTransitionDelay);
+ return new ActivityOptionsWrapper(
+ ActivityOptionsCompat.makeRemoteAnimation(adapterCompat), onEndCallback);
+ }
+
+ /**
+ * Whether the launch is a recents app transition and we should do a launch animation
+ * from the recents view. Note that if the remote animation targets are not provided, this
+ * may not always be correct as we may resolve the opening app to a task when the animation
+ * starts.
+ *
+ * @param v the view to launch from
+ * @param targets apps that are opening/closing
+ * @return true if the app is launching from recents, false if it most likely is not
+ */
+ protected boolean isLaunchingFromRecents(@NonNull View v,
+ @Nullable RemoteAnimationTargetCompat[] targets) {
+ return mLauncher.getStateManager().getState().overviewUi
+ && findTaskViewToLaunch(mLauncher.getOverviewPanel(), v, targets) != null;
+ }
+
+ /**
+ * Composes the animations for a launch from the recents list.
+ *
+ * @param anim the animator set to add to
+ * @param v the launching view
+ * @param appTargets the apps that are opening/closing
+ * @param launcherClosing true if the launcher app is closing
+ */
+ protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
+ @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+ @NonNull RemoteAnimationTargetCompat[] nonAppTargets, boolean launcherClosing) {
+ TaskViewUtils.composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets,
+ nonAppTargets, launcherClosing, mLauncher.getStateManager(),
+ mLauncher.getOverviewPanel(), mLauncher.getDepthController());
+ }
+
+ private boolean areAllTargetsTranslucent(@NonNull RemoteAnimationTargetCompat[] targets) {
+ boolean isAllOpeningTargetTrs = true;
+ for (int i = 0; i < targets.length; i++) {
+ RemoteAnimationTargetCompat target = targets[i];
+ if (target.mode == MODE_OPENING) {
+ isAllOpeningTargetTrs &= target.isTranslucent;
+ }
+ if (!isAllOpeningTargetTrs) break;
+ }
+ return isAllOpeningTargetTrs;
+ }
+
+ /**
+ * Compose the animations for a launch from the app icon.
+ *
+ * @param anim the animation to add to
+ * @param v the launching view with the icon
+ * @param appTargets the list of opening/closing apps
+ * @param launcherClosing true if launcher is closing
+ */
+ private void composeIconLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
+ @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+ @NonNull RemoteAnimationTargetCompat[] nonAppTargets,
+ boolean launcherClosing) {
+ // Set the state animation first so that any state listeners are called
+ // before our internal listeners.
+ mLauncher.getStateManager().setCurrentAnimation(anim);
+
+ final int rotationChange = getRotationChange(appTargets);
+ // Note: the targetBounds are relative to the launcher
+ int startDelay = getSingleFrameMs(mLauncher);
+ Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange);
+ Animator windowAnimator = getOpeningWindowAnimators(v, appTargets, wallpaperTargets,
+ nonAppTargets, windowTargetBounds, areAllTargetsTranslucent(appTargets),
+ rotationChange);
+ windowAnimator.setStartDelay(startDelay);
+ anim.play(windowAnimator);
+ if (launcherClosing) {
+ // Delay animation by a frame to avoid jank.
+ Pair<AnimatorSet, Runnable> launcherContentAnimator =
+ getLauncherContentAnimator(true /* isAppOpening */, startDelay);
+ anim.play(launcherContentAnimator.first);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ launcherContentAnimator.second.run();
+ }
+ });
+ } else {
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mLauncher.addOnResumeCallback(() ->
+ ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
+ mLauncher.getStateManager().getState().getDepth(
+ mLauncher)).start());
+ }
+ });
+ }
+ }
+
+ private void composeWidgetLaunchAnimator(
+ @NonNull AnimatorSet anim,
+ @NonNull LauncherAppWidgetHostView v,
+ @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+ @NonNull RemoteAnimationTargetCompat[] nonAppTargets) {
+ mLauncher.getStateManager().setCurrentAnimation(anim);
+
+ Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
+ anim.play(getOpeningWindowAnimatorsForWidget(v, appTargets, wallpaperTargets, nonAppTargets,
+ windowTargetBounds, areAllTargetsTranslucent(appTargets)));
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mLauncher.addOnResumeCallback(() ->
+ ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
+ mLauncher.getStateManager().getState().getDepth(
+ mLauncher)).start());
+ }
+ });
+ }
+
+ /**
+ * 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(@NonNull RemoteAnimationTargetCompat[] appTargets,
+ int rotationChange) {
+ RemoteAnimationTargetCompat target = null;
+ for (RemoteAnimationTargetCompat t : appTargets) {
+ if (t.mode != MODE_OPENING) continue;
+ target = t;
+ break;
+ }
+ if (target == null) return new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx);
+ final Rect bounds = new Rect(target.screenSpaceBounds);
+ if (target.localBounds != null) {
+ bounds.set(target.localBounds);
+ } else {
+ bounds.offsetTo(target.position.x, target.position.y);
+ }
+ if (rotationChange != 0) {
+ if ((rotationChange % 2) == 1) {
+ // undoing rotation, so our "original" parent size is actually flipped
+ Utilities.rotateBounds(bounds, mDeviceProfile.heightPx, mDeviceProfile.widthPx,
+ 4 - rotationChange);
+ } else {
+ Utilities.rotateBounds(bounds, mDeviceProfile.widthPx, mDeviceProfile.heightPx,
+ 4 - rotationChange);
+ }
+ }
+ return bounds;
+ }
+
+ public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider,
+ CancellationSignal cancellationSignal) {
+ mRemoteAnimationProvider = animationProvider;
+ cancellationSignal.setOnCancelListener(() -> {
+ if (animationProvider == mRemoteAnimationProvider) {
+ mRemoteAnimationProvider = null;
+ }
+ });
+ }
+
+ /**
+ * Content is everything on screen except the background and the floating view (if any).
+ *
+ * @param isAppOpening True when this is called when an app is opening.
+ * False when this is called when an app is closing.
+ * @param startDelay Start delay duration.
+ */
+ private Pair<AnimatorSet, Runnable> getLauncherContentAnimator(boolean isAppOpening,
+ int startDelay) {
+ AnimatorSet launcherAnimator = new AnimatorSet();
+ Runnable endListener;
+
+ float[] alphas = isAppOpening
+ ? new float[]{1, 0}
+ : new float[]{0, 1};
+
+ float[] scales = isAppOpening
+ ? new float[]{1, mContentScale}
+ : new float[]{mContentScale, 1};
+
+ 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 startScale = SCALE_PROPERTY.get(appsView);
+ appsView.setAlpha(alphas[0]);
+ SCALE_PROPERTY.set(appsView, scales[0]);
+
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas);
+ alpha.setDuration(CONTENT_ALPHA_DURATION);
+ 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 scale = ObjectAnimator.ofFloat(appsView, SCALE_PROPERTY, scales);
+ scale.setInterpolator(AGGRESSIVE_EASE);
+ scale.setDuration(CONTENT_SCALE_DURATION);
+
+ launcherAnimator.play(alpha);
+ launcherAnimator.play(scale);
+
+ endListener = () -> {
+ appsView.setAlpha(startAlpha);
+ SCALE_PROPERTY.set(appsView, startScale);
+ appsView.setLayerType(View.LAYER_TYPE_NONE, null);
+ };
+ } else if (mLauncher.isInState(OVERVIEW)) {
+ endListener = composeViewContentAnimator(launcherAnimator, alphas, scales);
+ } else {
+ List<View> viewsToAnimate = new ArrayList<>();
+ Workspace workspace = mLauncher.getWorkspace();
+ workspace.forEachVisiblePage(
+ view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets()));
+
+ viewsToAnimate.add(mLauncher.getHotseat());
+
+ viewsToAnimate.forEach(view -> {
+ view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+ ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scales)
+ .setDuration(CONTENT_SCALE_DURATION);
+ scaleAnim.setInterpolator(DEACCEL_1_5);
+ launcherAnimator.play(scaleAnim);
+ });
+
+ final boolean scrimEnabled = ENABLE_SCRIM_FOR_APP_LAUNCH.get();
+ if (scrimEnabled) {
+ int scrimColor = Themes.getAttrColor(mLauncher, R.attr.overviewScrimColor);
+ int scrimColorTrans = ColorUtils.setAlphaComponent(scrimColor, 0);
+ int[] colors = isAppOpening
+ ? new int[]{scrimColorTrans, scrimColor}
+ : new int[]{scrimColor, scrimColorTrans};
+ ScrimView scrimView = mLauncher.getScrimView();
+ if (scrimView.getBackground() instanceof ColorDrawable) {
+ scrimView.setBackgroundColor(colors[0]);
+
+ ObjectAnimator scrim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
+ colors);
+ scrim.setDuration(CONTENT_SCRIM_DURATION);
+ scrim.setInterpolator(DEACCEL_1_5);
+ launcherAnimator.play(scrim);
+ }
+ }
+
+ // Pause page indicator animations as they lead to layer trashing.
+ mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
+
+ endListener = () -> {
+ viewsToAnimate.forEach(view -> {
+ SCALE_PROPERTY.set(view, 1f);
+ view.setLayerType(View.LAYER_TYPE_NONE, null);
+ });
+ if (scrimEnabled) {
+ mLauncher.getScrimView().setBackgroundColor(Color.TRANSPARENT);
+ }
+ mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
+ };
+ }
+
+ launcherAnimator.setStartDelay(startDelay);
+ return new Pair<>(launcherAnimator, endListener);
+ }
+
+ /**
+ * Compose recents view alpha and translation Y animation when launcher opens/closes apps.
+ *
+ * @param anim the animator set to add to
+ * @param alphas the alphas to animate to over time
+ * @param scales the scale values to animator to over time
+ * @return listener to run when the animation ends
+ */
+ protected Runnable composeViewContentAnimator(@NonNull AnimatorSet anim,
+ float[] alphas, float[] scales) {
+ RecentsView overview = mLauncher.getOverviewPanel();
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(overview,
+ RecentsView.CONTENT_ALPHA, alphas);
+ alpha.setDuration(CONTENT_ALPHA_DURATION);
+ alpha.setInterpolator(LINEAR);
+ anim.play(alpha);
+ overview.setFreezeViewVisibility(true);
+
+ ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(overview, SCALE_PROPERTY, scales);
+ scaleAnim.setInterpolator(AGGRESSIVE_EASE);
+ scaleAnim.setDuration(CONTENT_SCALE_DURATION);
+ anim.play(scaleAnim);
+
+ return () -> {
+ overview.setFreezeViewVisibility(false);
+ SCALE_PROPERTY.set(overview, 1f);
+ mLauncher.getStateManager().reapplyState();
+ };
+ }
+
+ /**
+ * @return Animator that controls the window of the opening targets from app icons.
+ */
+ private Animator getOpeningWindowAnimators(View v,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ Rect windowTargetBounds, boolean appTargetsAreTranslucent, int rotationChange) {
+ RectF launcherIconBounds = new RectF();
+ FloatingIconView floatingView = FloatingIconView.getFloatingIconView(mLauncher, v,
+ !appTargetsAreTranslucent, launcherIconBounds, true /* isOpening */);
+ Rect crop = new Rect();
+ Matrix matrix = new Matrix();
+
+ RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
+ wallpaperTargets, nonAppTargets, MODE_OPENING);
+ SurfaceTransactionApplier surfaceApplier =
+ new SurfaceTransactionApplier(floatingView);
+ openingTargets.addReleaseCheck(surfaceApplier);
+ RemoteAnimationTargetCompat navBarTarget = openingTargets.getNavBarRemoteAnimationTarget();
+
+ int[] dragLayerBounds = new int[2];
+ mDragLayer.getLocationOnScreen(dragLayerBounds);
+
+ final boolean hasSplashScreen;
+ if (supportsSSplashScreen()) {
+ int taskId = openingTargets.getFirstAppTargetTaskId();
+ Pair<Integer, Integer> defaultParams = Pair.create(STARTING_WINDOW_TYPE_NONE, 0);
+ Pair<Integer, Integer> taskParams =
+ mTaskStartParams.getOrDefault(taskId, defaultParams);
+ mTaskStartParams.remove(taskId);
+ hasSplashScreen = taskParams.first == STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+ } else {
+ hasSplashScreen = false;
+ }
+
+ AnimOpenProperties prop = new AnimOpenProperties(mLauncher.getResources(), mDeviceProfile,
+ windowTargetBounds, launcherIconBounds, v, dragLayerBounds[0], dragLayerBounds[1],
+ hasSplashScreen, floatingView.isDifferentFromAppIcon());
+ int left = (int) (prop.cropCenterXStart - prop.cropWidthStart / 2);
+ int top = (int) (prop.cropCenterYStart - prop.cropHeightStart / 2);
+ int right = (int) (left + prop.cropWidthStart);
+ int bottom = (int) (top + prop.cropHeightStart);
+ // Set the crop here so we can calculate the corner radius below.
+ crop.set(left, top, right, bottom);
+
+ RectF floatingIconBounds = new RectF();
+ RectF tmpRectF = new RectF();
+ Point tmpPos = new Point();
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+ appAnimator.setDuration(APP_LAUNCH_DURATION);
+ appAnimator.setInterpolator(LINEAR);
+ appAnimator.addListener(floatingView);
+ appAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (v instanceof BubbleTextView) {
+ ((BubbleTextView) v).setStayPressed(false);
+ }
+ openingTargets.release();
+ }
+ });
+
+ final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources())
+ ? Math.max(crop.width(), crop.height()) / 2f
+ : 0f;
+ final float finalWindowRadius = mDeviceProfile.isMultiWindowMode
+ ? 0 : getWindowCornerRadius(mLauncher.getResources());
+ final float finalShadowRadius = appTargetsAreTranslucent ? 0 : mMaxShadowRadius;
+
+ MultiValueUpdateListener listener = new MultiValueUpdateListener() {
+ FloatProp mDx = new FloatProp(0, prop.dX, 0, prop.xDuration, AGGRESSIVE_EASE);
+ FloatProp mDy = new FloatProp(0, prop.dY, 0, prop.yDuration, AGGRESSIVE_EASE);
+
+ FloatProp mIconScaleToFitScreen = new FloatProp(prop.initialAppIconScale,
+ prop.finalAppIconScale, 0, APP_LAUNCH_DURATION, EXAGGERATED_EASE);
+ FloatProp mIconAlpha = new FloatProp(prop.iconAlphaStart, 0f,
+ APP_LAUNCH_ALPHA_START_DELAY, prop.alphaDuration, LINEAR);
+
+ FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius, 0,
+ RADIUS_DURATION, EXAGGERATED_EASE);
+ FloatProp mShadowRadius = new FloatProp(0, finalShadowRadius, 0,
+ APP_LAUNCH_DURATION, EXAGGERATED_EASE);
+
+ FloatProp mCropRectCenterX = new FloatProp(prop.cropCenterXStart, prop.cropCenterXEnd,
+ 0, CROP_DURATION, EXAGGERATED_EASE);
+ FloatProp mCropRectCenterY = new FloatProp(prop.cropCenterYStart, prop.cropCenterYEnd,
+ 0, CROP_DURATION, EXAGGERATED_EASE);
+ FloatProp mCropRectWidth = new FloatProp(prop.cropWidthStart, prop.cropWidthEnd, 0,
+ CROP_DURATION, EXAGGERATED_EASE);
+ FloatProp mCropRectHeight = new FloatProp(prop.cropHeightStart, prop.cropHeightEnd, 0,
+ CROP_DURATION, EXAGGERATED_EASE);
+
+ FloatProp mNavFadeOut = new FloatProp(1f, 0f, 0, ANIMATION_NAV_FADE_OUT_DURATION,
+ NAV_FADE_OUT_INTERPOLATOR);
+ FloatProp mNavFadeIn = new FloatProp(0f, 1f, ANIMATION_DELAY_NAV_FADE_IN,
+ ANIMATION_NAV_FADE_IN_DURATION, NAV_FADE_IN_INTERPOLATOR);
+
+ @Override
+ public void onUpdate(float percent, boolean initOnly) {
+ // Calculate the size of the scaled icon.
+ float iconWidth = launcherIconBounds.width() * mIconScaleToFitScreen.value;
+ float iconHeight = launcherIconBounds.height() * mIconScaleToFitScreen.value;
+
+ int left = (int) (mCropRectCenterX.value - mCropRectWidth.value / 2);
+ int top = (int) (mCropRectCenterY.value - mCropRectHeight.value / 2);
+ int right = (int) (left + mCropRectWidth.value);
+ int bottom = (int) (top + mCropRectHeight.value);
+ crop.set(left, top, right, bottom);
+
+ final int windowCropWidth = crop.width();
+ final int windowCropHeight = crop.height();
+ if (rotationChange != 0) {
+ Utilities.rotateBounds(crop, mDeviceProfile.widthPx,
+ mDeviceProfile.heightPx, rotationChange);
+ }
+
+ // Scale the size of the icon to match the size of the window crop.
+ float scaleX = iconWidth / windowCropWidth;
+ float scaleY = iconHeight / windowCropHeight;
+ float scale = Math.min(1f, Math.max(scaleX, scaleY));
+
+ float scaledCropWidth = windowCropWidth * scale;
+ float scaledCropHeight = windowCropHeight * scale;
+ float offsetX = (scaledCropWidth - iconWidth) / 2;
+ float offsetY = (scaledCropHeight - iconHeight) / 2;
+
+ // Calculate the window position to match the icon position.
+ tmpRectF.set(launcherIconBounds);
+ tmpRectF.offset(dragLayerBounds[0], dragLayerBounds[1]);
+ tmpRectF.offset(mDx.value, mDy.value);
+ Utilities.scaleRectFAboutCenter(tmpRectF, mIconScaleToFitScreen.value);
+ float windowTransX0 = tmpRectF.left - offsetX - crop.left * scale;
+ float windowTransY0 = tmpRectF.top - offsetY - crop.top * scale;
+
+ // Calculate the icon position.
+ floatingIconBounds.set(launcherIconBounds);
+ floatingIconBounds.offset(mDx.value, mDy.value);
+ Utilities.scaleRectFAboutCenter(floatingIconBounds, mIconScaleToFitScreen.value);
+ floatingIconBounds.left -= offsetX;
+ floatingIconBounds.top -= offsetY;
+ floatingIconBounds.right += offsetX;
+ floatingIconBounds.bottom += offsetY;
+
+ if (initOnly) {
+ // For the init pass, we want full alpha since the window is not yet ready.
+ floatingView.update(1f, 255, floatingIconBounds, percent, 0f,
+ mWindowRadius.value * scale, true /* isOpening */);
+ return;
+ }
+
+ ArrayList<SurfaceParams> params = new ArrayList<>();
+ for (int i = appTargets.length - 1; i >= 0; i--) {
+ RemoteAnimationTargetCompat target = appTargets[i];
+ SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+
+ if (target.mode == MODE_OPENING) {
+ matrix.setScale(scale, scale);
+ if (rotationChange == 1) {
+ matrix.postTranslate(windowTransY0,
+ mDeviceProfile.widthPx - (windowTransX0 + scaledCropWidth));
+ } else if (rotationChange == 2) {
+ matrix.postTranslate(
+ mDeviceProfile.widthPx - (windowTransX0 + scaledCropWidth),
+ mDeviceProfile.heightPx - (windowTransY0 + scaledCropHeight));
+ } else if (rotationChange == 3) {
+ matrix.postTranslate(
+ mDeviceProfile.heightPx - (windowTransY0 + scaledCropHeight),
+ windowTransX0);
+ } else {
+ matrix.postTranslate(windowTransX0, windowTransY0);
+ }
+
+ floatingView.update(mIconAlpha.value, 255, floatingIconBounds, percent, 0f,
+ mWindowRadius.value * scale, true /* isOpening */);
+ builder.withMatrix(matrix)
+ .withWindowCrop(crop)
+ .withAlpha(1f - mIconAlpha.value)
+ .withCornerRadius(mWindowRadius.value)
+ .withShadowRadius(mShadowRadius.value);
+ } else if (target.mode == MODE_CLOSING) {
+ if (target.localBounds != null) {
+ final Rect localBounds = target.localBounds;
+ tmpPos.set(target.localBounds.left, target.localBounds.top);
+ } else {
+ tmpPos.set(target.position.x, target.position.y);
+ }
+ final Rect crop = new Rect(target.screenSpaceBounds);
+ crop.offsetTo(0, 0);
+
+ if ((rotationChange % 2) == 1) {
+ int tmp = crop.right;
+ crop.right = crop.bottom;
+ crop.bottom = tmp;
+ tmp = tmpPos.x;
+ tmpPos.x = tmpPos.y;
+ tmpPos.y = tmp;
+ }
+ matrix.setTranslate(tmpPos.x, tmpPos.y);
+ builder.withMatrix(matrix)
+ .withWindowCrop(crop)
+ .withAlpha(1f);
+ }
+ params.add(builder.build());
+ }
+
+ if (navBarTarget != null) {
+ final SurfaceParams.Builder navBuilder =
+ new SurfaceParams.Builder(navBarTarget.leash);
+ if (mNavFadeIn.value > mNavFadeIn.getStartValue()) {
+ matrix.setScale(scale, scale);
+ matrix.postTranslate(windowTransX0, windowTransY0);
+ navBuilder.withMatrix(matrix)
+ .withWindowCrop(crop)
+ .withAlpha(mNavFadeIn.value);
+ } else {
+ navBuilder.withAlpha(mNavFadeOut.value);
+ }
+ params.add(navBuilder.build());
+ }
+
+ surfaceApplier.scheduleApply(params.toArray(new SurfaceParams[params.size()]));
+ }
+ };
+ appAnimator.addUpdateListener(listener);
+ // Since we added a start delay, call update here to init the FloatingIconView properly.
+ listener.onUpdate(0, true /* initOnly */);
+
+ animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+ return animatorSet;
+ }
+
+ private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets, Rect windowTargetBounds,
+ boolean appTargetsAreTranslucent) {
+ final RectF widgetBackgroundBounds = new RectF();
+ final Rect appWindowCrop = new Rect();
+ final Matrix matrix = new Matrix();
+ RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
+ wallpaperTargets, nonAppTargets, MODE_OPENING);
+
+ RemoteAnimationTargetCompat openingTarget = openingTargets.getFirstAppTarget();
+ int fallbackBackgroundColor = 0;
+ if (openingTarget != null && supportsSSplashScreen()) {
+ fallbackBackgroundColor = mTaskStartParams.containsKey(openingTarget.taskId)
+ ? mTaskStartParams.get(openingTarget.taskId).second : 0;
+ mTaskStartParams.remove(openingTarget.taskId);
+ }
+ if (fallbackBackgroundColor == 0) {
+ fallbackBackgroundColor =
+ FloatingWidgetView.getDefaultBackgroundColor(mLauncher, openingTarget);
+ }
+
+ final float finalWindowRadius = mDeviceProfile.isMultiWindowMode
+ ? 0 : getWindowCornerRadius(mLauncher.getResources());
+ final FloatingWidgetView floatingView = FloatingWidgetView.getFloatingWidgetView(mLauncher,
+ v, widgetBackgroundBounds,
+ new Size(windowTargetBounds.width(), windowTargetBounds.height()),
+ finalWindowRadius, appTargetsAreTranslucent, fallbackBackgroundColor);
+ final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources())
+ ? floatingView.getInitialCornerRadius() : 0;
+
+ SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(floatingView);
+ openingTargets.addReleaseCheck(surfaceApplier);
+
+ RemoteAnimationTargetCompat navBarTarget = openingTargets.getNavBarRemoteAnimationTarget();
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+ appAnimator.setDuration(APP_LAUNCH_DURATION);
+ appAnimator.setInterpolator(LINEAR);
+ appAnimator.addListener(floatingView);
+ appAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ openingTargets.release();
+ }
+ });
+ floatingView.setFastFinishRunnable(animatorSet::end);
+
+ appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+ float mAppWindowScale = 1;
+ final FloatProp mWidgetForegroundAlpha = new FloatProp(1 /* start */,
+ 0 /* end */, 0 /* delay */,
+ WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+ final FloatProp mWidgetFallbackBackgroundAlpha = new FloatProp(0 /* start */,
+ 1 /* end */, 0 /* delay */, 75 /* duration */, LINEAR);
+ final FloatProp mPreviewAlpha = new FloatProp(0 /* start */, 1 /* end */,
+ WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* delay */,
+ WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+ final FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius,
+ 0 /* start */, RADIUS_DURATION, LINEAR);
+ final FloatProp mCornerRadiusProgress = new FloatProp(0, 1, 0, RADIUS_DURATION, LINEAR);
+
+ // Window & widget background positioning bounds
+ final FloatProp mDx = new FloatProp(widgetBackgroundBounds.centerX(),
+ windowTargetBounds.centerX(), 0 /* delay */, APP_LAUNCH_CURVED_DURATION,
+ EXAGGERATED_EASE);
+ final FloatProp mDy = new FloatProp(widgetBackgroundBounds.centerY(),
+ windowTargetBounds.centerY(), 0 /* delay */, APP_LAUNCH_DURATION,
+ EXAGGERATED_EASE);
+ final FloatProp mWidth = new FloatProp(widgetBackgroundBounds.width(),
+ windowTargetBounds.width(), 0 /* delay */, APP_LAUNCH_DURATION,
+ EXAGGERATED_EASE);
+ final FloatProp mHeight = new FloatProp(widgetBackgroundBounds.height(),
+ windowTargetBounds.height(), 0 /* delay */, APP_LAUNCH_DURATION,
+ EXAGGERATED_EASE);
+
+ final FloatProp mNavFadeOut = new FloatProp(1f, 0f, 0, ANIMATION_NAV_FADE_OUT_DURATION,
+ NAV_FADE_OUT_INTERPOLATOR);
+ final FloatProp mNavFadeIn = new FloatProp(0f, 1f, ANIMATION_DELAY_NAV_FADE_IN,
+ ANIMATION_NAV_FADE_IN_DURATION, NAV_FADE_IN_INTERPOLATOR);
+
+ @Override
+ public void onUpdate(float percent, boolean initOnly) {
+ widgetBackgroundBounds.set(mDx.value - mWidth.value / 2f,
+ mDy.value - mHeight.value / 2f, mDx.value + mWidth.value / 2f,
+ mDy.value + mHeight.value / 2f);
+ // Set app window scaling factor to match widget background width
+ mAppWindowScale = widgetBackgroundBounds.width() / windowTargetBounds.width();
+ // Crop scaled app window to match widget
+ appWindowCrop.set(0 /* left */, 0 /* top */,
+ Math.round(windowTargetBounds.width()) /* right */,
+ Math.round(widgetBackgroundBounds.height() / mAppWindowScale) /* bottom */);
+ matrix.setTranslate(widgetBackgroundBounds.left, widgetBackgroundBounds.top);
+ matrix.postScale(mAppWindowScale, mAppWindowScale, widgetBackgroundBounds.left,
+ widgetBackgroundBounds.top);
+
+ ArrayList<SurfaceParams> params = new ArrayList<>();
+ float floatingViewAlpha = appTargetsAreTranslucent ? 1 - mPreviewAlpha.value : 1;
+ for (int i = appTargets.length - 1; i >= 0; i--) {
+ RemoteAnimationTargetCompat target = appTargets[i];
+ SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+ if (target.mode == MODE_OPENING) {
+ floatingView.update(widgetBackgroundBounds, floatingViewAlpha,
+ mWidgetForegroundAlpha.value, mWidgetFallbackBackgroundAlpha.value,
+ mCornerRadiusProgress.value);
+ builder.withMatrix(matrix)
+ .withWindowCrop(appWindowCrop)
+ .withAlpha(mPreviewAlpha.value)
+ .withCornerRadius(mWindowRadius.value / mAppWindowScale);
+ }
+ params.add(builder.build());
+ }
+
+ if (navBarTarget != null) {
+ final SurfaceParams.Builder navBuilder =
+ new SurfaceParams.Builder(navBarTarget.leash);
+ if (mNavFadeIn.value > mNavFadeIn.getStartValue()) {
+ navBuilder.withMatrix(matrix)
+ .withWindowCrop(appWindowCrop)
+ .withAlpha(mNavFadeIn.value);
+ } else {
+ navBuilder.withAlpha(mNavFadeOut.value);
+ }
+ params.add(navBuilder.build());
+ }
+
+ surfaceApplier.scheduleApply(params.toArray(new SurfaceParams[params.size()]));
+ }
+ });
+
+ animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+ return animatorSet;
+ }
+
+ private ObjectAnimator getBackgroundAnimator(RemoteAnimationTargetCompat[] appTargets) {
+ // When launching an app from overview that doesn't map to a task, we still want to just
+ // blur the wallpaper instead of the launcher surface as well
+ boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW;
+ DepthController depthController = mLauncher.getDepthController();
+ ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController, DEPTH,
+ BACKGROUND_APP.getDepth(mLauncher))
+ .setDuration(APP_LAUNCH_DURATION);
+ if (allowBlurringLauncher) {
+ final SurfaceControl dimLayer;
+ if (BlurUtils.supportsBlursOnWindows()) {
+ // Create a temporary effect layer, that lives on top of launcher, so we can apply
+ // the blur to it. The EffectLayer will be fullscreen, which will help with caching
+ // optimizations on the SurfaceFlinger side:
+ // - Results would be able to be cached as a texture
+ // - There won't be texture allocation overhead, because EffectLayers don't have
+ // buffers
+ ViewRootImpl viewRootImpl = mLauncher.getDragLayer().getViewRootImpl();
+ SurfaceControl parent = viewRootImpl != null
+ ? viewRootImpl.getSurfaceControl()
+ : null;
+ dimLayer = new SurfaceControl.Builder()
+ .setName("Blur layer")
+ .setParent(parent)
+ .setOpaque(false)
+ .setHidden(false)
+ .setEffectLayer()
+ .build();
+ } else {
+ dimLayer = null;
+ }
+
+ depthController.setSurface(dimLayer);
+ backgroundRadiusAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ depthController.setIsInLaunchTransition(true);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ depthController.setIsInLaunchTransition(false);
+ depthController.setSurface(null);
+ if (dimLayer != null) {
+ new SurfaceControl.Transaction()
+ .remove(dimLayer)
+ .apply();
+ }
+ }
+ });
+ }
+ return backgroundRadiusAnim;
+ }
+
+ /**
+ * Registers remote animations used when closing apps to home screen.
+ */
+ public void registerRemoteAnimations() {
+ if (SEPARATE_RECENTS_ACTIVITY.get()) {
+ return;
+ }
+ if (hasControlRemoteAppTransitionPermission()) {
+ mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
+
+ RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
+ definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN,
+ WindowManagerWrapper.ACTIVITY_TYPE_STANDARD,
+ new RemoteAnimationAdapterCompat(
+ new LauncherAnimationRunner(mHandler, mWallpaperOpenRunner,
+ false /* startAtFrontOfQueue */),
+ CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
+
+ if (KEYGUARD_ANIMATION.get()) {
+ mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */);
+ definition.addRemoteAnimation(
+ WindowManagerWrapper.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
+ new RemoteAnimationAdapterCompat(
+ new LauncherAnimationRunner(
+ mHandler, mKeyguardGoingAwayRunner,
+ true /* startAtFrontOfQueue */),
+ CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
+ }
+
+ new ActivityCompat(mLauncher).registerRemoteAnimations(definition);
+ }
+ }
+
+ /**
+ * Registers remote animations used when closing apps to home screen.
+ */
+ public void registerRemoteTransitions() {
+ if (SEPARATE_RECENTS_ACTIVITY.get()) {
+ return;
+ }
+ if (hasControlRemoteAppTransitionPermission()) {
+ mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */);
+ mLauncherOpenTransition = RemoteAnimationAdapterCompat.buildRemoteTransition(
+ new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner,
+ false /* startAtFrontOfQueue */));
+ mLauncherOpenTransition.addHomeOpenCheck();
+ SystemUiProxy.INSTANCE.getNoCreate().registerRemoteTransition(mLauncherOpenTransition);
+ }
+ }
+
+ public void onActivityDestroyed() {
+ unregisterRemoteAnimations();
+ unregisterRemoteTransitions();
+ mStartingWindowListener.setTransitionManager(null);
+ SystemUiProxy.INSTANCE.getNoCreate().setStartingWindowListener(null);
+ }
+
+ private void unregisterRemoteAnimations() {
+ if (SEPARATE_RECENTS_ACTIVITY.get()) {
+ return;
+ }
+ if (hasControlRemoteAppTransitionPermission()) {
+ new ActivityCompat(mLauncher).unregisterRemoteAnimations();
+
+ // Also clear strong references to the runners registered with the remote animation
+ // definition so we don't have to wait for the system gc
+ mWallpaperOpenRunner = null;
+ mAppLaunchRunner = null;
+ mKeyguardGoingAwayRunner = null;
+ }
+ }
+
+ private void unregisterRemoteTransitions() {
+ if (SEPARATE_RECENTS_ACTIVITY.get()) {
+ return;
+ }
+ if (hasControlRemoteAppTransitionPermission()) {
+ if (mLauncherOpenTransition == null) return;
+ SystemUiProxy.INSTANCE.getNoCreate().unregisterRemoteTransition(
+ mLauncherOpenTransition);
+ mLauncherOpenTransition = null;
+ mWallpaperOpenTransitionRunner = null;
+ }
+ }
+
+ 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.
+ */
+ RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) {
+ return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock);
+ }
+
+ /**
+ * Animator that controls the transformations of the windows when unlocking the device.
+ */
+ private Animator getUnlockWindowAnimator(RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets) {
+ SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
+ ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1);
+ unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
+ float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 :
+ QuickStepContract.getWindowCornerRadius(mLauncher.getResources());
+ unlockAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ SurfaceParams[] params = new SurfaceParams[appTargets.length];
+ for (int i = appTargets.length - 1; i >= 0; i--) {
+ RemoteAnimationTargetCompat target = appTargets[i];
+ params[i] = new SurfaceParams.Builder(target.leash)
+ .withAlpha(1f)
+ .withWindowCrop(target.screenSpaceBounds)
+ .withCornerRadius(cornerRadius)
+ .build();
+ }
+ surfaceApplier.scheduleApply(params);
+ }
+ });
+ return unlockAnimator;
+ }
+
+ private static int getRotationChange(RemoteAnimationTargetCompat[] appTargets) {
+ int rotationChange = 0;
+ for (RemoteAnimationTargetCompat target : appTargets) {
+ if (Math.abs(target.rotationChange) > Math.abs(rotationChange)) {
+ rotationChange = target.rotationChange;
+ }
+ }
+ return rotationChange;
+ }
+
+ /**
+ * Animator that controls the transformations of the windows the targets that are closing.
+ */
+ private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets) {
+ final int rotationChange = getRotationChange(appTargets);
+ SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
+ Matrix matrix = new Matrix();
+ Point tmpPos = new Point();
+ Rect tmpRect = new Rect();
+ ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1);
+ int duration = CLOSING_TRANSITION_DURATION_MS;
+ float windowCornerRadius = mDeviceProfile.isMultiWindowMode
+ ? 0 : getWindowCornerRadius(mLauncher.getResources());
+ float startShadowRadius = areAllTargetsTranslucent(appTargets) ? 0 : mMaxShadowRadius;
+ 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);
+ FloatProp mShadowRadius = new FloatProp(startShadowRadius, 0, 0, duration,
+ DEACCEL_1_7);
+
+ @Override
+ public void onUpdate(float percent, boolean initOnly) {
+ SurfaceParams[] params = new SurfaceParams[appTargets.length];
+ for (int i = appTargets.length - 1; i >= 0; i--) {
+ RemoteAnimationTargetCompat target = appTargets[i];
+ SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+
+ if (target.localBounds != null) {
+ tmpPos.set(target.localBounds.left, target.localBounds.top);
+ } else {
+ tmpPos.set(target.position.x, target.position.y);
+ }
+
+ final Rect crop = new Rect(target.screenSpaceBounds);
+ crop.offsetTo(0, 0);
+ if (target.mode == MODE_CLOSING) {
+ tmpRect.set(target.screenSpaceBounds);
+ if ((rotationChange % 2) != 0) {
+ final int right = crop.right;
+ crop.right = crop.bottom;
+ crop.bottom = right;
+ }
+ matrix.setScale(mScale.value, mScale.value,
+ tmpRect.centerX(),
+ tmpRect.centerY());
+ matrix.postTranslate(0, mDy.value);
+ matrix.postTranslate(tmpPos.x, tmpPos.y);
+ builder.withMatrix(matrix)
+ .withWindowCrop(crop)
+ .withAlpha(mAlpha.value)
+ .withCornerRadius(windowCornerRadius)
+ .withShadowRadius(mShadowRadius.value);
+ } else if (target.mode == MODE_OPENING) {
+ matrix.setTranslate(tmpPos.x, tmpPos.y);
+ builder.withMatrix(matrix)
+ .withWindowCrop(crop)
+ .withAlpha(1f);
+ }
+ params[i] = builder.build();
+ }
+ surfaceApplier.scheduleApply(params);
+ }
+ });
+
+ return closingAnimator;
+ }
+
+ private boolean supportsSSplashScreen() {
+ return hasControlRemoteAppTransitionPermission()
+ && Utilities.ATLEAST_S
+ && ENABLE_SHELL_STARTING_SURFACE;
+ }
+
+ /**
+ * Returns true if we have permission to control remote app transisions
+ */
+ public boolean hasControlRemoteAppTransitionPermission() {
+ return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private void addCujInstrumentation(Animator anim, int cuj) {
+ anim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mDragLayer.getViewTreeObserver().addOnDrawListener(
+ new ViewTreeObserver.OnDrawListener() {
+ boolean mHandled = false;
+
+ @Override
+ public void onDraw() {
+ if (mHandled) {
+ return;
+ }
+ mHandled = true;
+
+ InteractionJankMonitorWrapper.begin(mDragLayer, cuj);
+
+ mDragLayer.post(() ->
+ mDragLayer.getViewTreeObserver().removeOnDrawListener(
+ this));
+ }
+ });
+ super.onAnimationStart(animation);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ InteractionJankMonitorWrapper.cancel(cuj);
+ }
+
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ InteractionJankMonitorWrapper.end(cuj);
+ }
+ });
+ }
+
+ /**
+ * Remote animation runner for animation from the app to Launcher, including recents.
+ */
+ protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory {
+
+ private final Handler mHandler;
+ private final boolean mFromUnlock;
+
+ public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean fromUnlock) {
+ mHandler = handler;
+ mFromUnlock = fromUnlock;
+ }
+
+ @Override
+ public void onCreateAnimation(int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ LauncherAnimationRunner.AnimationResult result) {
+ if (mLauncher.isDestroyed()) {
+ AnimatorSet anim = new AnimatorSet();
+ anim.play(getClosingWindowAnimators(appTargets, wallpaperTargets));
+ result.setAnimation(anim, mLauncher.getApplicationContext());
+ return;
+ }
+
+ if (!mLauncher.hasBeenResumed()) {
+ // If launcher is not resumed, wait until new async-frame after resume
+ mLauncher.addOnResumeCallback(() ->
+ postAsyncCallback(mHandler, () ->
+ onCreateAnimation(transit, appTargets, wallpaperTargets,
+ nonAppTargets, result)));
+ return;
+ }
+
+ if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) {
+ mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS);
+ mLauncher.getStateManager().moveToRestState();
+ }
+
+ AnimatorSet anim = null;
+ RemoteAnimationProvider provider = mRemoteAnimationProvider;
+ if (provider != null) {
+ anim = provider.createWindowAnimation(appTargets, wallpaperTargets);
+ }
+
+ if (anim == null) {
+ anim = new AnimatorSet();
+ anim.play(mFromUnlock
+ ? getUnlockWindowAnimator(appTargets, wallpaperTargets)
+ : getClosingWindowAnimators(appTargets, wallpaperTargets));
+
+ // 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(appTargets, MODE_OPENING)
+ || mLauncher.isForceInvisible()) {
+ addCujInstrumentation(
+ anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
+ // Only register the content animation for cancellation when state changes
+ mLauncher.getStateManager().setCurrentAnimation(anim);
+
+ if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+ Pair<AnimatorSet, Runnable> contentAnimator =
+ getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY);
+ anim.play(contentAnimator.first);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ contentAnimator.second.run();
+ }
+ });
+ } else {
+ anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
+ }
+ }
+ }
+
+ mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
+ result.setAnimation(anim, mLauncher);
+ }
+ }
+
+ /**
+ * Remote animation runner for animation to launch an app.
+ */
+ private class AppLaunchAnimationRunner implements RemoteAnimationFactory {
+
+ private final View mV;
+ private final RunnableList mOnEndCallback;
+
+ AppLaunchAnimationRunner(View v, RunnableList onEndCallback) {
+ mV = v;
+ mOnEndCallback = onEndCallback;
+ }
+
+ @Override
+ public void onCreateAnimation(int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ LauncherAnimationRunner.AnimationResult result) {
+ AnimatorSet anim = new AnimatorSet();
+ boolean launcherClosing =
+ launcherIsATargetWithMode(appTargets, MODE_CLOSING);
+
+ final boolean launchingFromWidget = mV instanceof LauncherAppWidgetHostView;
+ final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
+ final boolean skipFirstFrame;
+ if (launchingFromWidget) {
+ composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
+ wallpaperTargets, nonAppTargets);
+ addCujInstrumentation(
+ anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_WIDGET);
+ skipFirstFrame = true;
+ } else if (launchingFromRecents) {
+ composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
+ launcherClosing);
+ addCujInstrumentation(
+ anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_RECENTS);
+ skipFirstFrame = true;
+ } else {
+ composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
+ launcherClosing);
+ addCujInstrumentation(anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_ICON);
+ skipFirstFrame = false;
+ }
+
+ if (launcherClosing) {
+ anim.addListener(mForceInvisibleListener);
+ }
+
+ result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy,
+ skipFirstFrame);
+ }
+
+ @Override
+ public void onAnimationCancelled() {
+ mOnEndCallback.executeAllAndDestroy();
+ }
+ }
+
+ /**
+ * Class that holds all the variables for the app open animation.
+ */
+ static class AnimOpenProperties {
+
+ public final int cropCenterXStart;
+ public final int cropCenterYStart;
+ public final int cropWidthStart;
+ public final int cropHeightStart;
+
+ public final int cropCenterXEnd;
+ public final int cropCenterYEnd;
+ public final int cropWidthEnd;
+ public final int cropHeightEnd;
+
+ public final float dX;
+ public final float dY;
+
+ public final long xDuration;
+ public final long yDuration;
+ public final long alphaDuration;
+
+ public final float initialAppIconScale;
+ public final float finalAppIconScale;
+
+ public final float iconAlphaStart;
+
+ AnimOpenProperties(Resources r, DeviceProfile dp, Rect windowTargetBounds,
+ RectF launcherIconBounds, View view, int dragLayerLeft, int dragLayerTop,
+ boolean hasSplashScreen, boolean hasDifferentAppIcon) {
+ // Scale the app icon to take up the entire screen. This simplifies the math when
+ // animating the app window position / scale.
+ float smallestSize = Math.min(windowTargetBounds.height(), windowTargetBounds.width());
+ float maxScaleX = smallestSize / launcherIconBounds.width();
+ float maxScaleY = smallestSize / launcherIconBounds.height();
+ float iconStartScale = 1f;
+ if (view instanceof BubbleTextView && !(view.getParent() instanceof DeepShortcutView)) {
+ Drawable dr = ((BubbleTextView) view).getIcon();
+ if (dr instanceof FastBitmapDrawable) {
+ iconStartScale = ((FastBitmapDrawable) dr).getAnimatedScale();
+ }
+ }
+
+ initialAppIconScale = iconStartScale;
+ finalAppIconScale = Math.max(maxScaleX, maxScaleY);
+
+ // Animate the app icon to the center of the window bounds in screen coordinates.
+ float centerX = windowTargetBounds.centerX() - dragLayerLeft;
+ float centerY = windowTargetBounds.centerY() - dragLayerTop;
+
+ dX = centerX - launcherIconBounds.centerX();
+ dY = centerY - launcherIconBounds.centerY();
+
+ boolean useUpwardAnimation = launcherIconBounds.top > centerY
+ || Math.abs(dY) < dp.cellHeightPx;
+ xDuration = useUpwardAnimation ? APP_LAUNCH_CURVED_DURATION
+ : APP_LAUNCH_DOWN_DURATION;
+ yDuration = useUpwardAnimation ? APP_LAUNCH_DURATION
+ : APP_LAUNCH_DOWN_CURVED_DURATION;
+ alphaDuration = useUpwardAnimation ? APP_LAUNCH_ALPHA_DURATION
+ : APP_LAUNCH_ALPHA_DOWN_DURATION;
+ iconAlphaStart = hasSplashScreen && !hasDifferentAppIcon ? 0 : 1f;
+
+ final int windowIconSize = ResourceUtils.getDimenByName("starting_surface_icon_size",
+ r, 108);
+
+ cropCenterXStart = windowTargetBounds.centerX();
+ cropCenterYStart = windowTargetBounds.centerY();
+
+ cropWidthStart = windowIconSize;
+ cropHeightStart = windowIconSize;
+
+ cropWidthEnd = windowTargetBounds.width();
+ cropHeightEnd = windowTargetBounds.height();
+
+ cropCenterXEnd = windowTargetBounds.centerX();
+ cropCenterYEnd = windowTargetBounds.centerY();
+ }
+ }
+
+ private static class StartingWindowListener extends IStartingWindowListener.Stub {
+ private QuickstepTransitionManager mTransitionManager;
+
+ public void setTransitionManager(QuickstepTransitionManager transitionManager) {
+ mTransitionManager = transitionManager;
+ }
+
+ @Override
+ public void onTaskLaunching(int taskId, int supportedType, int color) {
+ mTransitionManager.mTaskStartParams.put(taskId, Pair.create(supportedType, color));
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
deleted file mode 100644
index da2aee4..0000000
--- a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.os.Handler;
-
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Used with WrappedLauncherAnimationRunner as an interface for the runner to call back to the
- * implementation.
- */
-public interface WrappedAnimationRunnerImpl {
- Handler getHandler();
- void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets,
- LauncherAnimationRunner.AnimationResult result);
-}
diff --git a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
deleted file mode 100644
index 1753b62..0000000
--- a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.lang.ref.WeakReference;
-
-/**
- * This class is needed to wrap any animation runner that is a part of the
- * RemoteAnimationDefinition:
- * - Launcher creates a new instance of the LauncherAppTransitionManagerImpl whenever it is
- * created, which in turn registers a new definition
- * - When the definition is registered, window manager retains a strong binder reference to the
- * runner passed in
- * - If the Launcher activity is recreated, the new definition registered will replace the old
- * reference in the system's activity record, but until the system server is GC'd, the binder
- * reference will still exist, which references the runner in the Launcher process, which
- * references the (old) Launcher activity through this class
- *
- * Instead we make the runner provided to the definition static only holding a weak reference to
- * the runner implementation. When this animation manager is destroyed, we remove the Launcher
- * reference to the runner, leaving only the weak ref from the runner.
- */
-public class WrappedLauncherAnimationRunner<R extends WrappedAnimationRunnerImpl>
- extends LauncherAnimationRunner {
- private WeakReference<R> mImpl;
-
- public WrappedLauncherAnimationRunner(R animationRunnerImpl, boolean startAtFrontOfQueue) {
- super(animationRunnerImpl.getHandler(), startAtFrontOfQueue);
- mImpl = new WeakReference<>(animationRunnerImpl);
- }
-
- @Override
- public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
- R animationRunnerImpl = mImpl.get();
- if (animationRunnerImpl != null) {
- animationRunnerImpl.onCreateAnimation(appTargets, wallpaperTargets, result);
- }
- }
-}
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
new file mode 100644
index 0000000..63a569a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.appprediction;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.util.Themes;
+
+/**
+ * A view which shows a horizontal divider
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class AppsDividerView extends View implements StateListener<LauncherState>,
+ FloatingHeaderRow {
+
+ private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
+ private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20;
+
+ public enum DividerType {
+ NONE,
+ LINE,
+ ALL_APPS_LABEL
+ }
+
+ private final Launcher mLauncher;
+ private final TextPaint mPaint = new TextPaint();
+ private DividerType mDividerType = DividerType.NONE;
+
+ private final @ColorInt int mStrokeColor;
+ private final @ColorInt int mAllAppsLabelTextColor;
+
+ private Layout mAllAppsLabelLayout;
+ private boolean mShowAllAppsLabel;
+
+ private FloatingHeaderView mParent;
+ private boolean mTabsHidden;
+ private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS;
+
+ private boolean mIsScrolledOut = false;
+
+ private final int[] mDividerSize;
+
+ public AppsDividerView(Context context) {
+ this(context, null);
+ }
+
+ public AppsDividerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+
+ boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
+ mDividerSize = new int[]{
+ getResources().getDimensionPixelSize(R.dimen.all_apps_divider_width),
+ getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
+ };
+
+ mStrokeColor = ContextCompat.getColor(context, isMainColorDark
+ ? R.color.all_apps_prediction_row_separator_dark
+ : R.color.all_apps_prediction_row_separator);
+
+ mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark
+ ? R.color.all_apps_label_text_dark
+ : R.color.all_apps_label_text);
+ }
+
+ public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
+ mParent = parent;
+ mTabsHidden = tabsHidden;
+ mRows = rows;
+ updateDividerType();
+ }
+
+ @Override
+ public int getExpectedHeight() {
+ return getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ public boolean shouldDraw() {
+ return mDividerType != DividerType.NONE;
+ }
+
+ @Override
+ public boolean hasVisibleContent() {
+ return false;
+ }
+
+ private void updateDividerType() {
+ final DividerType dividerType;
+ if (!mTabsHidden) {
+ dividerType = DividerType.NONE;
+ } else {
+ // Check how many sections above me.
+ int sectionCount = 0;
+ for (FloatingHeaderRow row : mRows) {
+ if (row == this) {
+ break;
+ } else if (row.shouldDraw()) {
+ sectionCount++;
+ }
+ }
+
+ if (mShowAllAppsLabel && sectionCount > 0) {
+ dividerType = DividerType.ALL_APPS_LABEL;
+ } else if (sectionCount == 1) {
+ dividerType = DividerType.LINE;
+ } else {
+ dividerType = DividerType.NONE;
+ }
+ }
+
+ if (mDividerType != dividerType) {
+ mDividerType = dividerType;
+ int topPadding;
+ int bottomPadding;
+ switch (dividerType) {
+ case LINE:
+ topPadding = 0;
+ bottomPadding = getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
+ mPaint.setColor(mStrokeColor);
+ break;
+ case ALL_APPS_LABEL:
+ topPadding = getAllAppsLabelLayout().getHeight() + getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_label_top_padding);
+ bottomPadding = getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
+ mPaint.setColor(mAllAppsLabelTextColor);
+ break;
+ case NONE:
+ default:
+ topPadding = bottomPadding = 0;
+ break;
+ }
+ setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding);
+ updateViewVisibility();
+ invalidate();
+ requestLayout();
+ if (mParent != null) {
+ mParent.onHeightUpdated();
+ }
+ }
+ }
+
+ private void updateViewVisibility() {
+ setVisibility(mDividerType == DividerType.NONE
+ ? GONE
+ : (mIsScrolledOut ? INVISIBLE : VISIBLE));
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mDividerType == DividerType.LINE) {
+ int l = (getWidth() - getPaddingLeft() - mDividerSize[0]) / 2;
+ int t = getHeight() - (getPaddingBottom() / 2);
+ int radius = mDividerSize[1];
+ canvas.drawRoundRect(l, t, l + mDividerSize[0], t + mDividerSize[1], radius, radius,
+ mPaint);
+ } else if (mDividerType == DividerType.ALL_APPS_LABEL) {
+ Layout textLayout = getAllAppsLabelLayout();
+ int x = getWidth() / 2 - textLayout.getWidth() / 2;
+ int y = getHeight() - getPaddingBottom() - textLayout.getHeight();
+ canvas.translate(x, y);
+ textLayout.draw(canvas);
+ canvas.translate(-x, -y);
+ }
+ }
+
+ private Layout getAllAppsLabelLayout() {
+ if (mAllAppsLabelLayout == null) {
+ mPaint.setAntiAlias(true);
+ mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
+ mPaint.setTextSize(
+ getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size));
+
+ CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label);
+ mAllAppsLabelLayout = StaticLayout.Builder.obtain(
+ allAppsLabelText, 0, allAppsLabelText.length(), mPaint,
+ Math.round(mPaint.measureText(allAppsLabelText.toString())))
+ .setAlignment(Layout.Alignment.ALIGN_CENTER)
+ .setMaxLines(1)
+ .setIncludePad(true)
+ .build();
+ }
+ return mAllAppsLabelLayout;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+ getPaddingBottom() + getPaddingTop());
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (shouldShowAllAppsLabel()) {
+ mShowAllAppsLabel = true;
+ mLauncher.getStateManager().addStateListener(this);
+ updateDividerType();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mLauncher.getStateManager().removeStateListener(this);
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == ALL_APPS) {
+ setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
+ } else {
+ if (mShowAllAppsLabel != shouldShowAllAppsLabel()) {
+ mShowAllAppsLabel = !mShowAllAppsLabel;
+ updateDividerType();
+ }
+
+ if (!mShowAllAppsLabel) {
+ mLauncher.getStateManager().removeStateListener(this);
+ }
+ }
+ }
+
+ private void setAllAppsVisitedCount(int count) {
+ mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply();
+ }
+
+ private int getAllAppsVisitedCount() {
+ return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0);
+ }
+
+ private boolean shouldShowAllAppsLabel() {
+ return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT;
+ }
+
+ @Override
+ public void setInsets(Rect insets, DeviceProfile grid) {
+ int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+ + grid.cellLayoutPaddingLeftRightPx;
+ setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+ }
+
+ @Override
+ public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+ setTranslationY(scroll);
+ mIsScrolledOut = isScrolledOut;
+ updateViewVisibility();
+ }
+
+ @Override
+ public Class<AppsDividerView> getTypeClass() {
+ return AppsDividerView.class;
+ }
+
+ @Override
+ public View getFocusedChild() {
+ return null;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java b/quickstep/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
rename to quickstep/src/com/android/launcher3/appprediction/InstantAppItemInfo.java
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
new file mode 100644
index 0000000..6afbf9a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.appprediction;
+
+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.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.FloatingHeaderRow;
+import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.keyboard.FocusIndicatorHelper;
+import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class PredictionRowView extends LinearLayout implements
+ OnDeviceProfileChangeListener, FloatingHeaderRow {
+
+ private final Launcher mLauncher;
+ private int mNumPredictedAppsPerRow;
+
+ // Helper to drawing the focus indicator.
+ private final FocusIndicatorHelper mFocusHelper;
+
+ // The set of predicted apps resolved from the component names and the current set of apps
+ private final List<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
+
+ private FloatingHeaderView mParent;
+ private boolean mScrolledOut;
+
+ private boolean mPredictionsEnabled = false;
+
+ @Nullable
+ private List<ItemInfo> mPendingPredictedItems;
+
+ public PredictionRowView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(LinearLayout.HORIZONTAL);
+
+ mFocusHelper = new SimpleFocusIndicatorHelper(this);
+ mLauncher = Launcher.getLauncher(context);
+ mLauncher.addOnDeviceProfileChangeListener(this);
+ mNumPredictedAppsPerRow = mLauncher.getDeviceProfile().numShownAllAppsColumns;
+ updateVisibility();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ }
+
+ public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
+ mParent = parent;
+ }
+
+ private void updateVisibility() {
+ setVisibility(mPredictionsEnabled ? VISIBLE : GONE);
+ if (mLauncher.getAppsView() != null) {
+ if (mPredictionsEnabled) {
+ mLauncher.getAppsView().getAppsStore().registerIconContainer(this);
+ } else {
+ mLauncher.getAppsView().getAppsStore().unregisterIconContainer(this);
+ }
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(),
+ MeasureSpec.EXACTLY));
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ mFocusHelper.draw(canvas);
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ public int getExpectedHeight() {
+ return getVisibility() == GONE ? 0 :
+ Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx
+ + getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ public boolean shouldDraw() {
+ return getVisibility() != GONE;
+ }
+
+ @Override
+ public boolean hasVisibleContent() {
+ return mPredictionsEnabled;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return getVisibility() == VISIBLE;
+ }
+
+ /**
+ * Returns the predicted apps.
+ */
+ public List<ItemInfoWithIcon> getPredictedApps() {
+ return new ArrayList<>(mPredictedApps);
+ }
+
+ /**
+ * Sets the current set of predicted apps.
+ *
+ * This can be called before we get the full set of applications, we should merge the results
+ * only in onPredictionsUpdated() which is idempotent.
+ *
+ * If the number of predicted apps is the same as the previous list of predicted apps,
+ * we can optimize by swapping them in place.
+ */
+ public void setPredictedApps(List<ItemInfo> items) {
+ if (!FeatureFlags.ENABLE_APP_PREDICTIONS_WHILE_VISIBLE.get()
+ && !mLauncher.isWorkspaceLoading()
+ && isShown()
+ && getWindowVisibility() == View.VISIBLE) {
+ mPendingPredictedItems = items;
+ return;
+ }
+
+ applyPredictedApps(items);
+ }
+
+ private void applyPredictedApps(List<ItemInfo> items) {
+ mPendingPredictedItems = null;
+ mPredictedApps.clear();
+ items.stream()
+ .filter(itemInfo -> itemInfo instanceof WorkspaceItemInfo)
+ .map(itemInfo -> (WorkspaceItemInfo) itemInfo)
+ .forEach(mPredictedApps::add);
+ applyPredictionApps();
+ }
+
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile dp) {
+ mNumPredictedAppsPerRow = dp.numShownAllAppsColumns;
+ removeAllViews();
+ applyPredictionApps();
+ }
+
+ private void applyPredictionApps() {
+ if (getChildCount() != mNumPredictedAppsPerRow) {
+ while (getChildCount() > mNumPredictedAppsPerRow) {
+ removeViewAt(0);
+ }
+ LayoutInflater inflater = mLauncher.getAppsView().getLayoutInflater();
+ while (getChildCount() < mNumPredictedAppsPerRow) {
+ BubbleTextView icon = (BubbleTextView) inflater.inflate(
+ R.layout.all_apps_icon, this, false);
+ icon.setOnClickListener(ItemClickHandler.INSTANCE);
+ icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
+ icon.setLongPressTimeoutFactor(1f);
+ icon.setOnFocusChangeListener(mFocusHelper);
+
+ LayoutParams lp = (LayoutParams) icon.getLayoutParams();
+ // Ensure the all apps icon height matches the workspace icons in portrait mode.
+ lp.height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
+ lp.width = 0;
+ lp.weight = 1;
+ addView(icon);
+ }
+ }
+
+ int predictionCount = mPredictedApps.size();
+
+ for (int i = 0; i < getChildCount(); i++) {
+ BubbleTextView icon = (BubbleTextView) getChildAt(i);
+ icon.reset();
+ if (predictionCount > i) {
+ icon.setVisibility(View.VISIBLE);
+ icon.applyFromWorkspaceItem(mPredictedApps.get(i));
+ } else {
+ icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
+ }
+ }
+
+ boolean predictionsEnabled = predictionCount > 0;
+ if (predictionsEnabled != mPredictionsEnabled) {
+ mPredictionsEnabled = predictionsEnabled;
+ mLauncher.reapplyUi(false /* cancelCurrentAnimation */);
+ updateVisibility();
+ }
+ mParent.onHeightUpdated();
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+
+ @Override
+ public void setVerticalScroll(int scroll, boolean isScrolledOut) {
+ mScrolledOut = isScrolledOut;
+ if (!isScrolledOut) {
+ setTranslationY(scroll);
+ }
+ setAlpha(mScrolledOut ? 0 : 1);
+ if (getVisibility() != GONE) {
+ AlphaUpdateListener.updateVisibility(this);
+ }
+ }
+
+ @Override
+ public void setInsets(Rect insets, DeviceProfile grid) {
+ int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx
+ + grid.cellLayoutPaddingLeftRightPx;
+ setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
+ }
+
+ @Override
+ public Class<PredictionRowView> getTypeClass() {
+ return PredictionRowView.class;
+ }
+
+ @Override
+ public View getFocusedChild() {
+ return getChildAt(0);
+ }
+
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+
+ if (mPendingPredictedItems != null && !isVisible) {
+ applyPredictedApps(mPendingPredictedItems);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
new file mode 100644
index 0000000..3a7d821
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.hybridhotseat;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.ActivityTracker;
+
+/**
+ * Proxy activity to return user to home screen and show halfsheet education
+ */
+public class HotseatEduActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_HOME)
+ .setPackage(getPackageName())
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ Launcher.ACTIVITY_TRACKER.registerCallback(new HotseatActivityTracker());
+ startActivity(homeIntent);
+ finish();
+ }
+
+ static class HotseatActivityTracker<T extends QuickstepLauncher> implements
+ ActivityTracker.SchedulerCallback {
+
+ @Override
+ public boolean init(BaseActivity activity, boolean alreadyOnHome) {
+ QuickstepLauncher launcher = (QuickstepLauncher) activity;
+ if (launcher != null) {
+ launcher.getHotseatPredictionController().showEdu();
+ }
+ return false;
+ }
+
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
new file mode 100644
index 0000000..a6844e4
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
+ .LAUNCHER_HOTSEAT_EDU_ONLY_TIP;
+
+import android.content.Intent;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.views.ArrowTipView;
+import com.android.launcher3.views.Snackbar;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+
+/**
+ * Controller class for managing user onboaridng flow for hybrid hotseat
+ */
+public class HotseatEduController {
+
+ public static final String SETTINGS_ACTION =
+ "android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS";
+
+ private final Launcher mLauncher;
+ private final Hotseat mHotseat;
+ private List<WorkspaceItemInfo> mPredictedApps;
+ private HotseatEduDialog mActiveDialog;
+
+ private ArrayList<ItemInfo> mNewItems = new ArrayList<>();
+ private IntArray mNewScreens = null;
+
+ HotseatEduController(Launcher launcher) {
+ mLauncher = launcher;
+ mHotseat = launcher.getHotseat();
+ }
+
+ /**
+ * Checks what type of migration should be used and migrates hotseat
+ */
+ void migrate() {
+ HotseatRestoreHelper.createBackup(mLauncher);
+ if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
+ migrateToFolder();
+ } else {
+ migrateHotseatWhole();
+ }
+ Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled,
+ R.string.hotseat_prediction_settings, null,
+ () -> mLauncher.startActivity(getSettingsIntent()));
+ }
+
+ /**
+ * This migration places all non folder items in the hotseat into a folder and then moves
+ * all folders in the hotseat to a workspace page that has enough empty spots.
+ *
+ * @return pageId that has accepted the items.
+ */
+ private int migrateToFolder() {
+ ArrayDeque<FolderInfo> folders = new ArrayDeque<>();
+ ArrayList<WorkspaceItemInfo> putIntoFolder = new ArrayList<>();
+
+ //separate folders and items that can get in folders
+ for (int i = 0; i < mLauncher.getDeviceProfile().numShownHotseatIcons; i++) {
+ View view = mHotseat.getChildAt(i, 0);
+ if (view == null) continue;
+ ItemInfo info = (ItemInfo) view.getTag();
+ if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+ folders.add((FolderInfo) info);
+ } else if (info instanceof WorkspaceItemInfo && info.container == LauncherSettings
+ .Favorites.CONTAINER_HOTSEAT) {
+ putIntoFolder.add((WorkspaceItemInfo) info);
+ }
+ }
+
+ // create a temp folder and add non folder items to it
+ if (!putIntoFolder.isEmpty()) {
+ ItemInfo firstItem = putIntoFolder.get(0);
+ FolderInfo folderInfo = new FolderInfo();
+ mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container,
+ firstItem.screenId, firstItem.cellX, firstItem.cellY);
+ folderInfo.setTitle("", mLauncher.getModelWriter());
+ folderInfo.contents.addAll(putIntoFolder);
+ for (int i = 0; i < folderInfo.contents.size(); i++) {
+ ItemInfo item = folderInfo.contents.get(i);
+ item.rank = i;
+ mLauncher.getModelWriter().moveItemInDatabase(item, folderInfo.id, 0,
+ item.cellX, item.cellY);
+ }
+ folders.add(folderInfo);
+ }
+ mNewItems.addAll(folders);
+
+ return placeFoldersInWorkspace(folders);
+ }
+
+ private int placeFoldersInWorkspace(ArrayDeque<FolderInfo> folders) {
+ if (folders.isEmpty()) return 0;
+
+ Workspace workspace = mLauncher.getWorkspace();
+ InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
+
+ GridOccupancy[] occupancyList = new GridOccupancy[workspace.getChildCount()];
+ for (int i = 0; i < occupancyList.length; i++) {
+ occupancyList[i] = ((CellLayout) workspace.getChildAt(i)).cloneGridOccupancy();
+ }
+ //scan every screen to find available spots to place folders
+ int occupancyIndex = 0;
+ int[] itemXY = new int[2];
+ while (occupancyIndex < occupancyList.length && !folders.isEmpty()) {
+ GridOccupancy occupancy = occupancyList[occupancyIndex];
+ if (occupancy.findVacantCell(itemXY, 1, 1)) {
+ FolderInfo info = folders.poll();
+ mLauncher.getModelWriter().moveItemInDatabase(info,
+ LauncherSettings.Favorites.CONTAINER_DESKTOP,
+ workspace.getScreenIdForPageIndex(occupancyIndex), itemXY[0], itemXY[1]);
+ occupancy.markCells(info, true);
+ } else {
+ occupancyIndex++;
+ }
+ }
+ if (folders.isEmpty()) return workspace.getScreenIdForPageIndex(occupancyIndex);
+ int screenId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+ .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+ // if all screens are full and we still have folders left, put those on a new page
+ FolderInfo folderInfo;
+ int col = 0;
+ while ((folderInfo = folders.poll()) != null) {
+ mLauncher.getModelWriter().moveItemInDatabase(folderInfo,
+ LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, col++,
+ idp.numRows - 1);
+ }
+ mNewScreens = IntArray.wrap(screenId);
+ return workspace.getPageCount();
+ }
+
+ /**
+ * This migration option attempts to move the entire hotseat up to the first workspace that
+ * has space to host items. If no such page is found, it moves items to a new page.
+ *
+ * @return pageId where items are migrated
+ */
+ private int migrateHotseatWhole() {
+ Workspace workspace = mLauncher.getWorkspace();
+
+ int pageId = -1;
+ int toRow = 0;
+ for (int i = 0; i < workspace.getPageCount(); i++) {
+ CellLayout target = workspace.getScreenWithId(workspace.getScreenIdForPageIndex(i));
+ if (target.makeSpaceForHotseatMigration(true)) {
+ toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
+ pageId = i;
+ break;
+ }
+ }
+ if (pageId == -1) {
+ pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+ .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+ mNewScreens = IntArray.wrap(pageId);
+ }
+ for (int i = 0; i < mLauncher.getDeviceProfile().numShownHotseatIcons; i++) {
+ View child = mHotseat.getChildAt(i, 0);
+ if (child == null || child.getTag() == null) continue;
+ ItemInfo tag = (ItemInfo) child.getTag();
+ if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) continue;
+ mLauncher.getModelWriter().moveItemInDatabase(tag,
+ LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow);
+ mNewItems.add(tag);
+ }
+ return pageId;
+ }
+
+ void moveHotseatItems() {
+ mHotseat.removeAllViewsInLayout();
+ if (!mNewItems.isEmpty()) {
+ int lastPage = mNewItems.get(mNewItems.size() - 1).screenId;
+ ArrayList<ItemInfo> animated = new ArrayList<>();
+ ArrayList<ItemInfo> nonAnimated = new ArrayList<>();
+
+ for (ItemInfo info : mNewItems) {
+ if (info.screenId == lastPage) {
+ animated.add(info);
+ } else {
+ nonAnimated.add(info);
+ }
+ }
+ mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated);
+ }
+ }
+
+ void finishOnboarding() {
+ mLauncher.getModel().onWorkspaceUiChanged();
+ }
+
+ void showDimissTip() {
+ if (mHotseat.getShortcutsAndWidgets().getChildCount()
+ < mLauncher.getDeviceProfile().numShownHotseatIcons) {
+ Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
+ R.string.hotseat_prediction_settings, null,
+ () -> mLauncher.startActivity(getSettingsIntent()));
+ } else {
+ new ArrowTipView(mLauncher).show(
+ mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
+ }
+ }
+
+ void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
+ mPredictedApps = predictedApps;
+ }
+
+ void showEdu() {
+ int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
+ CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID);
+ // hotseat is already empty and does not require migration. show edu tip
+ boolean requiresMigration = IntStream.range(0, childCount).anyMatch(i -> {
+ View v = mHotseat.getShortcutsAndWidgets().getChildAt(i);
+ return v != null && v.getTag() != null && ((ItemInfo) v.getTag()).container
+ != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+ });
+ boolean canMigrateToFirstPage = cellLayout.makeSpaceForHotseatMigration(false);
+ if (requiresMigration && canMigrateToFirstPage) {
+ showDialog();
+ } else {
+ new ArrowTipView(mLauncher).show(mLauncher.getString(
+ requiresMigration ? R.string.hotseat_tip_no_empty_slots
+ : R.string.hotseat_auto_enrolled),
+ mHotseat.getTop());
+ mLauncher.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ONLY_TIP);
+ finishOnboarding();
+ }
+ }
+
+ void showDialog() {
+ if (mPredictedApps == null || mPredictedApps.isEmpty()) {
+ return;
+ }
+ if (mActiveDialog != null) {
+ mActiveDialog.handleClose(false);
+ }
+ mActiveDialog = HotseatEduDialog.getDialog(mLauncher);
+ mActiveDialog.setHotseatEduController(this);
+ mActiveDialog.show(mPredictedApps);
+ }
+
+ static Intent getSettingsIntent() {
+ return new Intent(SETTINGS_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
new file mode 100644
index 0000000..14b0c5d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
+ .LAUNCHER_HOTSEAT_EDU_ACCEPT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.views.AbstractSlideInView;
+
+import java.util.List;
+
+/**
+ * User education dialog for hybrid hotseat. Allows user to migrate hotseat items to a new page in
+ * the workspace and shows predictions on the whole hotseat
+ */
+public class HotseatEduDialog extends AbstractSlideInView<Launcher> implements Insettable {
+
+ private static final int DEFAULT_CLOSE_DURATION = 200;
+ protected static final int FINAL_SCRIM_BG_COLOR = 0x88000000;
+
+ // we use this value to keep track of migration logs as we experiment with different migrations
+ private static final int MIGRATION_EXPERIMENT_IDENTIFIER = 1;
+
+ private final Rect mInsets = new Rect();
+ private View mHotseatWrapper;
+ private CellLayout mSampleHotseat;
+ private Button mDismissBtn;
+
+ public void setHotseatEduController(HotseatEduController hotseatEduController) {
+ mHotseatEduController = hotseatEduController;
+ }
+
+ private HotseatEduController mHotseatEduController;
+
+ public HotseatEduDialog(Context context, AttributeSet attr) {
+ this(context, attr, 0);
+ }
+
+ public HotseatEduDialog(Context context, AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mContent = this;
+ }
+
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
+ mSampleHotseat = findViewById(R.id.sample_prediction);
+
+ DeviceProfile grid = mActivityContext.getDeviceProfile();
+ Rect padding = grid.getHotseatLayoutPadding();
+
+ mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
+ mSampleHotseat.setGridSize(grid.numShownHotseatIcons, 1);
+ mSampleHotseat.setPadding(padding.left, 0, padding.right, 0);
+
+ Button turnOnBtn = findViewById(R.id.turn_predictions_on);
+ turnOnBtn.setOnClickListener(this::onAccept);
+
+ mDismissBtn = findViewById(R.id.no_thanks);
+ mDismissBtn.setOnClickListener(this::onDismiss);
+
+ // update ui to reflect which migration method is going to be used
+ if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
+ ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
+ R.string.hotseat_edu_message_migrate_alt);
+ }
+ }
+
+ private void onAccept(View v) {
+ mHotseatEduController.migrate();
+ handleClose(true);
+
+ mHotseatEduController.moveHotseatItems();
+ mHotseatEduController.finishOnboarding();
+ mActivityContext.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_ACCEPT);
+ }
+
+ private void onDismiss(View v) {
+ mHotseatEduController.showDimissTip();
+ mHotseatEduController.finishOnboarding();
+ mActivityContext.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_DENY);
+ handleClose(true);
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_ON_BOARD_POPUP) != 0;
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ int leftInset = insets.left - mInsets.left;
+ int rightInset = insets.right - mInsets.right;
+ int bottomInset = insets.bottom - mInsets.bottom;
+ mInsets.set(insets);
+ if (mActivityContext.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
+ setPadding(leftInset, getPaddingTop(), rightInset, 0);
+ mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
+ mHotseatWrapper.getPaddingRight(), bottomInset);
+ mHotseatWrapper.getLayoutParams().height =
+ mActivityContext.getDeviceProfile().hotseatBarSizePx + insets.bottom;
+
+ } else {
+ setPadding(0, getPaddingTop(), 0, 0);
+ mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
+ mHotseatWrapper.getPaddingRight(),
+ (int) getResources().getDimension(R.dimen.bottom_sheet_edu_padding));
+ ((TextView) findViewById(R.id.hotseat_edu_heading)).setText(
+ R.string.hotseat_edu_title_migrate_landscape);
+ ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
+ R.string.hotseat_edu_message_migrate_landscape);
+ }
+ }
+
+ private void animateOpen() {
+ if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ return;
+ }
+ mIsOpen = true;
+ mOpenCloseAnimator.setValues(
+ PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+ mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mOpenCloseAnimator.start();
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ handleClose(true, DEFAULT_CLOSE_DURATION);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ handleClose(false);
+ }
+
+ @Override
+ protected int getScrimColor(Context context) {
+ return FINAL_SCRIM_BG_COLOR;
+ }
+
+ private void populatePreview(List<WorkspaceItemInfo> predictions) {
+ for (int i = 0; i < mActivityContext.getDeviceProfile().numShownHotseatIcons; i++) {
+ WorkspaceItemInfo info = predictions.get(i);
+ PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info);
+ icon.setEnabled(false);
+ icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ icon.verifyHighRes();
+ CellLayout.LayoutParams lp = new CellLayout.LayoutParams(i, 0, 1, 1);
+ mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true);
+ }
+ }
+
+ /**
+ * Opens User education dialog with a list of suggested apps
+ */
+ public void show(List<WorkspaceItemInfo> predictions) {
+ if (getParent() != null
+ || predictions.size() < mActivityContext.getDeviceProfile().numShownHotseatIcons
+ || mHotseatEduController == null) {
+ return;
+ }
+ AbstractFloatingView.closeAllOpenViews(mActivityContext);
+ attachToContainer();
+ mActivityContext.getStatsLogManager().logger().log(LAUNCHER_HOTSEAT_EDU_SEEN);
+ animateOpen();
+ populatePreview(predictions);
+ }
+
+ /**
+ * Factory method for HotseatPredictionUserEdu dialog
+ */
+ public static HotseatEduDialog getDialog(Launcher launcher) {
+ LayoutInflater layoutInflater = LayoutInflater.from(launcher);
+ return (HotseatEduDialog) layoutInflater.inflate(
+ R.layout.predicted_hotseat_edu, launcher.getDragLayer(),
+ false);
+
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
new file mode 100644
index 0000000..85e5ab0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import static com.android.launcher3.hybridhotseat.HotseatEduController.getSettingsIntent;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.ComponentName;
+import android.view.HapticFeedbackConstants;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.graphics.DragPreviewProvider;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.views.ArrowTipView;
+import com.android.launcher3.views.Snackbar;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows
+ * pinning of predicted apps and manages replacement of predicted apps with user drag.
+ */
+public class HotseatPredictionController implements DragController.DragListener,
+ SystemShortcut.Factory<QuickstepLauncher>, DeviceProfile.OnDeviceProfileChangeListener,
+ DragSource, ViewGroup.OnHierarchyChangeListener {
+
+ private static final int FLAG_UPDATE_PAUSED = 1 << 0;
+ private static final int FLAG_DRAG_IN_PROGRESS = 1 << 1;
+ private static final int FLAG_FILL_IN_PROGRESS = 1 << 2;
+ private static final int FLAG_REMOVING_PREDICTED_ICON = 1 << 3;
+
+ private int mHotSeatItemsCount;
+
+ private QuickstepLauncher mLauncher;
+ private final Hotseat mHotseat;
+ private final Runnable mUpdateFillIfNotLoading = this::updateFillIfNotLoading;
+
+ private List<ItemInfo> mPredictedItems = Collections.emptyList();
+
+ private AnimatorSet mIconRemoveAnimators;
+ private int mPauseFlags = 0;
+
+ private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
+
+ private final View.OnLongClickListener mPredictionLongClickListener = v -> {
+ if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
+ if (mLauncher.getWorkspace().isSwitchingState()) return false;
+ if (!mLauncher.getOnboardingPrefs().getBoolean(
+ OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN)) {
+ Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
+ R.string.hotseat_prediction_settings, null,
+ () -> mLauncher.startActivity(getSettingsIntent()));
+ mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN);
+ mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ return true;
+ }
+
+ // Start the drag
+ // Use a new itemInfo so that the original predicted item is stable
+ WorkspaceItemInfo dragItem = new WorkspaceItemInfo((WorkspaceItemInfo) v.getTag());
+ v.setVisibility(View.INVISIBLE);
+ mLauncher.getWorkspace().beginDragShared(
+ v, null, this, dragItem, new DragPreviewProvider(v),
+ mLauncher.getDefaultWorkspaceDragOptions());
+ return true;
+ };
+
+ public HotseatPredictionController(QuickstepLauncher launcher) {
+ mLauncher = launcher;
+ mHotseat = launcher.getHotseat();
+ mHotSeatItemsCount = mLauncher.getDeviceProfile().numShownHotseatIcons;
+ mLauncher.getDragController().addDragListener(this);
+
+ launcher.addOnDeviceProfileChangeListener(this);
+ mHotseat.getShortcutsAndWidgets().setOnHierarchyChangeListener(this);
+ }
+
+ @Override
+ public void onChildViewAdded(View parent, View child) {
+ onHotseatHierarchyChanged();
+ }
+
+ @Override
+ public void onChildViewRemoved(View parent, View child) {
+ onHotseatHierarchyChanged();
+ }
+
+ private void onHotseatHierarchyChanged() {
+ if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) {
+ // Post update after a single frame to avoid layout within layout
+ MAIN_EXECUTOR.getHandler().removeCallbacks(mUpdateFillIfNotLoading);
+ MAIN_EXECUTOR.getHandler().post(mUpdateFillIfNotLoading);
+ }
+ }
+
+ private void updateFillIfNotLoading() {
+ if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) {
+ fillGapsWithPrediction(true);
+ }
+ }
+
+ /**
+ * Shows appropriate hotseat education based on prediction enabled and migration states.
+ */
+ public void showEdu() {
+ mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
+ if (mPredictedItems.isEmpty()) {
+ // launcher has empty predictions set
+ Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_disabled,
+ R.string.hotseat_prediction_settings, null,
+ () -> mLauncher.startActivity(getSettingsIntent()));
+ } else if (getPredictedIcons().size() >= (mHotSeatItemsCount + 1) / 2) {
+ showDiscoveryTip();
+ } else {
+ HotseatEduController eduController = new HotseatEduController(mLauncher);
+ eduController.setPredictedApps(mPredictedItems.stream()
+ .map(i -> (WorkspaceItemInfo) i)
+ .collect(Collectors.toList()));
+ eduController.showEdu();
+ }
+ }));
+ }
+
+ /**
+ * Shows educational tip for hotseat if user does not go through Tips app.
+ */
+ private void showDiscoveryTip() {
+ if (getPredictedIcons().isEmpty()) {
+ new ArrowTipView(mLauncher).show(
+ mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
+ } else {
+ Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
+ R.string.hotseat_prediction_settings, null,
+ () -> mLauncher.startActivity(getSettingsIntent()));
+ }
+ }
+
+ /**
+ * Returns if hotseat client has predictions
+ */
+ public boolean hasPredictions() {
+ return !mPredictedItems.isEmpty();
+ }
+
+ private void fillGapsWithPrediction() {
+ fillGapsWithPrediction(false);
+ }
+
+ private void fillGapsWithPrediction(boolean animate) {
+ if (mPauseFlags != 0) {
+ return;
+ }
+
+ int predictionIndex = 0;
+ ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
+ // make sure predicted icon removal and filling predictions don't step on each other
+ if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) {
+ mIconRemoveAnimators.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ fillGapsWithPrediction(animate);
+ mIconRemoveAnimators.removeListener(this);
+ }
+ });
+ return;
+ }
+
+ mPauseFlags |= FLAG_FILL_IN_PROGRESS;
+ for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
+ View child = mHotseat.getChildAt(
+ mHotseat.getCellXFromOrder(rank),
+ mHotseat.getCellYFromOrder(rank));
+
+ if (child != null && !isPredictedIcon(child)) {
+ continue;
+ }
+ if (mPredictedItems.size() <= predictionIndex) {
+ // Remove predicted apps from the past
+ if (isPredictedIcon(child)) {
+ mHotseat.removeView(child);
+ }
+ continue;
+ }
+ WorkspaceItemInfo predictedItem =
+ (WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
+ if (isPredictedIcon(child) && child.isEnabled()) {
+ PredictedAppIcon icon = (PredictedAppIcon) child;
+ icon.applyFromWorkspaceItem(predictedItem);
+ icon.finishBinding(mPredictionLongClickListener);
+ } else {
+ newItems.add(predictedItem);
+ }
+ preparePredictionInfo(predictedItem, rank);
+ }
+ bindItems(newItems, animate);
+
+ mPauseFlags &= ~FLAG_FILL_IN_PROGRESS;
+ }
+
+ private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate) {
+ AnimatorSet animationSet = new AnimatorSet();
+ for (WorkspaceItemInfo item : itemsToAdd) {
+ PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
+ mLauncher.getWorkspace().addInScreenFromBind(icon, item);
+ icon.finishBinding(mPredictionLongClickListener);
+ if (animate) {
+ animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
+ }
+ }
+ if (animate) {
+ animationSet.addListener(
+ forSuccessCallback(this::removeOutlineDrawings));
+ animationSet.start();
+ } else {
+ removeOutlineDrawings();
+ }
+
+ if (mLauncher.getTaskbarUIController() != null) {
+ mLauncher.getTaskbarUIController().onHotseatUpdated();
+ }
+ }
+
+ private void removeOutlineDrawings() {
+ if (mOutlineDrawings.isEmpty()) return;
+ for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+ mHotseat.removeDelegatedCellDrawing(outlineDrawing);
+ }
+ mHotseat.invalidate();
+ mOutlineDrawings.clear();
+ }
+
+
+ /**
+ * Unregisters callbacks and frees resources
+ */
+ public void destroy() {
+ mLauncher.removeOnDeviceProfileChangeListener(this);
+ }
+
+ /**
+ * start and pauses predicted apps update on the hotseat
+ */
+ public void setPauseUIUpdate(boolean paused) {
+ mPauseFlags = paused
+ ? (mPauseFlags | FLAG_UPDATE_PAUSED)
+ : (mPauseFlags & ~FLAG_UPDATE_PAUSED);
+ if (!paused) {
+ fillGapsWithPrediction();
+ }
+ }
+
+ /**
+ * Sets or updates the predicted items
+ */
+ public void setPredictedItems(FixedContainerItems items) {
+ boolean shouldIgnoreVisibility = FeatureFlags.ENABLE_APP_PREDICTIONS_WHILE_VISIBLE.get()
+ || mLauncher.isWorkspaceLoading()
+ || mPredictedItems.equals(items.items)
+ || mHotseat.getShortcutsAndWidgets().getChildCount() < mHotSeatItemsCount;
+ if (!shouldIgnoreVisibility
+ && mHotseat.isShown()
+ && mHotseat.getWindowVisibility() == View.VISIBLE) {
+ mHotseat.setOnVisibilityAggregatedCallback((isVisible) -> {
+ if (isVisible) {
+ return;
+ }
+ mHotseat.setOnVisibilityAggregatedCallback(null);
+
+ applyPredictedItems(items);
+ });
+ } else {
+ mHotseat.setOnVisibilityAggregatedCallback(null);
+
+ applyPredictedItems(items);
+ }
+ }
+
+ /**
+ * Sets or updates the predicted items only once the hotseat becomes hidden to the user
+ */
+ private void applyPredictedItems(FixedContainerItems items) {
+ mPredictedItems = items.items;
+ if (mPredictedItems.isEmpty()) {
+ HotseatRestoreHelper.restoreBackup(mLauncher);
+ }
+ fillGapsWithPrediction();
+ }
+
+ /**
+ * Pins a predicted app icon into place.
+ */
+ public void pinPrediction(ItemInfo info) {
+ PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt(
+ mHotseat.getCellXFromOrder(info.rank),
+ mHotseat.getCellYFromOrder(info.rank));
+ if (icon == null) {
+ return;
+ }
+ WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
+ mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
+ LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
+ workspaceItemInfo.cellX, workspaceItemInfo.cellY);
+ ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
+ icon.pin(workspaceItemInfo);
+ mLauncher.getStatsLogManager().logger()
+ .withItemInfo(workspaceItemInfo)
+ .log(LAUNCHER_HOTSEAT_PREDICTION_PINNED);
+ }
+
+ private List<PredictedAppIcon> getPredictedIcons() {
+ List<PredictedAppIcon> icons = new ArrayList<>();
+ ViewGroup vg = mHotseat.getShortcutsAndWidgets();
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ View child = vg.getChildAt(i);
+ if (isPredictedIcon(child)) {
+ icons.add((PredictedAppIcon) child);
+ }
+ }
+ return icons;
+ }
+
+ private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines,
+ DropTarget.DragObject dragObject) {
+ if (mIconRemoveAnimators != null) {
+ mIconRemoveAnimators.end();
+ }
+ mIconRemoveAnimators = new AnimatorSet();
+ removeOutlineDrawings();
+ for (PredictedAppIcon icon : getPredictedIcons()) {
+ if (!icon.isEnabled()) {
+ continue;
+ }
+ if (dragObject.dragSource == this && icon.equals(dragObject.originalView)) {
+ removeIconWithoutNotify(icon);
+ continue;
+ }
+ int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
+ outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
+ mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
+ icon.setEnabled(false);
+ ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0);
+ animator.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (icon.getParent() != null) {
+ removeIconWithoutNotify(icon);
+ }
+ }
+ });
+ mIconRemoveAnimators.play(animator);
+ }
+ mIconRemoveAnimators.start();
+ }
+
+ /**
+ * Removes icon while suppressing any extra tasks performed on view-hierarchy changes.
+ * This avoids recursive/redundant updates as the control updates the UI anyway after
+ * it's animation.
+ */
+ private void removeIconWithoutNotify(PredictedAppIcon icon) {
+ mPauseFlags |= FLAG_REMOVING_PREDICTED_ICON;
+ mHotseat.removeView(icon);
+ mPauseFlags &= ~FLAG_REMOVING_PREDICTED_ICON;
+ }
+
+ @Override
+ public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
+ removePredictedApps(mOutlineDrawings, dragObject);
+ if (mOutlineDrawings.isEmpty()) return;
+ for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+ mHotseat.addDelegatedCellDrawing(outlineDrawing);
+ }
+ mPauseFlags |= FLAG_DRAG_IN_PROGRESS;
+ mHotseat.invalidate();
+ }
+
+ @Override
+ public void onDragEnd() {
+ mPauseFlags &= ~FLAG_DRAG_IN_PROGRESS;
+ fillGapsWithPrediction(true);
+ }
+
+ @Nullable
+ @Override
+ public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
+ ItemInfo itemInfo) {
+ if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+ return null;
+ }
+ return new PinPrediction(activity, itemInfo);
+ }
+
+ private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
+ itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+ itemInfo.rank = rank;
+ itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
+ itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
+ itemInfo.screenId = rank;
+ }
+
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile profile) {
+ this.mHotSeatItemsCount = profile.numShownHotseatIcons;
+ }
+
+ @Override
+ public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
+ //Does nothing
+ }
+
+ /**
+ * Logs rank info based on current list of predicted items
+ */
+ public void logLaunchedAppRankingInfo(@NonNull ItemInfo itemInfo, InstanceId instanceId) {
+ ComponentName targetCN = itemInfo.getTargetComponent();
+ if (targetCN == null) {
+ return;
+ }
+ int rank = -1;
+ for (int i = mPredictedItems.size() - 1; i >= 0; i--) {
+ ItemInfo info = mPredictedItems.get(i);
+ if (targetCN.equals(info.getTargetComponent()) && itemInfo.user.equals(info.user)) {
+ rank = i;
+ break;
+ }
+ }
+ if (rank < 0) {
+ return;
+ }
+
+ int cardinality = 0;
+ for (PredictedAppIcon icon : getPredictedIcons()) {
+ ItemInfo info = (ItemInfo) icon.getTag();
+ cardinality |= 1 << info.screenId;
+ }
+
+ PredictedHotseatContainer.Builder containerBuilder = PredictedHotseatContainer.newBuilder();
+ containerBuilder.setCardinality(cardinality);
+ if (itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+ containerBuilder.setIndex(rank);
+ }
+ mLauncher.getStatsLogManager().logger()
+ .withInstanceId(instanceId)
+ .withRank(rank)
+ .withContainerInfo(ContainerInfo.newBuilder()
+ .setPredictedHotseatContainer(containerBuilder)
+ .build())
+ .log(LAUNCHER_HOTSEAT_RANKED);
+ }
+
+ private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
+
+ private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) {
+ super(R.drawable.ic_pin, R.string.pin_prediction, target,
+ itemInfo);
+ }
+
+ @Override
+ public void onClick(View view) {
+ dismissTaskMenuView(mTarget);
+ pinPrediction(mItemInfo);
+ }
+ }
+
+ private static boolean isPredictedIcon(View view) {
+ return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo
+ && ((WorkspaceItemInfo) view.getTag()).container
+ == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
new file mode 100644
index 0000000..080633a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * Model helper for app predictions in workspace
+ */
+public class HotseatPredictionModel {
+ private static final String APP_LOCATION_HOTSEAT = "hotseat";
+ private static final String APP_LOCATION_WORKSPACE = "workspace";
+
+ private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events";
+ private static final String BUNDLE_KEY_CURRENT_ITEMS = "current_items";
+
+ /**
+ * Creates and returns bundle using workspace items
+ */
+ public static Bundle convertDataModelToAppTargetBundle(Context context, BgDataModel dataModel) {
+ Bundle bundle = new Bundle();
+ ArrayList<AppTargetEvent> events = new ArrayList<>();
+ ArrayList<ItemInfo> workspaceItems = dataModel.getAllWorkspaceItems();
+ for (ItemInfo item : workspaceItems) {
+ AppTarget target = getAppTargetFromInfo(context, item);
+ if (target != null && !isTrackedForPrediction(item)) continue;
+ events.add(wrapAppTargetWithLocation(target, AppTargetEvent.ACTION_PIN, item));
+ }
+ ArrayList<AppTarget> currentTargets = new ArrayList<>();
+ FixedContainerItems hotseatItems = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
+ if (hotseatItems != null) {
+ for (ItemInfo itemInfo : hotseatItems.items) {
+ AppTarget target = getAppTargetFromInfo(context, itemInfo);
+ if (target != null) currentTargets.add(target);
+ }
+ }
+ bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, events);
+ bundle.putParcelableArrayList(BUNDLE_KEY_CURRENT_ITEMS, currentTargets);
+ return bundle;
+ }
+
+ /**
+ * Creates and returns for {@link AppTarget} object given an {@link ItemInfo}. Returns null
+ * if item is not supported prediction
+ */
+ public static AppTarget getAppTargetFromInfo(Context context, ItemInfo info) {
+ if (info == null) return null;
+ if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+ && info instanceof LauncherAppWidgetInfo
+ && ((LauncherAppWidgetInfo) info).providerName != null) {
+ ComponentName cn = ((LauncherAppWidgetInfo) info).providerName;
+ return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()),
+ cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+ } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+ && info.getTargetComponent() != null) {
+ ComponentName cn = info.getTargetComponent();
+ return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
+ cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+ } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+ && info instanceof WorkspaceItemInfo) {
+ ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info);
+ //TODO: switch to using full shortcut info
+ return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()),
+ shortcutKey.componentName.getPackageName(), shortcutKey.user).build();
+ } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+ return new AppTarget.Builder(new AppTargetId("folder:" + info.id),
+ context.getPackageName(), info.user).build();
+ }
+ return null;
+ }
+
+ /**
+ * Creates and returns {@link AppTargetEvent} from an {@link AppTarget}, action, and item
+ * location using {@link ItemInfo}
+ */
+ public static AppTargetEvent wrapAppTargetWithLocation(
+ AppTarget target, int action, ItemInfo info) {
+ String location = String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]",
+ info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+ ? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE,
+ info.screenId, info.cellX, info.cellY, info.spanX, info.spanY);
+ return new AppTargetEvent.Builder(target, action).setLaunchLocation(location).build();
+ }
+
+ /**
+ * Helper method to determine if {@link ItemInfo} should be tracked and reported to predictors
+ */
+ public static boolean isTrackedForPrediction(ItemInfo info) {
+ return info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || (
+ info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
+ && info.screenId == Workspace.FIRST_SCREEN_ID);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
new file mode 100644
index 0000000..4956fa1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import static com.android.launcher3.LauncherSettings.Favorites.HYBRID_HOTSEAT_BACKUP_TABLE;
+import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.content.Context;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.GridBackupTable;
+import com.android.launcher3.provider.LauncherDbUtils;
+
+/**
+ * A helper class to manage migration revert restoration for hybrid hotseat
+ */
+public class HotseatRestoreHelper {
+
+ /**
+ * Creates a snapshot backup of Favorite table for future restoration use.
+ */
+ public static void createBackup(Context context) {
+ MODEL_EXECUTOR.execute(() -> {
+ try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
+ LauncherSettings.Settings.call(
+ context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
+ .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ GridBackupTable backupTable = new GridBackupTable(context,
+ transaction.getDb(), idp.numDatabaseHotseatIcons, idp.numColumns,
+ idp.numRows);
+ backupTable.createCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE);
+ transaction.commit();
+ LauncherSettings.Settings.call(context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE);
+ }
+ });
+ }
+
+ /**
+ * Finds and restores a previously saved snapshow of Favorites table
+ */
+ public static void restoreBackup(Context context) {
+ MODEL_EXECUTOR.execute(() -> {
+ try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
+ LauncherSettings.Settings.call(
+ context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
+ .getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
+ if (!tableExists(transaction.getDb(), HYBRID_HOTSEAT_BACKUP_TABLE)) {
+ return;
+ }
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ GridBackupTable backupTable = new GridBackupTable(context,
+ transaction.getDb(), idp.numDatabaseHotseatIcons, idp.numColumns,
+ idp.numRows);
+ backupTable.restoreFromCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE, true);
+ transaction.commit();
+ LauncherAppState.getInstance(context).getModel().forceReload();
+ }
+ });
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/model/AppEventProducer.java b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
new file mode 100644
index 0000000..eed493d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static android.app.prediction.AppTargetEvent.ACTION_DISMISS;
+import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
+import static android.app.prediction.AppTargetEvent.ACTION_PIN;
+import static android.app.prediction.AppTargetEvent.ACTION_UNPIN;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_REMOVE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_FOLDER_CREATED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.annotation.TargetApi;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ShortcutInfo;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FolderContainer;
+import com.android.launcher3.logger.LauncherAtom.HotseatContainer;
+import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer;
+import com.android.launcher3.logging.StatsLogManager.EventEnum;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.quickstep.logging.StatsLogCompatManager.StatsLogConsumer;
+
+import java.util.Locale;
+import java.util.Optional;
+import java.util.function.ObjIntConsumer;
+import java.util.function.Predicate;
+
+/**
+ * Utility class to track stats log and emit corresponding app events
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class AppEventProducer implements StatsLogConsumer {
+
+ private static final int MSG_LAUNCH = 0;
+
+ private final Context mContext;
+ private final Handler mMessageHandler;
+ private final ObjIntConsumer<AppTargetEvent> mCallback;
+
+ private LauncherAtom.ItemInfo mLastDragItem;
+
+ public AppEventProducer(Context context, ObjIntConsumer<AppTargetEvent> callback) {
+ mContext = context;
+ mMessageHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleMessage);
+ mCallback = callback;
+ }
+
+ @WorkerThread
+ private boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_LAUNCH: {
+ mCallback.accept((AppTargetEvent) msg.obj, msg.arg1);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @AnyThread
+ private void sendEvent(LauncherAtom.ItemInfo atomInfo, int eventId, int targetPredictor) {
+ sendEvent(toAppTarget(atomInfo), atomInfo, eventId, targetPredictor);
+ }
+
+ @AnyThread
+ private void sendEvent(AppTarget target, LauncherAtom.ItemInfo locationInfo, int eventId,
+ int targetPredictor) {
+ if (target != null) {
+ AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
+ .setLaunchLocation(getContainer(locationInfo))
+ .build();
+ mMessageHandler.obtainMessage(MSG_LAUNCH, targetPredictor, 0, event).sendToTarget();
+ }
+ }
+
+ @Override
+ public void consume(EventEnum event, LauncherAtom.ItemInfo atomInfo) {
+ if (event == LAUNCHER_APP_LAUNCH_TAP
+ || event == LAUNCHER_TASK_LAUNCH_SWIPE_DOWN
+ || event == LAUNCHER_TASK_LAUNCH_TAP
+ || event == LAUNCHER_QUICKSWITCH_RIGHT
+ || event == LAUNCHER_QUICKSWITCH_LEFT) {
+ sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION);
+ } else if (event == LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST) {
+ sendEvent(atomInfo, ACTION_DISMISS, CONTAINER_PREDICTION);
+ } else if (event == LAUNCHER_ITEM_DRAG_STARTED) {
+ mLastDragItem = atomInfo;
+ } else if (event == LAUNCHER_ITEM_DROP_COMPLETED) {
+ if (mLastDragItem == null) {
+ return;
+ }
+ if (isTrackedForHotseatPrediction(atomInfo)) {
+ sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
+ }
+ if (isTrackedForHotseatPrediction(mLastDragItem)) {
+ sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
+ }
+ mLastDragItem = null;
+ } else if (event == LAUNCHER_ITEM_DROP_FOLDER_CREATED) {
+ if (isTrackedForHotseatPrediction(atomInfo)) {
+ sendEvent(createTempFolderTarget(), atomInfo, ACTION_PIN,
+ CONTAINER_HOTSEAT_PREDICTION);
+ sendEvent(atomInfo, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
+ }
+ } else if (event == LAUNCHER_FOLDER_CONVERTED_TO_ICON) {
+ if (isTrackedForHotseatPrediction(atomInfo)) {
+ sendEvent(createTempFolderTarget(), atomInfo, ACTION_UNPIN,
+ CONTAINER_HOTSEAT_PREDICTION);
+ sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
+ }
+ } else if (event == LAUNCHER_ITEM_DROPPED_ON_REMOVE) {
+ if (mLastDragItem != null && isTrackedForHotseatPrediction(mLastDragItem)) {
+ sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
+ }
+ } else if (event == LAUNCHER_HOTSEAT_PREDICTION_PINNED) {
+ if (isTrackedForHotseatPrediction(atomInfo)) {
+ sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
+ }
+ }
+ }
+
+ @Nullable
+ private AppTarget toAppTarget(LauncherAtom.ItemInfo info) {
+ UserHandle userHandle = Process.myUserHandle();
+ if (info.getIsWork()) {
+ userHandle = UserCache.INSTANCE.get(mContext).getUserProfiles().stream()
+ .filter(((Predicate<UserHandle>) userHandle::equals).negate())
+ .findAny()
+ .orElse(null);
+ }
+ if (userHandle == null) {
+ return null;
+ }
+ ComponentName cn = null;
+ ShortcutInfo shortcutInfo = null;
+ String id = null;
+
+ switch (info.getItemCase()) {
+ case APPLICATION: {
+ LauncherAtom.Application app = info.getApplication();
+ if ((cn = parseNullable(app.getComponentName())) != null) {
+ id = "app:" + cn.getPackageName();
+ }
+ break;
+ }
+ case SHORTCUT: {
+ LauncherAtom.Shortcut si = info.getShortcut();
+ if (!TextUtils.isEmpty(si.getShortcutId())
+ && (cn = parseNullable(si.getShortcutName())) != null) {
+ Optional<ShortcutInfo> opt = new ShortcutRequest(mContext,
+ userHandle).forPackage(cn.getPackageName(), si.getShortcutId()).query(
+ ShortcutRequest.ALL).stream().findFirst();
+ if (opt.isPresent()) {
+ shortcutInfo = opt.get();
+ } else {
+ return null;
+ }
+ id = "shortcut:" + si.getShortcutId();
+ }
+ break;
+ }
+ case WIDGET: {
+ LauncherAtom.Widget widget = info.getWidget();
+ if ((cn = parseNullable(widget.getComponentName())) != null) {
+ id = "widget:" + cn.getPackageName();
+ }
+ break;
+ }
+ case TASK: {
+ LauncherAtom.Task task = info.getTask();
+ if ((cn = parseNullable(task.getComponentName())) != null) {
+ id = "app:" + cn.getPackageName();
+ }
+ break;
+ }
+ case FOLDER_ICON:
+ return createTempFolderTarget();
+ }
+ if (id != null && cn != null) {
+ if (shortcutInfo != null) {
+ return new AppTarget.Builder(new AppTargetId(id), shortcutInfo).build();
+ }
+ return new AppTarget.Builder(new AppTargetId(id), cn.getPackageName(), userHandle)
+ .setClassName(cn.getClassName())
+ .build();
+ }
+ return null;
+ }
+
+
+ private AppTarget createTempFolderTarget() {
+ return new AppTarget.Builder(new AppTargetId("folder:" + SystemClock.uptimeMillis()),
+ mContext.getPackageName(), Process.myUserHandle())
+ .build();
+ }
+
+ private String getContainer(LauncherAtom.ItemInfo info) {
+ ContainerInfo ci = info.getContainerInfo();
+ switch (ci.getContainerCase()) {
+ case WORKSPACE: {
+ // In case the item type is not widgets, the spaceX and spanY default to 1.
+ int spanX = info.getWidget().getSpanX();
+ int spanY = info.getWidget().getSpanY();
+ return getWorkspaceContainerString(ci.getWorkspace(), spanX, spanY);
+ }
+ case HOTSEAT: {
+ return getHotseatContainerString(ci.getHotseat());
+ }
+ case TASK_SWITCHER_CONTAINER: {
+ return "task-switcher";
+ }
+ case ALL_APPS_CONTAINER: {
+ return "all-apps";
+ }
+ case SEARCH_RESULT_CONTAINER: {
+ return "search-results";
+ }
+ case PREDICTED_HOTSEAT_CONTAINER: {
+ return "predictions/hotseat";
+ }
+ case PREDICTION_CONTAINER: {
+ return "predictions";
+ }
+ case SHORTCUTS_CONTAINER: {
+ return "deep-shortcuts";
+ }
+ case FOLDER: {
+ FolderContainer fc = ci.getFolder();
+ switch (fc.getParentContainerCase()) {
+ case WORKSPACE:
+ return "folder/" + getWorkspaceContainerString(fc.getWorkspace(), 1, 1);
+ case HOTSEAT:
+ return "folder/" + getHotseatContainerString(fc.getHotseat());
+ }
+ return "folder";
+ }
+ }
+ return "";
+ }
+
+ private static String getWorkspaceContainerString(WorkspaceContainer wc, int spanX, int spanY) {
+ return String.format(Locale.ENGLISH, "workspace/%d/[%d,%d]/[%d,%d]",
+ wc.getPageIndex(), wc.getGridX(), wc.getGridY(), spanX, spanY);
+ }
+
+ private static String getHotseatContainerString(HotseatContainer hc) {
+ return String.format(Locale.ENGLISH, "hotseat/%1$d/[%1$d,0]/[1,1]", hc.getIndex());
+ }
+
+ private static ComponentName parseNullable(String componentNameString) {
+ return TextUtils.isEmpty(componentNameString)
+ ? null : ComponentName.unflattenFromString(componentNameString);
+ }
+
+ /**
+ * Helper method to determine if {@link ItemInfo} should be tracked and reported to predictors
+ */
+ private static boolean isTrackedForHotseatPrediction(LauncherAtom.ItemInfo info) {
+ ContainerInfo ci = info.getContainerInfo();
+ switch (ci.getContainerCase()) {
+ case HOTSEAT:
+ return true;
+ case WORKSPACE:
+ return ci.getWorkspace().getPageIndex() == 0;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java
new file mode 100644
index 0000000..b0fba3d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Task to update model as a result of predicted apps update
+ */
+public class PredictionUpdateTask extends BaseModelUpdateTask {
+
+ private final List<AppTarget> mTargets;
+ private final PredictorState mPredictorState;
+
+ PredictionUpdateTask(PredictorState predictorState, List<AppTarget> targets) {
+ mPredictorState = predictorState;
+ mTargets = targets;
+ }
+
+ @Override
+ public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+ Context context = app.getContext();
+
+ // TODO: remove this
+ Utilities.getDevicePrefs(context).edit()
+ .putBoolean(LAST_PREDICTION_ENABLED_STATE, !mTargets.isEmpty()).apply();
+
+ FixedContainerItems fci = mPredictorState.items;
+ Set<UserHandle> usersForChangedShortcuts = new HashSet<>(fci.items.stream()
+ .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT)
+ .map(info -> info.user)
+ .collect(Collectors.toSet()));
+ fci.items.clear();
+
+ for (AppTarget target : mTargets) {
+ WorkspaceItemInfo itemInfo;
+ ShortcutInfo si = target.getShortcutInfo();
+ if (si != null) {
+ usersForChangedShortcuts.add(si.getUserHandle());
+ itemInfo = new WorkspaceItemInfo(si, context);
+ app.getIconCache().getShortcutIcon(itemInfo, si);
+ } else {
+ String className = target.getClassName();
+ if (COMPONENT_CLASS_MARKER.equals(className)) {
+ // TODO: Implement this
+ continue;
+ }
+ ComponentName cn = new ComponentName(target.getPackageName(), className);
+ UserHandle user = target.getUser();
+ itemInfo = apps.data.stream()
+ .filter(info -> user.equals(info.user) && cn.equals(info.componentName))
+ .map(ai -> {
+ app.getIconCache().getTitleAndIcon(ai, false);
+ return ai.makeWorkspaceItem();
+ })
+ .findAny()
+ .orElseGet(() -> {
+ LauncherActivityInfo lai = context.getSystemService(LauncherApps.class)
+ .resolveActivity(AppInfo.makeLaunchIntent(cn), user);
+ if (lai == null) {
+ return null;
+ }
+ AppInfo ai = new AppInfo(context, lai, user);
+ app.getIconCache().getTitleAndIcon(ai, lai, false);
+ return ai.makeWorkspaceItem();
+ });
+
+ if (itemInfo == null) {
+ continue;
+ }
+ }
+
+ itemInfo.container = fci.containerId;
+ fci.items.add(itemInfo);
+ }
+
+ bindExtraContainerItems(fci);
+ usersForChangedShortcuts.forEach(
+ u -> dataModel.updateShortcutPinnedState(app.getContext(), u));
+
+ // Save to disk
+ mPredictorState.storage.write(context, fci.items);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
new file mode 100644
index 0000000..a9c2a5e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.formatElapsedTime;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.Utilities.getDevicePrefs;
+import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle;
+
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.PersistedItemArray;
+import com.android.quickstep.logging.StatsLogCompatManager;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+/**
+ * Model delegate which loads prediction items
+ */
+public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChangeListener {
+
+ public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
+ private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS";
+ private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20;
+
+ private static final boolean IS_DEBUG = false;
+ private static final String TAG = "QuickstepModelDelegate";
+
+ private final PredictorState mAllAppsState =
+ new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
+ private final PredictorState mHotseatState =
+ new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions");
+ private final PredictorState mWidgetsRecommendationState =
+ new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction");
+
+ private final InvariantDeviceProfile mIDP;
+ private final AppEventProducer mAppEventProducer;
+
+ protected boolean mActive = false;
+
+ public QuickstepModelDelegate(Context context) {
+ mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
+
+ mIDP = InvariantDeviceProfile.INSTANCE.get(context);
+ mIDP.addOnChangeListener(this);
+ StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer);
+ }
+
+ @Override
+ @WorkerThread
+ public void loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) {
+ // TODO: Implement caching and preloading
+ super.loadItems(ums, pinnedShortcuts);
+
+ WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory(
+ mApp, ums, pinnedShortcuts, mIDP.numDatabaseAllAppsColumns);
+ mAllAppsState.items.setItems(
+ mAllAppsState.storage.read(mApp.getContext(), allAppsFactory, ums.allUsers::get));
+ mDataModel.extraItems.put(CONTAINER_PREDICTION, mAllAppsState.items);
+
+ WorkspaceItemFactory hotseatFactory =
+ new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numDatabaseHotseatIcons);
+ mHotseatState.items.setItems(
+ mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get));
+ mDataModel.extraItems.put(CONTAINER_HOTSEAT_PREDICTION, mHotseatState.items);
+
+ // Widgets prediction isn't used frequently. And thus, it is not persisted on disk.
+ mDataModel.extraItems.put(CONTAINER_WIDGETS_PREDICTION, mWidgetsRecommendationState.items);
+ mActive = true;
+ }
+
+ @Override
+ public void workspaceLoadComplete() {
+ super.workspaceLoadComplete();
+ recreatePredictors();
+ }
+
+ @Override
+ @WorkerThread
+ public void modelLoadComplete() {
+ super.modelLoadComplete();
+
+ // Log snapshot of the model
+ SharedPreferences prefs = getDevicePrefs(mApp.getContext());
+ long lastSnapshotTimeMillis = prefs.getLong(LAST_SNAPSHOT_TIME_MILLIS, 0);
+ // Log snapshot only if previous snapshot was older than a day
+ long now = System.currentTimeMillis();
+ if (now - lastSnapshotTimeMillis < DAY_IN_MILLIS) {
+ if (IS_DEBUG) {
+ String elapsedTime = formatElapsedTime((now - lastSnapshotTimeMillis) / 1000);
+ Log.d(TAG, String.format(
+ "Skipped snapshot logging since previous snapshot was %s old.",
+ elapsedTime));
+ }
+ } else {
+ IntSparseArrayMap<ItemInfo> itemsIdMap;
+ synchronized (mDataModel) {
+ itemsIdMap = mDataModel.itemsIdMap.clone();
+ }
+ InstanceId instanceId = new InstanceIdSequence().newInstanceId();
+ for (ItemInfo info : itemsIdMap) {
+ FolderInfo parent = info.container > 0
+ ? (FolderInfo) itemsIdMap.get(info.container) : null;
+ StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
+ }
+ additionalSnapshotEvents(instanceId);
+ prefs.edit().putLong(LAST_SNAPSHOT_TIME_MILLIS, now).apply();
+ }
+ }
+
+ protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){}
+
+ @Override
+ public void validateData() {
+ super.validateData();
+ if (mAllAppsState.predictor != null) {
+ mAllAppsState.predictor.requestPredictionUpdate();
+ }
+ if (mWidgetsRecommendationState.predictor != null) {
+ mWidgetsRecommendationState.predictor.requestPredictionUpdate();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ mActive = false;
+ StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer);
+
+ destroyPredictors();
+ mIDP.removeOnChangeListener(this);
+ }
+
+ private void destroyPredictors() {
+ mAllAppsState.destroyPredictor();
+ mHotseatState.destroyPredictor();
+ mWidgetsRecommendationState.destroyPredictor();
+ }
+
+ @WorkerThread
+ private void recreatePredictors() {
+ destroyPredictors();
+ if (!mActive) {
+ return;
+ }
+ Context context = mApp.getContext();
+ AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
+ if (apm == null) {
+ return;
+ }
+
+ registerPredictor(mAllAppsState, apm.createAppPredictionSession(
+ new AppPredictionContext.Builder(context)
+ .setUiSurface("home")
+ .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns)
+ .build()));
+
+ // TODO: get bundle
+ registerPredictor(mHotseatState, apm.createAppPredictionSession(
+ new AppPredictionContext.Builder(context)
+ .setUiSurface("hotseat")
+ .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons)
+ .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
+ .build()));
+
+ registerWidgetsPredictor(apm.createAppPredictionSession(
+ new AppPredictionContext.Builder(context)
+ .setUiSurface("widgets")
+ .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
+ .build()));
+ }
+
+ private void registerPredictor(PredictorState state, AppPredictor predictor) {
+ state.predictor = predictor;
+ state.predictor.registerPredictionUpdates(
+ Executors.MODEL_EXECUTOR, t -> handleUpdate(state, t));
+ state.predictor.requestPredictionUpdate();
+ }
+
+ private void handleUpdate(PredictorState state, List<AppTarget> targets) {
+ if (state.setTargets(targets)) {
+ // No diff, skip
+ return;
+ }
+ mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
+ }
+
+ private void registerWidgetsPredictor(AppPredictor predictor) {
+ mWidgetsRecommendationState.predictor = predictor;
+ mWidgetsRecommendationState.predictor.registerPredictionUpdates(
+ Executors.MODEL_EXECUTOR, targets -> {
+ if (mWidgetsRecommendationState.setTargets(targets)) {
+ // No diff, skip
+ return;
+ }
+ mApp.getModel().enqueueModelUpdateTask(
+ new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
+ });
+ mWidgetsRecommendationState.predictor.requestPredictionUpdate();
+ }
+
+ @Override
+ public void onIdpChanged(InvariantDeviceProfile profile) {
+ // Reinitialize everything
+ Executors.MODEL_EXECUTOR.execute(this::recreatePredictors);
+ }
+
+ private void onAppTargetEvent(AppTargetEvent event, int client) {
+ PredictorState state = client == CONTAINER_PREDICTION ? mAllAppsState : mHotseatState;
+ if (state.predictor != null) {
+ state.predictor.notifyAppTargetEvent(event);
+ }
+ }
+
+ static class PredictorState {
+
+ public final FixedContainerItems items;
+ public final PersistedItemArray<ItemInfo> storage;
+ public AppPredictor predictor;
+
+ private List<AppTarget> mLastTargets;
+
+ PredictorState(int container, String storageName) {
+ items = new FixedContainerItems(container);
+ storage = new PersistedItemArray<>(storageName);
+ mLastTargets = Collections.emptyList();
+ }
+
+ public void destroyPredictor() {
+ if (predictor != null) {
+ predictor.destroy();
+ predictor = null;
+ }
+ }
+
+ /**
+ * Sets the new targets and returns true if it was the same as before.
+ */
+ boolean setTargets(List<AppTarget> newTargets) {
+ List<AppTarget> oldTargets = mLastTargets;
+ mLastTargets = newTargets;
+
+ int size = oldTargets.size();
+ return size == newTargets.size() && IntStream.range(0, size)
+ .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i)));
+ }
+ }
+
+ /**
+ * Compares two targets for the properties which we care about
+ */
+ private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) {
+ if (!Objects.equals(t1.getPackageName(), t2.getPackageName())
+ || !Objects.equals(t1.getUser(), t2.getUser())
+ || !Objects.equals(t1.getClassName(), t2.getClassName())) {
+ return false;
+ }
+
+ ShortcutInfo s1 = t1.getShortcutInfo();
+ ShortcutInfo s2 = t2.getShortcutInfo();
+ if (s1 != null) {
+ if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) {
+ return false;
+ }
+ } else if (s2 != null) {
+ return false;
+ }
+ return true;
+ }
+
+ private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory<ItemInfo> {
+
+ private final LauncherAppState mAppState;
+ private final UserManagerState mUMS;
+ private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
+ private final int mMaxCount;
+
+ private int mReadCount = 0;
+
+ protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
+ Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount) {
+ mAppState = appState;
+ mUMS = ums;
+ mPinnedShortcuts = pinnedShortcuts;
+ mMaxCount = maxCount;
+ }
+
+ @Nullable
+ @Override
+ public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) {
+ if (mReadCount >= mMaxCount) {
+ return null;
+ }
+ switch (itemType) {
+ case ITEM_TYPE_APPLICATION: {
+ LauncherActivityInfo lai = mAppState.getContext()
+ .getSystemService(LauncherApps.class)
+ .resolveActivity(intent, user);
+ if (lai == null) {
+ return null;
+ }
+ AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user));
+ mAppState.getIconCache().getTitleAndIcon(info, lai, false);
+ mReadCount++;
+ return info.makeWorkspaceItem();
+ }
+ case ITEM_TYPE_DEEP_SHORTCUT: {
+ ShortcutKey key = ShortcutKey.fromIntent(intent, user);
+ if (key == null) {
+ return null;
+ }
+ ShortcutInfo si = mPinnedShortcuts.get(key);
+ if (si == null) {
+ return null;
+ }
+ WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext());
+ mAppState.getIconCache().getShortcutIcon(wii, si);
+ mReadCount++;
+ return wii;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index f42b124..154b78b 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -18,8 +18,7 @@
import static android.content.ContentResolver.SCHEME_CONTENT;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+import static com.android.launcher3.Utilities.newContentObserver;
import android.annotation.TargetApi;
import android.app.RemoteAction;
@@ -35,7 +34,7 @@
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Handler;
-import android.os.Message;
+import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
@@ -43,12 +42,19 @@
import android.util.Log;
import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.RemoteActionShortcut;
import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.util.BgObjectWithLooper;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Preconditions;
@@ -62,31 +68,35 @@
* Data model for digital wellbeing status of apps.
*/
@TargetApi(Build.VERSION_CODES.Q)
-public final class WellbeingModel {
+public final class WellbeingModel extends BgObjectWithLooper {
private static final String TAG = "WellbeingModel";
private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
private static final boolean DEBUG = false;
- private static final int MSG_PACKAGE_ADDED = 1;
- private static final int MSG_PACKAGE_REMOVED = 2;
- private static final int MSG_FULL_REFRESH = 3;
+ private static final int UNKNOWN_MINIMAL_DEVICE_STATE = 0;
+ private static final int IN_MINIMAL_DEVICE = 2;
// Welbeing contract
+ private static final String PATH_ACTIONS = "actions";
+ private static final String PATH_MINIMAL_DEVICE = "minimal_device";
+ private static final String METHOD_GET_MINIMAL_DEVICE_CONFIG = "get_minimal_device_config";
private static final String METHOD_GET_ACTIONS = "get_actions";
private static final String EXTRA_ACTIONS = "actions";
private static final String EXTRA_ACTION = "action";
private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown";
private static final String EXTRA_PACKAGES = "packages";
private static final String EXTRA_SUCCESS = "success";
+ private static final String EXTRA_MINIMAL_DEVICE_STATE = "minimal_device_state";
+ private static final String DB_NAME_MINIMAL_DEVICE = "minimal.db";
public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
new MainThreadInitializedObject<>(WellbeingModel::new);
private final Context mContext;
private final String mWellbeingProviderPkg;
- private final Handler mWorkerHandler;
- private final ContentObserver mContentObserver;
+ private Handler mWorkerHandler;
+ private ContentObserver mContentObserver;
private final Object mModelLock = new Object();
// Maps the action Id to the corresponding RemoteAction
@@ -97,64 +107,88 @@
private WellbeingModel(final Context context) {
mContext = context;
- mWorkerHandler =
- new Handler(createAndStartNewLooper("WellbeingHandler"), this::handleMessage);
-
mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
- mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- // Wellbeing reports that app actions have changed.
- if (DEBUG || mIsInTest) {
- Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
- + "], uri = [" + uri + "]");
- }
- Preconditions.assertUIThread();
- updateWellbeingData();
- }
- };
+ initializeInBackground("WellbeingHandler");
+ }
+ @Override
+ protected void onInitialized(Looper looper) {
+ mWorkerHandler = new Handler(looper);
+ mContentObserver = newContentObserver(mWorkerHandler, this::onWellbeingUriChanged);
if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
- context.registerReceiver(
- new SimpleBroadcastReceiver(this::onWellbeingProviderChanged),
+ mContext.registerReceiver(
+ new SimpleBroadcastReceiver(t -> restartObserver()),
PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg,
Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED,
Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED,
- Intent.ACTION_PACKAGE_RESTARTED));
+ Intent.ACTION_PACKAGE_RESTARTED),
+ null, mWorkerHandler);
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
- context.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged),
- filter);
+ mContext.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged),
+ filter, null, mWorkerHandler);
restartObserver();
}
}
+ @WorkerThread
+ private void onWellbeingUriChanged(Uri uri) {
+ Preconditions.assertNonUiThread();
+ if (DEBUG || mIsInTest) {
+ Log.d(TAG, "ContentObserver.onChange() called with: uri = [" + uri + "]");
+ }
+ if (uri.getPath().contains(PATH_ACTIONS)) {
+ // Wellbeing reports that app actions have changed.
+ updateAllPackages();
+ } else if (uri.getPath().contains(PATH_MINIMAL_DEVICE)) {
+ // Wellbeing reports that minimal device state or config is changed.
+ if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) {
+ return;
+ }
+
+ // Temporary bug fix for b/169771796. Wellbeing provides the layout configuration when
+ // minimal device is enabled. We always want to reload the configuration from Wellbeing
+ // since the layout configuration might have changed.
+ mContext.deleteDatabase(DB_NAME_MINIMAL_DEVICE);
+
+ final Bundle extras = new Bundle();
+ String dbFile;
+ if (isInMinimalDeviceMode()) {
+ dbFile = DB_NAME_MINIMAL_DEVICE;
+ extras.putString(LauncherProvider.KEY_LAYOUT_PROVIDER_AUTHORITY,
+ mWellbeingProviderPkg + ".api");
+ } else {
+ dbFile = InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
+ }
+ LauncherSettings.Settings.call(mContext.getContentResolver(),
+ LauncherSettings.Settings.METHOD_SWITCH_DATABASE,
+ dbFile, extras);
+ }
+ }
+
public void setInTest(boolean inTest) {
mIsInTest = inTest;
}
- protected void onWellbeingProviderChanged(Intent intent) {
- if (DEBUG || mIsInTest) {
- Log.d(TAG, "Changes to Wellbeing package: intent = [" + intent + "]");
- }
- restartObserver();
- }
-
+ @WorkerThread
private void restartObserver() {
final ContentResolver resolver = mContext.getContentResolver();
resolver.unregisterContentObserver(mContentObserver);
- Uri actionsUri = apiBuilder().path("actions").build();
+ Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
+ Uri minimalDeviceUri = apiBuilder().path(PATH_MINIMAL_DEVICE).build();
try {
resolver.registerContentObserver(
actionsUri, true /* notifyForDescendants */, mContentObserver);
+ resolver.registerContentObserver(
+ minimalDeviceUri, true /* notifyForDescendants */, mContentObserver);
} catch (Exception e) {
Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
if (mIsInTest) throw new RuntimeException(e);
}
- updateWellbeingData();
+ updateAllPackages();
}
@MainThread
@@ -187,17 +221,40 @@
}
}
- private void updateWellbeingData() {
- mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
- }
-
private Uri.Builder apiBuilder() {
return new Uri.Builder()
.scheme(SCHEME_CONTENT)
.authority(mWellbeingProviderPkg + ".api");
}
- private boolean updateActions(String... packageNames) {
+ @WorkerThread
+ private boolean isInMinimalDeviceMode() {
+ if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) {
+ return false;
+ }
+ if (DEBUG || mIsInTest) {
+ Log.d(TAG, "isInMinimalDeviceMode() called");
+ }
+ Preconditions.assertNonUiThread();
+
+ final Uri contentUri = apiBuilder().build();
+ try (ContentProviderClient client = mContext.getContentResolver()
+ .acquireUnstableContentProviderClient(contentUri)) {
+ final Bundle remoteBundle = client == null ? null : client.call(
+ METHOD_GET_MINIMAL_DEVICE_CONFIG, null /* args */, null /* extras */);
+ return remoteBundle != null
+ && remoteBundle.getInt(EXTRA_MINIMAL_DEVICE_STATE,
+ UNKNOWN_MINIMAL_DEVICE_STATE) == IN_MINIMAL_DEVICE;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
+ if (mIsInTest) throw new RuntimeException(e);
+ }
+ if (DEBUG || mIsInTest) Log.i(TAG, "isInMinimalDeviceMode(): finished");
+ return false;
+ }
+
+ @WorkerThread
+ private boolean updateActions(String[] packageNames) {
if (packageNames.length == 0) {
return true;
}
@@ -260,68 +317,60 @@
return true;
}
- private boolean handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_PACKAGE_REMOVED: {
- String packageName = (String) msg.obj;
- mWorkerHandler.removeCallbacksAndMessages(packageName);
- synchronized (mModelLock) {
- mPackageToActionId.remove(packageName);
- }
- return true;
- }
- case MSG_PACKAGE_ADDED: {
- String packageName = (String) msg.obj;
- mWorkerHandler.removeCallbacksAndMessages(packageName);
- if (!updateActions(packageName)) {
- scheduleRefreshRetry(msg);
- }
- return true;
- }
-
- case MSG_FULL_REFRESH: {
- // Remove all existing messages
- mWorkerHandler.removeCallbacksAndMessages(null);
- final String[] packageNames = mContext.getSystemService(LauncherApps.class)
- .getActivityList(null, Process.myUserHandle()).stream()
- .map(li -> li.getApplicationInfo().packageName).distinct()
- .toArray(String[]::new);
- if (!updateActions(packageNames)) {
- scheduleRefreshRetry(msg);
- }
- return true;
- }
+ @WorkerThread
+ private void updateActionsWithRetry(int retryCount, @Nullable String packageName) {
+ if (DEBUG || mIsInTest) {
+ Log.i(TAG,
+ "updateActionsWithRetry(); retryCount: " + retryCount + ", package: "
+ + packageName);
}
- return false;
- }
+ String[] packageNames = TextUtils.isEmpty(packageName)
+ ? mContext.getSystemService(LauncherApps.class)
+ .getActivityList(null, Process.myUserHandle()).stream()
+ .map(li -> li.getApplicationInfo().packageName).distinct()
+ .toArray(String[]::new)
+ : new String[]{packageName};
- private void scheduleRefreshRetry(Message originalMsg) {
- int retryCount = originalMsg.arg1;
+ mWorkerHandler.removeCallbacksAndMessages(packageName);
+ if (updateActions(packageNames)) {
+ return;
+ }
if (retryCount >= RETRY_TIMES_MS.length) {
// To many retries, skip
return;
}
-
- Message msg = Message.obtain(originalMsg);
- msg.arg1 = retryCount + 1;
- mWorkerHandler.sendMessageDelayed(msg, RETRY_TIMES_MS[retryCount]);
+ mWorkerHandler.postDelayed(
+ () -> {
+ if (DEBUG || mIsInTest) Log.i(TAG, "Retrying; attempt " + (retryCount + 1));
+ updateActionsWithRetry(retryCount + 1, packageName);
+ },
+ packageName, RETRY_TIMES_MS[retryCount]);
}
+ @WorkerThread
+ private void updateAllPackages() {
+ if (DEBUG || mIsInTest) Log.i(TAG, "updateAllPackages");
+ updateActionsWithRetry(0, null);
+ }
+
+ @WorkerThread
private void onAppPackageChanged(Intent intent) {
if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]");
- Preconditions.assertUIThread();
+ Preconditions.assertNonUiThread();
final String packageName = intent.getData().getSchemeSpecificPart();
if (packageName == null || packageName.length() == 0) {
// they sent us a bad intent
return;
}
-
final String action = intent.getAction();
if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
- Message.obtain(mWorkerHandler, MSG_PACKAGE_REMOVED, packageName).sendToTarget();
+ mWorkerHandler.removeCallbacksAndMessages(packageName);
+ synchronized (mModelLock) {
+ mPackageToActionId.remove(packageName);
+ }
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
- Message.obtain(mWorkerHandler, MSG_PACKAGE_ADDED, packageName).sendToTarget();
+ updateActionsWithRetry(0, packageName);
}
}
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
new file mode 100644
index 0000000..3d891e8
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
+
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.text.TextUtils;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** Task to update model as a result of predicted widgets update */
+public final class WidgetsPredictionUpdateTask extends BaseModelUpdateTask {
+ private final PredictorState mPredictorState;
+ private final List<AppTarget> mTargets;
+
+ WidgetsPredictionUpdateTask(PredictorState predictorState, List<AppTarget> targets) {
+ mPredictorState = predictorState;
+ mTargets = targets;
+ }
+
+ /**
+ * Uses the app predication result to infer widgets that the user may want to use.
+ *
+ * <p>The algorithm uses the app prediction ranking to create a widgets ranking which only
+ * includes one widget per app and excludes widgets that have already been added to the
+ * workspace.
+ */
+ @Override
+ public void execute(LauncherAppState appState, BgDataModel dataModel, AllAppsList apps) {
+ Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
+ widget -> new ComponentKey(widget.providerName, widget.user)).collect(
+ Collectors.toSet());
+ Map<PackageUserKey, List<WidgetItem>> allWidgets =
+ dataModel.widgetsModel.getAllWidgetsWithoutShortcuts();
+
+ FixedContainerItems fixedContainerItems = mPredictorState.items;
+ fixedContainerItems.items.clear();
+
+ if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) {
+ for (AppTarget app : mTargets) {
+ PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(),
+ app.getUser());
+ if (allWidgets.containsKey(packageUserKey)) {
+ List<WidgetItem> notAddedWidgets = allWidgets.get(packageUserKey).stream()
+ .filter(item ->
+ !widgetsInWorkspace.contains(
+ new ComponentKey(item.componentName, item.user)))
+ .collect(Collectors.toList());
+ if (notAddedWidgets.size() > 0) {
+ // Even an apps have more than one widgets, we only include one widget.
+ fixedContainerItems.items.add(
+ new PendingAddWidgetInfo(
+ notAddedWidgets.get(0).widgetInfo,
+ CONTAINER_WIDGETS_PREDICTION));
+ }
+ }
+ }
+ } else {
+ Map<ComponentKey, WidgetItem> widgetItems =
+ allWidgets.values().stream().flatMap(List::stream)
+ .collect(Collectors.toMap(widget -> (ComponentKey) widget,
+ widget -> widget));
+ for (AppTarget app : mTargets) {
+ if (TextUtils.isEmpty(app.getClassName())) {
+ continue;
+ }
+ ComponentKey targetWidget = new ComponentKey(
+ new ComponentName(app.getPackageName(), app.getClassName()), app.getUser());
+ if (widgetItems.containsKey(targetWidget)) {
+ fixedContainerItems.items.add(
+ new PendingAddWidgetInfo(widgetItems.get(
+ targetWidget).widgetInfo,
+ CONTAINER_WIDGETS_PREDICTION));
+ }
+ }
+ }
+ bindExtraContainerItems(fixedContainerItems);
+
+ // Don't store widgets prediction to disk because it is not used frequently.
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
index e302b4f..4d7cc85 100644
--- a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
+++ b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
@@ -17,6 +17,7 @@
package com.android.launcher3.proxy;
import android.app.Activity;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
@@ -48,19 +49,20 @@
return;
}
- if (mParams.intent != null) {
- startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
- return;
- } else if (mParams.intentSender != null) {
- try {
+ try {
+ if (mParams.intent != null) {
+ startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
+ return;
+ } else if (mParams.intentSender != null) {
startIntentSenderForResult(mParams.intentSender, mParams.requestCode,
mParams.fillInIntent, mParams.flagsMask, mParams.flagsValues,
mParams.extraFlags,
mParams.options);
return;
- } catch (SendIntentException e) {
- mParams.deliverResult(this, RESULT_CANCELED, null);
}
+ } catch (NullPointerException | ActivityNotFoundException | SecurityException
+ | SendIntentException e) {
+ mParams.deliverResult(this, RESULT_CANCELED, null);
}
finishAndRemoveTask();
}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
index 13501a4..1f268cc 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.quickstep.AnimatedFloat.VALUE;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
@@ -30,7 +31,7 @@
import com.android.quickstep.SystemUiProxy;
/**
- * State handler for animating back button alpha
+ * State handler for animating back button alpha in two-button nav mode.
*/
public class BackButtonAlphaHandler implements StateHandler<LauncherState> {
@@ -47,18 +48,11 @@
@Override
public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
PendingAnimation animation) {
- if (config.onlyPlayAtomicComponent()) {
+ if (SysUINavigationMode.getMode(mLauncher) != TWO_BUTTONS) {
return;
}
- if (!SysUINavigationMode.getMode(mLauncher).hasGestures) {
- // If the nav mode is not gestural, then force back button alpha to be 1
- UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
- BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, 1f, true /* animate */);
- return;
- }
-
- mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastBackButtonAlpha();
+ mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastNavButtonAlpha();
animation.setFloat(mBackAlpha, VALUE,
mLauncher.shouldBackButtonBeHidden(toState) ? 0 : 1, LINEAR);
}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index fe8f0c6..8c9ac12 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -24,8 +24,13 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.os.IBinder;
+import android.os.SystemProperties;
import android.util.FloatProperty;
+import android.view.AttachedSurfaceControl;
+import android.view.CrossWindowBlurListeners;
+import android.view.SurfaceControl;
import android.view.View;
+import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import com.android.launcher3.BaseActivity;
@@ -37,11 +42,10 @@
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.systemui.shared.system.BlurUtils;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SurfaceControlCompat;
-import com.android.systemui.shared.system.TransactionCompat;
import com.android.systemui.shared.system.WallpaperManagerCompat;
+import java.util.function.Consumer;
+
/**
* Controls blur and wallpaper zoom, for the Launcher surface only.
*/
@@ -91,23 +95,44 @@
@Override
public void onDraw() {
View view = mLauncher.getDragLayer();
- setSurface(new SurfaceControlCompat(view));
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
+ setSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null);
view.post(() -> view.getViewTreeObserver().removeOnDrawListener(this));
}
};
+ private final Consumer<Boolean> mCrossWindowBlurListener = new Consumer<Boolean>() {
+ @Override
+ public void accept(Boolean enabled) {
+ mCrossWindowBlursEnabled = enabled;
+ dispatchTransactionSurface(mDepth);
+ }
+ };
+
+ private final Runnable mOpaquenessListener = new Runnable() {
+ @Override
+ public void run() {
+ dispatchTransactionSurface(mDepth);
+ }
+ };
+
private final Launcher mLauncher;
/**
* Blur radius when completely zoomed out, in pixels.
*/
private int mMaxBlurRadius;
+ private boolean mCrossWindowBlursEnabled;
private WallpaperManagerCompat mWallpaperManager;
- private SurfaceControlCompat mSurface;
+ private SurfaceControl mSurface;
/**
* Ratio from 0 to 1, where 0 is fully zoomed out, and 1 is zoomed in.
* @see android.service.wallpaper.WallpaperService.Engine#onZoomChanged(float)
*/
private float mDepth;
+ /**
+ * If we're launching and app and should not be blurring the screen for performance reasons.
+ */
+ private boolean mBlurDisabledForAppLaunch;
// Workaround for animating the depth when multiwindow mode changes.
private boolean mIgnoreStateChangesDuringMultiWindowAnimation = false;
@@ -123,6 +148,7 @@
mMaxBlurRadius = mLauncher.getResources().getInteger(R.integer.max_depth_blur_radius);
mWallpaperManager = new WallpaperManagerCompat(mLauncher);
}
+
if (mLauncher.getRootView() != null && mOnAttachListener == null) {
mOnAttachListener = new View.OnAttachStateChangeListener() {
@Override
@@ -132,16 +158,28 @@
if (windowToken != null) {
mWallpaperManager.setWallpaperZoomOut(windowToken, mDepth);
}
+ onAttached();
}
@Override
public void onViewDetachedFromWindow(View view) {
+ CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener);
+ mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener);
}
};
mLauncher.getRootView().addOnAttachStateChangeListener(mOnAttachListener);
+ if (mLauncher.getRootView().isAttachedToWindow()) {
+ onAttached();
+ }
}
}
+ private void onAttached() {
+ CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(),
+ mCrossWindowBlurListener);
+ mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener);
+ }
+
/**
* Sets if the underlying activity is started or not
*/
@@ -157,22 +195,17 @@
/**
* Sets the specified app target surface to apply the blur to.
*/
- public void setSurfaceToApp(RemoteAnimationTargetCompat target) {
- if (target != null) {
- setSurface(target.leash);
- } else {
- setActivityStarted(mLauncher.isStarted());
+ public void setSurface(SurfaceControl surface) {
+ // Set launcher as the SurfaceControl when we don't need an external target anymore.
+ if (surface == null) {
+ ViewRootImpl viewRootImpl = mLauncher.getDragLayer().getViewRootImpl();
+ surface = viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null;
}
- }
- private void setSurface(SurfaceControlCompat surface) {
if (mSurface != surface) {
mSurface = surface;
if (surface != null) {
- setDepth(mDepth);
- } else {
- // If there is no surface, then reset the ratio
- setDepth(0f);
+ dispatchTransactionSurface(mDepth);
}
}
}
@@ -186,15 +219,15 @@
float toDepth = toState.getDepth(mLauncher);
if (Float.compare(mDepth, toDepth) != 0) {
setDepth(toDepth);
+ } else if (toState == LauncherState.OVERVIEW) {
+ dispatchTransactionSurface(mDepth);
}
}
@Override
public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
PendingAnimation animation) {
- if (mSurface == null
- || config.onlyPlayAtomicComponent()
- || config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER)
+ if (config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER)
|| mIgnoreStateChangesDuringMultiWindowAnimation) {
return;
}
@@ -205,6 +238,19 @@
}
}
+ /**
+ * If we're launching an app from the home screen.
+ */
+ public void setIsInLaunchTransition(boolean inLaunchTransition) {
+ boolean blurEnabled = SystemProperties.getBoolean("ro.launcher.blur.appLaunch", true);
+ mBlurDisabledForAppLaunch = inLaunchTransition && !blurEnabled;
+ if (!inLaunchTransition) {
+ // Reset depth at the end of the launch animation, so the wallpaper won't be
+ // zoomed out if an app crashes.
+ setDepth(0f);
+ }
+ }
+
private void setDepth(float depth) {
depth = Utilities.boundToRange(depth, 0, 1);
// Round out the depth to dedupe frequent, non-perceptable updates
@@ -213,32 +259,41 @@
if (Float.compare(mDepth, depthF) == 0) {
return;
}
+ if (dispatchTransactionSurface(depthF)) {
+ mDepth = depthF;
+ }
+ }
+ private boolean dispatchTransactionSurface(float depth) {
boolean supportsBlur = BlurUtils.supportsBlursOnWindows();
if (supportsBlur && (mSurface == null || !mSurface.isValid())) {
- return;
+ return false;
}
- mDepth = depthF;
ensureDependencies();
IBinder windowToken = mLauncher.getRootView().getWindowToken();
if (windowToken != null) {
- mWallpaperManager.setWallpaperZoomOut(windowToken, mDepth);
+ mWallpaperManager.setWallpaperZoomOut(windowToken, depth);
}
if (supportsBlur) {
- final int blur;
- if (mLauncher.isInState(LauncherState.ALL_APPS) && mDepth == 1) {
- // All apps has a solid background. We don't need to draw blurs after it's fully
- // visible. This will take us out of GPU composition, saving battery and increasing
- // performance.
- blur = 0;
- } else {
- blur = (int) (mDepth * mMaxBlurRadius);
- }
- new TransactionCompat()
+ // We cannot mark the window as opaque in overview because there will be an app window
+ // below the launcher layer, and we need to draw it -- without blurs.
+ boolean isOverview = mLauncher.isInState(LauncherState.OVERVIEW);
+ boolean opaque = mLauncher.getScrimView().isFullyOpaque() && !isOverview;
+
+ int blur = opaque || isOverview || !mCrossWindowBlursEnabled
+ || mBlurDisabledForAppLaunch ? 0 : (int) (depth * mMaxBlurRadius);
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()
.setBackgroundBlurRadius(mSurface, blur)
- .apply();
+ .setOpaque(mSurface, opaque);
+
+ AttachedSurfaceControl rootSurfaceControl =
+ mLauncher.getRootView().getRootSurfaceControl();
+ if (rootSurfaceControl != null) {
+ rootSurfaceControl.applyTransactionOnDraw(transaction);
+ }
}
+ return true;
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java b/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java
new file mode 100644
index 0000000..540f748
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar;
+
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
+
+import android.annotation.DrawableRes;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
+
+/**
+ * Creates Buttons for Taskbar for 3 button nav.
+ * Can add animations and state management for buttons in this class as things progress.
+ */
+public class ButtonProvider {
+
+ private final int mMarginLeftRight;
+ private final TaskbarActivityContext mContext;
+
+ public ButtonProvider(TaskbarActivityContext context) {
+ mContext = context;
+ mMarginLeftRight = context.getResources()
+ .getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
+ }
+
+ public View getBack() {
+ // Back button
+ return getButtonForDrawable(R.drawable.ic_sysbar_back, BUTTON_BACK);
+ }
+
+ public View getDown() {
+ // Ime down button
+ return getButtonForDrawable(R.drawable.ic_sysbar_back, BUTTON_BACK);
+ }
+
+ public View getHome() {
+ // Home button
+ return getButtonForDrawable(R.drawable.ic_sysbar_home, BUTTON_HOME);
+ }
+
+ public View getRecents() {
+ // Recents button
+ return getButtonForDrawable(R.drawable.ic_sysbar_recent, BUTTON_RECENTS);
+ }
+
+ public View getImeSwitcher() {
+ // IME Switcher Button
+ return getButtonForDrawable(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH);
+ }
+
+ private View getButtonForDrawable(@DrawableRes int drawableId, @TaskbarButton int buttonType) {
+ ImageView buttonView = new ImageView(mContext);
+ buttonView.setImageResource(drawableId);
+ buttonView.setBackgroundResource(R.drawable.taskbar_icon_click_feedback_roundrect);
+ buttonView.setPadding(mMarginLeftRight, 0, mMarginLeftRight, 0);
+ buttonView.setOnClickListener(view -> mContext.onNavigationButtonClick(buttonType));
+ return buttonView;
+ }
+
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java b/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java
new file mode 100644
index 0000000..287caab
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RelativeLayout;
+
+import com.android.launcher3.views.ActivityContext;
+
+public class ImeBarView extends RelativeLayout {
+
+ private ButtonProvider mButtonProvider;
+ private View mImeView;
+
+ public ImeBarView(Context context) {
+ this(context, null);
+ }
+
+ public ImeBarView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ImeBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void init(ButtonProvider buttonProvider) {
+ mButtonProvider = buttonProvider;
+
+ ActivityContext context = getActivityContext();
+ RelativeLayout.LayoutParams imeParams = new RelativeLayout.LayoutParams(
+ context.getDeviceProfile().iconSizePx,
+ context.getDeviceProfile().iconSizePx
+ );
+ RelativeLayout.LayoutParams downParams = new RelativeLayout.LayoutParams(imeParams);
+
+ imeParams.addRule(ALIGN_PARENT_END);
+ imeParams.setMarginEnd(context.getDeviceProfile().iconSizePx);
+ downParams.setMarginStart(context.getDeviceProfile().iconSizePx);
+ downParams.addRule(ALIGN_PARENT_START);
+
+ // Down Arrow
+ View downView = mButtonProvider.getDown();
+ downView.setLayoutParams(downParams);
+ downView.setRotation(-90);
+ addView(downView);
+
+ // IME switcher button
+ mImeView = mButtonProvider.getImeSwitcher();
+ mImeView.setLayoutParams(imeParams);
+ addView(mImeView);
+ }
+
+ public void setImeSwitcherVisibility(boolean show) {
+ mImeView.setVisibility(show ? VISIBLE : GONE);
+ }
+
+ private <T extends Context & ActivityContext> T getActivityContext() {
+ return ActivityContext.lookupContext(getContext());
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
new file mode 100644
index 0000000..c2d107c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.QuickstepTransitionManager;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.states.StateAnimationConfig;
+
+
+/**
+ * A data source which integrates with a Launcher instance
+ * TODO: Rename to have Launcher prefix
+ */
+
+public class LauncherTaskbarUIController extends TaskbarUIController {
+
+ private final BaseQuickstepLauncher mLauncher;
+ private final TaskbarStateHandler mTaskbarStateHandler;
+ private final TaskbarAnimationController mTaskbarAnimationController;
+ private final TaskbarHotseatController mHotseatController;
+
+ private final TaskbarActivityContext mContext;
+ final TaskbarDragLayer mTaskbarDragLayer;
+ final TaskbarView mTaskbarView;
+
+ private @Nullable Animator mAnimator;
+ private boolean mIsAnimatingToLauncher;
+
+ public LauncherTaskbarUIController(
+ BaseQuickstepLauncher launcher, TaskbarActivityContext context) {
+ mContext = context;
+ mTaskbarDragLayer = context.getDragLayer();
+ mTaskbarView = mTaskbarDragLayer.findViewById(R.id.taskbar_view);
+
+ mLauncher = launcher;
+ mTaskbarStateHandler = mLauncher.getTaskbarStateHandler();
+ mTaskbarAnimationController = new TaskbarAnimationController(mLauncher,
+ createTaskbarAnimationControllerCallbacks());
+ mHotseatController = new TaskbarHotseatController(
+ mLauncher, mTaskbarView::updateHotseatItems);
+ }
+
+ @Override
+ protected void onCreate() {
+ mTaskbarStateHandler.setAnimationController(mTaskbarAnimationController);
+ mTaskbarAnimationController.init();
+ mHotseatController.init();
+ setTaskbarViewVisible(!mLauncher.hasBeenResumed());
+ alignRealHotseatWithTaskbar();
+ mLauncher.setTaskbarUIController(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mAnimator != null) {
+ // End this first, in case it relies on properties that are about to be cleaned up.
+ mAnimator.end();
+ }
+ mTaskbarStateHandler.setAnimationController(null);
+ mTaskbarAnimationController.cleanup();
+ mHotseatController.cleanup();
+ setTaskbarViewVisible(true);
+ mLauncher.getHotseat().setIconsAlpha(1f);
+ mLauncher.setTaskbarUIController(null);
+ }
+
+ @Override
+ protected boolean isTaskbarTouchable() {
+ return !mIsAnimatingToLauncher;
+ }
+
+ private TaskbarAnimationControllerCallbacks createTaskbarAnimationControllerCallbacks() {
+ return new TaskbarAnimationControllerCallbacks() {
+ @Override
+ public void updateTaskbarBackgroundAlpha(float alpha) {
+ mTaskbarDragLayer.setTaskbarBackgroundAlpha(alpha);
+ }
+
+ @Override
+ public void updateTaskbarVisibilityAlpha(float alpha) {
+ mTaskbarView.setAlpha(alpha);
+ }
+
+ @Override
+ public void updateImeBarVisibilityAlpha(float alpha) {
+ mTaskbarDragLayer.updateImeBarVisibilityAlpha(alpha);
+ }
+
+ @Override
+ public void updateTaskbarScale(float scale) {
+ mTaskbarView.setScaleX(scale);
+ mTaskbarView.setScaleY(scale);
+ }
+
+ @Override
+ public void updateTaskbarTranslationY(float translationY) {
+ if (translationY < 0) {
+ // Resize to accommodate the max translation we'll reach.
+ mContext.setTaskbarWindowHeight(mContext.getDeviceProfile().taskbarSize
+ + mLauncher.getHotseat().getTaskbarOffsetY());
+ } else {
+ mContext.setTaskbarWindowHeight(mContext.getDeviceProfile().taskbarSize);
+ }
+ mTaskbarView.setTranslationY(translationY);
+ }
+ };
+ }
+
+ /**
+ * Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
+ */
+ public void onLauncherResumedOrPaused(boolean isResumed) {
+ long duration = QuickstepTransitionManager.CONTENT_ALPHA_DURATION;
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ if (isResumed) {
+ mAnimator = createAnimToLauncher(null, duration);
+ } else {
+ mAnimator = createAnimToApp(duration);
+ }
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimator = null;
+ }
+ });
+ mAnimator.start();
+ }
+
+ /**
+ * Create Taskbar animation when going from an app to Launcher.
+ * @param toState If known, the state we will end up in when reaching Launcher.
+ */
+ public Animator createAnimToLauncher(@Nullable LauncherState toState, long duration) {
+ PendingAnimation anim = new PendingAnimation(duration);
+ anim.add(mTaskbarAnimationController.createAnimToBackgroundAlpha(0, duration));
+ if (toState != null) {
+ mTaskbarStateHandler.setStateWithAnimation(toState, new StateAnimationConfig(), anim);
+ }
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mIsAnimatingToLauncher = true;
+ mTaskbarView.setHolesAllowedInLayout(true);
+ mTaskbarView.updateHotseatItemsVisibility();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIsAnimatingToLauncher = false;
+ setTaskbarViewVisible(false);
+ }
+ });
+
+ return anim.buildAnim();
+ }
+
+ private Animator createAnimToApp(long duration) {
+ PendingAnimation anim = new PendingAnimation(duration);
+ anim.add(mTaskbarAnimationController.createAnimToBackgroundAlpha(1, duration));
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mTaskbarView.updateHotseatItemsVisibility();
+ setTaskbarViewVisible(true);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mTaskbarView.setHolesAllowedInLayout(false);
+ }
+ });
+ return anim.buildAnim();
+ }
+
+ @Override
+ protected void onImeVisible(TaskbarDragLayer containerView, boolean isVisible) {
+ mTaskbarAnimationController.animateToVisibilityForIme(isVisible ? 0 : 1);
+ }
+
+ /**
+ * Should be called when one or more items in the Hotseat have changed.
+ */
+ public void onHotseatUpdated() {
+ mHotseatController.onHotseatUpdated();
+ }
+
+ /**
+ * @param ev MotionEvent in screen coordinates.
+ * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
+ */
+ public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
+ return mTaskbarView.isEventOverAnyItem(ev);
+ }
+
+ public boolean isDraggingItem() {
+ return mTaskbarView.isDraggingItem();
+ }
+
+ /**
+ * Pads the Hotseat to line up exactly with Taskbar's copy of the Hotseat.
+ */
+ @Override
+ public void alignRealHotseatWithTaskbar() {
+ Rect hotseatBounds = new Rect();
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ int hotseatHeight = grid.workspacePadding.bottom + grid.taskbarSize;
+ int taskbarOffset = mLauncher.getHotseat().getTaskbarOffsetY();
+ int hotseatTopDiff = hotseatHeight - grid.taskbarSize - taskbarOffset;
+ int hotseatBottomDiff = taskbarOffset;
+
+ RectF hotseatBoundsF = mTaskbarView.getHotseatBounds();
+ Utilities.scaleRectFAboutPivot(hotseatBoundsF, getTaskbarScaleOnHome(),
+ mTaskbarView.getPivotX(), mTaskbarView.getPivotY());
+ hotseatBoundsF.round(hotseatBounds);
+ mLauncher.getHotseat().setPadding(hotseatBounds.left,
+ hotseatBounds.top + hotseatTopDiff,
+ mTaskbarView.getWidth() - hotseatBounds.right,
+ mTaskbarView.getHeight() - hotseatBounds.bottom + hotseatBottomDiff);
+ }
+
+ /**
+ * Returns the ratio of the taskbar icon size on home vs in an app.
+ */
+ public float getTaskbarScaleOnHome() {
+ DeviceProfile inAppDp = mContext.getDeviceProfile();
+ DeviceProfile onHomeDp = mLauncher.getDeviceProfile();
+ return (float) onHomeDp.cellWidthPx / inAppDp.cellWidthPx;
+ }
+
+ void setTaskbarViewVisible(boolean isVisible) {
+ mTaskbarView.setIconsVisibility(isVisible);
+ mLauncher.getHotseat().setIconsAlpha(isVisible ? 0f : 1f);
+ }
+
+ /**
+ * Contains methods that TaskbarAnimationController can call to interface with
+ * TaskbarController.
+ */
+ protected interface TaskbarAnimationControllerCallbacks {
+ void updateTaskbarBackgroundAlpha(float alpha);
+ void updateTaskbarVisibilityAlpha(float alpha);
+ void updateImeBarVisibilityAlpha(float alpha);
+ void updateTaskbarScale(float scale);
+ void updateTaskbarTranslationY(float translationY);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
new file mode 100644
index 0000000..4ba0ee0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
+import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_EXTRA_NAVIGATION_BAR;
+
+import android.app.ActivityOptions;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Process;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+/**
+ * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements
+ * that are used by both Launcher and Taskbar (such as Folder) to reference a generic
+ * ActivityContext and BaseDragLayer instead of the Launcher activity and its DragLayer.
+ */
+public class TaskbarActivityContext extends ContextThemeWrapper implements ActivityContext {
+
+ private static final boolean ENABLE_THREE_BUTTON_TASKBAR =
+ SystemProperties.getBoolean("persist.debug.taskbar_three_button", false);
+ private static final String TAG = "TaskbarActivityContext";
+
+ private static final String WINDOW_TITLE = "Taskbar";
+
+ private final DeviceProfile mDeviceProfile;
+ private final LayoutInflater mLayoutInflater;
+ private final TaskbarDragLayer mDragLayer;
+ private final TaskbarIconController mIconController;
+ private final MyDragController mDragController;
+
+ private final WindowManager mWindowManager;
+ private WindowManager.LayoutParams mWindowLayoutParams;
+
+ private final SysUINavigationMode.Mode mNavMode;
+ private final TaskbarNavButtonController mNavButtonController;
+
+ private final boolean mIsSafeModeEnabled;
+
+ @NonNull
+ private TaskbarUIController mUIController = TaskbarUIController.DEFAULT;
+
+ private final View.OnClickListener mOnTaskbarIconClickListener;
+ private final View.OnLongClickListener mOnTaskbarIconLongClickListener;
+
+ public TaskbarActivityContext(Context windowContext, DeviceProfile dp,
+ TaskbarNavButtonController buttonController) {
+ super(windowContext, Themes.getActivityThemeRes(windowContext));
+ mDeviceProfile = dp;
+ mNavButtonController = buttonController;
+ mNavMode = SysUINavigationMode.getMode(windowContext);
+ mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
+ () -> getPackageManager().isSafeMode());
+
+ mOnTaskbarIconLongClickListener =
+ new TaskbarDragController(this)::startSystemDragOnLongClick;
+ mOnTaskbarIconClickListener = this::onTaskbarIconClicked;
+
+ float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
+ float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
+ mDeviceProfile.updateIconSize(iconScale, getResources());
+
+ mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
+ mDragLayer = (TaskbarDragLayer) mLayoutInflater
+ .inflate(R.layout.taskbar, null, false);
+ mIconController = new TaskbarIconController(this, mDragLayer);
+ mDragController = new MyDragController(this);
+
+ Display display = windowContext.getDisplay();
+ Context c = display.getDisplayId() == Display.DEFAULT_DISPLAY
+ ? windowContext.getApplicationContext()
+ : windowContext.getApplicationContext().createDisplayContext(display);
+ mWindowManager = c.getSystemService(WindowManager.class);
+ }
+
+ public void init() {
+ mWindowLayoutParams = new WindowManager.LayoutParams(
+ MATCH_PARENT,
+ mDeviceProfile.taskbarSize,
+ TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+ mWindowLayoutParams.setTitle(WINDOW_TITLE);
+ mWindowLayoutParams.packageName = getPackageName();
+ mWindowLayoutParams.gravity = Gravity.BOTTOM;
+ mWindowLayoutParams.setFitInsetsTypes(0);
+ mWindowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+ mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ mWindowLayoutParams.setSystemApplicationOverlay(true);
+
+ WindowManagerWrapper wmWrapper = WindowManagerWrapper.getInstance();
+ wmWrapper.setProvidesInsetsTypes(
+ mWindowLayoutParams,
+ new int[] { ITYPE_EXTRA_NAVIGATION_BAR, ITYPE_BOTTOM_TAPPABLE_ELEMENT }
+ );
+
+ mIconController.init(mOnTaskbarIconClickListener, mOnTaskbarIconLongClickListener);
+ mWindowManager.addView(mDragLayer, mWindowLayoutParams);
+ }
+
+ /**
+ * Updates the TaskbarContainer height (pass deviceProfile.taskbarSize to reset).
+ */
+ public void setTaskbarWindowHeight(int height) {
+ if (mWindowLayoutParams.height == height) {
+ return;
+ }
+ mWindowLayoutParams.height = height;
+ mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams);
+ }
+
+ public boolean canShowNavButtons() {
+ return ENABLE_THREE_BUTTON_TASKBAR && mNavMode == Mode.THREE_BUTTONS;
+ }
+
+ @Override
+ public LayoutInflater getLayoutInflater() {
+ return mLayoutInflater;
+ }
+
+ @Override
+ public TaskbarDragLayer getDragLayer() {
+ return mDragLayer;
+ }
+
+ @Override
+ public DeviceProfile getDeviceProfile() {
+ return mDeviceProfile;
+ }
+
+ @Override
+ public Rect getFolderBoundingBox() {
+ return mDragLayer.getFolderBoundingBox();
+ }
+
+ @Override
+ public DragController getDragController() {
+ return mDragController;
+ }
+
+ /**
+ * Sets a new data-source for this taskbar instance
+ */
+ public void setUIController(@NonNull TaskbarUIController uiController) {
+ mUIController.onDestroy();
+ mUIController = uiController;
+ mIconController.setUIController(mUIController);
+ mUIController.onCreate();
+ }
+
+ /**
+ * Called when this instance of taskbar is no longer needed
+ */
+ public void onDestroy() {
+ setUIController(TaskbarUIController.DEFAULT);
+ mIconController.onDestroy();
+ mWindowManager.removeViewImmediate(mDragLayer);
+ }
+
+ void onNavigationButtonClick(@TaskbarButton int buttonType) {
+ mNavButtonController.onButtonClick(buttonType);
+ }
+
+ /**
+ * Should be called when the IME visibility changes, so we can hide/show Taskbar accordingly.
+ */
+ public void setImeIsVisible(boolean isImeVisible) {
+ mIconController.setImeIsVisible(isImeVisible);
+ }
+
+ /**
+ * When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
+ * instantiating at all, which is what's responsible for sending sysui state flags over.
+ *
+ * @param vis IME visibility flag
+ */
+ public void updateImeStatus(int displayId, int vis, boolean showImeSwitcher) {
+ mIconController.updateImeStatus(displayId, vis, showImeSwitcher);
+ }
+
+ /**
+ * Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
+ */
+ protected void setTaskbarWindowFullscreen(boolean fullscreen) {
+ setTaskbarWindowHeight(fullscreen ? MATCH_PARENT : getDeviceProfile().taskbarSize);
+ }
+
+ protected void onTaskbarIconClicked(View view) {
+ Object tag = view.getTag();
+ if (tag instanceof Task) {
+ Task task = (Task) tag;
+ ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
+ ActivityOptions.makeBasic());
+ } else if (tag instanceof FolderInfo) {
+ FolderIcon folderIcon = (FolderIcon) view;
+ Folder folder = folderIcon.getFolder();
+ setTaskbarWindowFullscreen(true);
+
+ getDragLayer().post(() -> {
+ folder.animateOpen();
+
+ folder.iterateOverItems((itemInfo, itemView) -> {
+ itemView.setOnClickListener(mOnTaskbarIconClickListener);
+ itemView.setOnLongClickListener(mOnTaskbarIconLongClickListener);
+ // To play haptic when dragging, like other Taskbar items do.
+ itemView.setHapticFeedbackEnabled(true);
+ return false;
+ });
+ });
+ } else if (tag instanceof WorkspaceItemInfo) {
+ WorkspaceItemInfo info = (WorkspaceItemInfo) tag;
+ if (!(info.isDisabled() && ItemClickHandler.handleDisabledItemClicked(info, this))) {
+ Intent intent = new Intent(info.getIntent())
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) {
+ Toast.makeText(this, R.string.safemode_shortcut_error,
+ Toast.LENGTH_SHORT).show();
+ } else if (info.isPromise()) {
+ intent = new PackageManagerHelper(this)
+ .getMarketIntent(info.getTargetPackage())
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+
+ } else if (info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ String id = info.getDeepShortcutId();
+ String packageName = intent.getPackage();
+ getSystemService(LauncherApps.class)
+ .startShortcut(packageName, id, null, null, info.user);
+ } else if (info.user.equals(Process.myUserHandle())) {
+ startActivity(intent);
+ } else {
+ getSystemService(LauncherApps.class).startMainActivity(
+ intent.getComponent(), info.user, intent.getSourceBounds(), null);
+ }
+ } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
+ Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT)
+ .show();
+ Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e);
+ }
+ }
+ } else {
+ Log.e(TAG, "Unknown type clicked: " + tag);
+ }
+
+ AbstractFloatingView.closeAllOpenViews(this);
+ }
+
+ private static class MyDragController extends DragController<TaskbarActivityContext> {
+ MyDragController(TaskbarActivityContext activity) {
+ super(activity);
+ }
+
+ @Override
+ protected DragView startDrag(@Nullable Drawable drawable, @Nullable View view,
+ DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source,
+ ItemInfo dragInfo, Point dragOffset, Rect dragRegion, float initialDragViewScale,
+ float dragViewScaleOnDrop, DragOptions options) {
+ return null;
+ }
+
+ @Override
+ protected void exitDrag() {
+ }
+
+ @Override
+ protected DropTarget getDefaultDropTarget(int[] dropCoordinates) {
+ return null;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAnimationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAnimationController.java
new file mode 100644
index 0000000..e20ddf8
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAnimationController.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static com.android.launcher3.LauncherState.TASKBAR;
+
+import android.animation.Animator;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.taskbar.LauncherTaskbarUIController.TaskbarAnimationControllerCallbacks;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.system.QuickStepContract;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's visual properties based on factors such
+ * as LauncherState, whether Launcher is in the foreground, etc.
+ */
+public class TaskbarAnimationController {
+
+ private static final long IME_VISIBILITY_ALPHA_DURATION = 120;
+
+ private final BaseQuickstepLauncher mLauncher;
+ private final TaskbarAnimationControllerCallbacks mTaskbarCallbacks;
+
+ // Background alpha.
+ private final AnimatedFloat mTaskbarBackgroundAlpha = new AnimatedFloat(
+ this::onTaskbarBackgroundAlphaChanged);
+
+ // Overall visibility.
+ private final AnimatedFloat mTaskbarVisibilityAlphaForLauncherState = new AnimatedFloat(
+ this::updateVisibilityAlpha);
+ private final AnimatedFloat mTaskbarVisibilityAlphaForIme = new AnimatedFloat(
+ this::updateVisibilityAlphaForIme);
+
+ // Scale.
+ private final AnimatedFloat mTaskbarScaleForLauncherState = new AnimatedFloat(
+ this::updateScale);
+
+ // TranslationY.
+ private final AnimatedFloat mTaskbarTranslationYForLauncherState = new AnimatedFloat(
+ this::updateTranslationY);
+
+ public TaskbarAnimationController(BaseQuickstepLauncher launcher,
+ TaskbarAnimationControllerCallbacks taskbarCallbacks) {
+ mLauncher = launcher;
+ mTaskbarCallbacks = taskbarCallbacks;
+ }
+
+ protected void init() {
+ mTaskbarBackgroundAlpha.updateValue(mLauncher.hasBeenResumed() ? 0f : 1f);
+ boolean isVisibleForLauncherState = (mLauncher.getStateManager().getState()
+ .getVisibleElements(mLauncher) & TASKBAR) != 0;
+ mTaskbarVisibilityAlphaForLauncherState.updateValue(isVisibleForLauncherState ? 1f : 0f);
+ boolean isImeVisible = (SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags()
+ & QuickStepContract.SYSUI_STATE_IME_SHOWING) != 0;
+ mTaskbarVisibilityAlphaForIme.updateValue(isImeVisible ? 0f : 1f);
+
+ onTaskbarBackgroundAlphaChanged();
+ updateVisibilityAlpha();
+ }
+
+ protected void cleanup() {
+ setNavBarButtonAlpha(1f);
+ }
+
+ protected AnimatedFloat getTaskbarVisibilityForLauncherState() {
+ return mTaskbarVisibilityAlphaForLauncherState;
+ }
+
+ protected AnimatedFloat getTaskbarScaleForLauncherState() {
+ return mTaskbarScaleForLauncherState;
+ }
+
+ protected AnimatedFloat getTaskbarTranslationYForLauncherState() {
+ return mTaskbarTranslationYForLauncherState;
+ }
+
+ protected Animator createAnimToBackgroundAlpha(float toAlpha, long duration) {
+ return mTaskbarBackgroundAlpha.animateToValue(mTaskbarBackgroundAlpha.value, toAlpha)
+ .setDuration(duration);
+ }
+
+ protected void animateToVisibilityForIme(float toAlpha) {
+ mTaskbarVisibilityAlphaForIme.animateToValue(mTaskbarVisibilityAlphaForIme.value, toAlpha)
+ .setDuration(IME_VISIBILITY_ALPHA_DURATION).start();
+ }
+
+ private void onTaskbarBackgroundAlphaChanged() {
+ mTaskbarCallbacks.updateTaskbarBackgroundAlpha(mTaskbarBackgroundAlpha.value);
+ updateVisibilityAlpha();
+ updateScale();
+ updateTranslationY();
+ }
+
+ private void updateVisibilityAlpha() {
+ // We use mTaskbarBackgroundAlpha as a proxy for whether Launcher is resumed/paused, the
+ // assumption being that Taskbar should always be visible regardless of the current
+ // LauncherState if Launcher is paused.
+ float alphaDueToIme = mTaskbarVisibilityAlphaForIme.value;
+ float alphaDueToLauncher = Math.max(mTaskbarBackgroundAlpha.value,
+ mTaskbarVisibilityAlphaForLauncherState.value);
+ float taskbarAlpha = alphaDueToLauncher * alphaDueToIme;
+ mTaskbarCallbacks.updateTaskbarVisibilityAlpha(taskbarAlpha);
+
+ // Make the nav bar invisible if taskbar is visible.
+ setNavBarButtonAlpha(1f - taskbarAlpha);
+ }
+
+ private void updateVisibilityAlphaForIme() {
+ updateVisibilityAlpha();
+ float taskbarAlphaDueToIme = mTaskbarVisibilityAlphaForIme.value;
+ mTaskbarCallbacks.updateImeBarVisibilityAlpha(1f - taskbarAlphaDueToIme);
+ }
+
+ private void updateScale() {
+ // We use mTaskbarBackgroundAlpha as a proxy for whether Launcher is resumed/paused, the
+ // assumption being that Taskbar should always be at scale 1f regardless of the current
+ // LauncherState if Launcher is paused.
+ float scale = mTaskbarScaleForLauncherState.value;
+ scale = Utilities.mapRange(mTaskbarBackgroundAlpha.value, scale, 1f);
+ mTaskbarCallbacks.updateTaskbarScale(scale);
+ }
+
+ private void updateTranslationY() {
+ // We use mTaskbarBackgroundAlpha as a proxy for whether Launcher is resumed/paused, the
+ // assumption being that Taskbar should always be at translationY 0f regardless of the
+ // current LauncherState if Launcher is paused.
+ float translationY = mTaskbarTranslationYForLauncherState.value;
+ translationY = Utilities.mapRange(mTaskbarBackgroundAlpha.value, translationY, 0f);
+ mTaskbarCallbacks.updateTaskbarTranslationY(translationY);
+ }
+
+ private void setNavBarButtonAlpha(float navBarAlpha) {
+ SystemUiProxy.INSTANCE.get(mLauncher).setNavBarButtonAlpha(navBarAlpha, false);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
new file mode 100644
index 0000000..ee44927
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.os.UserHandle;
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ClipDescriptionCompat;
+import com.android.systemui.shared.system.LauncherAppsCompat;
+
+/**
+ * Handles long click on Taskbar items to start a system drag and drop operation.
+ */
+public class TaskbarDragController {
+
+ private final Context mContext;
+ private final int mDragIconSize;
+
+ public TaskbarDragController(Context context) {
+ mContext = context;
+ Resources resources = mContext.getResources();
+ mDragIconSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_drag_icon_size);
+ }
+
+ /**
+ * Attempts to start a system drag and drop operation for the given View, using its tag to
+ * generate the ClipDescription and Intent.
+ * @return Whether {@link View#startDragAndDrop} started successfully.
+ */
+ protected boolean startSystemDragOnLongClick(View view) {
+ if (!(view instanceof BubbleTextView)) {
+ return false;
+ }
+
+ BubbleTextView btv = (BubbleTextView) view;
+ View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view) {
+ @Override
+ public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
+ shadowSize.set(mDragIconSize, mDragIconSize);
+ // TODO: should be based on last touch point on the icon.
+ shadowTouchPoint.set(shadowSize.x / 2, shadowSize.y / 2);
+ }
+
+ @Override
+ public void onDrawShadow(Canvas canvas) {
+ canvas.save();
+ float scale = (float) mDragIconSize / btv.getIconSize();
+ canvas.scale(scale, scale);
+ btv.getIcon().draw(canvas);
+ canvas.restore();
+ }
+ };
+
+ Object tag = view.getTag();
+ ClipDescription clipDescription = null;
+ Intent intent = null;
+ if (tag instanceof WorkspaceItemInfo) {
+ WorkspaceItemInfo item = (WorkspaceItemInfo) tag;
+ LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+ clipDescription = new ClipDescription(item.title,
+ new String[] {
+ item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+ ? ClipDescriptionCompat.MIMETYPE_APPLICATION_SHORTCUT
+ : ClipDescriptionCompat.MIMETYPE_APPLICATION_ACTIVITY
+ });
+ intent = new Intent();
+ if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ID, item.getDeepShortcutId());
+ } else {
+ intent.putExtra(ClipDescriptionCompat.EXTRA_PENDING_INTENT,
+ LauncherAppsCompat.getMainActivityLaunchIntent(launcherApps,
+ item.getIntent().getComponent(), null, item.user));
+ }
+ intent.putExtra(Intent.EXTRA_USER, item.user);
+ } else if (tag instanceof Task) {
+ Task task = (Task) tag;
+ clipDescription = new ClipDescription(task.titleDescription,
+ new String[] {
+ ClipDescriptionCompat.MIMETYPE_APPLICATION_TASK
+ });
+ intent = new Intent();
+ intent.putExtra(ClipDescriptionCompat.EXTRA_TASK_ID, task.key.id);
+ intent.putExtra(Intent.EXTRA_USER, UserHandle.of(task.key.userId));
+ }
+
+ if (clipDescription != null && intent != null) {
+ ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent));
+ view.setOnDragListener(getDraggedViewDragListener());
+ return view.startDragAndDrop(clipData, shadowBuilder, null /* localState */,
+ View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE);
+ }
+ return false;
+ }
+
+ /**
+ * Hide the original Taskbar item while it is being dragged.
+ */
+ private View.OnDragListener getDraggedViewDragListener() {
+ return (view, dragEvent) -> {
+ switch (dragEvent.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ view.setVisibility(INVISIBLE);
+ return true;
+ case DragEvent.ACTION_DRAG_ENDED:
+ view.setVisibility(VISIBLE);
+ view.setOnDragListener(null);
+ return true;
+ }
+ return false;
+ };
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
new file mode 100644
index 0000000..45ec911
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.systemui.shared.system.ViewTreeObserverWrapper;
+import com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo;
+import com.android.systemui.shared.system.ViewTreeObserverWrapper.OnComputeInsetsListener;
+
+/**
+ * Top-level ViewGroup that hosts the TaskbarView as well as Views created by it such as Folder.
+ */
+public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
+
+ private final int mFolderMargin;
+ private final Paint mTaskbarBackgroundPaint;
+
+ private TaskbarIconController.Callbacks mControllerCallbacks;
+ private TaskbarView mTaskbarView;
+
+ private final OnComputeInsetsListener mTaskbarInsetsComputer = this::onComputeTaskbarInsets;
+
+ public TaskbarDragLayer(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, 1 /* alphaChannelCount */);
+ mFolderMargin = getResources().getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+ mTaskbarBackgroundPaint = new Paint();
+ mTaskbarBackgroundPaint.setColor(getResources().getColor(R.color.taskbar_background));
+ recreateControllers();
+ }
+
+ @Override
+ public void recreateControllers() {
+ mControllers = new TouchController[0];
+ }
+
+ public void init(TaskbarIconController.Callbacks callbacks, TaskbarView taskbarView) {
+ mControllerCallbacks = callbacks;
+ mTaskbarView = taskbarView;
+ }
+
+ private void onComputeTaskbarInsets(InsetsInfo insetsInfo) {
+ if (mControllerCallbacks != null) {
+ mControllerCallbacks.updateInsetsTouchability(insetsInfo);
+ }
+ }
+
+ protected void onDestroy() {
+ ViewTreeObserverWrapper.removeOnComputeInsetsListener(mTaskbarInsetsComputer);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ViewTreeObserverWrapper.addOnComputeInsetsListener(getViewTreeObserver(),
+ mTaskbarInsetsComputer);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ onDestroy();
+ }
+
+ @Override
+ protected boolean canFindActiveController() {
+ // Unlike super class, we want to be able to find controllers when touches occur in the
+ // gesture area. For example, this allows Folder to close itself when touching the Taskbar.
+ return true;
+ }
+
+ public void updateImeBarVisibilityAlpha(float alpha) {
+ if (mControllerCallbacks != null) {
+ mControllerCallbacks.updateImeBarVisibilityAlpha(alpha);
+ }
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ if (mControllerCallbacks != null) {
+ mControllerCallbacks.onDragLayerViewRemoved();
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ canvas.drawRect(0, canvas.getHeight() - mTaskbarView.getHeight(), canvas.getWidth(),
+ canvas.getHeight(), mTaskbarBackgroundPaint);
+ super.dispatchDraw(canvas);
+ }
+
+ /**
+ * @return Bounds (in our coordinates) where an opened Folder can display.
+ */
+ protected Rect getFolderBoundingBox() {
+ Rect boundingBox = new Rect(0, 0, getWidth(), getHeight() - mTaskbarView.getHeight());
+ boundingBox.inset(mFolderMargin, mFolderMargin);
+ return boundingBox;
+ }
+
+
+ /**
+ * Sets the alpha of the background color behind all the Taskbar contents.
+ * @param alpha 0 is fully transparent, 1 is fully opaque.
+ */
+ protected void setTaskbarBackgroundAlpha(float alpha) {
+ mTaskbarBackgroundPaint.setAlpha((int) (alpha * 255));
+ invalidate();
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
new file mode 100644
index 0000000..91cf7ef
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.model.data.ItemInfo;
+
+import java.util.function.Consumer;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's Hotseat items.
+ */
+public class TaskbarHotseatController {
+
+ private final BaseQuickstepLauncher mLauncher;
+ private final Hotseat mHotseat;
+ private final Consumer<ItemInfo[]> mTaskbarCallbacks;
+ private final int mNumHotseatIcons;
+
+ private final DragController.DragListener mDragListener = new DragController.DragListener() {
+ @Override
+ public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { }
+
+ @Override
+ public void onDragEnd() {
+ onHotseatUpdated();
+ }
+ };
+
+ public TaskbarHotseatController(
+ BaseQuickstepLauncher launcher, Consumer<ItemInfo[]> taskbarCallbacks) {
+ mLauncher = launcher;
+ mHotseat = mLauncher.getHotseat();
+ mTaskbarCallbacks = taskbarCallbacks;
+ mNumHotseatIcons = mLauncher.getDeviceProfile().numShownHotseatIcons;
+ }
+
+ protected void init() {
+ mLauncher.getDragController().addDragListener(mDragListener);
+ onHotseatUpdated();
+ }
+
+ protected void cleanup() {
+ mLauncher.getDragController().removeDragListener(mDragListener);
+ }
+
+ /**
+ * Called when any Hotseat item changes, and reports the new list of items to TaskbarController.
+ */
+ protected void onHotseatUpdated() {
+ ShortcutAndWidgetContainer shortcutsAndWidgets = mHotseat.getShortcutsAndWidgets();
+ ItemInfo[] hotseatItemInfos = new ItemInfo[mNumHotseatIcons];
+ for (int i = 0; i < shortcutsAndWidgets.getChildCount(); i++) {
+ View child = shortcutsAndWidgets.getChildAt(i);
+ Object tag = shortcutsAndWidgets.getChildAt(i).getTag();
+ if (tag instanceof ItemInfo) {
+ ItemInfo itemInfo = (ItemInfo) tag;
+ CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+ // Since the hotseat might be laid out vertically or horizontally, use whichever
+ // index is higher.
+ int index = Math.max(lp.cellX, lp.cellY);
+ if (0 <= index && index < hotseatItemInfos.length) {
+ hotseatItemInfos[index] = itemInfo;
+ }
+ }
+ }
+
+ mTaskbarCallbacks.accept(hotseatItemInfos);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java
new file mode 100644
index 0000000..683a5b9
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_FRAME;
+import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION;
+
+import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo;
+
+/**
+ * Controller for taskbar icon UI
+ */
+public class TaskbarIconController {
+
+ private final Rect mTempRect = new Rect();
+
+ private final TaskbarActivityContext mActivity;
+ private final TaskbarDragLayer mDragLayer;
+
+ private final TaskbarView mTaskbarView;
+ private final ImeBarView mImeBarView;
+
+ @NonNull
+ private TaskbarUIController mUIController = TaskbarUIController.DEFAULT;
+
+ TaskbarIconController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
+ mActivity = activity;
+ mDragLayer = dragLayer;
+ mTaskbarView = mDragLayer.findViewById(R.id.taskbar_view);
+ mImeBarView = mDragLayer.findViewById(R.id.ime_bar_view);
+ }
+
+ public void init(OnClickListener clickListener, OnLongClickListener longClickListener) {
+ mDragLayer.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) ->
+ mUIController.alignRealHotseatWithTaskbar());
+
+ ButtonProvider buttonProvider = new ButtonProvider(mActivity);
+ mImeBarView.init(buttonProvider);
+ mTaskbarView.construct(clickListener, longClickListener, buttonProvider);
+ mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
+
+ mDragLayer.init(new Callbacks(), mTaskbarView);
+ }
+
+ public void onDestroy() {
+ mDragLayer.onDestroy();
+ }
+
+ public void setUIController(@NonNull TaskbarUIController uiController) {
+ mUIController = uiController;
+ }
+
+ /**
+ * When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
+ * instantiating at all, which is what's responsible for sending sysui state flags over.
+ *
+ * @param vis IME visibility flag
+ */
+ public void updateImeStatus(int displayId, int vis, boolean showImeSwitcher) {
+ if (displayId != mActivity.getDisplayId() || !mActivity.canShowNavButtons()) {
+ return;
+ }
+
+ mImeBarView.setImeSwitcherVisibility(showImeSwitcher);
+ setImeIsVisible((vis & InputMethodService.IME_VISIBLE) != 0);
+ }
+
+ /**
+ * Should be called when the IME visibility changes, so we can hide/show Taskbar accordingly.
+ */
+ public void setImeIsVisible(boolean isImeVisible) {
+ mTaskbarView.setTouchesEnabled(!isImeVisible);
+ mUIController.onImeVisible(mDragLayer, isImeVisible);
+ }
+
+ /**
+ * Callbacks for {@link TaskbarDragLayer} to interact with the icon controller
+ */
+ public class Callbacks {
+
+ /**
+ * Called to update the touchable insets
+ */
+ public void updateInsetsTouchability(InsetsInfo insetsInfo) {
+ insetsInfo.touchableRegion.setEmpty();
+ if (mDragLayer.getAlpha() < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
+ // Let touches pass through us.
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+ } else if (mImeBarView.getVisibility() == VISIBLE) {
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
+ } else if (!mUIController.isTaskbarTouchable()) {
+ // Let touches pass through us.
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+ } else if (mTaskbarView.areIconsVisible()) {
+ // Buttons are visible, take over the full taskbar area
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
+ } else {
+ if (mTaskbarView.mSystemButtonContainer.getVisibility() == VISIBLE) {
+ mDragLayer.getDescendantRectRelativeToSelf(
+ mTaskbarView.mSystemButtonContainer, mTempRect);
+ insetsInfo.touchableRegion.set(mTempRect);
+ }
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+ }
+
+ // TaskbarContainerView provides insets to other apps based on contentInsets. These
+ // insets should stay consistent even if we expand TaskbarContainerView's bounds, e.g.
+ // to show a floating view like Folder. Thus, we set the contentInsets to be where
+ // mTaskbarView is, since its position never changes and insets rather than overlays.
+ insetsInfo.contentInsets.left = mTaskbarView.getLeft();
+ insetsInfo.contentInsets.top = mTaskbarView.getTop();
+ insetsInfo.contentInsets.right = mDragLayer.getWidth() - mTaskbarView.getRight();
+ insetsInfo.contentInsets.bottom = mDragLayer.getHeight() - mTaskbarView.getBottom();
+ }
+
+ public void onDragLayerViewRemoved() {
+ int count = mDragLayer.getChildCount();
+ // Ensure no other children present (like Folders, etc)
+ for (int i = 0; i < count; i++) {
+ View v = mDragLayer.getChildAt(i);
+ if (!((v instanceof TaskbarView) || (v instanceof ImeBarView))) {
+ return;
+ }
+ }
+ mActivity.setTaskbarWindowFullscreen(false);
+ }
+
+ public void updateImeBarVisibilityAlpha(float alpha) {
+ if (!mActivity.canShowNavButtons()) {
+ // TODO Remove sysui IME bar for gesture nav as well
+ return;
+ }
+ mImeBarView.setAlpha(alpha);
+ mImeBarView.setVisibility(alpha == 0 ? GONE : VISIBLE);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
new file mode 100644
index 0000000..d026bfb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
+import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
+import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.inputmethodservice.InputMethodService;
+import android.view.Display;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.DisplayController.Info;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.TouchInteractionService;
+
+/**
+ * Class to manager taskbar lifecycle
+ */
+public class TaskbarManager implements DisplayController.DisplayInfoChangeListener,
+ SysUINavigationMode.NavigationModeChangeListener {
+
+ private final Context mContext;
+ private final DisplayController mDisplayController;
+ private final SysUINavigationMode mSysUINavigationMode;
+ private final TaskbarNavButtonController mNavButtonController;
+
+ private TaskbarActivityContext mTaskbarActivityContext;
+ private BaseQuickstepLauncher mLauncher;
+
+ private static final int CHANGE_FLAGS =
+ CHANGE_ACTIVE_SCREEN | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS;
+
+ private boolean mUserUnlocked = false;
+
+ public TaskbarManager(TouchInteractionService service) {
+ mDisplayController = DisplayController.INSTANCE.get(service);
+ mSysUINavigationMode = SysUINavigationMode.INSTANCE.get(service);
+ Display display =
+ service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
+ mContext = service.createWindowContext(display, TYPE_APPLICATION_OVERLAY, null);
+ mNavButtonController = new TaskbarNavButtonController(service);
+
+ mDisplayController.addChangeListener(this);
+ mSysUINavigationMode.addModeChangeListener(this);
+ recreateTaskbar();
+ }
+
+ @Override
+ public void onNavigationModeChanged(Mode newMode) {
+ recreateTaskbar();
+ }
+
+ @Override
+ public void onDisplayInfoChanged(Context context, Info info, int flags) {
+ if ((flags & CHANGE_FLAGS) != 0) {
+ recreateTaskbar();
+ }
+ }
+
+ private void destroyExistingTaskbar() {
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.onDestroy();
+ mTaskbarActivityContext = null;
+ }
+ }
+
+ /**
+ * Called when the user is unlocked
+ */
+ public void onUserUnlocked() {
+ mUserUnlocked = true;
+ recreateTaskbar();
+ }
+
+ /**
+ * Sets or clears a launcher to act as taskbar callback
+ */
+ public void setLauncher(@Nullable BaseQuickstepLauncher launcher) {
+ mLauncher = launcher;
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.setUIController(mLauncher == null
+ ? TaskbarUIController.DEFAULT
+ : new LauncherTaskbarUIController(launcher, mTaskbarActivityContext));
+ }
+ }
+
+ private void recreateTaskbar() {
+ destroyExistingTaskbar();
+ if (!FeatureFlags.ENABLE_TASKBAR.get()) {
+ return;
+ }
+ if (!mUserUnlocked) {
+ return;
+ }
+ DeviceProfile dp = LauncherAppState.getIDP(mContext).getDeviceProfile(mContext);
+ if (!dp.isTaskbarPresent) {
+ return;
+ }
+ mTaskbarActivityContext = new TaskbarActivityContext(
+ mContext, dp.copy(mContext), mNavButtonController);
+ mTaskbarActivityContext.init();
+ if (mLauncher != null) {
+ mTaskbarActivityContext.setUIController(
+ new LauncherTaskbarUIController(mLauncher, mTaskbarActivityContext));
+ }
+ }
+
+ /**
+ * See {@link com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags}
+ * @param systemUiStateFlags The latest SystemUiStateFlags
+ */
+ public void onSystemUiFlagsChanged(int systemUiStateFlags) {
+ boolean isImeVisible = (systemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.setImeIsVisible(isImeVisible);
+ }
+ }
+
+ /**
+ * When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
+ * instantiating at all, which is what's responsible for sending sysui state flags over.
+ *
+ * @param vis IME visibility flag
+ * @param backDisposition Used to determine back button behavior for software keyboard
+ * See BACK_DISPOSITION_* constants in {@link InputMethodService}
+ */
+ public void updateImeStatus(int displayId, int vis, int backDisposition,
+ boolean showImeSwitcher) {
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.updateImeStatus(displayId, vis, showImeSwitcher);
+ }
+ }
+
+ /**
+ * Called when the manager is no longer needed
+ */
+ public void destroy() {
+ destroyExistingTaskbar();
+ mDisplayController.removeChangeListener(this);
+ mSysUINavigationMode.removeModeChangeListener(this);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
new file mode 100644
index 0000000..3b5afad
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.content.Intent;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.annotation.IntDef;
+
+import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TouchInteractionService;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Controller for 3 button mode in the taskbar.
+ * Handles all the functionality of the various buttons, making/routing the right calls into
+ * launcher or sysui/system.
+ *
+ * TODO: Create callbacks to hook into UI layer since state will change for more context buttons/
+ * assistant invocation.
+ */
+public class TaskbarNavButtonController {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BUTTON_BACK,
+ BUTTON_HOME,
+ BUTTON_RECENTS,
+ BUTTON_IME_SWITCH
+ })
+
+ public @interface TaskbarButton {}
+
+ static final int BUTTON_BACK = 1;
+ static final int BUTTON_HOME = BUTTON_BACK << 1;
+ static final int BUTTON_RECENTS = BUTTON_HOME << 1;
+ static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1;
+
+ private final TouchInteractionService mService;
+
+ public TaskbarNavButtonController(TouchInteractionService service) {
+ mService = service;
+ }
+
+ public void onButtonClick(@TaskbarButton int buttonType) {
+ switch (buttonType) {
+ case BUTTON_BACK:
+ executeBack();
+ break;
+ case BUTTON_HOME:
+ navigateHome();
+ break;
+ case BUTTON_RECENTS:
+ navigateToOverview();;
+ break;
+ case BUTTON_IME_SWITCH:
+ showIMESwitcher();
+ break;
+ }
+ }
+
+ private void navigateHome() {
+ mService.startActivity(new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_HOME)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+
+ private void navigateToOverview() {
+ mService.getOverviewCommandHelper()
+ .addCommand(OverviewCommandHelper.TYPE_SHOW);
+ }
+
+ private void executeBack() {
+ SystemUiProxy.INSTANCE.getNoCreate().onBackPressed();
+ }
+
+ private void showIMESwitcher() {
+ mService.getSystemService(InputMethodManager.class)
+ .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
+ DEFAULT_DISPLAY);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
new file mode 100644
index 0000000..a701aae
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static com.android.launcher3.LauncherState.TASKBAR;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * StateHandler to animate Taskbar according to Launcher's state machine. Does nothing if Taskbar
+ * isn't present (i.e. {@link #setAnimationController} is never called).
+ */
+public class TaskbarStateHandler implements StateManager.StateHandler<LauncherState> {
+
+ private final BaseQuickstepLauncher mLauncher;
+
+ // Contains Taskbar-related methods and fields we should aniamte. If null, don't do anything.
+ private @Nullable TaskbarAnimationController mAnimationController = null;
+
+ public TaskbarStateHandler(BaseQuickstepLauncher launcher) {
+ mLauncher = launcher;
+ }
+
+ public void setAnimationController(TaskbarAnimationController callbacks) {
+ mAnimationController = callbacks;
+ }
+
+ @Override
+ public void setState(LauncherState state) {
+ setState(state, PropertySetter.NO_ANIM_PROPERTY_SETTER);
+ }
+
+ @Override
+ public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
+ PendingAnimation animation) {
+ setState(toState, animation);
+ }
+
+ private void setState(LauncherState toState, PropertySetter setter) {
+ if (mAnimationController == null) {
+ return;
+ }
+
+ boolean isTaskbarVisible = (toState.getVisibleElements(mLauncher) & TASKBAR) != 0;
+ setter.setFloat(mAnimationController.getTaskbarVisibilityForLauncherState(),
+ AnimatedFloat.VALUE, isTaskbarVisible ? 1f : 0f, LINEAR);
+ setter.setFloat(mAnimationController.getTaskbarScaleForLauncherState(),
+ AnimatedFloat.VALUE, toState.getTaskbarScale(mLauncher), LINEAR);
+ setter.setFloat(mAnimationController.getTaskbarTranslationYForLauncherState(),
+ AnimatedFloat.VALUE, toState.getTaskbarTranslationY(mLauncher), ACCEL_DEACCEL);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
new file mode 100644
index 0000000..50adead
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+/**
+ * Base class for providing different taskbar UI
+ */
+public class TaskbarUIController {
+
+ public static final TaskbarUIController DEFAULT = new TaskbarUIController();
+
+ /**
+ * Pads the Hotseat to line up exactly with Taskbar's copy of the Hotseat.
+ */
+ public void alignRealHotseatWithTaskbar() { }
+
+ protected void onCreate() { }
+
+ protected void onDestroy() { }
+
+ protected boolean isTaskbarTouchable() {
+ return true;
+ }
+
+ protected void onImeVisible(TaskbarDragLayer container, boolean isVisible) {
+ container.updateImeBarVisibilityAlpha(isVisible ? 1 : 0);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
new file mode 100644
index 0000000..c6573a6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.LinearLayout;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
+
+/**
+ * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
+ */
+public class TaskbarView extends LinearLayout implements FolderIcon.FolderIconParent, Insettable {
+
+ private final int mIconTouchSize;
+ private final boolean mIsRtl;
+ private final int mTouchSlop;
+ private final RectF mTempDelegateBounds = new RectF();
+ private final RectF mDelegateSlopBounds = new RectF();
+ private final int[] mTempOutLocation = new int[2];
+
+ private final int mItemMarginLeftRight;
+
+ private final TaskbarActivityContext mActivityContext;
+
+ // Initialized in TaskbarController constructor.
+ private View.OnClickListener mIconClickListener;
+ private View.OnLongClickListener mIconLongClickListener;
+
+ LinearLayout mSystemButtonContainer;
+ LinearLayout mHotseatIconsContainer;
+
+ // Delegate touches to the closest view if within mIconTouchSize.
+ private boolean mDelegateTargeted;
+ private View mDelegateView;
+ // Prevents dispatching touches to children if true
+ private boolean mTouchEnabled = true;
+
+ private boolean mIsDraggingItem;
+ // Only non-null when the corresponding Folder is open.
+ private @Nullable FolderIcon mLeaveBehindFolderIcon;
+
+ /** Provider of buttons added to taskbar in 3 button nav */
+ private ButtonProvider mButtonProvider;
+
+ private boolean mDisableRelayout;
+ private boolean mAreHolesAllowed;
+
+ public TaskbarView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mActivityContext = ActivityContext.lookupContext(context);
+
+ Resources resources = getResources();
+ mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
+ mItemMarginLeftRight = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
+
+ mIsRtl = Utilities.isRtl(resources);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mSystemButtonContainer = findViewById(R.id.system_button_layout);
+ mHotseatIconsContainer = findViewById(R.id.hotseat_icons_layout);
+ }
+
+ protected void construct(OnClickListener clickListener, OnLongClickListener longClickListener,
+ ButtonProvider buttonProvider) {
+ mIconClickListener = clickListener;
+ mIconLongClickListener = longClickListener;
+ mButtonProvider = buttonProvider;
+
+ if (mActivityContext.canShowNavButtons()) {
+ createNavButtons();
+ } else {
+ mSystemButtonContainer.setVisibility(GONE);
+ }
+
+ int numHotseatIcons = mActivityContext.getDeviceProfile().numShownHotseatIcons;
+ updateHotseatItems(new ItemInfo[numHotseatIcons]);
+ }
+
+ /**
+ * Enables/disables empty icons in taskbar so that the layout matches with Launcher
+ */
+ public void setHolesAllowedInLayout(boolean areHolesAllowed) {
+ if (mAreHolesAllowed != areHolesAllowed) {
+ mAreHolesAllowed = areHolesAllowed;
+ updateHotseatItemsVisibility();
+ // TODO: Add animation
+ }
+ }
+
+ private void setHolesAllowedInLayoutNoAnimation(boolean areHolesAllowed) {
+ if (mAreHolesAllowed != areHolesAllowed) {
+ mAreHolesAllowed = areHolesAllowed;
+ updateHotseatItemsVisibility();
+ onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
+ makeMeasureSpec(getMeasuredHeight(), EXACTLY));
+ onLayout(false, getLeft(), getTop(), getRight(), getBottom());
+ }
+ }
+
+ /**
+ * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
+ */
+ protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+ for (int i = 0; i < hotseatItemInfos.length; i++) {
+ ItemInfo hotseatItemInfo = hotseatItemInfos[
+ !mIsRtl ? i : hotseatItemInfos.length - i - 1];
+ View hotseatView = mHotseatIconsContainer.getChildAt(i);
+
+ // Replace any Hotseat views with the appropriate type if it's not already that type.
+ final int expectedLayoutResId;
+ boolean isFolder = false;
+ boolean needsReinflate = false;
+ if (hotseatItemInfo != null && hotseatItemInfo.isPredictedItem()) {
+ expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
+ } else if (hotseatItemInfo instanceof FolderInfo) {
+ expectedLayoutResId = R.layout.folder_icon;
+ isFolder = true;
+ // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation, so
+ // if the info changes we need to reinflate. This should only happen if a new folder
+ // is dragged to the position that another folder previously existed.
+ needsReinflate = hotseatView != null && hotseatView.getTag() != hotseatItemInfo;
+ } else {
+ expectedLayoutResId = R.layout.taskbar_app_icon;
+ }
+ if (hotseatView == null
+ || hotseatView.getSourceLayoutResId() != expectedLayoutResId
+ || needsReinflate) {
+ mHotseatIconsContainer.removeView(hotseatView);
+ if (isFolder) {
+ FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
+ FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
+ mActivityContext, this, folderInfo);
+ folderIcon.setTextVisible(false);
+ hotseatView = folderIcon;
+ } else {
+ hotseatView = inflate(expectedLayoutResId);
+ }
+ int iconSize = mActivityContext.getDeviceProfile().iconSizePx;
+ LayoutParams lp = new LayoutParams(iconSize, iconSize);
+ lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+ mHotseatIconsContainer.addView(hotseatView, i, lp);
+ }
+
+ // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
+ if (hotseatView instanceof BubbleTextView
+ && hotseatItemInfo instanceof WorkspaceItemInfo) {
+ ((BubbleTextView) hotseatView).applyFromWorkspaceItem(
+ (WorkspaceItemInfo) hotseatItemInfo);
+ hotseatView.setOnClickListener(mIconClickListener);
+ hotseatView.setOnLongClickListener(mIconLongClickListener);
+ } else if (isFolder) {
+ hotseatView.setOnClickListener(mIconClickListener);
+ hotseatView.setOnLongClickListener(mIconLongClickListener);
+ } else {
+ hotseatView.setOnClickListener(null);
+ hotseatView.setOnLongClickListener(null);
+ hotseatView.setTag(null);
+ }
+ updateHotseatItemVisibility(hotseatView);
+ }
+ }
+
+ protected void updateHotseatItemsVisibility() {
+ for (int i = mHotseatIconsContainer.getChildCount() - 1; i >= 0; i--) {
+ updateHotseatItemVisibility(mHotseatIconsContainer.getChildAt(i));
+ }
+ }
+
+ private void updateHotseatItemVisibility(View hotseatView) {
+ hotseatView.setVisibility(
+ hotseatView.getTag() != null ? VISIBLE : (mAreHolesAllowed ? INVISIBLE : GONE));
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (!mTouchEnabled) {
+ return true;
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean handled = delegateTouchIfNecessary(event);
+ return super.onTouchEvent(event) || handled;
+ }
+
+ public void setTouchesEnabled(boolean touchEnabled) {
+ this.mTouchEnabled = touchEnabled;
+ }
+
+ /**
+ * User touched the Taskbar background. Determine whether the touch is close enough to a view
+ * that we should forward the touches to it.
+ * @return Whether a delegate view was chosen and it handled the touch event.
+ */
+ private boolean delegateTouchIfNecessary(MotionEvent event) {
+ final float x = event.getX();
+ final float y = event.getY();
+ if (mDelegateView == null && event.getAction() == MotionEvent.ACTION_DOWN) {
+ View delegateView = findDelegateView(x, y);
+ if (delegateView != null) {
+ mDelegateTargeted = true;
+ mDelegateView = delegateView;
+ mDelegateSlopBounds.set(mTempDelegateBounds);
+ mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop);
+ }
+ }
+
+ boolean sendToDelegate = mDelegateTargeted;
+ boolean inBounds = true;
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_MOVE:
+ inBounds = mDelegateSlopBounds.contains(x, y);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mDelegateTargeted = false;
+ break;
+ }
+
+ boolean handled = false;
+ if (sendToDelegate) {
+ if (inBounds) {
+ // Offset event coordinates to be inside the target view
+ event.setLocation(mDelegateView.getWidth() / 2f, mDelegateView.getHeight() / 2f);
+ } else {
+ // Offset event coordinates to be outside the target view (in case it does
+ // something like tracking pressed state)
+ event.setLocation(-mTouchSlop * 2, -mTouchSlop * 2);
+ }
+ handled = mDelegateView.dispatchTouchEvent(event);
+ // Cleanup if this was the last event to send to the delegate.
+ if (!mDelegateTargeted) {
+ mDelegateView = null;
+ }
+ }
+ return handled;
+ }
+
+ /**
+ * Return an item whose touch bounds contain the given coordinates,
+ * or null if no such item exists.
+ *
+ * Also sets {@link #mTempDelegateBounds} to be the touch bounds of the chosen delegate view.
+ */
+ private @Nullable View findDelegateView(float x, float y) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (!child.isShown() || !child.isClickable()) {
+ continue;
+ }
+ int childCenterX = child.getLeft() + child.getWidth() / 2;
+ int childCenterY = child.getTop() + child.getHeight() / 2;
+ mTempDelegateBounds.set(
+ childCenterX - mIconTouchSize / 2f,
+ childCenterY - mIconTouchSize / 2f,
+ childCenterX + mIconTouchSize / 2f,
+ childCenterY + mIconTouchSize / 2f);
+ if (mTempDelegateBounds.contains(x, y)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
+ * touch bounds.
+ */
+ public boolean isEventOverAnyItem(MotionEvent ev) {
+ getLocationOnScreen(mTempOutLocation);
+ float xInOurCoordinates = ev.getX() - mTempOutLocation[0];
+ float yInOurCoorindates = ev.getY() - mTempOutLocation[1];
+ return findDelegateView(xInOurCoordinates, yInOurCoorindates) != null;
+ }
+
+ /**
+ * Add back/home/recents buttons into a single ViewGroup that will be inserted at
+ * {@param navButtonStartIndex}
+ */
+ private void createNavButtons() {
+ LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
+ mActivityContext.getDeviceProfile().iconSizePx,
+ mActivityContext.getDeviceProfile().iconSizePx
+ );
+ buttonParams.gravity = Gravity.CENTER;
+
+ mSystemButtonContainer.addView(mButtonProvider.getBack(), buttonParams);
+ mSystemButtonContainer.addView(mButtonProvider.getHome(), buttonParams);
+ mSystemButtonContainer.addView(mButtonProvider.getRecents(), buttonParams);
+ }
+
+ @Override
+ public boolean onDragEvent(DragEvent event) {
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ mIsDraggingItem = true;
+ AbstractFloatingView.closeAllOpenViews(mActivityContext);
+ return true;
+ case DragEvent.ACTION_DRAG_ENDED:
+ mIsDraggingItem = false;
+ break;
+ }
+ return super.onDragEvent(event);
+ }
+
+ public boolean isDraggingItem() {
+ return mIsDraggingItem;
+ }
+
+ /**
+ * @return The bounding box of where the hotseat elements are relative to this TaskbarView.
+ */
+ protected RectF getHotseatBounds() {
+ RectF result;
+ mDisableRelayout = true;
+ boolean wereHolesAllowed = mAreHolesAllowed;
+ setHolesAllowedInLayoutNoAnimation(true);
+ result = new RectF(
+ mHotseatIconsContainer.getLeft(),
+ mHotseatIconsContainer.getTop(),
+ mHotseatIconsContainer.getRight(),
+ mHotseatIconsContainer.getBottom());
+ setHolesAllowedInLayoutNoAnimation(wereHolesAllowed);
+ mDisableRelayout = false;
+
+ return result;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mDisableRelayout) {
+ super.requestLayout();
+ }
+ }
+
+ // FolderIconParent implemented methods.
+
+ @Override
+ public void drawFolderLeaveBehindForIcon(FolderIcon child) {
+ mLeaveBehindFolderIcon = child;
+ invalidate();
+ }
+
+ @Override
+ public void clearFolderLeaveBehind(FolderIcon child) {
+ mLeaveBehindFolderIcon = null;
+ invalidate();
+ }
+
+ // End FolderIconParent implemented methods.
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mLeaveBehindFolderIcon != null) {
+ canvas.save();
+ canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
+ mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas);
+ canvas.restore();
+ }
+ }
+
+ private View inflate(@LayoutRes int layoutResId) {
+ return mActivityContext.getLayoutInflater().inflate(layoutResId, this, false);
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ // Ignore, we just implement Insettable to draw behind system insets.
+ }
+
+ public void setIconsVisibility(boolean isVisible) {
+ mHotseatIconsContainer.setVisibility(isVisible ? VISIBLE : INVISIBLE);
+ }
+
+ public boolean areIconsVisible() {
+ return mHotseatIconsContainer.getVisibility() == VISIBLE;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index 1e03b05..76a5782 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -18,13 +18,23 @@
import android.app.Person;
import android.content.pm.ShortcutInfo;
+import android.view.Display;
import com.android.launcher3.Utilities;
public class ApiWrapper {
+ public static final boolean TASKBAR_DRAWN_IN_PROCESS = true;
+
public static Person[] getPersons(ShortcutInfo si) {
Person[] persons = si.getPersons();
return persons == null ? Utilities.EMPTY_PERSON_ARRAY : persons;
}
+
+ /**
+ * Returns true if the display is an internal displays
+ */
+ public static boolean isInternalDisplay(Display display) {
+ return display.getType() == Display.TYPE_INTERNAL;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index ec3a490..1d52315 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -16,19 +16,20 @@
package com.android.launcher3.uioverrides;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCRIM_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import android.util.FloatProperty;
@@ -37,9 +38,9 @@
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.graphics.OverviewScrim;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.quickstep.views.RecentsView;
/**
@@ -61,22 +62,19 @@
@Override
public void setState(@NonNull LauncherState state) {
float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
- SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
- ADJACENT_PAGE_OFFSET.set(mRecentsView, scaleAndOffset[1]);
+ RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+ ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, scaleAndOffset[1]);
+ TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f);
getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
- OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
- SCRIM_PROGRESS.set(scrim, state.getOverviewScrimAlpha(mLauncher));
getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
+ RECENTS_GRID_PROGRESS.set(mRecentsView,
+ state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f);
}
@Override
public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
PendingAnimation builder) {
- if (!config.hasAnimationFlag(PLAY_ATOMIC_OVERVIEW_PEEK | PLAY_ATOMIC_OVERVIEW_SCALE)) {
- // The entire recents animation is played atomically.
- return;
- }
if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
return;
}
@@ -93,21 +91,29 @@
void setStateWithAnimationInternal(@NonNull final LauncherState toState,
@NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
- setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+ setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
- setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
+ setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1],
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
+ setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
+ config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
+ PagedOrientationHandler orientationHandler =
+ ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
+ FloatProperty taskViewsFloat = orientationHandler.getSplitSelectTaskOffset(
+ TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
+ mLauncher.getDeviceProfile());
+ setter.setFloat(mRecentsView, taskViewsFloat,
+ toState.getSplitSelectTranslation(mLauncher), LINEAR);
setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
- OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
- setter.setFloat(scrim, SCRIM_PROGRESS, toState.getOverviewScrimAlpha(mLauncher),
- config.getInterpolator(ANIM_OVERVIEW_SCRIM_FADE, LINEAR));
setter.setFloat(
mRecentsView, getTaskModalnessProperty(),
toState.getOverviewModalness(),
config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
+ setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
+ toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f, LINEAR);
}
abstract FloatProperty getTaskModalnessProperty();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
index 010694b..c115bbb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -60,6 +60,20 @@
mListeners.add(r);
}
+ @Override
+ public void removeChangeListener(Runnable r) {
+ if (mListeners == null) {
+ return;
+ }
+ mListeners.remove(r);
+ }
+
+ @Override
+ public boolean get() {
+ // Override this method in order to let Robolectric ShadowDeviceFlag to stub it.
+ return super.get();
+ }
+
private void registerDeviceConfigChangedListener(Context context) {
DeviceConfig.addOnPropertiesChangedListener(
NAMESPACE_LAUNCHER,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
new file mode 100644
index 0000000..d839a36
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import android.content.Context;
+import android.graphics.BlurMaskFilter;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.os.Process;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.GraphicsUtils;
+import com.android.launcher3.icons.IconNormalizer;
+import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.DoubleShadowBubbleTextView;
+
+/**
+ * A BubbleTextView with a ring around it's drawable
+ */
+public class PredictedAppIcon extends DoubleShadowBubbleTextView {
+
+ private static final int RING_SHADOW_COLOR = 0x99000000;
+ private static final float RING_EFFECT_RATIO = 0.095f;
+
+ boolean mIsDrawingDot = false;
+ private final DeviceProfile mDeviceProfile;
+ private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Path mRingPath = new Path();
+ private final int mNormalizedIconSize;
+ private final Path mShapePath;
+ private final Matrix mTmpMatrix = new Matrix();
+
+ private final BlurMaskFilter mShadowFilter;
+
+ private boolean mIsPinned = false;
+ private int mPlateColor;
+ boolean mDrawForDrag = false;
+
+ public PredictedAppIcon(Context context) {
+ this(context, null, 0);
+ }
+
+ public PredictedAppIcon(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
+ mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize());
+ int shadowSize = context.getResources().getDimensionPixelSize(
+ R.dimen.blur_size_thin_outline);
+ mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
+ mShapePath = GraphicsUtils.getShapePath(mNormalizedIconSize);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int count = canvas.save();
+ if (!mIsPinned) {
+ drawEffect(canvas);
+ canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
+ canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
+ }
+ super.onDraw(canvas);
+ canvas.restoreToCount(count);
+ }
+
+ @Override
+ protected void drawDotIfNecessary(Canvas canvas) {
+ mIsDrawingDot = true;
+ int count = canvas.save();
+ canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO);
+ canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO);
+ super.drawDotIfNecessary(canvas);
+ canvas.restoreToCount(count);
+ mIsDrawingDot = false;
+ }
+
+ @Override
+ public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
+ super.applyFromWorkspaceItem(info);
+ mPlateColor = ColorUtils.setAlphaComponent(mDotParams.color, 200);
+ if (mIsPinned) {
+ setContentDescription(info.contentDescription);
+ } else {
+ setContentDescription(
+ getContext().getString(R.string.hotseat_prediction_content_description,
+ info.contentDescription));
+ }
+ }
+
+ /**
+ * Removes prediction ring from app icon
+ */
+ public void pin(WorkspaceItemInfo info) {
+ if (mIsPinned) return;
+ mIsPinned = true;
+ applyFromWorkspaceItem(info);
+ setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+ ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
+ invalidate();
+ }
+
+ /**
+ * prepares prediction icon for usage after bind
+ */
+ public void finishBinding(OnLongClickListener longClickListener) {
+ setOnLongClickListener(longClickListener);
+ ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
+ setTextVisibility(false);
+ verifyHighRes();
+ }
+
+ @Override
+ public void getIconBounds(Rect outBounds) {
+ super.getIconBounds(outBounds);
+ if (!mIsPinned && !mIsDrawingDot) {
+ int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
+ outBounds.inset(predictionInset, predictionInset);
+ }
+ }
+
+ public boolean isPinned() {
+ return mIsPinned;
+ }
+
+ private int getOutlineOffsetX() {
+ return (getMeasuredWidth() - mNormalizedIconSize) / 2;
+ }
+
+ private int getOutlineOffsetY() {
+ if (mDisplay != DISPLAY_TASKBAR) {
+ return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+ }
+ return (getMeasuredHeight() - mNormalizedIconSize) / 2;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateRingPath();
+ }
+
+ @Override
+ public void setTag(Object tag) {
+ super.setTag(tag);
+ updateRingPath();
+ }
+
+ private void updateRingPath() {
+ boolean isBadged = false;
+ if (getTag() instanceof WorkspaceItemInfo) {
+ WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
+ isBadged = !Process.myUserHandle().equals(info.user)
+ || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
+ || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+ }
+
+ mRingPath.reset();
+ mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
+
+ mRingPath.addPath(mShapePath, mTmpMatrix);
+ if (isBadged) {
+ float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
+ float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
+ float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
+ float scale = badgeSize / mNormalizedIconSize;
+ mTmpMatrix.postTranslate(mNormalizedIconSize, mNormalizedIconSize);
+ mTmpMatrix.preScale(scale, scale);
+ mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize);
+ mRingPath.addPath(mShapePath, mTmpMatrix);
+ }
+ }
+
+ private void drawEffect(Canvas canvas) {
+ // Don't draw ring effect if item is about to be dragged.
+ if (mDrawForDrag) {
+ return;
+ }
+ mIconRingPaint.setColor(RING_SHADOW_COLOR);
+ mIconRingPaint.setMaskFilter(mShadowFilter);
+ canvas.drawPath(mRingPath, mIconRingPaint);
+ mIconRingPaint.setColor(mPlateColor);
+ mIconRingPaint.setMaskFilter(null);
+ canvas.drawPath(mRingPath, mIconRingPaint);
+ }
+
+ @Override
+ public void getSourceVisualDragBounds(Rect bounds) {
+ super.getSourceVisualDragBounds(bounds);
+ if (!mIsPinned) {
+ int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO);
+ bounds.inset(internalSize, internalSize);
+ }
+ }
+
+ @Override
+ public SafeCloseable prepareDrawDragView() {
+ mDrawForDrag = true;
+ invalidate();
+ SafeCloseable r = super.prepareDrawDragView();
+ return () -> {
+ r.close();
+ mDrawForDrag = false;
+ };
+ }
+
+ /**
+ * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
+ */
+ public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
+ PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.predicted_app_icon, parent, false);
+ icon.applyFromWorkspaceItem(info);
+ icon.setOnClickListener(ItemClickHandler.INSTANCE);
+ icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler());
+ return icon;
+ }
+
+ /**
+ * Draws Predicted Icon outline on cell layout
+ */
+ public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
+
+ private final PredictedAppIcon mIcon;
+ private final Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
+ mDelegateCellX = cellX;
+ mDelegateCellY = cellY;
+ mIcon = icon;
+ mOutlinePaint.setStyle(Paint.Style.FILL);
+ mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
+ }
+
+ /**
+ * Draws predicted app icon outline under CellLayout
+ */
+ @Override
+ public void drawUnderItem(Canvas canvas) {
+ canvas.save();
+ canvas.translate(mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY());
+ canvas.drawPath(mIcon.mShapePath, mOutlinePaint);
+ canvas.restore();
+ }
+
+ /**
+ * Draws PredictedAppIcon outline over CellLayout
+ */
+ @Override
+ public void drawOverItem(Canvas canvas) {
+ // Does nothing
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIconInflater.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
rename to quickstep/src/com/android/launcher3/uioverrides/PredictedAppIconInflater.java
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
new file mode 100644
index 0000000..1cf50f7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.window.SplashScreen;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+
+/** Provides a Quickstep specific animation when launching an activity from an app widget. */
+class QuickstepInteractionHandler implements RemoteViews.InteractionHandler {
+
+ private static final String TAG = "QuickstepInteractionHandler";
+
+ private final QuickstepLauncher mLauncher;
+
+ QuickstepInteractionHandler(QuickstepLauncher launcher) {
+ mLauncher = launcher;
+ }
+
+ @SuppressWarnings("NewApi")
+ @Override
+ public boolean onInteraction(View view, PendingIntent pendingIntent,
+ RemoteViews.RemoteResponse remoteResponse) {
+ LauncherAppWidgetHostView hostView = findHostViewAncestor(view);
+ if (hostView == null) {
+ Log.e(TAG, "View did not have a LauncherAppWidgetHostView ancestor.");
+ return RemoteViews.startPendingIntent(hostView, pendingIntent,
+ remoteResponse.getLaunchOptions(view));
+ }
+ Pair<Intent, ActivityOptions> options = remoteResponse.getLaunchOptions(view);
+ ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager()
+ .getActivityLaunchOptions(hostView);
+ if (Utilities.ATLEAST_S && !pendingIntent.isActivity()) {
+ // In the event this pending intent eventually launches an activity, i.e. a trampoline,
+ // use the Quickstep transition animation.
+ try {
+ ActivityTaskManager.getService()
+ .registerRemoteAnimationForNextActivityStart(
+ pendingIntent.getCreatorPackage(),
+ activityOptions.options.getRemoteAnimationAdapter());
+ } catch (RemoteException e) {
+ // Do nothing.
+ }
+ }
+ activityOptions.options.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activityOptions.options.setSplashscreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_EMPTY);
+ Object itemInfo = hostView.getTag();
+ if (itemInfo instanceof ItemInfo) {
+ mLauncher.addLaunchCookie((ItemInfo) itemInfo, activityOptions.options);
+ }
+ options = Pair.create(options.first, activityOptions.options);
+ if (pendingIntent.isActivity()) {
+ logAppLaunch(itemInfo);
+ }
+ return RemoteViews.startPendingIntent(hostView, pendingIntent, options);
+ }
+
+ /**
+ * Logs that the app was launched from the widget.
+ * @param itemInfo the widget info.
+ */
+ private void logAppLaunch(Object itemInfo) {
+ StatsLogManager.StatsLogger logger = mLauncher.getStatsLogManager().logger();
+ if (itemInfo instanceof ItemInfo) {
+ logger.withItemInfo((ItemInfo) itemInfo);
+ }
+ logger.log(LAUNCHER_APP_LAUNCH_TAP);
+ }
+
+ private LauncherAppWidgetHostView findHostViewAncestor(View v) {
+ while (v != null) {
+ if (v instanceof LauncherAppWidgetHostView) return (LauncherAppWidgetHostView) v;
+ v = (View) v.getParent();
+ }
+ return null;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
new file mode 100644
index 0000000..ec9893c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
+import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_WIDGET_APP_START;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.HINT_STATE_TWO_BUTTON_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.view.HapticFeedbackConstants;
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.QuickstepAccessibilityDelegate;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.appprediction.PredictionRowView;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
+import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.QuickstepOnboardingPrefs;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+public class QuickstepLauncher extends BaseQuickstepLauncher {
+
+ public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
+ /**
+ * Reusable command for applying the shelf height on the background thread.
+ */
+ public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
+ SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
+
+ private FixedContainerItems mAllAppsPredictions;
+ private HotseatPredictionController mHotseatPredictionController;
+
+ @Override
+ protected void setupViews() {
+ super.setupViews();
+ mHotseatPredictionController = new HotseatPredictionController(this);
+ }
+
+ @Override
+ protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
+ // If the app launch is from any of the surfaces in AllApps then add the InstanceId from
+ // LiveSearchManager to recreate the AllApps session on the server side.
+ if (mAllAppsSessionLogId != null && ALL_APPS.equals(
+ getStateManager().getCurrentStableState())) {
+ instanceId = mAllAppsSessionLogId;
+ }
+
+ StatsLogger logger = getStatsLogManager()
+ .logger().withItemInfo(info).withInstanceId(instanceId);
+
+ if (mAllAppsPredictions != null
+ && (info.itemType == ITEM_TYPE_APPLICATION
+ || info.itemType == ITEM_TYPE_SHORTCUT
+ || info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) {
+ int count = mAllAppsPredictions.items.size();
+ for (int i = 0; i < count; i++) {
+ ItemInfo targetInfo = mAllAppsPredictions.items.get(i);
+ if (targetInfo.itemType == info.itemType
+ && targetInfo.user.equals(info.user)
+ && Objects.equals(targetInfo.getIntent(), info.getIntent())) {
+ logger.withRank(i);
+ break;
+ }
+
+ }
+ }
+ logger.log(LAUNCHER_APP_LAUNCH_TAP);
+
+ mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
+ }
+
+ @Override
+ protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
+ return new QuickstepAccessibilityDelegate(this);
+ }
+
+ /**
+ * Returns Prediction controller for hybrid hotseat
+ */
+ public HotseatPredictionController getHotseatPredictionController() {
+ return mHotseatPredictionController;
+ }
+
+ @Override
+ protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) {
+ return new QuickstepOnboardingPrefs(this, sharedPrefs);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ onStateOrResumeChanging(false /* inTransition */);
+ }
+
+ @Override
+ public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
+ // Only pause is taskbar controller is not present
+ mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null);
+ return super.startActivitySafely(v, intent, item);
+ }
+
+ @Override
+ protected void onActivityFlagsChanged(int changeBits) {
+ super.onActivityFlagsChanged(changeBits);
+ if ((changeBits & (ACTIVITY_STATE_DEFERRED_RESUMED | ACTIVITY_STATE_STARTED
+ | ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
+ onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
+ }
+
+ if (((changeBits & ACTIVITY_STATE_STARTED) != 0
+ || (changeBits & getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
+ mHotseatPredictionController.setPauseUIUpdate(false);
+ }
+ }
+
+ @Override
+ protected void showAllAppsFromIntent(boolean alreadyOnHome) {
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY);
+ super.showAllAppsFromIntent(alreadyOnHome);
+ }
+
+ @Override
+ public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+ return Stream.concat(
+ Stream.of(mHotseatPredictionController), super.getSupportedShortcuts());
+ }
+
+ /**
+ * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
+ */
+ private void onStateOrResumeChanging(boolean inTransition) {
+ LauncherState state = getStateManager().getState();
+ boolean started = ((getActivityFlags() & ACTIVITY_STATE_STARTED)) != 0;
+ if (started) {
+ DeviceProfile profile = getDeviceProfile();
+ boolean willUserBeActive =
+ (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
+ boolean visible = (state == NORMAL || state == OVERVIEW)
+ && (willUserBeActive || isUserActive())
+ && !profile.isVerticalBarLayout()
+ && profile.isPhone && !profile.isLandscape;
+ UiThreadHelper.runAsyncCommand(this, SET_SHELF_HEIGHT, visible ? 1 : 0,
+ profile.hotseatBarSizePx);
+ }
+ if (state == NORMAL && !inTransition) {
+ ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
+ }
+ }
+
+ @Override
+ public void bindExtraContainerItems(FixedContainerItems item) {
+ if (item.containerId == Favorites.CONTAINER_PREDICTION) {
+ mAllAppsPredictions = item;
+ getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
+ .setPredictedApps(item.items);
+ } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+ mHotseatPredictionController.setPredictedItems(item);
+ } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
+ getPopupDataProvider().setRecommendedWidgets(item.items);
+ }
+ }
+
+ @Override
+ public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
+ super.bindWorkspaceItemsChanged(updated);
+ if (getTaskbarUIController() != null && updated.stream()
+ .filter(w -> w.container == CONTAINER_HOTSEAT).findFirst().isPresent()) {
+ getTaskbarUIController().onHotseatUpdated();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mHotseatPredictionController.destroy();
+ }
+
+ @Override
+ public void onStateSetEnd(LauncherState state) {
+ super.onStateSetEnd(state);
+
+ switch (state.ordinal) {
+ case HINT_STATE_ORDINAL: {
+ Workspace workspace = getWorkspace();
+ getStateManager().goToState(NORMAL);
+ if (workspace.getNextPage() != Workspace.DEFAULT_PAGE) {
+ workspace.post(workspace::moveToDefaultScreen);
+ }
+ break;
+ }
+ case HINT_STATE_TWO_BUTTON_ORDINAL: {
+ getStateManager().goToState(OVERVIEW);
+ getDragLayer().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+ break;
+ }
+ case OVERVIEW_STATE_ORDINAL: {
+ RecentsView rv = getOverviewPanel();
+ sendCustomAccessibilityEvent(
+ rv.getPageAt(rv.getCurrentPage()), TYPE_VIEW_FOCUSED, null);
+ break;
+ }
+ case QUICK_SWITCH_STATE_ORDINAL: {
+ RecentsView rv = getOverviewPanel();
+ TaskView tasktolaunch = rv.getTaskViewAt(0);
+ if (tasktolaunch != null) {
+ tasktolaunch.launchTask(success -> {
+ if (!success) {
+ getStateManager().goToState(OVERVIEW);
+ } else {
+ getStateManager().moveToRestState();
+ }
+ });
+ } else {
+ getStateManager().goToState(NORMAL);
+ }
+ break;
+ }
+
+ }
+ }
+
+ @Override
+ public TouchController[] createTouchControllers() {
+ Mode mode = SysUINavigationMode.getMode(this);
+
+ ArrayList<TouchController> list = new ArrayList<>();
+ list.add(getDragController());
+ switch (mode) {
+ case NO_BUTTON:
+ list.add(new NoButtonQuickSwitchTouchController(this));
+ list.add(new NavBarToHomeTouchController(this));
+ list.add(new NoButtonNavbarToOverviewTouchController(this));
+ break;
+ case TWO_BUTTONS:
+ list.add(new TwoButtonNavbarTouchController(this));
+ list.add(getDeviceProfile().isVerticalBarLayout()
+ ? new TransposedQuickSwitchTouchController(this)
+ : new QuickSwitchTouchController(this));
+ list.add(new PortraitStatesTouchController(this));
+ break;
+ case THREE_BUTTONS:
+ default:
+ list.add(new PortraitStatesTouchController(this));
+ }
+
+ if (!getDeviceProfile().isMultiWindowMode) {
+ list.add(new StatusBarTouchController(this));
+ }
+
+ list.add(new LauncherTaskViewController(this));
+ return list.toArray(new TouchController[list.size()]);
+ }
+
+ @Override
+ public AtomicAnimationFactory createAtomicAnimationFactory() {
+ return new QuickstepAtomicAnimationFactory(this);
+ }
+
+ protected LauncherAppWidgetHost createAppWidgetHost() {
+ LauncherAppWidgetHost appWidgetHost = super.createAppWidgetHost();
+ if (ENABLE_QUICKSTEP_WIDGET_APP_START.get()) {
+ appWidgetHost.setInteractionHandler(new QuickstepInteractionHandler(this));
+ }
+ return appWidgetHost;
+ }
+
+ private static final class LauncherTaskViewController extends
+ TaskViewTouchController<Launcher> {
+
+ LauncherTaskViewController(Launcher activity) {
+ super(activity);
+ }
+
+ @Override
+ protected boolean isRecentsInteractive() {
+ return mActivity.isInState(OVERVIEW) || mActivity.isInState(OVERVIEW_MODAL_TASK);
+ }
+
+ @Override
+ protected boolean isRecentsModal() {
+ return mActivity.isInState(OVERVIEW_MODAL_TASK);
+ }
+
+ @Override
+ protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+ mActivity.getStateManager().setCurrentUserControlledAnimation(animController);
+ }
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ RecentsView recentsView = getOverviewPanel();
+ writer.println("\nQuickstepLauncher:");
+ writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" :
+ recentsView.getPagedViewOrientedState()));
+ }
+}
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 0000000..996d36a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -0,0 +1,130 @@
+/*
+ * 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.CLEAR_ALL_BUTTON;
+import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
+import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
+import static com.android.launcher3.LauncherState.SPLIT_PLACHOLDER_VIEW;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
+import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
+import static com.android.quickstep.views.SplitPlaceholderView.ALPHA_FLOAT;
+import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.util.FloatProperty;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.views.ClearAllButton;
+import com.android.quickstep.views.LauncherRecentsView;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
+ * the basic view properties, this class also manages changes in the task visuals.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public final class RecentsViewStateController extends
+ BaseRecentsViewStateController<LauncherRecentsView> {
+
+ public RecentsViewStateController(BaseQuickstepLauncher launcher) {
+ super(launcher);
+ }
+
+ @Override
+ public void setState(@NonNull LauncherState state) {
+ super.setState(state);
+ if (state.overviewUi) {
+ mRecentsView.updateEmptyMessage();
+ mRecentsView.resetTaskVisuals();
+ }
+ setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
+ mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
+ }
+
+ @Override
+ void setStateWithAnimationInternal(@NonNull LauncherState toState,
+ @NonNull StateAnimationConfig config, @NonNull PendingAnimation builder) {
+ super.setStateWithAnimationInternal(toState, config, builder);
+
+ if (toState.overviewUi) {
+ // While animating into recents, update the visible task data as needed
+ builder.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
+ mRecentsView.updateEmptyMessage();
+ } else {
+ builder.addListener(
+ AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals));
+ }
+
+ // Create or dismiss split screen select animations
+ LauncherState currentState = mLauncher.getStateManager().getState();
+ if (isSplitSelectionState(toState) && !isSplitSelectionState(currentState)) {
+ builder.add(mRecentsView.createSplitSelectInitAnimation().buildAnim());
+ } else if (!isSplitSelectionState(toState) && isSplitSelectionState(currentState)) {
+ builder.add(mRecentsView.cancelSplitSelect(true).buildAnim());
+ }
+
+ setAlphas(builder, config, toState);
+ builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
+ toState.getOverviewFullscreenProgress(), LINEAR);
+ }
+
+ /**
+ * @return true if {@param toState} is {@link LauncherState#OVERVIEW_SPLIT_SELECT}
+ */
+ private boolean isSplitSelectionState(@NonNull LauncherState toState) {
+ return toState == OVERVIEW_SPLIT_SELECT;
+ }
+
+ private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
+ LauncherState state) {
+ float clearAllButtonAlpha = state.areElementsVisible(mLauncher, CLEAR_ALL_BUTTON) ? 1 : 0;
+ propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
+ clearAllButtonAlpha, LINEAR);
+ float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS)
+ && mRecentsView.shouldShowOverviewActionsForState(state) ? 1 : 0;
+ propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
+ MultiValueAlpha.VALUE, overviewButtonAlpha, config.getInterpolator(
+ ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
+
+ float splitPlaceholderAlpha = state.areElementsVisible(mLauncher, SPLIT_PLACHOLDER_VIEW) ?
+ 0.85f : 0;
+ propertySetter.setFloat(mRecentsView.getSplitPlaceholder(), ALPHA_FLOAT,
+ splitPlaceholderAlpha, LINEAR);
+ }
+
+ @Override
+ FloatProperty<RecentsView> getTaskModalnessProperty() {
+ return TASK_MODALNESS;
+ }
+
+ @Override
+ FloatProperty<RecentsView> getContentAlphaProperty() {
+ return CONTENT_ALPHA;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
deleted file mode 100644
index 36c0e34..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.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.launcher3.util.MainThreadInitializedObject;
-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 int MAIN_COLOR_LIGHT = 0xffdadce0;
- private static final int MAIN_COLOR_DARK = 0xff202124;
- private static final int MAIN_COLOR_REGULAR = 0xff000000;
-
- public static final MainThreadInitializedObject<WallpaperColorInfo> INSTANCE =
- new MainThreadInitializedObject<>(WallpaperColorInfo::new);
-
- private final ArrayList<OnChangeListener> 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;
- }
-
- public boolean isMainColorDark() {
- return mExtractionInfo.mainColor == MAIN_COLOR_DARK;
- }
-
- @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/launcher3/uioverrides/plugins/PluginInitializerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
index 7beb9db..d14e8ef 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.os.Looper;
+import com.android.launcher3.Utilities;
import com.android.systemui.shared.plugins.PluginInitializer;
public class PluginInitializerImpl implements PluginInitializer {
@@ -44,4 +45,8 @@
@Override
public void handleWtfs() {
}
+
+ public boolean isDebuggable() {
+ return Utilities.IS_DEBUG_DEVICE;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index e7cd393..f8c9fd1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -16,16 +16,15 @@
package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
import android.content.Context;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.quickstep.SysUINavigationMode;
+import com.android.launcher3.util.Themes;
/**
* Definition for AllApps state
@@ -42,7 +41,7 @@
};
public AllAppsState(int id) {
- super(id, ContainerType.ALLAPPS, STATE_FLAGS);
+ super(id, LAUNCHER_STATE_ALLAPPS, STATE_FLAGS);
}
@Override
@@ -65,19 +64,15 @@
public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
ScaleAndTranslation scaleAndTranslation = LauncherState.OVERVIEW
.getWorkspaceScaleAndTranslation(launcher);
- if (SysUINavigationMode.getMode(launcher) == NO_BUTTON && !ENABLE_OVERVIEW_ACTIONS.get()) {
- float normalScale = 1;
- // Scale down halfway to where we'd be in overview, to prepare for a potential pause.
- scaleAndTranslation.scale = (scaleAndTranslation.scale + normalScale) / 2;
- } else {
- scaleAndTranslation.scale = 1;
- }
+ scaleAndTranslation.scale = 1;
return scaleAndTranslation;
}
@Override
protected float getDepthUnchecked(Context context) {
- return 1f;
+ // The scrim fades in at approximately 50% of the swipe gesture.
+ // This means that the depth should be greater than 1, in order to fully zoom out.
+ return 2f;
}
@Override
@@ -87,16 +82,16 @@
@Override
public int getVisibleElements(Launcher launcher) {
- return ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT;
- }
-
- @Override
- public float[] getOverviewScaleAndOffset(Launcher launcher) {
- return new float[] {0.9f, 0};
+ return ALL_APPS_CONTENT;
}
@Override
public LauncherState getHistoryForState(LauncherState previousState) {
return previousState == OVERVIEW ? OVERVIEW : NORMAL;
}
+
+ @Override
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return Themes.getAttrColor(launcher, R.attr.allAppsScrimColor);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
new file mode 100644
index 0000000..fe5a347
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -0,0 +1,101 @@
+/*
+ * 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.states;
+
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * State indicating that the Launcher is behind an app
+ */
+public class BackgroundAppState extends OverviewState {
+
+ private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI
+ | FLAG_WORKSPACE_INACCESSIBLE | FLAG_NON_INTERACTIVE | FLAG_CLOSE_POPUPS;
+
+ public BackgroundAppState(int id) {
+ this(id, LAUNCHER_STATE_BACKGROUND);
+ }
+
+ protected BackgroundAppState(int id, int logContainer) {
+ super(id, logContainer, STATE_FLAGS);
+ }
+
+ @Override
+ public float getVerticalProgress(Launcher launcher) {
+ if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+ return super.getVerticalProgress(launcher);
+ }
+ RecentsView recentsView = launcher.getOverviewPanel();
+ int transitionLength = LayoutUtils.getShelfTrackingDistance(launcher,
+ launcher.getDeviceProfile(),
+ recentsView.getPagedOrientationHandler());
+ AllAppsTransitionController controller = launcher.getAllAppsController();
+ float scrollRange = Math.max(controller.getShiftRange(), 1);
+ float progressDelta = (transitionLength / scrollRange);
+ return super.getVerticalProgress(launcher) + progressDelta;
+ }
+
+ @Override
+ public float[] getOverviewScaleAndOffset(Launcher launcher) {
+ return getOverviewScaleAndOffsetForBackgroundState(launcher);
+ }
+
+ @Override
+ public float getOverviewFullscreenProgress() {
+ return 1;
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return super.getVisibleElements(launcher)
+ & ~OVERVIEW_ACTIONS
+ & ~CLEAR_ALL_BUTTON
+ & ~VERTICAL_SWIPE_INDICATOR
+ | TASKBAR;
+ }
+
+ @Override
+ public boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) {
+ return false;
+ }
+
+ @Override
+ protected float getDepthUnchecked(Context context) {
+ return 1;
+ }
+
+ @Override
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return Color.TRANSPARENT;
+ }
+
+ public static float[] getOverviewScaleAndOffsetForBackgroundState(
+ BaseDraggingActivity activity) {
+ return new float[] {
+ ((RecentsView) activity.getOverviewPanel()).getMaxScaleForFullScreen(),
+ NO_OFFSET};
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
new file mode 100644
index 0000000..6f084a1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.states;
+
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * An Overview state that shows the current task in a modal fashion. Modal state is where the
+ * current task is shown on its own without other tasks visible.
+ */
+public class OverviewModalTaskState extends OverviewState {
+
+ private static final int STATE_FLAGS =
+ FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_WORKSPACE_INACCESSIBLE;
+
+ public OverviewModalTaskState(int id) {
+ super(id, LAUNCHER_STATE_OVERVIEW, STATE_FLAGS);
+ }
+
+ @Override
+ public int getTransitionDuration(Context launcher) {
+ return 300;
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return OVERVIEW_ACTIONS | CLEAR_ALL_BUTTON;
+ }
+
+ @Override
+ public float[] getOverviewScaleAndOffset(Launcher launcher) {
+ return getOverviewScaleAndOffsetForModalState(launcher);
+ }
+
+ @Override
+ public float getOverviewModalness() {
+ return 1.0f;
+ }
+
+ @Override
+ public void onBackPressed(Launcher launcher) {
+ launcher.getStateManager().goToState(LauncherState.OVERVIEW);
+ RecentsView recentsView = launcher.<RecentsView>getOverviewPanel();
+ if (recentsView != null) {
+ recentsView.resetModalVisuals();
+ } else {
+ super.onBackPressed(launcher);
+ }
+ }
+
+ public static float[] getOverviewScaleAndOffsetForModalState(BaseDraggingActivity activity) {
+ Point taskSize = activity.<RecentsView>getOverviewPanel().getSelectedTaskSize();
+ Rect modalTaskSize = new Rect();
+ activity.<RecentsView>getOverviewPanel().getModalTaskSize(modalTaskSize);
+
+ float scale = Math.min((float) modalTaskSize.height() / taskSize.y,
+ (float) modalTaskSize.width() / taskSize.x);
+
+ return new float[] {scale, NO_OFFSET};
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
new file mode 100644
index 0000000..8c128c8
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -0,0 +1,167 @@
+/*
+ * 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.states;
+
+import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.view.View;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Definition for overview state
+ */
+public class OverviewState extends LauncherState {
+
+ protected static final Rect sTempRect = new Rect();
+
+ private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED
+ | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_WORKSPACE_INACCESSIBLE
+ | FLAG_CLOSE_POPUPS;
+
+ public OverviewState(int id) {
+ this(id, STATE_FLAGS);
+ }
+
+ protected OverviewState(int id, int stateFlags) {
+ this(id, LAUNCHER_STATE_OVERVIEW, stateFlags);
+ }
+
+ protected OverviewState(int id, int logContainer, int stateFlags) {
+ super(id, logContainer, stateFlags);
+ }
+
+ @Override
+ public int getTransitionDuration(Context context) {
+ // In gesture modes, overview comes in all the way from the side, so give it more time.
+ return SysUINavigationMode.INSTANCE.get(context).getMode().hasGestures ? 380 : 250;
+ }
+
+ @Override
+ public ScaleAndTranslation 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 ScaleAndTranslation(scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor);
+ }
+
+ @Override
+ public float[] getOverviewScaleAndOffset(Launcher launcher) {
+ return new float[] {NO_SCALE, NO_OFFSET};
+ }
+
+ @Override
+ public float getTaskbarScale(Launcher launcher) {
+ return 1f;
+ }
+
+ @Override
+ public float getTaskbarTranslationY(Launcher launcher) {
+ return 0f;
+ }
+
+ @Override
+ public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
+ return new PageAlphaProvider(DEACCEL_2) {
+ @Override
+ public float getPageAlpha(int pageIndex) {
+ return 0;
+ }
+ };
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS;
+ }
+
+ @Override
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return Themes.getAttrColor(launcher, R.attr.overviewScrimColor);
+ }
+
+ @Override
+ public boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) {
+ return deviceProfile.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
+ }
+
+ @Override
+ public String getDescription(Launcher launcher) {
+ return launcher.getString(R.string.accessibility_recent_apps);
+ }
+
+ public static float getDefaultSwipeHeight(Launcher launcher) {
+ return LayoutUtils.getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
+ }
+
+ @Override
+ protected float getDepthUnchecked(Context context) {
+ //TODO revert when b/178661709 is fixed
+ return SystemProperties.getBoolean("ro.launcher.depth.overview", true) ? 1 : 0;
+ }
+
+ @Override
+ public void onBackPressed(Launcher launcher) {
+ TaskView taskView = launcher.<RecentsView>getOverviewPanel().getRunningTaskView();
+ if (taskView != null) {
+ taskView.launchTaskAnimated();
+ } else {
+ super.onBackPressed(launcher);
+ }
+ }
+
+ public static OverviewState newBackgroundState(int id) {
+ return new BackgroundAppState(id);
+ }
+
+ public static OverviewState newSwitchState(int id) {
+ return new QuickSwitchState(id);
+ }
+
+ /**
+ * New Overview substate that represents the overview in modal mode (one task shown on its own)
+ */
+ public static OverviewState newModalTaskState(int id) {
+ return new OverviewModalTaskState(id);
+ }
+
+ /**
+ * New Overview substate representing state where 1 app for split screen has been selected and
+ * pinned and user is selecting the second one
+ */
+ public static OverviewState newSplitSelectState(int id) {
+ return new SplitScreenSelectState(id);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
new file mode 100644
index 0000000..d36e76b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.states;
+
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+/**
+ * State to indicate we are about to launch a recent task. Note that this state is only used when
+ * quick switching from launcher; quick switching from an app uses LauncherSwipeHandler.
+ * @see com.android.quickstep.GestureState.GestureEndTarget#NEW_TASK
+ */
+public class QuickSwitchState extends BackgroundAppState {
+
+ public QuickSwitchState(int id) {
+ super(id, LAUNCHER_STATE_BACKGROUND);
+ }
+
+ @Override
+ public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
+ float shiftRange = launcher.getAllAppsController().getShiftRange();
+ float shiftProgress = getVerticalProgress(launcher) - NORMAL.getVerticalProgress(launcher);
+ float translationY = shiftProgress * shiftRange;
+ return new ScaleAndTranslation(0.9f, 0, translationY);
+ }
+
+ @Override
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return Themes.getAttrColor(launcher, R.attr.overviewScrimColor);
+ }
+
+ @Override
+ public float getVerticalProgress(Launcher launcher) {
+ // Don't move all apps shelf while quick-switching (just let it fade).
+ return 1f;
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return TASKBAR;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
new file mode 100644
index 0000000..0e2fbbc
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.states;
+
+import static android.view.View.VISIBLE;
+
+import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.HINT_STATE_TWO_BUTTON;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+
+import android.animation.ValueAnimator;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.RecentsAtomicAnimationFactory;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Animation factory for quickstep specific transitions
+ */
+public class QuickstepAtomicAnimationFactory extends
+ RecentsAtomicAnimationFactory<QuickstepLauncher, LauncherState> {
+
+ // Scale recents takes before animating in
+ private static final float RECENTS_PREPARE_SCALE = 1.33f;
+ // Scale workspace takes before animating in
+ private static final float WORKSPACE_PREPARE_SCALE = 0.92f;
+ // Constants to specify how to scroll RecentsView to the default page if it's not already there.
+ private static final int DEFAULT_PAGE = 0;
+ private static final int PER_PAGE_SCROLL_DURATION = 150;
+ private static final int MAX_PAGE_SCROLL_DURATION = 750;
+
+ // Due to use of physics, duration may differ between devices so we need to calculate and
+ // cache the value.
+ private int mHintToNormalDuration = -1;
+
+ public QuickstepAtomicAnimationFactory(QuickstepLauncher activity) {
+ super(activity);
+ }
+
+ @Override
+ public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
+ StateAnimationConfig config) {
+ RecentsView overview = mActivity.getOverviewPanel();
+ if (toState == NORMAL && fromState == OVERVIEW) {
+ config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
+ config.setInterpolator(ANIM_SCRIM_FADE, LINEAR);
+ config.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL);
+ config.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL);
+
+ if (SysUINavigationMode.getMode(mActivity).hasGestures
+ && overview.getTaskViewCount() > 0) {
+ // Overview is going offscreen, so keep it at its current scale and opacity.
+ config.setInterpolator(ANIM_OVERVIEW_SCALE, FINAL_FRAME);
+ config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME);
+ config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X,
+ clampToProgress(FAST_OUT_SLOW_IN, 0, 0.75f));
+ config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, FINAL_FRAME);
+ } else {
+ config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL_DEACCEL);
+ config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
+ config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
+ }
+
+ // Scroll RecentsView to page 0 as it goes offscreen, if necessary.
+ int numPagesToScroll = overview.getNextPage() - DEFAULT_PAGE;
+ long scrollDuration = Math.min(MAX_PAGE_SCROLL_DURATION,
+ numPagesToScroll * PER_PAGE_SCROLL_DURATION);
+ config.duration = Math.max(config.duration, scrollDuration);
+ overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration));
+
+ Workspace workspace = mActivity.getWorkspace();
+ // Start from a higher workspace scale, but only if we're invisible so we don't jump.
+ boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE;
+ if (isWorkspaceVisible) {
+ CellLayout currentChild = (CellLayout) workspace.getChildAt(
+ workspace.getCurrentPage());
+ isWorkspaceVisible = currentChild.getVisibility() == VISIBLE
+ && currentChild.getShortcutsAndWidgets().getAlpha() > 0;
+ }
+ if (!isWorkspaceVisible) {
+ workspace.setScaleX(WORKSPACE_PREPARE_SCALE);
+ workspace.setScaleY(WORKSPACE_PREPARE_SCALE);
+ }
+ Hotseat hotseat = mActivity.getHotseat();
+ boolean isHotseatVisible = hotseat.getVisibility() == VISIBLE && hotseat.getAlpha() > 0;
+ if (!isHotseatVisible) {
+ hotseat.setScaleX(WORKSPACE_PREPARE_SCALE);
+ hotseat.setScaleY(WORKSPACE_PREPARE_SCALE);
+ }
+ } else if ((fromState == NORMAL || fromState == HINT_STATE
+ || fromState == HINT_STATE_TWO_BUTTON) && toState == OVERVIEW) {
+ if (SysUINavigationMode.getMode(mActivity).hasGestures) {
+ config.setInterpolator(ANIM_WORKSPACE_SCALE,
+ fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
+ config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
+
+ // Scrolling in tasks, so show straight away
+ if (overview.getTaskViewCount() > 0) {
+ config.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
+ } else {
+ config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
+ }
+ } else {
+ config.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
+ config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
+
+ // Scale up the recents, if it is not coming from the side
+ if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
+ RECENTS_SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
+ }
+ }
+ config.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
+ config.setInterpolator(ANIM_ALL_APPS_FADE, OVERSHOOT_1_2);
+ config.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
+ config.setInterpolator(ANIM_DEPTH, OVERSHOOT_1_2);
+ config.setInterpolator(ANIM_SCRIM_FADE, t -> {
+ // Animate at the same rate until reaching progress 1, and skip the overshoot.
+ return Math.min(1, OVERSHOOT_1_2.getInterpolation(t));
+ });
+ config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_2);
+ config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_1_2);
+ } else if (fromState == HINT_STATE && toState == NORMAL) {
+ config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
+ if (mHintToNormalDuration == -1) {
+ ValueAnimator va = getSpringScaleAnimator(mActivity, mActivity.getWorkspace(),
+ toState.getWorkspaceScaleAndTranslation(mActivity).scale);
+ mHintToNormalDuration = (int) va.getDuration();
+ }
+ config.duration = Math.max(config.duration, mHintToNormalDuration);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
new file mode 100644
index 0000000..6968494
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides.states;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * New Overview substate representing state where 1 app for split screen has been selected and
+ * pinned and user is selecting the second one
+ */
+public class SplitScreenSelectState extends OverviewState {
+ public SplitScreenSelectState(int id) {
+ super(id);
+ }
+
+ @Override
+ public void onBackPressed(Launcher launcher) {
+ launcher.getStateManager().goToState(OVERVIEW);
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return SPLIT_PLACHOLDER_VIEW;
+ }
+
+ @Override
+ public float getSplitSelectTranslation(Launcher launcher) {
+ RecentsView recentsView = launcher.getOverviewPanel();
+ int splitPosition = recentsView.getSplitPlaceholder().getSplitController()
+ .getActiveSplitPositionOption().mStagePosition;
+ if (!recentsView.shouldShiftThumbnailsForSplitSelect(splitPosition)) {
+ return 0f;
+ }
+ PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+ int direction = orientationHandler.getSplitTranslationDirectionFactor(splitPosition);
+ return launcher.getResources().getDimension(R.dimen.split_placeholder_size) * direction;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
deleted file mode 100644
index bef191e..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.android.launcher3.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.Utilities.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.states.StateAnimationConfig.AnimationFlags;
-import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.quickstep.SystemUiProxy;
-
-/**
- * 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, SingleAxisSwipeDetector.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(MotionEvent ev) {
- return LauncherLogProto.ContainerType.NAVBAR;
- }
-
- @Override
- protected float getShiftRange() {
- return mLauncher.getDragLayer().getWidth();
- }
-
- @Override
- protected float initCurrentAnimation(@AnimationFlags 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) {
- SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
- }
- }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
new file mode 100644
index 0000000..86c42ca
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
+import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.animation.ValueAnimator;
+import android.view.MotionEvent;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.AssistantUtilities;
+import com.android.quickstep.util.OverviewToHomeAnim;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
+ */
+public class NavBarToHomeTouchController implements TouchController,
+ SingleAxisSwipeDetector.Listener {
+
+ private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
+ // The min amount of overview scrim we keep during the transition.
+ private static final float OVERVIEW_TO_HOME_SCRIM_MULTIPLIER = 0.5f;
+
+ private final Launcher mLauncher;
+ private final SingleAxisSwipeDetector mSwipeDetector;
+ private final float mPullbackDistance;
+
+ private boolean mNoIntercept;
+ private LauncherState mStartState;
+ private LauncherState mEndState = NORMAL;
+ private AnimatorPlaybackController mCurrentAnimation;
+
+ public NavBarToHomeTouchController(Launcher launcher) {
+ mLauncher = launcher;
+ mSwipeDetector = new SingleAxisSwipeDetector(mLauncher, this,
+ SingleAxisSwipeDetector.VERTICAL);
+ mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
+ }
+
+ @Override
+ public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mStartState = mLauncher.getStateManager().getState();
+ mNoIntercept = !canInterceptTouch(ev);
+ if (mNoIntercept) {
+ return false;
+ }
+ mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_POSITIVE,
+ false /* ignoreSlop */);
+ }
+
+ if (mNoIntercept) {
+ return false;
+ }
+
+ onControllerTouchEvent(ev);
+ return mSwipeDetector.isDraggingOrSettling();
+ }
+
+ private boolean canInterceptTouch(MotionEvent ev) {
+ boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
+ if (!cameFromNavBar) {
+ return false;
+ }
+ if (mStartState.overviewUi || mStartState == ALL_APPS) {
+ return true;
+ }
+ int typeToClose = ENABLE_ALL_APPS_EDU.get() ? TYPE_ALL & ~TYPE_ALL_APPS_EDU : TYPE_ALL;
+ if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, typeToClose) != null) {
+ return true;
+ }
+ if (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+ && AssistantUtilities.isExcludedAssistantRunning()) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public final boolean onControllerTouchEvent(MotionEvent ev) {
+ return mSwipeDetector.onTouchEvent(ev);
+ }
+
+ private float getShiftRange() {
+ return mLauncher.getDeviceProfile().heightPx;
+ }
+
+ @Override
+ public void onDragStart(boolean start, float startDisplacement) {
+ initCurrentAnimation();
+ }
+
+ private void initCurrentAnimation() {
+ long accuracy = (long) (getShiftRange() * 2);
+ final PendingAnimation builder = new PendingAnimation(accuracy);
+ if (mStartState.overviewUi) {
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ AnimatorControllerWithResistance.createRecentsResistanceFromOverviewAnim(mLauncher,
+ builder);
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ builder.addOnFrameCallback(recentsView::redrawLiveTile);
+ }
+
+ AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_TASK_MENU);
+ } else if (mStartState == ALL_APPS) {
+ AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
+ builder.setFloat(allAppsController, ALL_APPS_PROGRESS,
+ -mPullbackDistance / allAppsController.getShiftRange(), PULLBACK_INTERPOLATOR);
+
+ // Slightly fade out all apps content to further distinguish from scrolling.
+ StateAnimationConfig config = new StateAnimationConfig();
+ config.duration = accuracy;
+ config.setInterpolator(StateAnimationConfig.ANIM_ALL_APPS_FADE, Interpolators
+ .mapToProgress(PULLBACK_INTERPOLATOR, 0, 0.5f));
+
+ allAppsController.setAlphas(mEndState, config, builder);
+ }
+ AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mLauncher);
+ if (topView != null) {
+ topView.addHintCloseAnim(mPullbackDistance, PULLBACK_INTERPOLATOR, builder);
+ }
+ mCurrentAnimation = builder.createPlaybackController();
+ mCurrentAnimation.getTarget().addListener(newCancelListener(this::clearState));
+ }
+
+ private void clearState() {
+ mCurrentAnimation = null;
+ mSwipeDetector.finishedScrolling();
+ mSwipeDetector.setDetectableScrollConditions(0, false);
+ }
+
+ @Override
+ public boolean onDrag(float displacement) {
+ // Only allow swipe up.
+ displacement = Math.min(0, displacement);
+ float progress = Utilities.getProgress(displacement, 0, getShiftRange());
+ mCurrentAnimation.setPlayFraction(progress);
+ return true;
+ }
+
+ @Override
+ public void onDragEnd(float velocity) {
+ boolean fling = mSwipeDetector.isFling(velocity);
+ float progress = mCurrentAnimation.getProgressFraction();
+ float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress);
+ boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
+ || (velocity < 0 && fling);
+ if (success) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ recentsView.switchToScreenshot(null,
+ () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
+ }
+ if (mStartState.overviewUi) {
+ new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState))
+ .animateWithVelocity(velocity);
+ } else {
+ mLauncher.getStateManager().goToState(mEndState, true,
+ forSuccessCallback(() -> onSwipeInteractionCompleted(mEndState)));
+ }
+ if (mStartState != mEndState) {
+ logHomeGesture();
+ }
+ AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher);
+ if (topOpenView != null) {
+ AbstractFloatingView.closeAllOpenViews(mLauncher);
+ // TODO: add to WW log
+ }
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ } else {
+ // Quickly return to the state we came from (we didn't move far).
+ ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+ anim.setFloatValues(progress, 0);
+ anim.addListener(forSuccessCallback(() -> onSwipeInteractionCompleted(mStartState)));
+ anim.setDuration(80).start();
+ }
+ }
+
+ private void onSwipeInteractionCompleted(LauncherState targetState) {
+ clearState();
+ mLauncher.getStateManager().goToState(targetState, false /* animated */);
+ AccessibilityManagerCompat.sendStateEventToTest(mLauncher, targetState.ordinal);
+ }
+
+ private void logHomeGesture() {
+ mLauncher.getStatsLogManager().logger()
+ .withSrcState(mStartState.statsLogOrdinal)
+ .withDstState(mEndState.statsLogOrdinal)
+ .log(LAUNCHER_HOME_GESTURE);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
new file mode 100644
index 0000000..283743d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
+import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
+import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.OverviewToHomeAnim;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above
+ * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the
+ * first home screen instead of to Overview.
+ */
+public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouchController {
+ private static final float ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER = 2.5f;
+
+ // How much of the movement to use for translating overview after swipe and hold.
+ private static final float OVERVIEW_MOVEMENT_FACTOR = 0.25f;
+ private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
+ private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
+
+ private final RecentsView mRecentsView;
+ private final MotionPauseDetector mMotionPauseDetector;
+ private final float mMotionPauseMinDisplacement;
+
+ private boolean mDidTouchStartInNavBar;
+ private boolean mStartedOverview;
+ private boolean mReachedOverview;
+ // The last recorded displacement before we reached overview.
+ private PointF mStartDisplacement = new PointF();
+ private float mStartY;
+ private AnimatorPlaybackController mOverviewResistYAnim;
+
+ // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
+ private ObjectAnimator mNormalToHintOverviewScrimAnimator;
+
+ public NoButtonNavbarToOverviewTouchController(Launcher l) {
+ super(l);
+ mRecentsView = l.getOverviewPanel();
+ mMotionPauseDetector = new MotionPauseDetector(l);
+ mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
+ }
+
+ @Override
+ protected boolean canInterceptTouch(MotionEvent ev) {
+ mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+ return super.canInterceptTouch(ev);
+ }
+
+ @Override
+ protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+ if (fromState == NORMAL && mDidTouchStartInNavBar) {
+ return HINT_STATE;
+ } else if (fromState == OVERVIEW && isDragTowardPositive) {
+ // Don't allow swiping up to all apps.
+ return OVERVIEW;
+ }
+ return super.getTargetState(fromState, isDragTowardPositive);
+ }
+
+ @Override
+ protected float initCurrentAnimation() {
+ float progressMultiplier = super.initCurrentAnimation();
+ if (mToState == HINT_STATE) {
+ // Track the drag across the entire height of the screen.
+ progressMultiplier = -1f / mLauncher.getDeviceProfile().heightPx;
+ }
+ return progressMultiplier;
+ }
+
+ @Override
+ public void onDragStart(boolean start, float startDisplacement) {
+ super.onDragStart(start, startDisplacement);
+
+ mMotionPauseDetector.clear();
+
+ if (handlingOverviewAnim()) {
+ mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
+ }
+
+ if (mFromState == NORMAL && mToState == HINT_STATE) {
+ mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofArgb(
+ mLauncher.getScrimView(),
+ VIEW_BACKGROUND_COLOR,
+ mFromState.getWorkspaceScrimColor(mLauncher),
+ mToState.getWorkspaceScrimColor(mLauncher));
+ }
+ mStartedOverview = false;
+ mReachedOverview = false;
+ mOverviewResistYAnim = null;
+ }
+
+ @Override
+ protected void updateProgress(float fraction) {
+ super.updateProgress(fraction);
+ if (mNormalToHintOverviewScrimAnimator != null) {
+ mNormalToHintOverviewScrimAnimator.setCurrentFraction(fraction);
+ }
+ }
+
+ @Override
+ public void onDragEnd(float velocity) {
+ if (mStartedOverview) {
+ goToOverviewOrHomeOnDragEnd(velocity);
+ } else {
+ super.onDragEnd(velocity);
+ }
+
+ mMotionPauseDetector.clear();
+ mNormalToHintOverviewScrimAnimator = null;
+ if (mLauncher.isInState(OVERVIEW)) {
+ // Normally we would cleanup the state based on mCurrentAnimation, but since we stop
+ // using that when we pause to go to Overview, we need to clean up ourselves.
+ clearState();
+ }
+ }
+
+ @Override
+ protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
+ LauncherState targetState, float velocity, boolean isFling) {
+ super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity,
+ isFling);
+ if (targetState == HINT_STATE) {
+ // Normally we compute the duration based on the velocity and distance to the given
+ // state, but since the hint state tracks the entire screen without a clear endpoint, we
+ // need to manually set the duration to a reasonable value.
+ animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher));
+ }
+ }
+
+ private void onMotionPauseDetected() {
+ if (mCurrentAnimation == null) {
+ return;
+ }
+ mNormalToHintOverviewScrimAnimator = null;
+ mCurrentAnimation.getTarget().addListener(newCancelListener(() ->
+ mLauncher.getStateManager().goToState(OVERVIEW, true, forSuccessCallback(() -> {
+ mOverviewResistYAnim = AnimatorControllerWithResistance
+ .createRecentsResistanceFromOverviewAnim(mLauncher, null)
+ .createPlaybackController();
+ mReachedOverview = true;
+ maybeSwipeInteractionToOverviewComplete();
+ }))));
+
+ mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
+ mCurrentAnimation.dispatchOnCancel();
+ mStartedOverview = true;
+ VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+ }
+
+ private void maybeSwipeInteractionToOverviewComplete() {
+ if (mReachedOverview && !mDetector.isDraggingState()) {
+ onSwipeInteractionCompleted(OVERVIEW);
+ }
+ }
+
+ private boolean handlingOverviewAnim() {
+ int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
+ return mDidTouchStartInNavBar && mStartState == NORMAL
+ && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
+ }
+
+ @Override
+ public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
+ if (mStartedOverview) {
+ if (!mReachedOverview) {
+ mStartDisplacement.set(xDisplacement, yDisplacement);
+ mStartY = event.getY();
+ } else {
+ mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
+ * OVERVIEW_MOVEMENT_FACTOR);
+ float yProgress = (mStartDisplacement.y - yDisplacement) / mStartY;
+ if (yProgress > 0 && mOverviewResistYAnim != null) {
+ mOverviewResistYAnim.setPlayFraction(yProgress);
+ } else {
+ mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
+ * OVERVIEW_MOVEMENT_FACTOR);
+ }
+ }
+ }
+
+ float upDisplacement = -yDisplacement;
+ mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim()
+ || upDisplacement < mMotionPauseMinDisplacement);
+ mMotionPauseDetector.addPosition(event);
+
+ // Stay in Overview.
+ return mStartedOverview || super.onDrag(yDisplacement, xDisplacement, event);
+ }
+
+ private void goToOverviewOrHomeOnDragEnd(float velocity) {
+ boolean goToHomeInsteadOfOverview = !mMotionPauseDetector.isPaused();
+ if (goToHomeInsteadOfOverview) {
+ new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(NORMAL))
+ .animateWithVelocity(velocity);
+ }
+ if (mReachedOverview) {
+ float distanceDp = dpiFromPx(Math.max(
+ Math.abs(mRecentsView.getTranslationX()),
+ Math.abs(mRecentsView.getTranslationY())));
+ long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
+ distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
+ mRecentsView.animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(ACCEL_DEACCEL)
+ .setDuration(duration)
+ .withEndAction(goToHomeInsteadOfOverview
+ ? null
+ : this::maybeSwipeInteractionToOverviewComplete);
+ if (!goToHomeInsteadOfOverview) {
+ // Return to normal properties for the overview state.
+ StateAnimationConfig config = new StateAnimationConfig();
+ config.duration = duration;
+ LauncherState state = mLauncher.getStateManager().getState();
+ mLauncher.getStateManager().createAtomicAnimation(state, state, config).start();
+ }
+ }
+ }
+
+ private float dpiFromPx(float pixels) {
+ return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics().densityDpi);
+ }
+
+ @Override
+ public void onOneHandedModeStateChanged(boolean activated) {
+ if (activated) {
+ mDetector.setTouchSlopMultiplier(ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER);
+ } else {
+ // Reset touch slop multiplier to default 1.0f
+ mDetector.setTouchSlopMultiplier(1f /* default */);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
new file mode 100644
index 0000000..40c3e02
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
+import static com.android.launcher3.LauncherState.QUICK_SWITCH;
+import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
+import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
+import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
+import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.BothAxesSwipeDetector;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.WorkspaceRevealAnim;
+import com.android.quickstep.views.LauncherRecentsView;
+
+/**
+ * Handles quick switching to a recent task from the home screen. To give as much flexibility to
+ * the user as possible, also handles swipe up and hold to go to overview and swiping back home.
+ */
+public class NoButtonQuickSwitchTouchController implements TouchController,
+ BothAxesSwipeDetector.Listener {
+
+ private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
+ private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_3;
+ private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
+ private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR;
+ private static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
+
+ private final BaseQuickstepLauncher mLauncher;
+ private final BothAxesSwipeDetector mSwipeDetector;
+ private final float mXRange;
+ private final float mYRange;
+ private final float mMaxYProgress;
+ private final MotionPauseDetector mMotionPauseDetector;
+ private final float mMotionPauseMinDisplacement;
+ private final LauncherRecentsView mRecentsView;
+ protected final AnimatorListener mClearStateOnCancelListener =
+ newCancelListener(this::clearState);
+
+ private boolean mNoIntercept;
+ private LauncherState mStartState;
+
+ private boolean mIsHomeScreenVisible = true;
+
+ // As we drag, we control 3 animations: one to get non-overview components out of the way,
+ // and the other two to set overview properties based on x and y progress.
+ private AnimatorPlaybackController mNonOverviewAnim;
+ private AnimatorPlaybackController mXOverviewAnim;
+ private AnimatedFloat mYOverviewAnim;
+
+ public NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher) {
+ mLauncher = launcher;
+ mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
+ mRecentsView = mLauncher.getOverviewPanel();
+ mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
+ mYRange = LayoutUtils.getShelfTrackingDistance(
+ mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
+ mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
+ mMotionPauseDetector = new MotionPauseDetector(mLauncher);
+ mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
+ R.dimen.motion_pause_detector_min_displacement_from_app);
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mNoIntercept = !canInterceptTouch(ev);
+ if (mNoIntercept) {
+ return false;
+ }
+
+ // Only detect horizontal swipe for intercept, then we will allow swipe up as well.
+ mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
+ false /* ignoreSlopWhenSettling */);
+ }
+
+ if (mNoIntercept) {
+ return false;
+ }
+
+ onControllerTouchEvent(ev);
+ return mSwipeDetector.isDraggingOrSettling();
+ }
+
+ @Override
+ public boolean onControllerTouchEvent(MotionEvent ev) {
+ return mSwipeDetector.onTouchEvent(ev);
+ }
+
+ private boolean canInterceptTouch(MotionEvent ev) {
+ if (!mLauncher.isInState(LauncherState.NORMAL)) {
+ return false;
+ }
+ if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
+ return false;
+ }
+ int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
+ if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onDragStart(boolean start) {
+ mMotionPauseDetector.clear();
+ if (start) {
+ mStartState = mLauncher.getStateManager().getState();
+
+ mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
+
+ // We have detected horizontal drag start, now allow swipe up as well.
+ mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP,
+ false /* ignoreSlopWhenSettling */);
+
+ setupAnimators();
+ }
+ }
+
+ private void onMotionPauseDetected() {
+ VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+ }
+
+ private void setupAnimators() {
+ // Animate the non-overview components (e.g. workspace, shelf) out of the way.
+ StateAnimationConfig nonOverviewBuilder = new StateAnimationConfig();
+ nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR);
+ nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR);
+ nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_SCALE, FADE_OUT_INTERPOLATOR);
+ nonOverviewBuilder.setInterpolator(ANIM_DEPTH, FADE_OUT_INTERPOLATOR);
+ nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR);
+ updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder);
+ mNonOverviewAnim.dispatchOnStart();
+
+ if (mRecentsView.getTaskViewCount() == 0) {
+ mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
+ if (!isEmpty && mSwipeDetector.isDraggingState()) {
+ // We have loaded tasks, update the animators to start at the correct scale etc.
+ setupOverviewAnimators();
+ }
+ });
+ }
+
+ setupOverviewAnimators();
+ }
+
+ /** Create state animation to control non-overview components. */
+ private void updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config) {
+ config.duration = (long) (Math.max(mXRange, mYRange) * 2);
+ config.animFlags |= SKIP_OVERVIEW | SKIP_SCRIM;
+ mNonOverviewAnim = mLauncher.getStateManager()
+ .createAnimationToNewWorkspace(toState, config);
+ mNonOverviewAnim.getTarget().addListener(mClearStateOnCancelListener);
+ }
+
+ private void setupOverviewAnimators() {
+ final LauncherState fromState = QUICK_SWITCH;
+ final LauncherState toState = OVERVIEW;
+
+ // Set RecentView's initial properties.
+ RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+ ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, 1f);
+ mRecentsView.setContentAlpha(1);
+ mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
+ mLauncher.getActionsView().getVisibilityAlpha().setValue(
+ (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
+
+ float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
+ // As we drag right, animate the following properties:
+ // - RecentsView translationX
+ // - OverviewScrim
+ // - RecentsView fade (if it's empty)
+ PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
+ xAnim.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1], LINEAR);
+ xAnim.setViewBackgroundColor(mLauncher.getScrimView(),
+ toState.getWorkspaceScrimColor(mLauncher), LINEAR);
+ if (mRecentsView.getTaskViewCount() == 0) {
+ xAnim.addFloat(mRecentsView, CONTENT_ALPHA, 0f, 1f, LINEAR);
+ }
+ mXOverviewAnim = xAnim.createPlaybackController();
+ mXOverviewAnim.dispatchOnStart();
+
+ // As we drag up, animate the following properties:
+ // - RecentsView scale
+ // - RecentsView fullscreenProgress
+ PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
+ yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
+ SCALE_DOWN_INTERPOLATOR);
+ yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
+ toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
+ AnimatorPlaybackController yNormalController = yAnim.createPlaybackController();
+ AnimatorControllerWithResistance yAnimWithResistance = AnimatorControllerWithResistance
+ .createForRecents(yNormalController, mLauncher,
+ mRecentsView.getPagedViewOrientedState(), mLauncher.getDeviceProfile(),
+ mRecentsView, RECENTS_SCALE_PROPERTY, mRecentsView,
+ TASK_SECONDARY_TRANSLATION);
+ mYOverviewAnim = new AnimatedFloat(() -> {
+ if (mYOverviewAnim != null) {
+ yAnimWithResistance.setProgress(mYOverviewAnim.value, mMaxYProgress);
+ }
+ });
+ yNormalController.dispatchOnStart();
+ }
+
+ @Override
+ public boolean onDrag(PointF displacement, MotionEvent ev) {
+ float xProgress = Math.max(0, displacement.x) / mXRange;
+ float yProgress = Math.max(0, -displacement.y) / mYRange;
+ yProgress = Utilities.mapRange(yProgress, Y_ANIM_MIN_PROGRESS, 1f);
+
+ boolean wasHomeScreenVisible = mIsHomeScreenVisible;
+ if (wasHomeScreenVisible && mNonOverviewAnim != null) {
+ mNonOverviewAnim.setPlayFraction(xProgress);
+ }
+ mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress)
+ <= 1 - ALPHA_CUTOFF_THRESHOLD;
+
+ mMotionPauseDetector.setDisallowPause(-displacement.y < mMotionPauseMinDisplacement);
+ mMotionPauseDetector.addPosition(ev);
+
+ if (mXOverviewAnim != null) {
+ mXOverviewAnim.setPlayFraction(xProgress);
+ }
+ if (mYOverviewAnim != null) {
+ mYOverviewAnim.updateValue(yProgress);
+ }
+ return true;
+ }
+
+ @Override
+ public void onDragEnd(PointF velocity) {
+ boolean horizontalFling = mSwipeDetector.isFling(velocity.x);
+ boolean verticalFling = mSwipeDetector.isFling(velocity.y);
+ boolean noFling = !horizontalFling && !verticalFling;
+ if (mMotionPauseDetector.isPaused() && noFling) {
+ cancelAnimations();
+
+ StateAnimationConfig config = new StateAnimationConfig();
+ config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
+ Animator overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
+ mStartState, OVERVIEW, config);
+ overviewAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onAnimationToStateCompleted(OVERVIEW);
+ }
+ });
+ overviewAnim.start();
+ return;
+ }
+
+ final LauncherState targetState;
+ if (horizontalFling && verticalFling) {
+ if (velocity.x < 0) {
+ // Flinging left and up or down both go back home.
+ targetState = NORMAL;
+ } else {
+ if (velocity.y > 0) {
+ // Flinging right and down goes to quick switch.
+ targetState = QUICK_SWITCH;
+ } else {
+ // Flinging up and right could go either home or to quick switch.
+ // Determine the target based on the higher velocity.
+ targetState = Math.abs(velocity.x) > Math.abs(velocity.y)
+ ? QUICK_SWITCH : NORMAL;
+ }
+ }
+ } else if (horizontalFling) {
+ targetState = velocity.x > 0 ? QUICK_SWITCH : NORMAL;
+ } else if (verticalFling) {
+ targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL;
+ } else {
+ // If user isn't flinging, just snap to the closest state.
+ boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
+ boolean passedVerticalThreshold = mYOverviewAnim.value > 1f;
+ targetState = passedHorizontalThreshold && !passedVerticalThreshold
+ ? QUICK_SWITCH : NORMAL;
+ }
+
+ // Animate the various components to the target state.
+
+ float xProgress = mXOverviewAnim.getProgressFraction();
+ float startXProgress = Utilities.boundToRange(xProgress
+ + velocity.x * getSingleFrameMs(mLauncher) / mXRange, 0f, 1f);
+ final float endXProgress = targetState == NORMAL ? 0 : 1;
+ long xDuration = BaseSwipeDetector.calculateDuration(velocity.x,
+ Math.abs(endXProgress - startXProgress));
+ ValueAnimator xOverviewAnim = mXOverviewAnim.getAnimationPlayer();
+ xOverviewAnim.setFloatValues(startXProgress, endXProgress);
+ xOverviewAnim.setDuration(xDuration)
+ .setInterpolator(scrollInterpolatorForVelocity(velocity.x));
+ mXOverviewAnim.dispatchOnStart();
+
+ boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
+
+ float yProgress = mYOverviewAnim.value;
+ float startYProgress = Utilities.boundToRange(yProgress
+ - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, mMaxYProgress);
+ final float endYProgress;
+ if (flingUpToNormal) {
+ endYProgress = 1;
+ } else if (targetState == NORMAL) {
+ // Keep overview at its current scale/translationY as it slides off the screen.
+ endYProgress = startYProgress;
+ } else {
+ endYProgress = 0;
+ }
+ float yDistanceToCover = Math.abs(endYProgress - startYProgress) * mYRange;
+ long yDuration = (long) (yDistanceToCover / Math.max(1f, Math.abs(velocity.y)));
+ ValueAnimator yOverviewAnim = mYOverviewAnim.animateToValue(startYProgress, endYProgress);
+ yOverviewAnim.setDuration(yDuration);
+ mYOverviewAnim.updateValue(startYProgress);
+
+ ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
+ if (flingUpToNormal && !mIsHomeScreenVisible) {
+ // We are flinging to home while workspace is invisible, run the same staggered
+ // animation as from an app.
+ StateAnimationConfig config = new StateAnimationConfig();
+ // Update mNonOverviewAnim to do nothing so it doesn't interfere.
+ config.animFlags = SKIP_ALL_ANIMATIONS;
+ updateNonOverviewAnim(targetState, config);
+ nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
+
+ new WorkspaceRevealAnim(mLauncher, false /* animateOverviewScrim */).start();
+ } else {
+ boolean canceled = targetState == NORMAL;
+ if (canceled) {
+ // Let the state manager know that the animation didn't go to the target state,
+ // but don't clean up yet (we already clean up when the animation completes).
+ mNonOverviewAnim.getTarget().removeListener(mClearStateOnCancelListener);
+ mNonOverviewAnim.dispatchOnCancel();
+ }
+ float startProgress = mNonOverviewAnim.getProgressFraction();
+ float endProgress = canceled ? 0 : 1;
+ nonOverviewAnim.setFloatValues(startProgress, endProgress);
+ mNonOverviewAnim.dispatchOnStart();
+ }
+
+ nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
+ mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState));
+
+ cancelAnimations();
+ xOverviewAnim.start();
+ yOverviewAnim.start();
+ nonOverviewAnim.start();
+ }
+
+ private void onAnimationToStateCompleted(LauncherState targetState) {
+ mLauncher.getStatsLogManager().logger()
+ .withSrcState(LAUNCHER_STATE_HOME)
+ .withDstState(targetState.statsLogOrdinal)
+ .log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal,
+ targetState.ordinal > mStartState.ordinal
+ ? LAUNCHER_UNKNOWN_SWIPEUP
+ : LAUNCHER_UNKNOWN_SWIPEDOWN));
+ mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
+ }
+
+ private void cancelAnimations() {
+ if (mNonOverviewAnim != null) {
+ mNonOverviewAnim.getAnimationPlayer().cancel();
+ }
+ if (mXOverviewAnim != null) {
+ mXOverviewAnim.getAnimationPlayer().cancel();
+ }
+ if (mYOverviewAnim != null) {
+ mYOverviewAnim.cancelAnimation();
+ }
+ mMotionPauseDetector.clear();
+ }
+
+ private void clearState() {
+ cancelAnimations();
+ mNonOverviewAnim = null;
+ mXOverviewAnim = null;
+ mYOverviewAnim = null;
+ mIsHomeScreenVisible = true;
+ mSwipeDetector.finishedScrolling();
+ mRecentsView.setOnEmptyMessageUpdatedListener(null);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
new file mode 100644
index 0000000..4df0f63
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController.isTouchOverHotseat;
+
+import android.view.MotionEvent;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Helper class for {@link PortraitStatesTouchController} that determines swipeable regions and
+ * animations on the overview state that depend on the recents implementation.
+ */
+public final class PortraitOverviewStateTouchHelper {
+
+ RecentsView mRecentsView;
+ Launcher mLauncher;
+
+ public PortraitOverviewStateTouchHelper(Launcher launcher) {
+ mLauncher = launcher;
+ mRecentsView = launcher.getOverviewPanel();
+ }
+
+ /**
+ * Whether or not {@link PortraitStatesTouchController} should intercept the touch when on the
+ * overview state.
+ *
+ * @param ev the motion event
+ * @return true if we should intercept the motion event
+ */
+ boolean canInterceptTouch(MotionEvent ev) {
+ if (mRecentsView.getTaskViewCount() > 0) {
+ // Allow swiping up in the gap between the hotseat and overview.
+ return ev.getY() >= mRecentsView.getTaskViewAt(0).getBottom();
+ } else {
+ // If there are no tasks, we only intercept if we're below the hotseat height.
+ return isTouchOverHotseat(mLauncher, ev);
+ }
+ }
+
+ /**
+ * Whether or not swiping down to leave overview state should return to the currently running
+ * task app.
+ *
+ * @return true if going back should take the user to the currently running task
+ */
+ boolean shouldSwipeDownReturnToApp() {
+ TaskView taskView = mRecentsView.getNextPageTaskView();
+ return taskView != null && mRecentsView.shouldSwipeDownLaunchApp();
+ }
+
+ /**
+ * Create the animation for going from overview to the task app via swiping. Should only be
+ * called when {@link #shouldSwipeDownReturnToApp()} returns true.
+ *
+ * @param duration how long the animation should be
+ * @return the animation
+ */
+ PendingAnimation createSwipeDownToTaskAppAnimation(long duration, Interpolator interpolator) {
+ mRecentsView.setCurrentPage(mRecentsView.getDestinationPage());
+ TaskView taskView = mRecentsView.getCurrentPageTaskView();
+ if (taskView == null) {
+ throw new IllegalStateException("There is no task view to animate to.");
+ }
+ return mRecentsView.createTaskLaunchAnimation(taskView, duration, interpolator);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 20ee61d..3c83d25 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -23,20 +23,10 @@
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.anim.Interpolators.ACCEL;
import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.util.Log;
import android.view.MotionEvent;
-import android.view.animation.Interpolator;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
@@ -44,17 +34,13 @@
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.AbstractStateChangeTouchController;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.uioverrides.states.OverviewState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.TouchInteractionService;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
/**
* Touch controller for handling various state transitions in portrait UI.
@@ -64,48 +50,45 @@
private static final String TAG = "PortraitStatesTouchCtrl";
/**
- * The progress at which all apps content will be fully visible when swiping up from overview.
+ * The progress at which all apps content will be fully visible.
*/
- protected static final float ALL_APPS_CONTENT_FADE_THRESHOLD = 0.08f;
+ protected static final float ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD = 0.8f;
/**
- * The progress at which recents will begin fading out when swiping up from overview.
+ * Minimum clamping progress for fading in all apps content
*/
- private static final float RECENTS_FADE_THRESHOLD = 0.88f;
+ protected static final float ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD = 0.5f;
+
+ /**
+ * Minimum clamping progress for fading in all apps scrim
+ */
+ protected static final float ALL_APPS_SCRIM_VISIBLE_THRESHOLD = .1f;
+
+ /**
+ * Maximum clamping progress for opaque all apps scrim
+ */
+ protected static final float ALL_APPS_SCRIM_OPAQUE_THRESHOLD = .5f;
private final PortraitOverviewStateTouchHelper mOverviewPortraitStateTouchHelper;
- private final InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper();
-
- private final boolean mAllowDragToOverview;
-
- // If true, we will finish the current animation instantly on second touch.
- private boolean mFinishFastOnSecondTouch;
-
- public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) {
+ public PortraitStatesTouchController(Launcher l) {
super(l, SingleAxisSwipeDetector.VERTICAL);
mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
- mAllowDragToOverview = allowDragToOverview;
}
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
+ // If we are swiping to all apps instead of overview, allow it from anywhere.
+ boolean interceptAnywhere = mLauncher.isInState(NORMAL);
if (mCurrentAnimation != null) {
- if (mFinishFastOnSecondTouch) {
- mCurrentAnimation.getAnimationPlayer().end();
- }
-
AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
- if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()) {
+ if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()
+ || interceptAnywhere) {
// If we are already animating from a previous state, we can intercept as long as
// the touch is below the current all apps progress (to allow for double swipe).
return true;
}
- // Otherwise, make sure everything is settled and don't intercept so they can scroll
- // recents, dismiss a task, etc.
- if (mAtomicAnim != null) {
- mAtomicAnim.end();
- }
+ // Otherwise, don't intercept so they can scroll recents, dismiss a task, etc.
return false;
}
if (mLauncher.isInState(ALL_APPS)) {
@@ -118,9 +101,7 @@
return false;
}
} else {
- // If we are swiping to all apps instead of overview, allow it from anywhere.
- boolean interceptAnywhere = mLauncher.isInState(NORMAL) && !mAllowDragToOverview;
- // For all other states, only listen if the event originated below the hotseat height
+ // For non-normal states, only listen if the event originated below the hotseat height
if (!interceptAnywhere && !isTouchOverHotseat(mLauncher, ev)) {
return false;
}
@@ -133,83 +114,35 @@
@Override
protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "PortraitStatesTouchController.getTargetState");
- }
if (fromState == ALL_APPS && !isDragTowardPositive) {
- // Should swipe down go to OVERVIEW instead?
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
- "PortraitStatesTouchController.getTargetState 1");
- }
- return TouchInteractionService.isConnected() ?
- mLauncher.getStateManager().getLastState() : NORMAL;
+ return NORMAL;
} else if (fromState == OVERVIEW) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
- "PortraitStatesTouchController.getTargetState 2");
- }
- LauncherState positiveDragTarget = ALL_APPS;
- if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(mLauncher)) {
- // Don't allow swiping up to all apps.
- positiveDragTarget = OVERVIEW;
- }
- return isDragTowardPositive ? positiveDragTarget : NORMAL;
+ return isDragTowardPositive ? OVERVIEW : NORMAL;
} else if (fromState == NORMAL && isDragTowardPositive) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
- "PortraitStatesTouchController.getTargetState 3");
- }
- int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
- return mAllowDragToOverview && TouchInteractionService.isConnected()
- && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0
- ? OVERVIEW : ALL_APPS;
+ return ALL_APPS;
}
return fromState;
}
- @Override
- protected int getLogContainerTypeForNormalState(MotionEvent ev) {
- return isTouchOverHotseat(mLauncher, ev) ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
- }
-
- private StateAnimationConfig getNormalToOverviewAnimation() {
- mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR;
-
- StateAnimationConfig builder = new StateAnimationConfig();
- builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper);
- return builder;
- }
-
- private static StateAnimationConfig getOverviewToAllAppsAnimation() {
- StateAnimationConfig builder = new StateAnimationConfig();
- builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
- 0, ALL_APPS_CONTENT_FADE_THRESHOLD));
- builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(DEACCEL,
- RECENTS_FADE_THRESHOLD, 1));
- return builder;
- }
-
- private StateAnimationConfig getAllAppsToOverviewAnimation() {
- StateAnimationConfig builder = new StateAnimationConfig();
- builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
- 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1));
- builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(ACCEL,
- 0f, 1 - RECENTS_FADE_THRESHOLD));
- return builder;
- }
-
private StateAnimationConfig getNormalToAllAppsAnimation() {
StateAnimationConfig builder = new StateAnimationConfig();
builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
- 0, ALL_APPS_CONTENT_FADE_THRESHOLD));
+ ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD,
+ ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD));
+ builder.setInterpolator(ANIM_SCRIM_FADE, Interpolators.clampToProgress(ACCEL,
+ ALL_APPS_SCRIM_VISIBLE_THRESHOLD,
+ ALL_APPS_SCRIM_OPAQUE_THRESHOLD));
return builder;
}
private StateAnimationConfig getAllAppsToNormalAnimation() {
StateAnimationConfig builder = new StateAnimationConfig();
builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
- 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1));
+ 1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD,
+ 1 - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD));
+ builder.setInterpolator(ANIM_SCRIM_FADE, Interpolators.clampToProgress(DEACCEL,
+ 1 - ALL_APPS_SCRIM_OPAQUE_THRESHOLD,
+ 1 - ALL_APPS_SCRIM_VISIBLE_THRESHOLD));
return builder;
}
@@ -217,24 +150,18 @@
protected StateAnimationConfig getConfigForStates(
LauncherState fromState, LauncherState toState) {
final StateAnimationConfig config;
- if (fromState == NORMAL && toState == OVERVIEW) {
- config = getNormalToOverviewAnimation();
- } else if (fromState == OVERVIEW && toState == ALL_APPS) {
- config = getOverviewToAllAppsAnimation();
- } else if (fromState == ALL_APPS && toState == OVERVIEW) {
- config = getAllAppsToOverviewAnimation();
- } else if (fromState == NORMAL && toState == ALL_APPS) {
+ if (fromState == NORMAL && toState == ALL_APPS) {
config = getNormalToAllAppsAnimation();
} else if (fromState == ALL_APPS && toState == NORMAL) {
config = getAllAppsToNormalAnimation();
- } else {
+ } else {
config = new StateAnimationConfig();
}
return config;
}
@Override
- protected float initCurrentAnimation(@AnimationFlags int animFlags) {
+ protected float initCurrentAnimation() {
float range = getShiftRange();
long maxAccuracy = (long) (2 * range);
@@ -245,32 +172,31 @@
final StateAnimationConfig config = totalShift == 0 ? new StateAnimationConfig()
: getConfigForStates(mFromState, mToState);
- config.animFlags = updateAnimComponentsOnReinit(animFlags);
config.duration = maxAccuracy;
- cancelPendingAnim();
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
+ mCurrentAnimation.dispatchOnCancel();
+ }
+ mGoingBetweenStates = true;
if (mFromState == OVERVIEW && mToState == NORMAL
&& mOverviewPortraitStateTouchHelper.shouldSwipeDownReturnToApp()) {
// Reset the state manager, when changing the interaction mode
mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */);
- mPendingAnimation = mOverviewPortraitStateTouchHelper
- .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR);
- Runnable onCancelRunnable = () -> {
- cancelPendingAnim();
- clearState();
- };
- mCurrentAnimation = mPendingAnimation.createPlaybackController()
- .setOnCancelRunnable(onCancelRunnable);
+ mGoingBetweenStates = false;
+ mCurrentAnimation = mOverviewPortraitStateTouchHelper
+ .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR)
+ .createPlaybackController();
mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
RecentsView recentsView = mLauncher.getOverviewPanel();
totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher,
mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler());
} else {
mCurrentAnimation = mLauncher.getStateManager()
- .createAnimationToNewWorkspace(mToState, config)
- .setOnCancelRunnable(this::clearState);
+ .createAnimationToNewWorkspace(mToState, config);
}
+ mCurrentAnimation.getTarget().addListener(mClearStateOnCancelListener);
if (totalShift == 0) {
totalShift = Math.signum(mFromState.ordinal - mToState.ordinal)
@@ -279,53 +205,9 @@
return 1 / totalShift;
}
- /**
- * Give subclasses the chance to update the animation when we re-initialize towards a new state.
- */
- @AnimationFlags
- protected int updateAnimComponentsOnReinit(@AnimationFlags int animComponents) {
- return animComponents;
- }
-
- 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 (UNSTABLE_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
- && targetState == OVERVIEW) {
- mFinishFastOnSecondTouch = true;
- } else 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(
- Interpolators.overshootInterpolatorForVelocity(velocity), 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);
+ protected void onSwipeInteractionCompleted(LauncherState targetState) {
+ super.onSwipeInteractionCompleted(targetState);
if (mStartState == NORMAL && targetState == OVERVIEW) {
SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
}
@@ -335,7 +217,7 @@
* Whether the motion event is over the hotseat.
*
* @param launcher the launcher activity
- * @param ev the event to check
+ * @param ev the event to check
* @return true if the event is over the hotseat
*/
static boolean isTouchOverHotseat(Launcher launcher, MotionEvent ev) {
@@ -348,13 +230,43 @@
return launcher.getDragLayer().getHeight() - hotseatHeight;
}
- private static class InterpolatorWrapper implements Interpolator {
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ InteractionJankMonitorWrapper.begin(
+ mLauncher.getRootView(), InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+ break;
- public TimeInterpolator baseInterpolator = LINEAR;
-
- @Override
- public float getInterpolation(float v) {
- return baseInterpolator.getInterpolation(v);
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ InteractionJankMonitorWrapper.cancel(
+ InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+ break;
}
+ return super.onControllerInterceptTouchEvent(ev);
+
+ }
+
+ @Override
+ protected void onReinitToState(LauncherState newToState) {
+ super.onReinitToState(newToState);
+ if (newToState != ALL_APPS) {
+ InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+ }
+ }
+
+ @Override
+ protected void onReachedFinalState(LauncherState toState) {
+ super.onReinitToState(toState);
+ if (toState == ALL_APPS) {
+ InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+ }
+ }
+
+ @Override
+ protected void clearState() {
+ super.clearState();
+ InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
new file mode 100644
index 0000000..f0ef9cc
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.QUICK_SWITCH;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+
+import android.view.MotionEvent;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.AbstractStateChangeTouchController;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Handles quick switching to a recent task from the home screen.
+ */
+public class QuickSwitchTouchController extends AbstractStateChangeTouchController {
+
+ protected final RecentsView mOverviewPanel;
+
+ public QuickSwitchTouchController(Launcher launcher) {
+ this(launcher, SingleAxisSwipeDetector.HORIZONTAL);
+ }
+
+ protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
+ super(l, dir);
+ mOverviewPanel = l.getOverviewPanel();
+ }
+
+ @Override
+ protected boolean canInterceptTouch(MotionEvent ev) {
+ if (mCurrentAnimation != null) {
+ return true;
+ }
+ if (!mLauncher.isInState(LauncherState.NORMAL)) {
+ return false;
+ }
+ if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+ int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
+ if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
+ return NORMAL;
+ }
+ return isDragTowardPositive ? QUICK_SWITCH : NORMAL;
+ }
+
+ @Override
+ public void onDragStart(boolean start, float startDisplacement) {
+ super.onDragStart(start, startDisplacement);
+ mStartContainerType = LAUNCHER_STATE_BACKGROUND;
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ }
+
+ @Override
+ protected void onSwipeInteractionCompleted(LauncherState targetState) {
+ super.onSwipeInteractionCompleted(targetState);
+ }
+
+ @Override
+ protected float initCurrentAnimation() {
+ StateAnimationConfig config = new StateAnimationConfig();
+ setupInterpolators(config);
+ config.duration = (long) (getShiftRange() * 2);
+
+ // Set RecentView's initial properties for coming in from the side.
+ RECENTS_SCALE_PROPERTY.set(mOverviewPanel,
+ QUICK_SWITCH.getOverviewScaleAndOffset(mLauncher)[0] * 0.85f);
+ ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mOverviewPanel, 1f);
+ mOverviewPanel.setContentAlpha(1);
+
+ mCurrentAnimation = mLauncher.getStateManager()
+ .createAnimationToNewWorkspace(mToState, config);
+ mCurrentAnimation.getTarget().addListener(mClearStateOnCancelListener);
+ mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator ->
+ updateFullscreenProgress((Float) valueAnimator.getAnimatedValue()));
+ return 1 / getShiftRange();
+ }
+
+ private void setupInterpolators(StateAnimationConfig stateAnimationConfig) {
+ stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_2);
+ stateAnimationConfig.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_2);
+ if (SysUINavigationMode.getMode(mLauncher) == Mode.NO_BUTTON) {
+ // Overview lives to the left of workspace, so translate down later than over
+ stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL_2);
+ stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, ACCEL_2);
+ stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_SCALE, ACCEL_2);
+ stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, ACCEL_2);
+ stateAnimationConfig.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
+ } else {
+ stateAnimationConfig.setInterpolator(ANIM_WORKSPACE_TRANSLATE, LINEAR);
+ stateAnimationConfig.setInterpolator(ANIM_VERTICAL_PROGRESS, LINEAR);
+ }
+ }
+
+ @Override
+ protected void updateProgress(float progress) {
+ super.updateProgress(progress);
+ updateFullscreenProgress(Utilities.boundToRange(progress, 0, 1));
+ }
+
+ private void updateFullscreenProgress(float progress) {
+ mOverviewPanel.setFullscreenProgress(progress);
+ if (progress > UPDATE_SYSUI_FLAGS_THRESHOLD) {
+ int sysuiFlags = 0;
+ TaskView tv = mOverviewPanel.getTaskViewAt(0);
+ if (tv != null) {
+ sysuiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
+ }
+ mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags);
+ } else {
+ mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
+ }
+ }
+
+ @Override
+ protected float getShiftRange() {
+ return mLauncher.getDeviceProfile().widthPx / 2f;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index 16bd9ed..fe69c9b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -15,10 +15,12 @@
*/
package com.android.launcher3.uioverrides.touchcontrollers;
+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_UP;
-import static android.view.MotionEvent.ACTION_CANCEL;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN;
import android.graphics.PointF;
import android.util.SparseArray;
@@ -31,12 +33,9 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-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.TouchController;
-
import com.android.quickstep.SystemUiProxy;
+
import java.io.PrintWriter;
/**
@@ -133,9 +132,8 @@
int action = ev.getAction();
if (action == ACTION_UP || action == ACTION_CANCEL) {
dispatchTouchEvent(ev);
- mLauncher.getUserEventDispatcher().logActionOnContainer(action == ACTION_UP ?
- Touch.FLING : Touch.SWIPE, Direction.DOWN, ContainerType.WORKSPACE,
- mLauncher.getWorkspace().getCurrentPage());
+ mLauncher.getStatsLogManager().logger()
+ .log(LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN);
setWindowSlippery(false);
return true;
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
new file mode 100644
index 0000000..180af0b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -0,0 +1,373 @@
+/*
+ * 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.touchcontrollers;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
+import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+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.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.util.FlingBlockCheck;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Touch controller for handling task view card swipes
+ */
+public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
+ extends AnimatorListenerAdapter implements TouchController,
+ SingleAxisSwipeDetector.Listener {
+
+ private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
+ private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
+ private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600;
+
+ protected final T mActivity;
+ private final SingleAxisSwipeDetector mDetector;
+ private final RecentsView mRecentsView;
+ private final int[] mTempCords = new int[2];
+ private final boolean mIsRtl;
+
+ private AnimatorPlaybackController mCurrentAnimation;
+ private boolean mCurrentAnimationIsGoingUp;
+ private boolean mAllowGoingUp;
+ private boolean mAllowGoingDown;
+
+ private boolean mNoIntercept;
+
+ private float mDisplacementShift;
+ private float mProgressMultiplier;
+ private float mEndDisplacement;
+ private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
+ private Float mOverrideVelocity = null;
+
+ private TaskView mTaskBeingDragged;
+
+ public TaskViewTouchController(T activity) {
+ mActivity = activity;
+ mRecentsView = activity.getOverviewPanel();
+ mIsRtl = Utilities.isRtl(activity.getResources());
+ SingleAxisSwipeDetector.Direction dir =
+ mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection();
+ mDetector = new SingleAxisSwipeDetector(activity, this, dir);
+ }
+
+ private boolean canInterceptTouch(MotionEvent ev) {
+ if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) {
+ // Don't intercept swipes on the nav bar, as user might be trying to go home
+ // during a task dismiss animation.
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.getAnimationPlayer().end();
+ }
+ return false;
+ }
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.forceFinishIfCloseToEnd();
+ }
+ if (mCurrentAnimation != null) {
+ // If we are already animating from a previous state, we can intercept.
+ return true;
+ }
+ if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
+ return false;
+ }
+ return isRecentsInteractive();
+ }
+
+ protected abstract boolean isRecentsInteractive();
+
+ /** Is recents view showing a single task in a modal way. */
+ protected abstract boolean isRecentsModal();
+
+ 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_UP || ev.getAction() == MotionEvent.ACTION_CANCEL)
+ && mCurrentAnimation == null) {
+ clearState();
+ }
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mNoIntercept = !canInterceptTouch(ev);
+ 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 = DIRECTION_BOTH;
+ ignoreSlopWhenSettling = true;
+ } else {
+ mTaskBeingDragged = null;
+
+ for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
+ TaskView view = mRecentsView.getTaskViewAt(i);
+
+ if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
+ .isEventOverView(view, ev)) {
+ // Disable swiping up and down if the task overlay is modal.
+ if (isRecentsModal()) {
+ mTaskBeingDragged = null;
+ break;
+ }
+ mTaskBeingDragged = view;
+ int upDirection = mRecentsView.getPagedOrientationHandler()
+ .getUpDirection(mIsRtl);
+ if (!SysUINavigationMode.getMode(mActivity).hasGestures || (
+ mActivity.getDeviceProfile().isTablet
+ && FeatureFlags.ENABLE_OVERVIEW_GRID.get())) {
+ // Don't allow swipe down to open if we don't support swipe up
+ // to enter overview, or when grid layout is enabled.
+ directionsToDetectScroll = upDirection;
+ mAllowGoingUp = true;
+ mAllowGoingDown = false;
+ } else {
+ // The task can be dragged up to dismiss it,
+ // and down to open if it's the current page.
+ mAllowGoingUp = true;
+ if (i == mRecentsView.getCurrentPage()) {
+ mAllowGoingDown = true;
+ directionsToDetectScroll = DIRECTION_BOTH;
+ } else {
+ mAllowGoingDown = false;
+ directionsToDetectScroll = upDirection;
+ }
+ }
+ 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;
+ }
+ if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) {
+ // Trying to re-init in an unsupported direction.
+ return;
+ }
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.setPlayFraction(0);
+ mCurrentAnimation.getTarget().removeListener(this);
+ mCurrentAnimation.dispatchOnCancel();
+ }
+
+ PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+ mCurrentAnimationIsGoingUp = goingUp;
+ BaseDragLayer dl = mActivity.getDragLayer();
+ final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
+ long maxDuration = 2 * secondaryLayerDimension;
+ int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
+ int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
+ // The interpolator controlling the most prominent visual movement. We use this to determine
+ // whether we passed SUCCESS_TRANSITION_PROGRESS.
+ final Interpolator currentInterpolator;
+ PendingAnimation pa;
+ if (goingUp) {
+ currentInterpolator = Interpolators.LINEAR;
+ pa = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
+ true /* animateTaskView */, true /* removeTask */, maxDuration);
+
+ mEndDisplacement = -secondaryTaskDimension;
+ } else {
+ currentInterpolator = Interpolators.ZOOM_IN;
+ pa = mRecentsView.createTaskLaunchAnimation(
+ mTaskBeingDragged, maxDuration, currentInterpolator);
+
+ // Since the thumbnail is what is filling the screen, based the end displacement on it.
+ View thumbnailView = mTaskBeingDragged.getThumbnail();
+ mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
+ dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
+ mEndDisplacement = secondaryLayerDimension - mTempCords[1];
+ }
+ mEndDisplacement *= verticalFactor;
+ mCurrentAnimation = pa.createPlaybackController();
+
+ // Setting this interpolator doesn't affect the visual motion, but is used to determine
+ // whether we successfully reached the target state in onDragEnd().
+ mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
+ onUserControlledAnimationCreated(mCurrentAnimation);
+ mCurrentAnimation.getTarget().addListener(this);
+ mCurrentAnimation.dispatchOnStart();
+ mProgressMultiplier = 1 / mEndDisplacement;
+ }
+
+ @Override
+ public void onDragStart(boolean start, float startDisplacement) {
+ PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+ if (mCurrentAnimation == null) {
+ reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
+ mDisplacementShift = 0;
+ } else {
+ mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
+ mCurrentAnimation.pause();
+ }
+ mFlingBlockCheck.unblockFling();
+ mOverrideVelocity = null;
+ }
+
+ @Override
+ public boolean onDrag(float displacement) {
+ PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+ float totalDisplacement = displacement + mDisplacementShift;
+ boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
+ orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
+ if (isGoingUp != mCurrentAnimationIsGoingUp) {
+ reInitAnimationController(isGoingUp);
+ mFlingBlockCheck.blockFling();
+ } else {
+ mFlingBlockCheck.onEvent();
+ }
+
+ if (isGoingUp) {
+ if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) {
+ // Halve the value when dismissing, as we are animating the drag across the full
+ // length for only the first half of the progress
+ mCurrentAnimation.setPlayFraction(
+ Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1));
+ } else {
+ // Set mOverrideVelocity to control task dismiss velocity in onDragEnd
+ int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity;
+ if (mRecentsView.showAsGrid()) {
+ if (mTaskBeingDragged.isFocusedTask()) {
+ velocityDimenId =
+ R.dimen.default_task_dismiss_drag_velocity_grid_focus_task;
+ } else {
+ velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid;
+ }
+ }
+ mOverrideVelocity = -mTaskBeingDragged.getResources().getDimension(velocityDimenId);
+
+ // Once halfway through task dismissal interpolation, switch from reversible
+ // dragging-task animation to playing the remaining task translation animations
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent upAction = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_UP, 0.0f, 0.0f, 0);
+ mDetector.onTouchEvent(upAction);
+ upAction.recycle();
+ }
+ } else {
+ mCurrentAnimation.setPlayFraction(
+ Utilities.boundToRange(totalDisplacement * mProgressMultiplier, 0, 1));
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onDragEnd(float velocity) {
+ if (mOverrideVelocity != null) {
+ velocity = mOverrideVelocity;
+ mOverrideVelocity = null;
+ }
+ // Limit velocity, as very large scalar values make animations play too quickly
+ float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension(
+ R.dimen.max_task_dismiss_drag_velocity);
+ velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity,
+ maxTaskDismissDragVelocity);
+ boolean fling = mDetector.isFling(velocity);
+ final boolean goingToEnd;
+ boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
+ if (blockedFling) {
+ fling = false;
+ }
+ PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+ float progress = mCurrentAnimation.getProgressFraction();
+ float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
+ if (fling) {
+ boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
+ goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
+ } else {
+ goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
+ }
+ long animationDuration = BaseSwipeDetector.calculateDuration(
+ velocity, goingToEnd ? (1 - progress) : progress);
+ if (blockedFling && !goingToEnd) {
+ animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
+ }
+ // Due to very high or low velocity dismissals, animation durations can be inconsistently
+ // long or short. Bound the duration for animation of task translations for a more
+ // standardized feel.
+ animationDuration = Utilities.boundToRange(animationDuration,
+ MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION);
+
+ mCurrentAnimation.setEndAction(this::clearState);
+ mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
+ velocity * orientationHandler.getSecondaryTranslationDirectionFactor(),
+ mEndDisplacement, animationDuration);
+ }
+
+ private void clearState() {
+ mDetector.finishedScrolling();
+ mDetector.setDetectableScrollConditions(0, false);
+ mTaskBeingDragged = null;
+ mCurrentAnimation = null;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
new file mode 100644
index 0000000..8f9c014
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
+
+public class TransposedQuickSwitchTouchController extends QuickSwitchTouchController {
+
+ public TransposedQuickSwitchTouchController(Launcher launcher) {
+ super(launcher, SingleAxisSwipeDetector.VERTICAL);
+ }
+
+ @Override
+ protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+ return super.getTargetState(fromState,
+ isDragTowardPositive ^ mLauncher.getDeviceProfile().isSeascape());
+ }
+
+ @Override
+ protected float initCurrentAnimation() {
+ float multiplier = super.initCurrentAnimation();
+ return mLauncher.getDeviceProfile().isSeascape() ? multiplier : -multiplier;
+ }
+
+ @Override
+ protected float getShiftRange() {
+ return mLauncher.getDeviceProfile().heightPx / 2f;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
new file mode 100644
index 0000000..e2747df
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
+import static com.android.launcher3.AbstractFloatingView.getOpenView;
+import static com.android.launcher3.LauncherState.HINT_STATE_TWO_BUTTON;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+
+import android.animation.ValueAnimator;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.AbstractStateChangeTouchController;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.AllAppsEduView;
+
+/**
+ * Touch controller for handling edge swipes in 2-button mode
+ */
+public class TwoButtonNavbarTouchController extends AbstractStateChangeTouchController {
+
+ private static final int MAX_NUM_SWIPES_TO_TRIGGER_EDU = 3;
+
+ private static final String TAG = "2BtnNavbarTouchCtrl";
+
+ private final boolean mIsTransposed;
+
+ // If true, we will finish the current animation instantly on second touch.
+ private boolean mFinishFastOnSecondTouch;
+
+ private int mContinuousTouchCount = 0;
+
+ public TwoButtonNavbarTouchController(Launcher l) {
+ super(l, l.getDeviceProfile().isVerticalBarLayout()
+ ? SingleAxisSwipeDetector.HORIZONTAL : SingleAxisSwipeDetector.VERTICAL);
+ mIsTransposed = l.getDeviceProfile().isVerticalBarLayout();
+ }
+
+ @Override
+ protected boolean canInterceptTouch(MotionEvent ev) {
+ boolean canIntercept = canInterceptTouchInternal(ev);
+ if (!canIntercept) {
+ mContinuousTouchCount = 0;
+ }
+ return canIntercept;
+ }
+
+ private boolean canInterceptTouchInternal(MotionEvent ev) {
+ if (mCurrentAnimation != null) {
+ if (mFinishFastOnSecondTouch) {
+ mCurrentAnimation.getAnimationPlayer().end();
+ }
+
+ // If we are already animating from a previous state, we can intercept.
+ return true;
+ }
+ if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+ return false;
+ }
+ if ((ev.getEdgeFlags() & EDGE_NAV_BAR) == 0) {
+ return false;
+ }
+ if (!mIsTransposed && mLauncher.isInState(OVERVIEW)) {
+ return true;
+ }
+ return mLauncher.isInState(NORMAL);
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ boolean intercept = super.onControllerInterceptTouchEvent(ev);
+ return intercept;
+ }
+
+ @Override
+ protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+ if (mIsTransposed) {
+ boolean draggingFromNav =
+ mLauncher.getDeviceProfile().isSeascape() == isDragTowardPositive;
+ return draggingFromNav ? HINT_STATE_TWO_BUTTON : NORMAL;
+ } else {
+ LauncherState startState = mStartState != null ? mStartState : fromState;
+ return isDragTowardPositive ^ (startState == OVERVIEW) ? HINT_STATE_TWO_BUTTON : NORMAL;
+ }
+ }
+
+ @Override
+ protected void onReinitToState(LauncherState newToState) {
+ super.onReinitToState(newToState);
+ }
+
+ @Override
+ protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
+ LauncherState targetState, float velocity, boolean isFling) {
+ super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState,
+ velocity, isFling);
+ mFinishFastOnSecondTouch = !mIsTransposed && mFromState == NORMAL;
+
+ if (targetState == HINT_STATE_TWO_BUTTON) {
+ // We were going to HINT_STATE_TWO_BUTTON, but end that animation immediately so we go
+ // to OVERVIEW instead.
+ animator.setDuration(0);
+ }
+ }
+
+ @Override
+ protected float getShiftRange() {
+ // Should be in sync with TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT
+ return LayoutUtils.getDefaultSwipeHeight(mLauncher, mLauncher.getDeviceProfile());
+ }
+
+ @Override
+ protected float initCurrentAnimation() {
+ float range = getShiftRange();
+ long maxAccuracy = (long) (2 * range);
+ mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState,
+ maxAccuracy);
+ return (mLauncher.getDeviceProfile().isSeascape() ? 1 : -1) / range;
+ }
+
+ @Override
+ protected void updateProgress(float fraction) {
+ super.updateProgress(fraction);
+
+ // We have reached HINT_STATE, end the gesture now to go to OVERVIEW.
+ if (fraction >= 1 && mToState == HINT_STATE_TWO_BUTTON) {
+ final long now = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_UP, 0.0f, 0.0f, 0);
+ mDetector.onTouchEvent(event);
+ event.recycle();
+ }
+ }
+
+ @Override
+ protected void onSwipeInteractionCompleted(LauncherState targetState) {
+ super.onSwipeInteractionCompleted(targetState);
+ if (!mIsTransposed) {
+ mContinuousTouchCount++;
+ }
+ if (mStartState == NORMAL && targetState == HINT_STATE_TWO_BUTTON) {
+ SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
+ } else if (targetState == NORMAL
+ && mContinuousTouchCount >= MAX_NUM_SWIPES_TO_TRIGGER_EDU) {
+ mContinuousTouchCount = 0;
+ if (getOpenView(mLauncher, TYPE_ALL_APPS_EDU) == null) {
+ AllAppsEduView.show(mLauncher);
+ }
+ }
+ mStartState = null;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
new file mode 100644
index 0000000..ac1772c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -0,0 +1,1817 @@
+/*
+ * 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.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+import static android.widget.Toast.LENGTH_SHORT;
+
+import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
+import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
+import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
+import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
+import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
+import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
+import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
+import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELED;
+import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnApplyWindowInsetsListener;
+import android.view.ViewTreeObserver.OnDrawListener;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.view.WindowInsets;
+import android.view.animation.Interpolator;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.tracing.InputConsumerProto;
+import com.android.launcher3.tracing.SwipeHandlerProto;
+import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.launcher3.util.WindowBounds;
+import com.android.quickstep.BaseActivityInterface.AnimationFactory;
+import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.InputConsumerProxy;
+import com.android.quickstep.util.InputProxyHandlerFactory;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.ProtoTracer;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.SwipePipToHomeAnimator;
+import com.android.quickstep.util.TransformParams;
+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.InteractionJankMonitorWrapper;
+import com.android.systemui.shared.system.LatencyTrackerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * Handles the navigation gestures when Launcher is the default home activity.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
+ Q extends RecentsView, S extends BaseState<S>>
+ extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener,
+ RecentsAnimationCallbacks.RecentsAnimationListener {
+ private static final String TAG = "AbsSwipeUpHandler";
+
+ private static final String[] STATE_NAMES = DEBUG_STATES ? new String[17] : null;
+
+ protected final BaseActivityInterface<S, T> mActivityInterface;
+ protected final InputConsumerProxy mInputConsumerProxy;
+ protected final ActivityInitListener mActivityInitListener;
+ // Callbacks to be made once the recents animation starts
+ private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
+ private final OnScrollChangedListener mOnRecentsScrollListener = this::onRecentsViewScroll;
+
+ // Null if the recents animation hasn't started yet or has been canceled or finished.
+ protected @Nullable RecentsAnimationController mRecentsAnimationController;
+ protected RecentsAnimationTargets mRecentsAnimationTargets;
+ protected T mActivity;
+ protected Q mRecentsView;
+ protected Runnable mGestureEndCallback;
+ protected MultiStateCallback mStateCallback;
+ protected boolean mCanceled;
+ private boolean mRecentsViewScrollLinked = false;
+
+ private static int getFlagForIndex(int index, String name) {
+ if (DEBUG_STATES) {
+ STATE_NAMES[index] = name;
+ }
+ return 1 << index;
+ }
+
+ // Launcher UI related states
+ protected static final int STATE_LAUNCHER_PRESENT =
+ getFlagForIndex(0, "STATE_LAUNCHER_PRESENT");
+ protected static final int STATE_LAUNCHER_STARTED =
+ getFlagForIndex(1, "STATE_LAUNCHER_STARTED");
+ protected static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN");
+
+ // Internal initialization states
+ private static final int STATE_APP_CONTROLLER_RECEIVED =
+ getFlagForIndex(3, "STATE_APP_CONTROLLER_RECEIVED");
+
+ // Interaction finish states
+ private static final int STATE_SCALED_CONTROLLER_HOME =
+ getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME");
+ private static final int STATE_SCALED_CONTROLLER_RECENTS =
+ getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
+
+ protected static final int STATE_HANDLER_INVALIDATED =
+ getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
+ private static final int STATE_GESTURE_STARTED =
+ getFlagForIndex(7, "STATE_GESTURE_STARTED");
+ private static final int STATE_GESTURE_CANCELLED =
+ getFlagForIndex(8, "STATE_GESTURE_CANCELLED");
+ private static final int STATE_GESTURE_COMPLETED =
+ getFlagForIndex(9, "STATE_GESTURE_COMPLETED");
+
+ private static final int STATE_CAPTURE_SCREENSHOT =
+ getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT");
+ protected static final int STATE_SCREENSHOT_CAPTURED =
+ getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED");
+ private static final int STATE_SCREENSHOT_VIEW_SHOWN =
+ getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN");
+
+ private static final int STATE_RESUME_LAST_TASK =
+ getFlagForIndex(13, "STATE_RESUME_LAST_TASK");
+ private static final int STATE_START_NEW_TASK =
+ getFlagForIndex(14, "STATE_START_NEW_TASK");
+ private static final int STATE_CURRENT_TASK_FINISHED =
+ getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED");
+ private static final int STATE_FINISH_WITH_NO_END =
+ getFlagForIndex(16, "STATE_FINISH_WITH_NO_END");
+
+ private static final int LAUNCHER_UI_STATES =
+ STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
+
+ public static final long MAX_SWIPE_DURATION = 350;
+ public static final long HOME_DURATION = StaggeredWorkspaceAnim.DURATION_MS;
+
+ public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
+ private static final float SWIPE_DURATION_MULTIPLIER =
+ Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
+ private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
+
+ public static final long RECENTS_ATTACH_DURATION = 300;
+
+ /**
+ * Used as the page index for logging when we return to the last task at the end of the gesture.
+ */
+ private static final int LOG_NO_OP_PAGE_INDEX = -1;
+
+ protected final TaskAnimationManager mTaskAnimationManager;
+
+ // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
+ private RunningWindowAnim mRunningWindowAnim;
+ // Possible second animation running at the same time as mRunningWindowAnim
+ private Animator mParallelRunningAnim;
+ private boolean mIsMotionPaused;
+ private boolean mHasMotionEverBeenPaused;
+
+ private boolean mContinuingLastGesture;
+
+ private ThumbnailData mTaskSnapshot;
+
+ // Used to control launcher components throughout the swipe gesture.
+ private AnimatorControllerWithResistance mLauncherTransitionController;
+ private boolean mHasEndedLauncherTransition;
+
+ private AnimationFactory mAnimationFactory = (t) -> { };
+
+ private boolean mWasLauncherAlreadyVisible;
+
+ private boolean mPassedOverviewThreshold;
+ private boolean mGestureStarted;
+ private boolean mLogDirectionUpOrLeft = true;
+ private PointF mDownPos;
+ private boolean mIsLikelyToStartNewTask;
+
+ private final long mTouchTimeMs;
+ private long mLauncherFrameDrawnTime;
+
+ private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
+
+ private SwipePipToHomeAnimator mSwipePipToHomeAnimator;
+ protected boolean mIsSwipingPipToHome;
+
+ public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
+ TaskAnimationManager taskAnimationManager, GestureState gestureState,
+ long touchTimeMs, boolean continuingLastGesture,
+ InputConsumerController inputConsumer) {
+ super(context, deviceState, gestureState, new TransformParams());
+ mActivityInterface = gestureState.getActivityInterface();
+ mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
+ mInputConsumerProxy =
+ new InputConsumerProxy(inputConsumer, () -> {
+ endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
+ endLauncherTransitionController();
+ }, new InputProxyHandlerFactory(mActivityInterface, mGestureState));
+ mTaskAnimationManager = taskAnimationManager;
+ mTouchTimeMs = touchTimeMs;
+ mContinuingLastGesture = continuingLastGesture;
+
+ initAfterSubclassConstructor();
+ initStateCallbacks();
+ }
+
+ private void initStateCallbacks() {
+ mStateCallback = new MultiStateCallback(STATE_NAMES);
+
+ mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
+ this::onLauncherPresentAndGestureStarted);
+
+ mStateCallback.runOnceAtState(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
+ this::initializeLauncherAnimationController);
+
+ mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
+ this::launcherFrameDrawn);
+
+ mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
+ | STATE_GESTURE_CANCELLED,
+ this::resetStateForAnimationCancel);
+
+ mStateCallback.runOnceAtState(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
+ this::resumeLastTask);
+ mStateCallback.runOnceAtState(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
+ this::startNewTask);
+
+ mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+ | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT,
+ this::switchToScreenshot);
+
+ mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+ | STATE_SCALED_CONTROLLER_RECENTS,
+ this::finishCurrentTransitionToRecents);
+
+ mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+ | STATE_SCALED_CONTROLLER_HOME,
+ this::finishCurrentTransitionToHome);
+ mStateCallback.runOnceAtState(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
+ this::reset);
+
+ mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+ | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
+ | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
+ | STATE_GESTURE_STARTED,
+ this::setupLauncherUiAfterSwipeUpToRecentsAnimation);
+
+ mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED,
+ this::continueComputingRecentsScrollIfNecessary);
+ mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED
+ | STATE_RECENTS_SCROLLING_FINISHED,
+ this::onSettledOnEndTarget);
+
+ mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
+ mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+ this::invalidateHandlerWithLauncher);
+ mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
+ this::resetStateForAnimationCancel);
+ mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_FINISH_WITH_NO_END,
+ this::resetStateForAnimationCancel);
+
+ if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
+ | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
+ (b) -> mRecentsView.setRunningTaskHidden(!b));
+ }
+ }
+
+ protected boolean onActivityInit(Boolean alreadyOnHome) {
+ if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+ return false;
+ }
+
+ T createdActivity = mActivityInterface.getCreatedActivity();
+ if (createdActivity != null) {
+ initTransitionEndpoints(createdActivity.getDeviceProfile());
+ }
+ final T activity = mActivityInterface.getCreatedActivity();
+ if (mActivity == activity) {
+ return true;
+ }
+
+ if (mActivity != null) {
+ if (mStateCallback.hasStates(STATE_GESTURE_COMPLETED)) {
+ // If the activity has restarted between setting the page scroll settling callback
+ // and actually receiving the callback, just mark the gesture completed
+ mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
+ return true;
+ }
+
+ // The launcher may have been recreated as a result of device rotation.
+ int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
+ initStateCallbacks();
+ mStateCallback.setState(oldState);
+ }
+ 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(STATE_HANDLER_INVISIBILITY_FLAGS);
+ } else {
+ mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+ }
+
+ mRecentsView = activity.getOverviewPanel();
+ mRecentsView.setOnPageTransitionEndCallback(null);
+
+ mStateCallback.setState(STATE_LAUNCHER_PRESENT);
+ if (alreadyOnHome) {
+ onLauncherStart();
+ } else {
+ activity.runOnceOnStart(this::onLauncherStart);
+ }
+
+ // Set up a entire animation lifecycle callback to notify the current recents view when
+ // the animation is canceled
+ mGestureState.runOnceAtState(STATE_RECENTS_ANIMATION_CANCELED, () -> {
+ ThumbnailData snapshot = mGestureState.consumeRecentsAnimationCanceledSnapshot();
+ if (snapshot != null) {
+ mRecentsView.switchToScreenshot(snapshot, () -> {
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.cleanupScreenshot();
+ }
+ });
+ mRecentsView.onRecentsAnimationComplete();
+ }
+ });
+
+ setupRecentsViewUi();
+ linkRecentsViewScroll();
+
+ return true;
+ }
+
+ /**
+ * Return true if the window should be translated horizontally if the recents view scrolls
+ */
+ protected boolean moveWindowWithRecentsScroll() {
+ return mGestureState.getEndTarget() != HOME;
+ }
+
+ private void onLauncherStart() {
+ final T activity = mActivityInterface.getCreatedActivity();
+ if (mActivity != activity) {
+ return;
+ }
+ if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+ return;
+ }
+ // RecentsView never updates the display rotation until swipe-up, force update
+ // RecentsOrientedState before passing to TaskViewSimulator.
+ mRecentsView.updateRecentsRotation();
+ mTaskViewSimulator.setOrientationState(mRecentsView.getPagedViewOrientedState());
+
+ // If we've already ended the gesture and are going home, don't prepare recents UI,
+ // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
+ if (mGestureState.getEndTarget() != HOME) {
+ Runnable initAnimFactory = () -> {
+ mAnimationFactory = mActivityInterface.prepareRecentsUI(mDeviceState,
+ mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated);
+ maybeUpdateRecentsAttachedState(false /* animate */);
+ };
+ if (mWasLauncherAlreadyVisible) {
+ // Launcher is visible, but might be about to stop. Thus, if we prepare recents
+ // now, it might get overridden by moveToRestState() in onStop(). To avoid this,
+ // wait until the next gesture (and possibly launcher) starts.
+ mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, initAnimFactory);
+ } else {
+ initAnimFactory.run();
+ }
+ }
+ AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible,
+ AbstractFloatingView.TYPE_LISTENER);
+
+ if (mWasLauncherAlreadyVisible) {
+ mStateCallback.setState(STATE_LAUNCHER_DRAWN);
+ } else {
+ Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init");
+ View dragLayer = activity.getDragLayer();
+ dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
+ boolean mHandled = false;
+
+ @Override
+ public void onDraw() {
+ if (mHandled) {
+ return;
+ }
+ mHandled = true;
+
+ TraceHelper.INSTANCE.endSection(traceToken);
+ dragLayer.post(() ->
+ dragLayer.getViewTreeObserver().removeOnDrawListener(this));
+ if (activity != mActivity) {
+ return;
+ }
+
+ mStateCallback.setState(STATE_LAUNCHER_DRAWN);
+ }
+ });
+ }
+
+ activity.getRootView().setOnApplyWindowInsetsListener(this);
+ mStateCallback.setState(STATE_LAUNCHER_STARTED);
+ }
+
+ private void onLauncherPresentAndGestureStarted() {
+ // Re-setup the recents UI when gesture starts, as the state could have been changed during
+ // that time by a previous window transition.
+ setupRecentsViewUi();
+
+ // For the duration of the gesture, in cases where an activity is launched while the
+ // activity is not yet resumed, finish the animation to ensure we get resumed
+ mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback(
+ mOnDeferredActivityLaunch);
+
+ mGestureState.runOnceAtState(STATE_END_TARGET_SET,
+ () -> {
+ mDeviceState.getRotationTouchHelper()
+ .onEndTargetCalculated(mGestureState.getEndTarget(),
+ mActivityInterface);
+ });
+
+ notifyGestureStartedAsync();
+ }
+
+ private void onDeferredActivityLaunch() {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mActivityInterface.switchRunningTaskViewToScreenshot(
+ null, () -> {
+ mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
+ });
+ } else {
+ mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
+ }
+ }
+
+ private void setupRecentsViewUi() {
+ if (mContinuingLastGesture) {
+ updateSysUiFlags(mCurrentShift.value);
+ return;
+ }
+ notifyGestureAnimationStartToRecents();
+ }
+
+ protected void notifyGestureAnimationStartToRecents() {
+ mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
+ }
+
+ private void launcherFrameDrawn() {
+ mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
+ }
+
+ private void initializeLauncherAnimationController() {
+ buildAnimationController();
+
+ Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents",
+ TraceHelper.FLAG_IGNORE_BINDERS);
+ LatencyTrackerCompat.logToggleRecents(
+ mContext, (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
+ TraceHelper.INSTANCE.endSection(traceToken);
+
+ // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
+ // high-res thumbnail loader here once we are sure that we will end up in an overview state
+ RecentsModel.INSTANCE.get(mContext).getThumbnailCache()
+ .getHighResLoadingState().setVisible(true);
+ }
+
+ public MotionPauseDetector.OnMotionPauseListener getMotionPauseListener() {
+ return new MotionPauseDetector.OnMotionPauseListener() {
+ @Override
+ public void onMotionPauseDetected() {
+ mHasMotionEverBeenPaused = true;
+ maybeUpdateRecentsAttachedState();
+ performHapticFeedback();
+ }
+
+ @Override
+ public void onMotionPauseChanged(boolean isPaused) {
+ mIsMotionPaused = isPaused;
+ }
+ };
+ }
+
+ public void maybeUpdateRecentsAttachedState() {
+ maybeUpdateRecentsAttachedState(true /* animate */);
+ }
+
+ /**
+ * Determines whether to show or hide RecentsView. The window is always
+ * synchronized with its corresponding TaskView in RecentsView, so if
+ * RecentsView is shown, it will appear to be attached to the window.
+ *
+ * Note this method has no effect unless the navigation mode is NO_BUTTON.
+ */
+ private void maybeUpdateRecentsAttachedState(boolean animate) {
+ if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
+ return;
+ }
+ RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
+ ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+ : null;
+ final boolean recentsAttachedToAppWindow;
+ if (mGestureState.getEndTarget() != null) {
+ recentsAttachedToAppWindow = mGestureState.getEndTarget().recentsAttachedToAppWindow;
+ } else if (mContinuingLastGesture
+ && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
+ recentsAttachedToAppWindow = true;
+ } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
+ // The window is going away so make sure recents is always visible in this case.
+ recentsAttachedToAppWindow = true;
+ } else {
+ recentsAttachedToAppWindow = mHasMotionEverBeenPaused || mIsLikelyToStartNewTask;
+ }
+ mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+
+ // Reapply window transform throughout the attach animation, as the animation affects how
+ // much the window is bound by overscroll (vs moving freely).
+ if (animate) {
+ ValueAnimator reapplyWindowTransformAnim = ValueAnimator.ofFloat(0, 1);
+ reapplyWindowTransformAnim.addUpdateListener(anim -> {
+ if (mRunningWindowAnim == null) {
+ applyWindowTransform();
+ }
+ });
+ reapplyWindowTransformAnim.setDuration(RECENTS_ATTACH_DURATION).start();
+ mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED,
+ reapplyWindowTransformAnim::cancel);
+ } else {
+ applyWindowTransform();
+ }
+ }
+
+ public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
+ setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
+ }
+
+ private void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask, boolean animate) {
+ if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
+ mIsLikelyToStartNewTask = isLikelyToStartNewTask;
+ maybeUpdateRecentsAttachedState(animate);
+ }
+ }
+
+ private void buildAnimationController() {
+ if (!canCreateNewOrUpdateExistingLauncherTransitionController()) {
+ return;
+ }
+ initTransitionEndpoints(mActivity.getDeviceProfile());
+ mAnimationFactory.createActivityInterface(mTransitionDragLength);
+ }
+
+ /**
+ * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
+ * (it has its own animation) or if we explicitly ended the controller already.
+ * @return Whether we can create the launcher controller or update its progress.
+ */
+ private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
+ return mGestureState.getEndTarget() != HOME && !mHasEndedLauncherTransition;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
+ WindowInsets result = view.onApplyWindowInsets(windowInsets);
+ buildAnimationController();
+ return result;
+ }
+
+ private void onAnimatorPlaybackControllerCreated(AnimatorControllerWithResistance anim) {
+ mLauncherTransitionController = anim;
+ mLauncherTransitionController.getNormalController().dispatchOnStart();
+ updateLauncherTransitionProgress();
+ }
+
+ public Intent getLaunchIntent() {
+ return mGestureState.getOverviewIntent();
+ }
+
+ /**
+ * Called when the value of {@link #mCurrentShift} changes
+ */
+ @UiThread
+ @Override
+ public void updateFinalShift() {
+ final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
+ if (passed != mPassedOverviewThreshold) {
+ mPassedOverviewThreshold = passed;
+ if (mDeviceState.isTwoButtonNavMode() && !mGestureState.isHandlingAtomicEvent()) {
+ performHapticFeedback();
+ }
+ }
+
+ updateSysUiFlags(mCurrentShift.value);
+ applyWindowTransform();
+
+ updateLauncherTransitionProgress();
+ }
+
+ private void updateLauncherTransitionProgress() {
+ if (mLauncherTransitionController == null
+ || !canCreateNewOrUpdateExistingLauncherTransitionController()) {
+ return;
+ }
+ mLauncherTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
+ }
+
+ /**
+ * @param windowProgress 0 == app, 1 == overview
+ */
+ private void updateSysUiFlags(float windowProgress) {
+ if (mRecentsAnimationController != null && mRecentsView != null) {
+ TaskView runningTask = mRecentsView.getRunningTaskView();
+ TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
+ int centermostTaskFlags = centermostTask == null ? 0
+ : centermostTask.getThumbnail().getSysUiStatusNavFlags();
+ boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
+ boolean quickswitchThresholdPassed = centermostTask != runningTask;
+
+ // We will handle the sysui flags based on the centermost task view.
+ mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed
+ || (quickswitchThresholdPassed && centermostTaskFlags != 0));
+ mRecentsAnimationController.setSplitScreenMinimized(swipeUpThresholdPassed);
+ // Provide a hint to WM the direction that we will be settling in case the animation
+ // needs to be canceled
+ mRecentsAnimationController.setWillFinishToHome(swipeUpThresholdPassed);
+
+ if (swipeUpThresholdPassed) {
+ mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
+ } else {
+ mActivity.getSystemUiController().updateUiState(
+ UI_STATE_FULLSCREEN_TASK, centermostTaskFlags);
+ }
+ }
+ }
+
+ @Override
+ public void onRecentsAnimationStart(RecentsAnimationController controller,
+ RecentsAnimationTargets targets) {
+ ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length);
+ mRecentsAnimationController = controller;
+ mRecentsAnimationTargets = targets;
+ mTransformParams.setTargetSet(mRecentsAnimationTargets);
+ RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
+ mGestureState.getRunningTaskId());
+
+ if (runningTaskTarget != null) {
+ mTaskViewSimulator.setPreview(runningTaskTarget);
+ }
+
+ // Only initialize the device profile, if it has not been initialized before, as in some
+ // configurations targets.homeContentInsets may not be correct.
+ if (mActivity == null) {
+ DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
+ if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
+ Rect overviewStackBounds = mActivityInterface
+ .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
+ dp = dp.getMultiWindowProfile(mContext,
+ new WindowBounds(overviewStackBounds, targets.homeContentInsets));
+ } else {
+ // If we are not in multi-window mode, home insets should be same as system insets.
+ dp = dp.copy(mContext);
+ }
+ dp.updateInsets(targets.homeContentInsets);
+ dp.updateIsSeascape(mContext);
+ initTransitionEndpoints(dp);
+ mTaskViewSimulator.getOrientationState().setMultiWindowMode(dp.isMultiWindowMode);
+ }
+
+ // Notify when the animation starts
+ if (!mRecentsAnimationStartCallbacks.isEmpty()) {
+ for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
+ action.run();
+ }
+ mRecentsAnimationStartCallbacks.clear();
+ }
+
+ // Only add the callback to enable the input consumer after we actually have the controller
+ mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
+ mRecentsAnimationController::enableInputConsumer);
+ mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
+
+ mPassedOverviewThreshold = false;
+ }
+
+ @Override
+ public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+ ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
+ mActivityInitListener.unregister();
+ mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+
+ // Defer clearing the controller and the targets until after we've updated the state
+ mRecentsAnimationController = null;
+ mRecentsAnimationTargets = null;
+ if (mRecentsView != null) {
+ mRecentsView.setRecentsAnimationTargets(null, null);
+ }
+ }
+
+ @UiThread
+ public void onGestureStarted(boolean isLikelyToStartNewTask) {
+ mActivityInterface.closeOverlay();
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+
+ if (mRecentsView != null) {
+ mRecentsView.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
+ boolean mHandled = false;
+
+ @Override
+ public void onDraw() {
+ if (mHandled) {
+ return;
+ }
+ mHandled = true;
+
+ InteractionJankMonitorWrapper.begin(mRecentsView,
+ InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH, 2000 /* ms timeout */);
+ InteractionJankMonitorWrapper.begin(mRecentsView,
+ InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
+
+ mRecentsView.post(() ->
+ mRecentsView.getViewTreeObserver().removeOnDrawListener(this));
+ }
+ });
+ }
+ notifyGestureStartedAsync();
+ setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
+ mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
+ mGestureStarted = true;
+ SystemUiProxy.INSTANCE.get(mContext).notifySwipeUpGestureStarted();
+ }
+
+ /**
+ * Notifies the launcher that the swipe gesture has started. This can be called multiple times.
+ */
+ @UiThread
+ private void notifyGestureStartedAsync() {
+ final T curActivity = mActivity;
+ if (curActivity != null) {
+ // Once the gesture starts, we can no longer transition home through the button, so
+ // reset the force override of the activity visibility
+ mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+ }
+ }
+
+ /**
+ * Called as a result on ACTION_CANCEL to return the UI to the start state.
+ */
+ @UiThread
+ public void onGestureCancelled() {
+ updateDisplacement(0);
+ mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
+ handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */);
+ }
+
+ /**
+ * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
+ * @param velocity The x and y components of the velocity when the gesture ends.
+ * @param downPos The x and y value of where the gesture started.
+ */
+ @UiThread
+ public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
+ float flingThreshold = mContext.getResources()
+ .getDimension(R.dimen.quickstep_fling_threshold_speed);
+ boolean isFling = mGestureStarted && !mIsMotionPaused
+ && Math.abs(endVelocity) > flingThreshold;
+ mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
+ boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
+ if (isVelocityVertical) {
+ mLogDirectionUpOrLeft = velocity.y < 0;
+ } else {
+ mLogDirectionUpOrLeft = velocity.x < 0;
+ }
+ mDownPos = downPos;
+ handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
+ }
+
+ private void endRunningWindowAnim(boolean cancel) {
+ if (mRunningWindowAnim != null) {
+ if (cancel) {
+ mRunningWindowAnim.cancel();
+ } else {
+ mRunningWindowAnim.end();
+ }
+ }
+ if (mParallelRunningAnim != null) {
+ // Unlike the above animation, the parallel animation won't have anything to take up
+ // the work if it's canceled, so just end it instead.
+ mParallelRunningAnim.end();
+ }
+ }
+
+ private void onSettledOnEndTarget() {
+ // Fast-finish the attaching animation if it's still running.
+ maybeUpdateRecentsAttachedState(false);
+ final GestureEndTarget endTarget = mGestureState.getEndTarget();
+ if (endTarget != NEW_TASK) {
+ InteractionJankMonitorWrapper.cancel(
+ InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+ }
+ if (endTarget != HOME) {
+ InteractionJankMonitorWrapper.cancel(
+ InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
+ }
+ switch (endTarget) {
+ case HOME:
+ mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
+ // Notify swipe-to-home (recents animation) is finished
+ SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished();
+ break;
+ case RECENTS:
+ mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
+ | STATE_SCREENSHOT_VIEW_SHOWN);
+ break;
+ case NEW_TASK:
+ mStateCallback.setState(STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT);
+ break;
+ case LAST_TASK:
+ mStateCallback.setState(STATE_RESUME_LAST_TASK);
+ break;
+ }
+ ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + endTarget);
+ }
+
+ /** @return Whether this was the task we were waiting to appear, and thus handled it. */
+ protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+ if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+ return false;
+ }
+ if (mStateCallback.hasStates(STATE_START_NEW_TASK)
+ && appearedTaskTarget.taskId == mGestureState.getLastStartedTaskId()) {
+ reset();
+ return true;
+ }
+ return false;
+ }
+
+ private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling,
+ boolean isCancel) {
+ if (mGestureState.isHandlingAtomicEvent()) {
+ // Button mode, this is only used to go to recents
+ return RECENTS;
+ }
+ final GestureEndTarget endTarget;
+ final boolean goingToNewTask;
+ if (mRecentsView != null) {
+ if (!hasTargets()) {
+ // If there are no running tasks, then we can assume that this is a continuation of
+ // the last gesture, but after the recents animation has finished
+ goingToNewTask = true;
+ } else {
+ final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
+ final int taskToLaunch = mRecentsView.getNextPage();
+ goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
+ }
+ } else {
+ goingToNewTask = false;
+ }
+ final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
+ if (!isFling) {
+ if (isCancel) {
+ endTarget = LAST_TASK;
+ } else if (mDeviceState.isFullyGesturalNavMode()) {
+ if (mIsMotionPaused) {
+ endTarget = RECENTS;
+ } else if (goingToNewTask) {
+ endTarget = NEW_TASK;
+ } else {
+ endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME;
+ }
+ } else {
+ endTarget = reachedOverviewThreshold && mGestureStarted
+ ? RECENTS
+ : goingToNewTask
+ ? NEW_TASK
+ : LAST_TASK;
+ }
+ } else {
+ // If swiping at a diagonal, base end target on the faster velocity.
+ boolean isSwipeUp = endVelocity < 0;
+ boolean willGoToNewTaskOnSwipeUp =
+ goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity);
+
+ if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
+ endTarget = HOME;
+ } else if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp) {
+ // If swiping at a diagonal, base end target on the faster velocity.
+ endTarget = NEW_TASK;
+ } else if (isSwipeUp) {
+ endTarget = !reachedOverviewThreshold && willGoToNewTaskOnSwipeUp
+ ? NEW_TASK : RECENTS;
+ } else {
+ endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
+ }
+ }
+
+ if (mDeviceState.isOverviewDisabled() && (endTarget == RECENTS || endTarget == LAST_TASK)) {
+ return LAST_TASK;
+ }
+ return endTarget;
+ }
+
+ @UiThread
+ private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity,
+ boolean isCancel) {
+ long duration = MAX_SWIPE_DURATION;
+ float currentShift = mCurrentShift.value;
+ final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
+ isFling, isCancel);
+ // Set the state, but don't notify until the animation completes
+ mGestureState.setEndTarget(endTarget, false /* isAtomic */);
+
+ float endShift = endTarget.isLauncher ? 1 : 0;
+ final float startShift;
+ if (!isFling) {
+ long expectedDuration = Math.abs(Math.round((endShift - currentShift)
+ * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
+ duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
+ startShift = currentShift;
+ } else {
+ startShift = Utilities.boundToRange(currentShift - velocity.y
+ * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
+ if (mTransitionDragLength > 0) {
+ float distanceToTravel = (endShift - currentShift) * mTransitionDragLength;
+
+ // we want the page's snap velocity to approximately match the velocity at
+ // which the user flings, so we scale the duration by a value near to the
+ // derivative of the scroll interpolator at zero, ie. 2.
+ long baseDuration = Math.round(Math.abs(distanceToTravel / velocity.y));
+ duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
+ }
+ }
+ Interpolator interpolator;
+ S state = mActivityInterface.stateFromGestureEndTarget(endTarget);
+ if (state.displayOverviewTasksAsGrid(mDp)) {
+ interpolator = ACCEL_DEACCEL;
+ } else if (endTarget == RECENTS) {
+ interpolator = OVERSHOOT_1_2;
+ } else {
+ interpolator = DEACCEL;
+ }
+
+ if (endTarget.isLauncher) {
+ mInputConsumerProxy.enable();
+ }
+ if (endTarget == HOME) {
+ duration = HOME_DURATION;
+ // Early detach the nav bar once the endTarget is determined as HOME
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.detachNavigationBarFromApp(true);
+ }
+ } else if (endTarget == RECENTS) {
+ if (mRecentsView != null) {
+ int nearestPage = mRecentsView.getDestinationPage();
+ boolean isScrolling = false;
+ // Update page scroll before snapping to page to make sure we snapped to the
+ // position calculated with target gesture in mind.
+ mRecentsView.updateScrollSynchronously();
+ if (mRecentsView.getNextPage() != nearestPage) {
+ // We shouldn't really scroll to the next page when swiping up to recents.
+ // Only allow settling on the next page if it's nearest to the center.
+ mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration));
+ isScrolling = true;
+ }
+ if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) {
+ mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION);
+ isScrolling = true;
+ }
+ if (!mGestureState.isHandlingAtomicEvent() || isScrolling) {
+ duration = Math.max(duration, mRecentsView.getScroller().getDuration());
+ }
+ }
+ }
+
+ // Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
+ // or resumeLastTask().
+ if (mRecentsView != null) {
+ mRecentsView.setOnPageTransitionEndCallback(
+ () -> mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED));
+ } else {
+ mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
+ }
+
+ animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocity);
+ }
+
+ private void doLogGesture(GestureEndTarget endTarget, @Nullable TaskView targetTask) {
+ StatsLogManager.EventEnum event;
+ switch (endTarget) {
+ case HOME:
+ event = LAUNCHER_HOME_GESTURE;
+ break;
+ case RECENTS:
+ event = LAUNCHER_OVERVIEW_GESTURE;
+ break;
+ case LAST_TASK:
+ case NEW_TASK:
+ event = mLogDirectionUpOrLeft ? LAUNCHER_QUICKSWITCH_LEFT
+ : LAUNCHER_QUICKSWITCH_RIGHT;
+ break;
+ default:
+ event = IGNORE;
+ }
+ StatsLogger logger = StatsLogManager.newInstance(mContext).logger()
+ .withSrcState(LAUNCHER_STATE_BACKGROUND)
+ .withDstState(endTarget.containerType);
+ if (targetTask != null) {
+ logger.withItemInfo(targetTask.getItemInfo());
+ }
+
+ DeviceProfile dp = mDp;
+ if (dp == null || mDownPos == null) {
+ // We probably never received an animation controller, skip logging.
+ return;
+ }
+ int pageIndex = endTarget == LAST_TASK
+ ? LOG_NO_OP_PAGE_INDEX
+ : mRecentsView.getNextPage();
+ // TODO: set correct container using the pageIndex
+ logger.log(event);
+ }
+
+ /** Animates to the given progress, where 0 is the current app and 1 is overview. */
+ @UiThread
+ private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
+ GestureEndTarget target, PointF velocityPxPerMs) {
+ runOnRecentsAnimationStart(() -> animateToProgressInternal(start, end, duration,
+ interpolator, target, velocityPxPerMs));
+ }
+
+ protected abstract HomeAnimationFactory createHomeAnimationFactory(
+ ArrayList<IBinder> launchCookies, long duration, boolean isTargetTranslucent,
+ boolean appCanEnterPip, RemoteAnimationTargetCompat runningTaskTarget);
+
+ private final TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() {
+ @Override
+ public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+ boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
+ if (task.taskId == mGestureState.getRunningTaskId()
+ && task.configuration.windowConfiguration.getActivityType()
+ != ACTIVITY_TYPE_HOME) {
+ // Since this is an edge case, just cancel and relaunch with default activity
+ // options (since we don't know if there's an associated app icon to launch from)
+ endRunningWindowAnim(true /* cancel */);
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+ mActivityRestartListener);
+ ActivityManagerWrapper.getInstance().startActivityFromRecents(task.taskId, null);
+ }
+ }
+ };
+
+ @UiThread
+ private void animateToProgressInternal(float start, float end, long duration,
+ Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
+ maybeUpdateRecentsAttachedState();
+
+ // If we are transitioning to launcher, then listen for the activity to be restarted while
+ // the transition is in progress
+ if (mGestureState.getEndTarget().isLauncher) {
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(
+ mActivityRestartListener);
+
+ mParallelRunningAnim = mActivityInterface.getParallelAnimationToLauncher(
+ mGestureState.getEndTarget(), duration);
+ if (mParallelRunningAnim != null) {
+ mParallelRunningAnim.start();
+ }
+ }
+
+ if (mGestureState.getEndTarget() == HOME) {
+ getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
+ final RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
+ ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+ : null;
+ final ArrayList<IBinder> cookies = runningTaskTarget != null
+ ? runningTaskTarget.taskInfo.launchCookies
+ : new ArrayList<>();
+ boolean isTranslucent = runningTaskTarget != null && runningTaskTarget.isTranslucent;
+ boolean appCanEnterPip = !mDeviceState.isPipActive()
+ && runningTaskTarget != null
+ && runningTaskTarget.taskInfo.pictureInPictureParams != null
+ && runningTaskTarget.taskInfo.pictureInPictureParams.isAutoEnterEnabled();
+ HomeAnimationFactory homeAnimFactory =
+ createHomeAnimationFactory(cookies, duration, isTranslucent, appCanEnterPip,
+ runningTaskTarget);
+ mIsSwipingPipToHome = homeAnimFactory.supportSwipePipToHome() && appCanEnterPip;
+ final RectFSpringAnim windowAnim;
+ if (mIsSwipingPipToHome) {
+ mSwipePipToHomeAnimator = createWindowAnimationToPip(
+ homeAnimFactory, runningTaskTarget, start);
+ windowAnim = mSwipePipToHomeAnimator;
+ } else {
+ mSwipePipToHomeAnimator = null;
+ windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
+ windowAnim.addAnimatorListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (mRecentsAnimationController == null) {
+ // If the recents animation is interrupted, we still end the running
+ // animation (not canceled) so this is still called. In that case,
+ // we can skip doing any future work here for the current gesture.
+ return;
+ }
+ // Finalize the state and notify of the change
+ mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+ }
+ });
+ }
+ windowAnim.start(mContext, velocityPxPerMs);
+ mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+ homeAnimFactory.setSwipeVelocity(velocityPxPerMs.y);
+ homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
+ mLauncherTransitionController = null;
+
+ if (mRecentsView != null) {
+ mRecentsView.onPrepareGestureEndAnimation(null, mGestureState.getEndTarget());
+ }
+ } else {
+ AnimatorSet animatorSet = new AnimatorSet();
+ ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
+ windowAnim.addUpdateListener(valueAnimator -> {
+ computeRecentsScrollIfInvisible();
+ });
+ windowAnim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (mRecentsAnimationController == null) {
+ // If the recents animation is interrupted, we still end the running
+ // animation (not canceled) so this is still called. In that case, we can
+ // skip doing any future work here for the current gesture.
+ return;
+ }
+ if (mRecentsView != null) {
+ int taskToLaunch = mRecentsView.getNextPage();
+ int runningTask = getLastAppearedTaskIndex();
+ boolean hasStartedNewTask = hasStartedNewTask();
+ if (target == NEW_TASK && taskToLaunch == runningTask
+ && !hasStartedNewTask) {
+ // We are about to launch the current running task, so use LAST_TASK
+ // state instead of NEW_TASK. This could happen, for example, if our
+ // scroll is aborted after we determined the target to be NEW_TASK.
+ mGestureState.setEndTarget(LAST_TASK);
+ } else if (target == LAST_TASK && hasStartedNewTask) {
+ // We are about to re-launch the previously running task, but we can't
+ // just finish the controller like we normally would because that would
+ // instead resume the last task that appeared, and not ensure that this
+ // task is restored to the top. To address this, re-launch the task as
+ // if it were a new task.
+ mGestureState.setEndTarget(NEW_TASK);
+ }
+ }
+ mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+ }
+ });
+ animatorSet.play(windowAnim);
+ if (mRecentsView != null) {
+ mRecentsView.onPrepareGestureEndAnimation(
+ animatorSet, mGestureState.getEndTarget());
+ }
+ animatorSet.setDuration(duration).setInterpolator(interpolator);
+ animatorSet.start();
+ mRunningWindowAnim = RunningWindowAnim.wrap(animatorSet);
+ }
+ }
+
+ private SwipePipToHomeAnimator createWindowAnimationToPip(HomeAnimationFactory homeAnimFactory,
+ RemoteAnimationTargetCompat runningTaskTarget, float startProgress) {
+ // Directly animate the app to PiP (picture-in-picture) mode
+ final ActivityManager.RunningTaskInfo taskInfo = mGestureState.getRunningTask();
+ final RecentsOrientedState orientationState = mTaskViewSimulator.getOrientationState();
+ final int windowRotation = orientationState.getDisplayRotation();
+ final int homeRotation = orientationState.getRecentsActivityRotation();
+
+ final Matrix homeToWindowPositionMap = new Matrix();
+ final RectF startRect = updateProgressForStartRect(homeToWindowPositionMap, startProgress);
+ // Move the startRect to Launcher space as floatingIconView runs in Launcher
+ final Matrix windowToHomePositionMap = new Matrix();
+ homeToWindowPositionMap.invert(windowToHomePositionMap);
+ windowToHomePositionMap.mapRect(startRect);
+
+ final Rect destinationBounds = SystemUiProxy.INSTANCE.get(mContext)
+ .startSwipePipToHome(taskInfo.topActivity,
+ taskInfo.topActivityInfo,
+ runningTaskTarget.taskInfo.pictureInPictureParams,
+ homeRotation,
+ mDp.hotseatBarSizePx);
+ final SwipePipToHomeAnimator.Builder builder = new SwipePipToHomeAnimator.Builder()
+ .setContext(mContext)
+ .setTaskId(runningTaskTarget.taskId)
+ .setComponentName(taskInfo.topActivity)
+ .setLeash(runningTaskTarget.leash.getSurfaceControl())
+ .setSourceRectHint(
+ runningTaskTarget.taskInfo.pictureInPictureParams.getSourceRectHint())
+ .setAppBounds(taskInfo.configuration.windowConfiguration.getBounds())
+ .setHomeToWindowPositionMap(homeToWindowPositionMap)
+ .setStartBounds(startRect)
+ .setDestinationBounds(destinationBounds)
+ .setCornerRadius(mRecentsView.getPipCornerRadius())
+ .setAttachedView(mRecentsView);
+ // We would assume home and app window always in the same rotation While homeRotation
+ // is not ROTATION_0 (which implies the rotation is turned on in launcher settings).
+ if (homeRotation == ROTATION_0
+ && (windowRotation == ROTATION_90 || windowRotation == ROTATION_270)) {
+ builder.setFromRotation(mTaskViewSimulator, windowRotation,
+ taskInfo.displayCutoutInsets);
+ }
+ final SwipePipToHomeAnimator swipePipToHomeAnimator = builder.build();
+ AnimatorPlaybackController activityAnimationToHome =
+ homeAnimFactory.createActivityAnimationToHome();
+ swipePipToHomeAnimator.addAnimatorListener(new AnimatorListenerAdapter() {
+ private boolean mHasAnimationEnded;
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mHasAnimationEnded) return;
+ // Ensure Launcher ends in NORMAL state
+ activityAnimationToHome.dispatchOnStart();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mHasAnimationEnded) return;
+ mHasAnimationEnded = true;
+ activityAnimationToHome.getAnimationPlayer().end();
+ if (mRecentsAnimationController == null) {
+ // If the recents animation is interrupted, we still end the running
+ // animation (not canceled) so this is still called. In that case, we can
+ // skip doing any future work here for the current gesture.
+ return;
+ }
+ // Finalize the state and notify of the change
+ mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+ }
+ });
+ setupWindowAnimation(swipePipToHomeAnimator);
+ return swipePipToHomeAnimator;
+ }
+
+ private void computeRecentsScrollIfInvisible() {
+ if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
+ // Views typically don't compute scroll when invisible as an optimization,
+ // but in our case we need to since the window offset depends on the scroll.
+ mRecentsView.computeScroll();
+ }
+ }
+
+ private void continueComputingRecentsScrollIfNecessary() {
+ if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)
+ && !mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)
+ && !mCanceled) {
+ computeRecentsScrollIfInvisible();
+ mRecentsView.postOnAnimation(this::continueComputingRecentsScrollIfNecessary);
+ }
+ }
+
+ /**
+ * Creates an animation that transforms the current app window into the home app.
+ * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+ * @param homeAnimationFactory The home animation factory.
+ */
+ @Override
+ protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
+ HomeAnimationFactory homeAnimationFactory) {
+ RectFSpringAnim anim =
+ super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
+ setupWindowAnimation(anim);
+ return anim;
+ }
+
+ private void setupWindowAnimation(RectFSpringAnim anim) {
+ anim.addOnUpdateListener((v, r, p) -> {
+ updateSysUiFlags(Math.max(p, mCurrentShift.value));
+ });
+ anim.addAnimatorListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (mRecentsView != null) {
+ mRecentsView.post(mRecentsView::resetTaskVisuals);
+ }
+ // Make sure recents is in its final state
+ maybeUpdateRecentsAttachedState(false);
+ mActivityInterface.onSwipeUpToHomeComplete(mDeviceState);
+ }
+ });
+ if (mRecentsAnimationTargets != null) {
+ mRecentsAnimationTargets.addReleaseCheck(anim);
+ }
+ }
+
+ public void onConsumerAboutToBeSwitched() {
+ if (mActivity != null) {
+ // In the off chance that the gesture ends before Launcher is started, we should clear
+ // the callback here so that it doesn't update with the wrong state
+ mActivity.clearRunOnceOnStartCallback();
+ resetLauncherListeners();
+ }
+ if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
+ cancelCurrentAnimation();
+ } else {
+ mStateCallback.setStateOnUiThread(STATE_FINISH_WITH_NO_END);
+ reset();
+ }
+ }
+
+ public boolean isCanceled() {
+ return mCanceled;
+ }
+
+ @UiThread
+ private void resumeLastTask() {
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.finish(false /* toRecents */, null);
+ ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
+ }
+ doLogGesture(LAST_TASK, null);
+ reset();
+ }
+
+ @UiThread
+ private void startNewTask() {
+ TaskView taskToLaunch = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
+ startNewTask(success -> {
+ if (!success) {
+ reset();
+ // We couldn't launch the task, so take user to overview so they can
+ // decide what to do instead of staying in this broken state.
+ endLauncherTransitionController();
+ updateSysUiFlags(1 /* windowProgress == overview */);
+ }
+ doLogGesture(NEW_TASK, taskToLaunch);
+ });
+ }
+
+ /**
+ * Called when we successfully startNewTask() on the task that was previously running. Normally
+ * we call resumeLastTask() when returning to the previously running task, but this handles a
+ * specific edge case: if we switch from A to B, and back to A before B appears, we need to
+ * start A again to ensure it stays on top.
+ */
+ @androidx.annotation.CallSuper
+ protected void onRestartPreviouslyAppearedTask() {
+ // Finish the controller here, since we won't get onTaskAppeared() for a task that already
+ // appeared.
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.finish(false, null);
+ }
+ reset();
+ }
+
+ private void reset() {
+ mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+ }
+
+ /**
+ * Cancels any running animation so that the active target can be overriden by a new swipe
+ * handler (in case of quick switch).
+ */
+ private void cancelCurrentAnimation() {
+ mCanceled = true;
+ mCurrentShift.cancelAnimation();
+
+ // Cleanup when switching handlers
+ mInputConsumerProxy.unregisterCallback();
+ mActivityInitListener.unregister();
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mActivityRestartListener);
+ mTaskSnapshot = null;
+ }
+
+ private void invalidateHandler() {
+ if (!ENABLE_QUICKSTEP_LIVE_TILE.get() || !mActivityInterface.isInLiveTileMode()
+ || mGestureState.getEndTarget() != RECENTS) {
+ mInputConsumerProxy.destroy();
+ mTaskAnimationManager.setLiveTileCleanUpHandler(null);
+ }
+ mInputConsumerProxy.unregisterCallback();
+ endRunningWindowAnim(false /* cancel */);
+
+ if (mGestureEndCallback != null) {
+ mGestureEndCallback.run();
+ }
+
+ mActivityInitListener.unregister();
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+ mActivityRestartListener);
+ mTaskSnapshot = null;
+ }
+
+ private void invalidateHandlerWithLauncher() {
+ endLauncherTransitionController();
+
+ mRecentsView.onGestureAnimationEnd();
+ resetLauncherListeners();
+ }
+
+ private void endLauncherTransitionController() {
+ mHasEndedLauncherTransition = true;
+
+ if (mLauncherTransitionController != null) {
+ // End the animation, but stay at the same visual progress.
+ mLauncherTransitionController.getNormalController().dispatchSetInterpolator(
+ t -> Utilities.boundToRange(mCurrentShift.value, 0, 1));
+ mLauncherTransitionController.getNormalController().getAnimationPlayer().end();
+ mLauncherTransitionController = null;
+ }
+
+ if (mRecentsView != null) {
+ mRecentsView.abortScrollerAnimation();
+ }
+ }
+
+ /**
+ * Unlike invalidateHandlerWithLauncher, this is called even when switching consumers, e.g. on
+ * continued quick switch gesture, which cancels the previous handler but doesn't invalidate it.
+ */
+ private void resetLauncherListeners() {
+ // Reset the callback for deferred activity launches
+ if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mActivityInterface.setOnDeferredActivityLaunchCallback(null);
+ }
+ mActivity.getRootView().setOnApplyWindowInsetsListener(null);
+
+ mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
+ }
+
+ private void resetStateForAnimationCancel() {
+ boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
+ mActivityInterface.onTransitionCancelled(wasVisible, mGestureState.getEndTarget());
+
+ // Leave the pending invisible flag, as it may be used by wallpaper open animation.
+ if (mActivity != null) {
+ mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
+ }
+ }
+
+ protected void switchToScreenshot() {
+ if (!hasTargets()) {
+ // If there are no targets, then we don't need to capture anything
+ mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+ } else {
+ final int runningTaskId = mGestureState.getRunningTaskId();
+ final boolean refreshView = !ENABLE_QUICKSTEP_LIVE_TILE.get() /* refreshView */;
+ boolean finishTransitionPosted = false;
+ if (mRecentsAnimationController != null) {
+ // Update the screenshot of the task
+ if (mTaskSnapshot == null) {
+ UI_HELPER_EXECUTOR.execute(() -> {
+ if (mRecentsAnimationController == null) return;
+ final ThumbnailData taskSnapshot =
+ mRecentsAnimationController.screenshotTask(runningTaskId);
+ MAIN_EXECUTOR.execute(() -> {
+ mTaskSnapshot = taskSnapshot;
+ if (!updateThumbnail(runningTaskId, refreshView)) {
+ setScreenshotCapturedState();
+ }
+ });
+ });
+ return;
+ }
+ finishTransitionPosted = updateThumbnail(runningTaskId, refreshView);
+ }
+ if (!finishTransitionPosted) {
+ setScreenshotCapturedState();
+ }
+ }
+ }
+
+ // Returns whether finish transition was posted.
+ private boolean updateThumbnail(int runningTaskId, boolean refreshView) {
+ boolean finishTransitionPosted = false;
+ final TaskView taskView;
+ if (mGestureState.getEndTarget() == HOME || mGestureState.getEndTarget() == NEW_TASK) {
+ // Capture the screenshot before finishing the transition to home or quickswitching to
+ // ensure it's taken in the correct orientation, but no need to update the thumbnail.
+ taskView = null;
+ } else {
+ taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, refreshView);
+ }
+ if (taskView != null && refreshView && !mCanceled) {
+ // Defer finishing the animation until the next launcher frame with the
+ // new thumbnail
+ finishTransitionPosted = ViewUtils.postFrameDrawn(taskView,
+ () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
+ this::isCanceled);
+ }
+ return finishTransitionPosted;
+ }
+
+ private void setScreenshotCapturedState() {
+ // If we haven't posted a draw callback, set the state immediately.
+ Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
+ TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
+ mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+ TraceHelper.INSTANCE.endSection(traceToken);
+ }
+
+ private void finishCurrentTransitionToRecents() {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.detachNavigationBarFromApp(true);
+ }
+ } else if (!hasTargets() || mRecentsAnimationController == null) {
+ // If there are no targets or the animation not started, then there is nothing to finish
+ mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+ } else {
+ mRecentsAnimationController.finish(true /* toRecents */,
+ () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+ }
+ ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
+ }
+
+ private void finishCurrentTransitionToHome() {
+ if (!hasTargets() || mRecentsAnimationController == null) {
+ // If there are no targets or the animation not started, then there is nothing to finish
+ mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+ } else {
+ maybeFinishSwipePipToHome();
+ finishRecentsControllerToHome(
+ () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+ }
+ ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
+ doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView());
+ }
+
+ /**
+ * Resets the {@link #mIsSwipingPipToHome} and notifies SysUI that transition is finished
+ * if applicable. This should happen before {@link #finishRecentsControllerToHome(Runnable)}.
+ */
+ private void maybeFinishSwipePipToHome() {
+ if (mIsSwipingPipToHome && mSwipePipToHomeAnimator != null) {
+ SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
+ mSwipePipToHomeAnimator.getComponentName(),
+ mSwipePipToHomeAnimator.getDestinationBounds(),
+ mSwipePipToHomeAnimator.getContentOverlay());
+ mRecentsAnimationController.setFinishTaskTransaction(
+ mSwipePipToHomeAnimator.getTaskId(),
+ mSwipePipToHomeAnimator.getFinishTransaction(),
+ mSwipePipToHomeAnimator.getContentOverlay());
+ mIsSwipingPipToHome = false;
+ }
+ }
+
+ protected abstract void finishRecentsControllerToHome(Runnable callback);
+
+ private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
+ if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+ return;
+ }
+ endLauncherTransitionController();
+ mRecentsView.onSwipeUpAnimationSuccess();
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mTaskAnimationManager.setLiveTileCleanUpHandler(mInputConsumerProxy::destroy);
+ mTaskAnimationManager.enableLiveTileRestartListener();
+ }
+
+ SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG);
+ doLogGesture(RECENTS, mRecentsView.getCurrentPageTaskView());
+ reset();
+ }
+
+ private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
+ return app.isNotInRecents
+ || app.activityType == ACTIVITY_TYPE_HOME;
+ }
+
+ /**
+ * To be called at the end of constructor of subclasses. This calls various methods which can
+ * depend on proper class initialization.
+ */
+ protected void initAfterSubclassConstructor() {
+ initTransitionEndpoints(
+ mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
+ }
+
+ protected void performHapticFeedback() {
+ VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+ }
+
+ public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
+ return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
+ }
+
+ public void setGestureEndCallback(Runnable gestureEndCallback) {
+ mGestureEndCallback = gestureEndCallback;
+ }
+
+ protected void linkRecentsViewScroll() {
+ SurfaceTransactionApplier.create(mRecentsView, applier -> {
+ mTransformParams.setSyncTransactionApplier(applier);
+ runOnRecentsAnimationStart(() ->
+ mRecentsAnimationTargets.addReleaseCheck(applier));
+ });
+
+ mRecentsView.addOnScrollChangedListener(mOnRecentsScrollListener);
+ runOnRecentsAnimationStart(() ->
+ mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
+ mRecentsAnimationTargets));
+ mRecentsViewScrollLinked = true;
+ }
+
+ private void onRecentsViewScroll() {
+ if (moveWindowWithRecentsScroll()) {
+ updateFinalShift();
+ }
+ }
+
+ protected void startNewTask(Consumer<Boolean> resultCallback) {
+ // Launch the task user scrolled to (mRecentsView.getNextPage()).
+ if (!mCanceled) {
+ TaskView nextTask = mRecentsView.getNextPageTaskView();
+ if (nextTask != null) {
+ int taskId = nextTask.getTask().key.id;
+ mGestureState.updateLastStartedTaskId(taskId);
+ boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
+ .contains(taskId);
+ nextTask.launchTask(success -> {
+ resultCallback.accept(success);
+ if (success) {
+ if (hasTaskPreviouslyAppeared) {
+ onRestartPreviouslyAppearedTask();
+ }
+ } else {
+ mActivityInterface.onLaunchTaskFailed();
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.finish(true /* toRecents */, null);
+ }
+ }
+ }, true /* freezeTaskList */);
+ } else {
+ mActivityInterface.onLaunchTaskFailed();
+ Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.finish(true /* toRecents */, null);
+ }
+ }
+ }
+ mCanceled = false;
+ }
+
+ /**
+ * Runs the given {@param action} if the recents animation has already started, or queues it to
+ * be run when it is next started.
+ */
+ protected void runOnRecentsAnimationStart(Runnable action) {
+ if (mRecentsAnimationTargets == null) {
+ mRecentsAnimationStartCallbacks.add(action);
+ } else {
+ action.run();
+ }
+ }
+
+ /**
+ * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
+ * @return whether the recents animation has started and there are valid app targets.
+ */
+ protected boolean hasTargets() {
+ return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
+ }
+
+ @Override
+ public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+ mRecentsAnimationController = null;
+ mRecentsAnimationTargets = null;
+ if (mRecentsView != null) {
+ mRecentsView.setRecentsAnimationTargets(null, null);
+ }
+ }
+
+ @Override
+ public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+ if (mRecentsAnimationController != null) {
+ if (handleTaskAppeared(appearedTaskTarget)) {
+ mRecentsAnimationController.finish(false /* toRecents */,
+ null /* onFinishComplete */);
+ mActivityInterface.onLaunchTaskSuccess();
+ ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
+ }
+ }
+ }
+
+ /**
+ * @return The index of the TaskView in RecentsView whose taskId matches the task that will
+ * resume if we finish the controller.
+ */
+ protected int getLastAppearedTaskIndex() {
+ return mGestureState.getLastAppearedTaskId() != -1
+ ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
+ : mRecentsView.getRunningTaskIndex();
+ }
+
+ /**
+ * @return Whether we are continuing a gesture that already landed on a new task,
+ * but before that task appeared.
+ */
+ protected boolean hasStartedNewTask() {
+ return mGestureState.getLastStartedTaskId() != -1;
+ }
+
+ /**
+ * Registers a callback to run when the activity is ready.
+ */
+ public void initWhenReady() {
+ // Preload the plan
+ RecentsModel.INSTANCE.get(mContext).getTasks(null);
+
+ mActivityInitListener.register();
+ }
+
+ /**
+ * Applies the transform on the recents animation
+ */
+ protected void applyWindowTransform() {
+ if (mWindowTransitionController != null) {
+ mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
+ }
+ // No need to apply any transform if there is ongoing swipe-pip-to-home animator since
+ // that animator handles the leash solely.
+ if (mRecentsAnimationTargets != null && !mIsSwipingPipToHome) {
+ if (mRecentsViewScrollLinked && mRecentsView != null) {
+ mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
+ }
+ mTaskViewSimulator.apply(mTransformParams);
+ }
+ ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
+ }
+
+ /**
+ * Used for winscope tracing, see launcher_trace.proto
+ * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
+ * @param inputConsumerProto The parent of this proto message.
+ */
+ public void writeToProto(InputConsumerProto.Builder inputConsumerProto) {
+ SwipeHandlerProto.Builder swipeHandlerProto = SwipeHandlerProto.newBuilder();
+
+ mGestureState.writeToProto(swipeHandlerProto);
+
+ swipeHandlerProto.setIsRecentsAttachedToAppWindow(
+ mAnimationFactory.isRecentsAttachedToAppWindow());
+ swipeHandlerProto.setScrollOffset(mRecentsView == null
+ ? 0
+ : mRecentsView.getScrollOffset());
+ swipeHandlerProto.setAppToOverviewProgress(mCurrentShift.value);
+
+ inputConsumerProto.setSwipeHandler(swipeHandlerProto);
+ }
+
+ public interface Factory {
+
+ AbsSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 2b698bd..fac4d52 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,28 +15,31 @@
*/
package com.android.quickstep;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
import static com.android.launcher3.anim.Interpolators.ACCEL_2;
import static com.android.launcher3.anim.Interpolators.INSTANT;
import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
import static com.android.quickstep.SysUINavigationMode.getMode;
-import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import android.animation.Animator;
+import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
+import android.view.Gravity;
import android.view.MotionEvent;
-import android.view.animation.Interpolator;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -45,16 +48,20 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.WindowBounds;
+import com.android.launcher3.views.ScrimView;
import com.android.quickstep.SysUINavigationMode.Mode;
import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
import com.android.quickstep.util.SplitScreenBounds;
+import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -79,12 +86,22 @@
mBackgroundState = backgroundState;
}
- public void onTransitionCancelled(boolean activityVisible) {
+ /**
+ * Called when the current gesture transition is cancelled.
+ * @param activityVisible Whether the user can see the changes we make here, so try to animate.
+ * @param endTarget If the gesture ended before we got cancelled, where we were headed.
+ */
+ public void onTransitionCancelled(boolean activityVisible,
+ @Nullable GestureState.GestureEndTarget endTarget) {
ACTIVITY_TYPE activity = getCreatedActivity();
if (activity == null) {
return;
}
STATE_TYPE startState = activity.getStateManager().getRestState();
+ if (endTarget != null) {
+ // We were on our way to this state when we got canceled, end there instead.
+ startState = stateFromGestureEndTarget(endTarget);
+ }
activity.getStateManager().goToState(startState, activityVisible);
}
@@ -92,21 +109,16 @@
DeviceProfile dp, Context context, Rect outRect,
PagedOrientationHandler orientationHandler);
- public void onSwipeUpToRecentsComplete() {
- // Re apply state in case we did something funky during the transition.
- ACTIVITY_TYPE activity = getCreatedActivity();
- if (activity == null) {
- return;
- }
- activity.getStateManager().reapplyState();
- }
-
- public abstract void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState);
+ /** Called when the animation to home has fully settled. */
+ public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {}
public abstract void onAssistantVisibilityChanged(float visibility);
+ /** Called when one handed mode activated or deactivated. */
+ public abstract void onOneHandedModeStateChanged(boolean activated);
+
public abstract AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
- boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
+ boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback);
public abstract ActivityInitListener createActivityInitListener(
Predicate<Boolean> onInitListener);
@@ -150,20 +162,15 @@
return deviceState.isInDeferredGestureRegion(ev);
}
- public abstract void onExitOverview(RecentsAnimationDeviceState deviceState,
- Runnable exitRunnable);
-
/**
- * Updates the prediction state to the overview state.
+ * @return Whether the gesture in progress should be cancelled.
*/
- public void updateOverviewPredictionState() {
- // By public overview predictions are not supported
+ public boolean shouldCancelCurrentGesture() {
+ return false;
}
- /**
- * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
- */
- public abstract int getContainerType();
+ public abstract void onExitOverview(RotationTouchHelper deviceState,
+ Runnable exitRunnable);
public abstract boolean isInLiveTileMode();
@@ -195,135 +202,214 @@
}
/**
- * Calculates the taskView size for the provided device configuration
+ * Calculates the taskView size for the provided device configuration.
*/
public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
PagedOrientationHandler orientedState) {
- calculateTaskSize(context, dp, getExtraSpace(context, dp, orientedState),
- outRect, orientedState);
- }
-
- protected abstract float getExtraSpace(Context context, DeviceProfile dp,
- PagedOrientationHandler orientedState);
-
- private void calculateTaskSize(
- Context context, DeviceProfile dp, float extraVerticalSpace, Rect outRect,
- PagedOrientationHandler orientationHandler) {
Resources res = context.getResources();
- final boolean showLargeTaskSize = showOverviewActions(context) ||
- hideShelfInTwoButtonLandscape(context, orientationHandler);
+ if (dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+ Rect gridRect = new Rect();
+ calculateGridSize(context, dp, gridRect);
- final int paddingResId;
- if (dp.isMultiWindowMode) {
- paddingResId = R.dimen.multi_window_task_card_horz_space;
- } else if (dp.isVerticalBarLayout()) {
- paddingResId = R.dimen.landscape_task_card_horz_space;
- } else if (showLargeTaskSize) {
- paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
+ int verticalMargin = res.getDimensionPixelSize(
+ R.dimen.overview_grid_focus_vertical_margin);
+ float taskHeight = gridRect.height() - verticalMargin * 2;
+
+ PointF taskDimension = getTaskDimension(context, dp);
+ float scale = taskHeight / Math.max(taskDimension.x, taskDimension.y);
+ int outWidth = Math.round(scale * taskDimension.x);
+ int outHeight = Math.round(scale * taskDimension.y);
+
+ int gravity = Gravity.CENTER_VERTICAL;
+ gravity |= orientedState.getRecentsRtlSetting(res) ? Gravity.RIGHT : Gravity.LEFT;
+ Gravity.apply(gravity, outWidth, outHeight, gridRect, outRect);
} else {
- paddingResId = R.dimen.portrait_task_card_horz_space;
+ int taskMargin = dp.overviewTaskMarginPx;
+ int proactiveRowAndMargin;
+ if (!TaskView.SHOW_PROACTIVE_ACTIONS || dp.isVerticalBarLayout()) {
+ // In Vertical Bar Layout the proactive row doesn't have its own space, it's inside
+ // the actions row.
+ proactiveRowAndMargin = 0;
+ } else {
+ proactiveRowAndMargin = res.getDimensionPixelSize(
+ R.dimen.overview_proactive_row_height)
+ + res.getDimensionPixelSize(R.dimen.overview_proactive_row_bottom_margin);
+ }
+ calculateTaskSizeInternal(context, dp,
+ dp.overviewTaskThumbnailTopMarginPx,
+ proactiveRowAndMargin + getOverviewActionsHeight(context, dp),
+ res.getDimensionPixelSize(R.dimen.overview_minimum_next_prev_size) + taskMargin,
+ outRect);
}
- float paddingHorz = res.getDimension(paddingResId);
- float paddingVert = showLargeTaskSize
- ? 0 : res.getDimension(R.dimen.task_card_vert_space);
-
- calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
- res.getDimension(R.dimen.task_thumbnail_top_margin), outRect);
}
private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
- float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin,
+ int claimedSpaceAbove, int claimedSpaceBelow, int minimumHorizontalPadding,
Rect outRect) {
- float taskWidth, taskHeight;
+ PointF taskDimension = getTaskDimension(context, dp);
Rect insets = dp.getInsets();
+
+ Rect potentialTaskRect = new Rect(0, 0, dp.widthPx, dp.heightPx);
+ potentialTaskRect.inset(insets.left, insets.top, insets.right, insets.bottom);
+ potentialTaskRect.inset(
+ minimumHorizontalPadding,
+ claimedSpaceAbove,
+ minimumHorizontalPadding,
+ claimedSpaceBelow);
+
+ float scale = Math.min(
+ potentialTaskRect.width() / taskDimension.x,
+ potentialTaskRect.height() / taskDimension.y);
+ int outWidth = Math.round(scale * taskDimension.x);
+ int outHeight = Math.round(scale * taskDimension.y);
+
+ Gravity.apply(Gravity.CENTER, outWidth, outHeight, potentialTaskRect, outRect);
+ }
+
+ private static PointF getTaskDimension(Context context, DeviceProfile dp) {
+ PointF dimension = new PointF();
+ getTaskDimension(context, dp, dimension);
+ return dimension;
+ }
+
+ /**
+ * Gets the dimension of the task in the current system state.
+ */
+ public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) {
if (dp.isMultiWindowMode) {
WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
- taskWidth = bounds.availableSize.x;
- taskHeight = bounds.availableSize.y;
+ if (TaskView.CLIP_STATUS_AND_NAV_BARS) {
+ out.x = bounds.availableSize.x;
+ out.y = bounds.availableSize.y;
+ } else {
+ out.x = bounds.availableSize.x + bounds.insets.left + bounds.insets.right;
+ out.y = bounds.availableSize.y + bounds.insets.top + bounds.insets.bottom;
+ }
+ } else if (TaskView.CLIP_STATUS_AND_NAV_BARS) {
+ out.x = dp.availableWidthPx;
+ out.y = dp.availableHeightPx;
} else {
- taskWidth = dp.availableWidthPx;
- taskHeight = dp.availableHeightPx;
+ out.x = dp.widthPx;
+ out.y = dp.heightPx;
}
+ }
- // 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;
+ /**
+ * Calculates the overview grid size for the provided device configuration.
+ */
+ public final void calculateGridSize(Context context, DeviceProfile dp, Rect outRect) {
+ Resources res = context.getResources();
+ int topMargin = res.getDimensionPixelSize(R.dimen.overview_grid_top_margin);
+ int bottomMargin = res.getDimensionPixelSize(R.dimen.overview_grid_bottom_margin);
+ int sideMargin = res.getDimensionPixelSize(R.dimen.overview_grid_side_margin);
- float availableHeight = launcherVisibleHeight
- - topIconMargin - extraVerticalSpace - paddingVert;
- float availableWidth = launcherVisibleWidth - paddingHorz;
+ Rect insets = dp.getInsets();
+ outRect.set(0, 0, dp.widthPx, dp.heightPx);
+ outRect.inset(Math.max(insets.left, sideMargin), Math.max(insets.top, topMargin),
+ Math.max(insets.right, sideMargin), Math.max(insets.bottom, bottomMargin));
+ }
- float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
- float outWidth = scale * taskWidth;
- float outHeight = scale * taskHeight;
+ /**
+ * Calculates the overview grid non-focused task size for the provided device configuration.
+ */
+ public final void calculateGridTaskSize(Context context, DeviceProfile dp, Rect outRect,
+ PagedOrientationHandler orientedState) {
+ Resources res = context.getResources();
+ Rect gridRect = new Rect();
+ calculateGridSize(context, dp, gridRect);
- // 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) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
+ int rowSpacing = res.getDimensionPixelSize(R.dimen.overview_grid_row_spacing);
+ float rowHeight = (gridRect.height() - rowSpacing) / 2f;
+
+ PointF taskDimension = getTaskDimension(context, dp);
+ float scale = (rowHeight - dp.overviewTaskThumbnailTopMarginPx) / Math.max(
+ taskDimension.x, taskDimension.y);
+ int outWidth = Math.round(scale * taskDimension.x);
+ int outHeight = Math.round(scale * taskDimension.y);
+
+ int gravity = Gravity.TOP;
+ gravity |= orientedState.getRecentsRtlSetting(res) ? Gravity.RIGHT : Gravity.LEFT;
+ gridRect.inset(0, dp.overviewTaskThumbnailTopMarginPx, 0, 0);
+ Gravity.apply(gravity, outWidth, outHeight, gridRect, outRect);
}
/**
* Calculates the modal taskView size for the provided device configuration
*/
public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
- float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
- ? R.dimen.multi_window_task_card_horz_space
- : dp.isVerticalBarLayout()
- ? R.dimen.landscape_task_card_horz_space
- : R.dimen.portrait_modal_task_card_horz_space);
- float extraVerticalSpace = getOverviewActionsHeight(context);
- float paddingVert = 0;
- float topIconMargin = 0;
- calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
- topIconMargin, outRect);
+ calculateTaskSizeInternal(
+ context, dp,
+ dp.overviewTaskMarginPx,
+ getOverviewActionsHeight(context, dp),
+ dp.overviewTaskMarginPx,
+ outRect);
}
- /** Gets the space that the overview actions will take, including margins. */
- public final float getOverviewActionsHeight(Context context) {
+ /** Gets the space that the overview actions will take, including bottom margin. */
+ private int getOverviewActionsHeight(Context context, DeviceProfile dp) {
Resources res = context.getResources();
- float actionsBottomMargin = 0;
- if (getMode(context) == Mode.THREE_BUTTONS) {
- actionsBottomMargin = res.getDimensionPixelSize(
- R.dimen.overview_actions_bottom_margin_three_button);
- } else {
- actionsBottomMargin = res.getDimensionPixelSize(
- R.dimen.overview_actions_bottom_margin_gesture);
- }
- float overviewActionsHeight = actionsBottomMargin
+ return OverviewActionsView.getOverviewActionsBottomMarginPx(getMode(context), dp)
+ + OverviewActionsView.getOverviewActionsTopMarginPx(getMode(context), dp)
+ res.getDimensionPixelSize(R.dimen.overview_actions_height);
- return overviewActionsHeight;
}
+ /**
+ * Called when the gesture ends and the animation starts towards the given target. Used to add
+ * an optional additional animation with the same duration.
+ */
+ public @Nullable Animator getParallelAnimationToLauncher(
+ GestureState.GestureEndTarget endTarget, long duration) {
+ if (endTarget == RECENTS) {
+ ACTIVITY_TYPE activity = getCreatedActivity();
+ if (activity == null) {
+ return null;
+ }
+ STATE_TYPE state = stateFromGestureEndTarget(endTarget);
+ ScrimView scrimView = activity.getScrimView();
+ ObjectAnimator anim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
+ getOverviewScrimColorForState(activity, state));
+ anim.setDuration(duration);
+ return anim;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the color of the scrim behind overview when at rest in this state.
+ * Return {@link Color#TRANSPARENT} for no scrim.
+ */
+ protected abstract int getOverviewScrimColorForState(ACTIVITY_TYPE activity, STATE_TYPE state);
+
+ /**
+ * Returns the expected STATE_TYPE from the provided GestureEndTarget.
+ */
+ public abstract STATE_TYPE stateFromGestureEndTarget(GestureState.GestureEndTarget endTarget);
+
public interface AnimationFactory {
void createActivityInterface(long transitionLength);
- default void onTransitionCancelled() { }
-
- default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
- Interpolator interpolator, long duration) { }
-
/**
* @param attached Whether to show RecentsView alongside the app window. If false, recents
* will be hidden by some property we can animate, e.g. alpha.
* @param animate Whether to animate recents to/from its new attached state.
*/
default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
+
+ default boolean isRecentsAttachedToAppWindow() {
+ return false;
+ }
}
class DefaultAnimationFactory implements AnimationFactory {
protected final ACTIVITY_TYPE mActivity;
private final STATE_TYPE mStartState;
- private final Consumer<AnimatorPlaybackController> mCallback;
+ private final Consumer<AnimatorControllerWithResistance> mCallback;
private boolean mIsAttachedToWindow;
- DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback) {
+ DefaultAnimationFactory(Consumer<AnimatorControllerWithResistance> callback) {
mCallback = callback;
mActivity = getCreatedActivity();
@@ -351,7 +437,14 @@
controller.setEndAction(() -> mActivity.getStateManager().goToState(
controller.getInterpolatedProgress() > 0.5 ? mOverviewState : mBackgroundState,
false));
- mCallback.accept(controller);
+
+ RecentsView recentsView = mActivity.getOverviewPanel();
+ AnimatorControllerWithResistance controllerWithResistance =
+ AnimatorControllerWithResistance.createForRecents(controller, mActivity,
+ recentsView.getPagedViewOrientedState(), mActivity.getDeviceProfile(),
+ recentsView, RECENTS_SCALE_PROPERTY, recentsView,
+ TASK_SECONDARY_TRANSLATION);
+ mCallback.accept(controllerWithResistance);
// Creating the activity controller animation sometimes reapplies the launcher state
// (because we set the animation as the current state animation), so we reapply the
@@ -362,11 +455,6 @@
}
@Override
- public void onTransitionCancelled() {
- mActivity.getStateManager().goToState(mStartState, false /* animate */);
- }
-
- @Override
public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
if (mIsAttachedToWindow == attached && animate) {
return;
@@ -381,12 +469,12 @@
mActivity.getStateManager()
.cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
if (!recentsView.isShown() && animate) {
- ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
+ ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, fromTranslation);
} else {
- fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
+ fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(recentsView);
}
if (!animate) {
- ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
+ ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, toTranslation);
} else {
mActivity.getStateManager().createStateElementAnimation(
INDEX_RECENTS_TRANSLATE_X_ANIM,
@@ -397,16 +485,22 @@
fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
}
+ @Override
+ public boolean isRecentsAttachedToAppWindow() {
+ return mIsAttachedToWindow;
+ }
+
protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
// Scale down recents from being full screen to being in overview.
RecentsView recentsView = activity.getOverviewPanel();
- pa.addFloat(recentsView, SCALE_PROPERTY,
+ pa.addFloat(recentsView, RECENTS_SCALE_PROPERTY,
recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
}
}
- protected static boolean showOverviewActions(Context context) {
- return ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context);
+ /** Called when OverviewService is bound to this process */
+ void onOverviewServiceBound() {
+ // Do nothing
}
}
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
new file mode 100644
index 0000000..7fb8e16
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
+import static com.android.quickstep.fallback.RecentsState.DEFAULT;
+import static com.android.quickstep.fallback.RecentsState.HOME;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * {@link BaseActivityInterface} for recents when the default launcher is different than the
+ * currently running one and apps should interact with the {@link RecentsActivity} as opposed
+ * to the in-launcher one.
+ */
+public final class FallbackActivityInterface extends
+ BaseActivityInterface<RecentsState, RecentsActivity> {
+
+ public static final FallbackActivityInterface INSTANCE = new FallbackActivityInterface();
+
+ private FallbackActivityInterface() {
+ super(false, DEFAULT, BACKGROUND_APP);
+ }
+
+ /** 2 */
+ @Override
+ public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
+ PagedOrientationHandler orientationHandler) {
+ calculateTaskSize(context, dp, outRect, orientationHandler);
+ if (dp.isVerticalBarLayout()
+ && SysUINavigationMode.INSTANCE.get(context).getMode() != NO_BUTTON) {
+ return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
+ } else {
+ return dp.heightPx - outRect.bottom;
+ }
+ }
+
+ /** 5 */
+ @Override
+ public void onAssistantVisibilityChanged(float visibility) {
+ // This class becomes active when the screen is locked.
+ // Rather than having it handle assistant visibility changes, the assistant visibility is
+ // set to zero prior to this class becoming active.
+ }
+
+ @Override
+ public void onOneHandedModeStateChanged(boolean activated) {
+ // Do nothing for FallbackActivityInterface
+ }
+
+ /** 6 */
+ @Override
+ public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+ boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
+ notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+ DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
+ factory.initUI();
+ return factory;
+ }
+
+ @Override
+ public ActivityInitListener createActivityInitListener(
+ Predicate<Boolean> onInitListener) {
+ return new ActivityInitListener<>((activity, alreadyOnHome) ->
+ onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
+ }
+
+ @Nullable
+ @Override
+ public RecentsActivity getCreatedActivity() {
+ return RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+ }
+
+ @Nullable
+ @Override
+ public RecentsView getVisibleRecentsView() {
+ RecentsActivity activity = getCreatedActivity();
+ if (activity != null) {
+ if (activity.hasBeenResumed()
+ || (ENABLE_QUICKSTEP_LIVE_TILE.get() && isInLiveTileMode())) {
+ return activity.getOverviewPanel();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+ return false;
+ }
+
+ @Override
+ public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
+ // TODO: Remove this once b/77875376 is fixed
+ return target.screenSpaceBounds;
+ }
+
+ @Override
+ public boolean allowMinimizeSplitScreen() {
+ // TODO: Remove this once b/77875376 is fixed
+ return false;
+ }
+
+ @Override
+ public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+ // In non-gesture mode, user might be clicking on the home button which would directly
+ // start the home activity instead of going through recents. In that case, defer starting
+ // recents until we are sure it is a gesture.
+ return !deviceState.isFullyGesturalNavMode()
+ || super.deferStartingActivity(deviceState, ev);
+ }
+
+ @Override
+ public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
+ final StateManager<RecentsState> stateManager = getCreatedActivity().getStateManager();
+ if (stateManager.getState() == HOME) {
+ exitRunnable.run();
+ notifyRecentsOfOrientation(deviceState);
+ return;
+ }
+
+ stateManager.addStateListener(
+ new StateManager.StateListener<RecentsState>() {
+ @Override
+ public void onStateTransitionComplete(RecentsState toState) {
+ // Are we going from Recents to Workspace?
+ if (toState == HOME) {
+ exitRunnable.run();
+ notifyRecentsOfOrientation(deviceState);
+ stateManager.removeStateListener(this);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean isInLiveTileMode() {
+ RecentsActivity activity = getCreatedActivity();
+ return activity != null && activity.getStateManager().getState() == DEFAULT &&
+ activity.isStarted();
+ }
+
+ @Override
+ public void onLaunchTaskFailed() {
+ // TODO: probably go back to overview instead.
+ RecentsActivity activity = getCreatedActivity();
+ if (activity == null) {
+ return;
+ }
+ activity.<RecentsView>getOverviewPanel().startHome();
+ }
+
+ @Override
+ public RecentsState stateFromGestureEndTarget(GestureState.GestureEndTarget endTarget) {
+ switch (endTarget) {
+ case RECENTS:
+ return DEFAULT;
+ case NEW_TASK:
+ case LAST_TASK:
+ return BACKGROUND_APP;
+ case HOME:
+ default:
+ return HOME;
+ }
+ }
+
+ private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
+ // reset layout on swipe to home
+ RecentsView recentsView = getCreatedActivity().getOverviewPanel();
+ recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+ rotationTouchHelper.getDisplayRotation());
+ }
+
+ @Override
+ protected int getOverviewScrimColorForState(RecentsActivity activity, RecentsState state) {
+ return state.getScrimColor(activity);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
new file mode 100644
index 0000000..fd44e02
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE;
+import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK;
+import static com.android.launcher3.Utilities.createHomeIntent;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
+
+import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
+import android.app.ActivityOptions;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelUuid;
+import android.os.UserHandle;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.AppCloseConfig;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+/**
+ * Handles the navigation gestures when a 3rd party launcher is the default home activity.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class FallbackSwipeHandler extends
+ AbsSwipeUpHandler<RecentsActivity, FallbackRecentsView, RecentsState> {
+
+ /**
+ * Message used for receiving gesture nav contract information. We use a static messenger to
+ * avoid leaking too make binders in case the receiving launcher does not handle the contract
+ * properly.
+ */
+ private static StaticMessageReceiver sMessageReceiver = null;
+
+ private FallbackHomeAnimationFactory mActiveAnimationFactory;
+ private final boolean mRunningOverHome;
+
+ private final Matrix mTmpMatrix = new Matrix();
+ private float mMaxLauncherScale = 1;
+
+ public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+ TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+ boolean continuingLastGesture, InputConsumerController inputConsumer) {
+ super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+ continuingLastGesture, inputConsumer);
+
+ mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
+ if (mRunningOverHome) {
+ mTransformParams.setHomeBuilderProxy(this::updateHomeActivityTransformDuringSwipeUp);
+ }
+ }
+
+ @Override
+ protected void initTransitionEndpoints(DeviceProfile dp) {
+ super.initTransitionEndpoints(dp);
+ if (mRunningOverHome) {
+ mMaxLauncherScale = 1 / mTaskViewSimulator.getFullScreenScale();
+ }
+ }
+
+ private void updateHomeActivityTransformDuringSwipeUp(SurfaceParams.Builder builder,
+ RemoteAnimationTargetCompat app, TransformParams params) {
+ setHomeScaleAndAlpha(builder, app, mCurrentShift.value,
+ Utilities.boundToRange(1 - mCurrentShift.value, 0, 1));
+ }
+
+ private void setHomeScaleAndAlpha(SurfaceParams.Builder builder,
+ RemoteAnimationTargetCompat app, float verticalShift, float alpha) {
+ float scale = Utilities.mapRange(verticalShift, 1, mMaxLauncherScale);
+ mTmpMatrix.setScale(scale, scale,
+ app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
+ builder.withMatrix(mTmpMatrix).withAlpha(alpha);
+ }
+
+ @Override
+ protected HomeAnimationFactory createHomeAnimationFactory(ArrayList<IBinder> launchCookies,
+ long duration, boolean isTargetTranslucent, boolean appCanEnterPip,
+ RemoteAnimationTargetCompat runningTaskTarget) {
+ mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
+ ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
+ Intent intent = new Intent(mGestureState.getHomeIntent());
+ mActiveAnimationFactory.addGestureContract(intent);
+ try {
+ mContext.startActivity(intent, options.toBundle());
+ } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
+ mContext.startActivity(createHomeIntent());
+ }
+ return mActiveAnimationFactory;
+ }
+
+ @Override
+ protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+ if (mActiveAnimationFactory != null
+ && mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTarget)) {
+ mActiveAnimationFactory = null;
+ return false;
+ }
+
+ return super.handleTaskAppeared(appearedTaskTarget);
+ }
+
+ @Override
+ protected void finishRecentsControllerToHome(Runnable callback) {
+ mRecentsAnimationController.finish(
+ false /* toRecents */, callback, true /* sendUserLeaveHint */);
+ }
+
+ @Override
+ protected void switchToScreenshot() {
+ if (mRunningOverHome) {
+ // When the current task is home, then we don't need to capture anything
+ mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+ } else {
+ super.switchToScreenshot();
+ }
+ }
+
+ @Override
+ protected void notifyGestureAnimationStartToRecents() {
+ if (mRunningOverHome) {
+ mRecentsView.onGestureAnimationStartOnHome(mGestureState.getRunningTask());
+ } else {
+ super.notifyGestureAnimationStartToRecents();
+ }
+ }
+
+ private class FallbackHomeAnimationFactory extends HomeAnimationFactory {
+ private final Rect mTempRect = new Rect();
+ private final TransformParams mHomeAlphaParams = new TransformParams();
+ private final AnimatedFloat mHomeAlpha;
+
+ private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat();
+ private final AnimatedFloat mRecentsAlpha = new AnimatedFloat();
+
+ private final RectF mTargetRect = new RectF();
+ private SurfaceControl mSurfaceControl;
+
+ private final long mDuration;
+
+ private RectFSpringAnim mSpringAnim;
+ FallbackHomeAnimationFactory(long duration) {
+ mDuration = duration;
+
+ if (mRunningOverHome) {
+ mHomeAlpha = new AnimatedFloat();
+ mHomeAlpha.value = Utilities.boundToRange(1 - mCurrentShift.value, 0, 1);
+ mVerticalShiftForScale.value = mCurrentShift.value;
+ mTransformParams.setHomeBuilderProxy(
+ this::updateHomeActivityTransformDuringHomeAnim);
+ } else {
+ mHomeAlpha = new AnimatedFloat(this::updateHomeAlpha);
+ mHomeAlpha.value = 0;
+
+ mHomeAlphaParams.setHomeBuilderProxy(
+ this::updateHomeActivityTransformDuringHomeAnim);
+ }
+
+ mRecentsAlpha.value = 1;
+ mTransformParams.setBaseBuilderProxy(
+ this::updateRecentsActivityTransformDuringHomeAnim);
+ }
+
+ @NonNull
+ @Override
+ public RectF getWindowTargetRect() {
+ if (mTargetRect.isEmpty()) {
+ mTargetRect.set(super.getWindowTargetRect());
+ }
+ return mTargetRect;
+ }
+
+ private void updateRecentsActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
+ RemoteAnimationTargetCompat app, TransformParams params) {
+ builder.withAlpha(mRecentsAlpha.value);
+ }
+
+ private void updateHomeActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
+ RemoteAnimationTargetCompat app, TransformParams params) {
+ setHomeScaleAndAlpha(builder, app, mVerticalShiftForScale.value, mHomeAlpha.value);
+ }
+
+ @NonNull
+ @Override
+ public AnimatorPlaybackController createActivityAnimationToHome() {
+ PendingAnimation pa = new PendingAnimation(mDuration);
+ pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCEL);
+ return pa.createPlaybackController();
+ }
+
+ private void updateHomeAlpha() {
+ if (mHomeAlphaParams.getTargetSet() != null) {
+ mHomeAlphaParams.applySurfaceParams(
+ mHomeAlphaParams.createSurfaceParams(BuilderProxy.NO_OP));
+ }
+ }
+
+ public boolean handleHomeTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+ if (appearedTaskTarget.activityType == ACTIVITY_TYPE_HOME) {
+ RemoteAnimationTargets targets = new RemoteAnimationTargets(
+ new RemoteAnimationTargetCompat[] {appearedTaskTarget},
+ new RemoteAnimationTargetCompat[0], new RemoteAnimationTargetCompat[0],
+ appearedTaskTarget.mode);
+ mHomeAlphaParams.setTargetSet(targets);
+ updateHomeAlpha();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void playAtomicAnimation(float velocity) {
+ ObjectAnimator alphaAnim = mHomeAlpha.animateToValue(mHomeAlpha.value, 1);
+ alphaAnim.setDuration(mDuration).setInterpolator(ACCEL);
+ alphaAnim.start();
+
+ if (mRunningOverHome) {
+ // Spring back launcher scale
+ new SpringAnimationBuilder(mContext)
+ .setStartValue(mVerticalShiftForScale.value)
+ .setEndValue(0)
+ .setStartVelocity(-velocity / mTransitionDragLength)
+ .setMinimumVisibleChange(1f / mDp.heightPx)
+ .setDampingRatio(0.6f)
+ .setStiffness(800)
+ .build(mVerticalShiftForScale, AnimatedFloat.VALUE)
+ .start();
+ }
+ }
+
+ @Override
+ public void setAnimation(RectFSpringAnim anim) {
+ mSpringAnim = anim;
+ }
+
+ private void onMessageReceived(Message msg) {
+ try {
+ Bundle data = msg.getData();
+ RectF position = data.getParcelable(EXTRA_ICON_POSITION);
+ if (!position.isEmpty()) {
+ mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE);
+ mTargetRect.set(position);
+ if (mSpringAnim != null) {
+ mSpringAnim.onTargetPositionChanged();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public void update(@Nullable AppCloseConfig config, RectF currentRect, float progress,
+ float radius) {
+ if (mSurfaceControl != null) {
+ currentRect.roundOut(mTempRect);
+ Transaction t = new Transaction();
+ try {
+ t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+ t.apply();
+ } catch (RuntimeException e) {
+ // Ignore
+ }
+ }
+ }
+
+ private void addGestureContract(Intent intent) {
+ if (mRunningOverHome || mGestureState.getRunningTask() == null) {
+ return;
+ }
+
+ TaskKey key = new TaskKey(mGestureState.getRunningTask());
+ if (key.getComponent() != null) {
+ if (sMessageReceiver == null) {
+ sMessageReceiver = new StaticMessageReceiver();
+ }
+
+ Bundle gestureNavContract = new Bundle();
+ gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent());
+ gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId));
+ gestureNavContract.putParcelable(EXTRA_REMOTE_CALLBACK,
+ sMessageReceiver.newCallback(this::onMessageReceived));
+ intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract);
+ }
+ }
+ }
+
+ private static class StaticMessageReceiver implements Handler.Callback {
+
+ private final Messenger mMessenger =
+ new Messenger(new Handler(Looper.getMainLooper(), this));
+
+ private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID());
+ private WeakReference<Consumer<Message>> mCurrentCallback = new WeakReference<>(null);
+
+ public Message newCallback(Consumer<Message> callback) {
+ mCurrentUID = new ParcelUuid(UUID.randomUUID());
+ mCurrentCallback = new WeakReference<>(callback);
+
+ Message msg = Message.obtain();
+ msg.replyTo = mMessenger;
+ msg.obj = mCurrentUID;
+ return msg;
+ }
+
+ @Override
+ public boolean handleMessage(@NonNull Message message) {
+ if (mCurrentUID.equals(message.obj)) {
+ Consumer<Message> consumer = mCurrentCallback.get();
+ if (consumer != null) {
+ consumer.accept(message);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 00b5eb9..e3ae361 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -15,6 +15,9 @@
*/
package com.android.quickstep;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
import android.annotation.TargetApi;
@@ -22,8 +25,10 @@
import android.content.Intent;
import android.os.Build;
+import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.tracing.GestureStateProto;
+import com.android.launcher3.tracing.SwipeHandlerProto;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -44,19 +49,22 @@
* Defines the end targets of a gesture and the associated state.
*/
public enum GestureEndTarget {
- HOME(true, ContainerType.WORKSPACE, false),
+ HOME(true, LAUNCHER_STATE_HOME, false, GestureStateProto.GestureEndTarget.HOME),
- RECENTS(true, ContainerType.TASKSWITCHER, true),
+ RECENTS(true, LAUNCHER_STATE_OVERVIEW, true, GestureStateProto.GestureEndTarget.RECENTS),
- NEW_TASK(false, ContainerType.APP, true),
+ NEW_TASK(false, LAUNCHER_STATE_BACKGROUND, true,
+ GestureStateProto.GestureEndTarget.NEW_TASK),
- LAST_TASK(false, ContainerType.APP, true);
+ LAST_TASK(false, LAUNCHER_STATE_BACKGROUND, true,
+ GestureStateProto.GestureEndTarget.LAST_TASK);
- GestureEndTarget(boolean isLauncher, int containerType,
- boolean recentsAttachedToAppWindow) {
+ GestureEndTarget(boolean isLauncher, int containerType, boolean recentsAttachedToAppWindow,
+ GestureStateProto.GestureEndTarget protoEndTarget) {
this.isLauncher = isLauncher;
this.containerType = containerType;
this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
+ this.protoEndTarget = protoEndTarget;
}
/** Whether the target is in the launcher activity. Implicitly, if the end target is going
@@ -66,6 +74,8 @@
public final int containerType;
/** Whether RecentsView should be attached to the window as we animate to this target */
public final boolean recentsAttachedToAppWindow;
+ /** The GestureStateProto enum value, used for winscope tracing. See launcher_trace.proto */
+ public final GestureStateProto.GestureEndTarget protoEndTarget;
}
private static final String TAG = "GestureState";
@@ -132,6 +142,13 @@
private RemoteAnimationTargetCompat mLastAppearedTaskTarget;
private Set<Integer> mPreviouslyAppearedTaskIds = new HashSet<>();
private int mLastStartedTaskId = -1;
+ private RecentsAnimationController mRecentsAnimationController;
+ private ThumbnailData mRecentsAnimationCanceledSnapshot;
+
+ /** The time when the swipe up gesture is triggered. */
+ private long mSwipeUpStartTimeMs;
+
+ private boolean mHandlingAtomicEvent;
public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
mHomeIntent = componentObserver.getHomeIntent();
@@ -201,7 +218,8 @@
/**
* @return the interface to the activity handing the UI updates for this gesture.
*/
- public <T extends StatefulActivity<?>> BaseActivityInterface<?, T> getActivityInterface() {
+ public <S extends BaseState<S>,
+ T extends StatefulActivity<S>> BaseActivityInterface<S, T> getActivityInterface() {
return mActivityInterface;
}
@@ -301,6 +319,22 @@
}
/**
+ * Indicates if the gesture is handling an atomic event like a click and not a
+ * user controlled gesture.
+ */
+ public void setHandlingAtomicEvent(boolean handlingAtomicEvent) {
+ mHandlingAtomicEvent = true;
+ }
+
+ /**
+ * Returns true if the gesture is handling an atomic event like a click and not a
+ * user controlled gesture.
+ */
+ public boolean isHandlingAtomicEvent() {
+ return mHandlingAtomicEvent;
+ }
+
+ /**
* @return whether the current gesture is still running a recents animation to a state in the
* Launcher or Recents activity.
*/
@@ -319,13 +353,22 @@
@Override
public void onRecentsAnimationStart(RecentsAnimationController controller,
RecentsAnimationTargets targets) {
+ mRecentsAnimationController = controller;
mStateCallback.setState(STATE_RECENTS_ANIMATION_STARTED);
}
@Override
public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+ mRecentsAnimationCanceledSnapshot = thumbnailData;
mStateCallback.setState(STATE_RECENTS_ANIMATION_CANCELED);
mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
+ if (mRecentsAnimationCanceledSnapshot != null) {
+ // Clean up the screenshot to finalize the recents animation cancel
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.cleanupScreenshot();
+ }
+ mRecentsAnimationCanceledSnapshot = null;
+ }
}
@Override
@@ -334,6 +377,25 @@
mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
}
+ /**
+ * Returns and clears the canceled animation thumbnail data. This call only returns a value
+ * while STATE_RECENTS_ANIMATION_CANCELED state is being set, and the caller is responsible for
+ * calling {@link RecentsAnimationController#cleanupScreenshot()}.
+ */
+ ThumbnailData consumeRecentsAnimationCanceledSnapshot() {
+ ThumbnailData data = mRecentsAnimationCanceledSnapshot;
+ mRecentsAnimationCanceledSnapshot = null;
+ return data;
+ }
+
+ void setSwipeUpStartTimeMs(long uptimeMs) {
+ mSwipeUpStartTimeMs = uptimeMs;
+ }
+
+ long getSwipeUpStartTimeMs() {
+ return mSwipeUpStartTimeMs;
+ }
+
public void dump(PrintWriter pw) {
pw.println("GestureState:");
pw.println(" gestureID=" + mGestureId);
@@ -343,4 +405,17 @@
pw.println(" lastStartedTaskId=" + mLastStartedTaskId);
pw.println(" isRecentsAnimationRunning=" + isRecentsAnimationRunning());
}
+
+ /**
+ * Used for winscope tracing, see launcher_trace.proto
+ * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
+ * @param swipeHandlerProto The parent of this proto message.
+ */
+ public void writeToProto(SwipeHandlerProto.Builder swipeHandlerProto) {
+ GestureStateProto.Builder gestureStateProto = GestureStateProto.newBuilder();
+ gestureStateProto.setEndTarget(mEndTarget == null
+ ? GestureStateProto.GestureEndTarget.UNSET
+ : mEndTarget.protoEndTarget);
+ swipeHandlerProto.setGestureState(gestureStateProto);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/ImageActionsApi.java b/quickstep/src/com/android/quickstep/ImageActionsApi.java
new file mode 100644
index 0000000..8cb64c2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/ImageActionsApi.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static android.content.Intent.EXTRA_STREAM;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.util.ImageActionUtils.persistBitmapAndStartActivity;
+
+import android.app.prediction.AppTarget;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BuildConfig;
+import com.android.quickstep.util.ImageActionUtils;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.function.Supplier;
+
+/**
+ * Contains image selection functions necessary to complete overview action button functions.
+ */
+public class ImageActionsApi {
+
+ private static final String TAG = BuildConfig.APPLICATION_ID + "ImageActionsApi";
+
+ protected final Context mContext;
+ protected final Supplier<Bitmap> mBitmapSupplier;
+ protected final SystemUiProxy mSystemUiProxy;
+
+ public ImageActionsApi(Context context, Supplier<Bitmap> bitmapSupplier) {
+ mContext = context;
+ mBitmapSupplier = bitmapSupplier;
+ mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
+ }
+
+ /**
+ * Share the image this api was constructed with using the provided intent. The implementation
+ * should add an {@link Intent#EXTRA_STREAM} with the URI pointing to the image to the intent.
+ */
+ @UiThread
+ public void shareWithExplicitIntent(@Nullable Rect crop, Intent intent) {
+ addImageAndSendIntent(crop, intent, false);
+ }
+
+ /**
+ * Share the image this api was constructed with using the provided intent. The implementation
+ * should set the intent's data field to the URI pointing to the image.
+ */
+ @UiThread
+ public void shareAsDataWithExplicitIntent(@Nullable Rect crop, Intent intent) {
+ addImageAndSendIntent(crop, intent, true);
+ }
+
+ @UiThread
+ private void addImageAndSendIntent(@Nullable Rect crop, Intent intent, boolean setData) {
+ if (mBitmapSupplier.get() == null) {
+ Log.e(TAG, "No snapshot available, not starting share.");
+ return;
+ }
+
+ UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(mContext,
+ mBitmapSupplier.get(), crop, intent, (uri, intentForUri) -> {
+ intentForUri.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
+ if (setData) {
+ intentForUri.setData(uri);
+ } else {
+ intentForUri.putExtra(EXTRA_STREAM, uri);
+ }
+ return new Intent[]{intentForUri};
+ }, TAG));
+ }
+
+ /**
+ * Share the image this api was constructed with.
+ */
+ @UiThread
+ public void startShareActivity(Rect crop) {
+ ImageActionUtils.startShareActivity(mContext, mBitmapSupplier, crop, null, TAG);
+ }
+
+ /**
+ * @param screenshot to be saved to the media store.
+ * @param screenshotBounds the location of where the bitmap was laid out on the screen in
+ * screen coordinates.
+ * @param visibleInsets that are used to draw the screenshot within the bounds.
+ * @param task of the task that the screenshot was taken of.
+ */
+ public void saveScreenshot(Bitmap screenshot, Rect screenshotBounds,
+ Insets visibleInsets, Task.TaskKey task) {
+ ImageActionUtils.saveScreenshot(mSystemUiProxy, screenshot, screenshotBounds, visibleInsets,
+ task);
+ }
+
+ /**
+ * Share the image when user taps on overview share targets.
+ */
+ @UiThread
+ public void shareImage(RectF rectF, ShortcutInfo shortcutInfo, AppTarget appTarget) {
+ ImageActionUtils.shareImage(mContext, mBitmapSupplier, rectF, shortcutInfo, appTarget, TAG);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index ec720d5..0b2a057 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -21,6 +21,9 @@
import android.view.KeyEvent;
import android.view.MotionEvent;
+import com.android.launcher3.tracing.InputConsumerProto;
+import com.android.launcher3.tracing.TouchInteractionServiceProto;
+
@TargetApi(Build.VERSION_CODES.O)
public interface InputConsumer {
@@ -35,6 +38,7 @@
int TYPE_RESET_GESTURE = 1 << 8;
int TYPE_OVERSCROLL = 1 << 9;
int TYPE_SYSUI_OVERLAY = 1 << 10;
+ int TYPE_ONE_HANDED = 1 << 11;
String[] NAMES = new String[] {
"TYPE_NO_OP", // 0
@@ -47,7 +51,8 @@
"TYPE_OVERVIEW_WITHOUT_FOCUS", // 7
"TYPE_RESET_GESTURE", // 8
"TYPE_OVERSCROLL", // 9
- "TYPE_SYSUI_OVERLAY" // 10
+ "TYPE_SYSUI_OVERLAY", // 10
+ "TYPE_ONE_HANDED", // 11
};
InputConsumer NO_OP = () -> TYPE_NO_OP;
@@ -114,4 +119,21 @@
}
return name.toString();
}
+
+ /**
+ * Used for winscope tracing, see launcher_trace.proto
+ * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
+ * @param serviceProto The parent of this proto message.
+ */
+ default void writeToProto(TouchInteractionServiceProto.Builder serviceProto) {
+ InputConsumerProto.Builder inputConsumerProto = InputConsumerProto.newBuilder();
+ inputConsumerProto.setName(getName());
+ writeToProtoInternal(inputConsumerProto);
+ serviceProto.setInputConsumer(inputConsumerProto);
+ }
+
+ /**
+ * @see #writeToProto - allows subclasses to write additional info to the proto.
+ */
+ default void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {}
}
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
new file mode 100644
index 0000000..fb1391a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.QUICK_SWITCH;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherInitListener;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.LauncherTaskbarUIController;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.plugins.shared.LauncherOverlayManager;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * {@link BaseActivityInterface} for the in-launcher recents.
+ */
+public final class LauncherActivityInterface extends
+ BaseActivityInterface<LauncherState, BaseQuickstepLauncher> {
+
+ public static final LauncherActivityInterface INSTANCE = new LauncherActivityInterface();
+
+ private LauncherActivityInterface() {
+ super(true, OVERVIEW, BACKGROUND_APP);
+ }
+
+ @Override
+ public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
+ PagedOrientationHandler orientationHandler) {
+ calculateTaskSize(context, dp, outRect, orientationHandler);
+ if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
+ return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
+ } else {
+ return LayoutUtils.getShelfTrackingDistance(context, dp, orientationHandler);
+ }
+ }
+
+ @Override
+ public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {
+ Launcher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return;
+ }
+ // When going to home, the state animator we use has SKIP_OVERVIEW because we assume that
+ // setRecentsAttachedToAppWindow() will handle animating Overview instead. Thus, at the end
+ // of the animation, we should ensure recents is at the correct position for NORMAL state.
+ // For example, when doing a long swipe to home, RecentsView may be scaled down. This is
+ // relatively expensive, so do it on the next frame instead of critical path.
+ MAIN_EXECUTOR.getHandler().post(launcher.getStateManager()::reapplyState);
+
+ launcher.getRootView().setForceHideBackArrow(false);
+ notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+ }
+
+ @Override
+ public void onAssistantVisibilityChanged(float visibility) {
+ Launcher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return;
+ }
+ launcher.onAssistantVisibilityChanged(visibility);
+ }
+
+ @Override
+ public void onOneHandedModeStateChanged(boolean activated) {
+ Launcher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return;
+ }
+ launcher.onOneHandedStateChanged(activated);
+ }
+
+ @Override
+ public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
+ boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
+ notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+ DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
+ @Override
+ protected void createBackgroundToOverviewAnim(BaseQuickstepLauncher activity,
+ PendingAnimation pa) {
+ super.createBackgroundToOverviewAnim(activity, pa);
+
+ // Animate the blur and wallpaper zoom
+ float fromDepthRatio = BACKGROUND_APP.getDepth(activity);
+ float toDepthRatio = OVERVIEW.getDepth(activity);
+ pa.addFloat(getDepthController(),
+ new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
+ fromDepthRatio, toDepthRatio, LINEAR);
+
+ }
+ };
+
+ BaseQuickstepLauncher launcher = factory.initUI();
+ // Since all apps is not visible, we can safely reset the scroll position.
+ // This ensures then the next swipe up to all-apps starts from scroll 0.
+ launcher.getAppsView().reset(false /* animate */);
+ return factory;
+ }
+
+ @Override
+ public ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
+ return new LauncherInitListener((activity, alreadyOnHome) ->
+ onInitListener.test(alreadyOnHome));
+ }
+
+ @Override
+ public void setOnDeferredActivityLaunchCallback(Runnable r) {
+ Launcher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return;
+ }
+ launcher.setOnDeferredActivityLaunchCallback(r);
+ }
+
+ @Nullable
+ @Override
+ public BaseQuickstepLauncher getCreatedActivity() {
+ return BaseQuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+ }
+
+ @Nullable
+ @Override
+ public DepthController getDepthController() {
+ BaseQuickstepLauncher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return null;
+ }
+ return launcher.getDepthController();
+ }
+
+ @Nullable
+ private LauncherTaskbarUIController getTaskbarController() {
+ BaseQuickstepLauncher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return null;
+ }
+ return launcher.getTaskbarUIController();
+ }
+
+ @Nullable
+ @Override
+ public RecentsView getVisibleRecentsView() {
+ Launcher launcher = getVisibleLauncher();
+ RecentsView recentsView =
+ launcher != null && launcher.getStateManager().getState().overviewUi
+ ? launcher.getOverviewPanel() : null;
+ if (recentsView == null || (!launcher.hasBeenResumed()
+ && recentsView.getRunningTaskId() == -1)) {
+ // If live tile has ended, return null.
+ return null;
+ }
+ return recentsView;
+ }
+
+ @Nullable
+ @UiThread
+ private Launcher getVisibleLauncher() {
+ Launcher launcher = getCreatedActivity();
+ return (launcher != null) && launcher.isStarted()
+ && ((ENABLE_QUICKSTEP_LIVE_TILE.get() && isInLiveTileMode())
+ || launcher.hasBeenResumed()) ? launcher : null;
+ }
+
+ @Override
+ public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+ Launcher launcher = getVisibleLauncher();
+ if (launcher == null) {
+ return false;
+ }
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isInLiveTileMode()) {
+ RecentsView recentsView = getVisibleRecentsView();
+ if (recentsView == null) {
+ return false;
+ }
+ }
+
+ closeOverlay();
+ launcher.getStateManager().goToState(OVERVIEW,
+ launcher.getStateManager().shouldAnimateStateChange(),
+ onCompleteCallback == null ? null : forEndCallback(onCompleteCallback));
+ return true;
+ }
+
+
+ @Override
+ public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
+ final StateManager<LauncherState> stateManager = getCreatedActivity().getStateManager();
+ stateManager.addStateListener(
+ new StateManager.StateListener<LauncherState>() {
+ @Override
+ public void onStateTransitionComplete(LauncherState toState) {
+ // Are we going from Recents to Workspace?
+ if (toState == LauncherState.NORMAL) {
+ exitRunnable.run();
+ notifyRecentsOfOrientation(deviceState);
+ stateManager.removeStateListener(this);
+ }
+ }
+ });
+ }
+
+ private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
+ // reset layout on swipe to home
+ RecentsView recentsView = getCreatedActivity().getOverviewPanel();
+ recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+ rotationTouchHelper.getDisplayRotation());
+ }
+
+ @Override
+ public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
+ return homeBounds;
+ }
+
+ @Override
+ public boolean allowMinimizeSplitScreen() {
+ return true;
+ }
+
+ @Override
+ public boolean isInLiveTileMode() {
+ Launcher launcher = getCreatedActivity();
+ return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
+ launcher.isStarted();
+ }
+
+ @Override
+ public void onLaunchTaskFailed() {
+ Launcher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return;
+ }
+ launcher.getStateManager().goToState(OVERVIEW);
+ }
+
+ @Override
+ public void closeOverlay() {
+ Launcher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return;
+ }
+ LauncherOverlayManager om = launcher.getOverlayManager();
+ if (!launcher.isStarted() || launcher.isForceInvisible()) {
+ om.hideOverlay(false /* animate */);
+ } else {
+ om.hideOverlay(150);
+ }
+ }
+
+ @Override
+ void onOverviewServiceBound() {
+ final BaseQuickstepLauncher activity = getCreatedActivity();
+ if (activity == null) return;
+ activity.getAppTransitionManager().registerRemoteTransitions();
+ }
+
+ @Override
+ public @Nullable Animator getParallelAnimationToLauncher(GestureEndTarget endTarget,
+ long duration) {
+ LauncherTaskbarUIController uiController = getTaskbarController();
+ Animator superAnimator = super.getParallelAnimationToLauncher(endTarget, duration);
+ if (uiController == null) {
+ return superAnimator;
+ }
+ LauncherState toState = stateFromGestureEndTarget(endTarget);
+ Animator taskbarAnimator = uiController.createAnimToLauncher(toState, duration);
+ if (superAnimator == null) {
+ return taskbarAnimator;
+ } else {
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(superAnimator, taskbarAnimator);
+ return animatorSet;
+ }
+ }
+
+ @Override
+ protected int getOverviewScrimColorForState(BaseQuickstepLauncher launcher,
+ LauncherState state) {
+ return state.getWorkspaceScrimColor(launcher);
+ }
+
+ @Override
+ public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+ LauncherTaskbarUIController uiController = getTaskbarController();
+ if (uiController == null) {
+ return super.deferStartingActivity(deviceState, ev);
+ }
+ return uiController.isEventOverAnyTaskbarItem(ev);
+ }
+
+ @Override
+ public boolean shouldCancelCurrentGesture() {
+ LauncherTaskbarUIController uiController = getTaskbarController();
+ if (uiController == null) {
+ return super.shouldCancelCurrentGesture();
+ }
+ return uiController.isDraggingItem();
+ }
+
+ @Override
+ public LauncherState stateFromGestureEndTarget(GestureEndTarget endTarget) {
+ switch (endTarget) {
+ case RECENTS:
+ return OVERVIEW;
+ case NEW_TASK:
+ case LAST_TASK:
+ return QUICK_SWITCH;
+ case HOME:
+ default:
+ return NORMAL;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
new file mode 100644
index 0000000..19cad53
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.Utilities.boundToRange;
+import static com.android.launcher3.Utilities.dpToPx;
+import static com.android.launcher3.Utilities.mapBoundToRange;
+import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.PROTOTYPE_APP_CLOSE;
+import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
+
+import static java.lang.Math.round;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.util.Size;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.ObjectWrapper;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.views.FloatingView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.quickstep.util.AppCloseConfig;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.util.WorkspaceRevealAnim;
+import com.android.quickstep.views.FloatingWidgetView;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.plugins.ResourceProvider;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.ArrayList;
+
+/**
+ * Temporary class to allow easier refactoring
+ */
+public class LauncherSwipeHandlerV2 extends
+ AbsSwipeUpHandler<BaseQuickstepLauncher, RecentsView, LauncherState> {
+
+ public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
+ TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+ boolean continuingLastGesture, InputConsumerController inputConsumer) {
+ super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+ continuingLastGesture, inputConsumer);
+ }
+
+
+ @Override
+ protected HomeAnimationFactory createHomeAnimationFactory(ArrayList<IBinder> launchCookies,
+ long duration, boolean isTargetTranslucent, boolean appCanEnterPip,
+ RemoteAnimationTargetCompat runningTaskTarget) {
+ if (mActivity == null) {
+ mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+ isPresent -> mRecentsView.startHome());
+ return new HomeAnimationFactory() {
+ @Override
+ public AnimatorPlaybackController createActivityAnimationToHome() {
+ return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
+ }
+ };
+ }
+
+ final View workspaceView = findWorkspaceView(launchCookies,
+ mRecentsView.getRunningTaskView());
+ boolean canUseWorkspaceView = workspaceView != null && workspaceView.isAttachedToWindow();
+
+ mActivity.getRootView().setForceHideBackArrow(true);
+ mActivity.setHintUserWillBeActive();
+
+ if (!canUseWorkspaceView || appCanEnterPip) {
+ return new LauncherHomeAnimationFactory();
+ }
+ if (workspaceView instanceof LauncherAppWidgetHostView) {
+ return createWidgetHomeAnimationFactory((LauncherAppWidgetHostView) workspaceView,
+ isTargetTranslucent, runningTaskTarget);
+ }
+ return createIconHomeAnimationFactory(workspaceView);
+ }
+
+ private HomeAnimationFactory createIconHomeAnimationFactory(View workspaceView) {
+ final ResourceProvider rp = DynamicResource.provider(mActivity);
+ final float transY = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp));
+ RectF iconLocation = new RectF();
+ FloatingIconView floatingIconView = getFloatingIconView(mActivity, workspaceView,
+ true /* hideOriginal */, iconLocation, false /* isOpening */);
+
+ // We want the window alpha to be 0 once this threshold is met, so that the
+ // FolderIconView can be seen morphing into the icon shape.
+ float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION;
+
+ return new FloatingViewHomeAnimationFactory(floatingIconView) {
+
+ // There is a delay in loading the icon, so we need to keep the window
+ // opaque until it is ready.
+ private boolean mIsFloatingIconReady = false;
+
+ @Override
+ public RectF getWindowTargetRect() {
+ super.getWindowTargetRect();
+ return iconLocation;
+ }
+
+ @Override
+ public void setAnimation(RectFSpringAnim anim) {
+ super.setAnimation(anim);
+ anim.addAnimatorListener(floatingIconView);
+ floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged);
+ floatingIconView.setFastFinishRunnable(anim::end);
+ }
+
+ @Override
+ public boolean keepWindowOpaque() {
+ if (mIsFloatingIconReady || floatingIconView.isVisibleToUser()) {
+ mIsFloatingIconReady = true;
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void update(@Nullable AppCloseConfig config, RectF currentRect,
+ float progress, float radius) {
+ super.update(config, currentRect, progress, radius);
+ int fgAlpha = 255;
+ if (config != null && PROTOTYPE_APP_CLOSE.get()) {
+ progress = config.getInterpolatedProgress();
+ fgAlpha = config.getFgAlpha();
+ }
+ floatingIconView.update(1f, fgAlpha, currentRect, progress,
+ windowAlphaThreshold, radius, false);
+ }
+ };
+ }
+
+ private HomeAnimationFactory createWidgetHomeAnimationFactory(
+ LauncherAppWidgetHostView hostView, boolean isTargetTranslucent,
+ RemoteAnimationTargetCompat runningTaskTarget) {
+ final float floatingWidgetAlpha = isTargetTranslucent ? 0 : 1;
+ RectF backgroundLocation = new RectF();
+ Rect crop = new Rect();
+ mTaskViewSimulator.getCurrentCropRect().roundOut(crop);
+ Size windowSize = new Size(crop.width(), crop.height());
+ int fallbackBackgroundColor =
+ FloatingWidgetView.getDefaultBackgroundColor(mContext, runningTaskTarget);
+ FloatingWidgetView floatingWidgetView = FloatingWidgetView.getFloatingWidgetView(mActivity,
+ hostView, backgroundLocation, windowSize,
+ mTaskViewSimulator.getCurrentCornerRadius(), isTargetTranslucent,
+ fallbackBackgroundColor);
+
+ return new FloatingViewHomeAnimationFactory(floatingWidgetView) {
+
+ @Override
+ @Nullable
+ protected View getViewIgnoredInWorkspaceRevealAnimation() {
+ return hostView;
+ }
+
+ @Override
+ public RectF getWindowTargetRect() {
+ super.getWindowTargetRect();
+ return backgroundLocation;
+ }
+
+ @Override
+ public float getEndRadius(RectF cropRectF) {
+ return floatingWidgetView.getInitialCornerRadius();
+ }
+
+ @Override
+ public void setAnimation(RectFSpringAnim anim) {
+ super.setAnimation(anim);
+
+ anim.addAnimatorListener(floatingWidgetView);
+ floatingWidgetView.setOnTargetChangeListener(anim::onTargetPositionChanged);
+ floatingWidgetView.setFastFinishRunnable(anim::end);
+ }
+
+ @Override
+ public boolean keepWindowOpaque() {
+ return false;
+ }
+
+ @Override
+ public void update(@Nullable AppCloseConfig config, RectF currentRect, float progress,
+ float radius) {
+ super.update(config, currentRect, progress, radius);
+ final float fallbackBackgroundAlpha =
+ 1 - mapBoundToRange(progress, 0.8f, 1, 0, 1, EXAGGERATED_EASE);
+ final float foregroundAlpha =
+ mapBoundToRange(progress, 0.5f, 1, 0, 1, EXAGGERATED_EASE);
+ floatingWidgetView.update(currentRect, floatingWidgetAlpha, foregroundAlpha,
+ fallbackBackgroundAlpha, 1 - progress);
+ }
+
+ @Override
+ protected float getWindowAlpha(float progress) {
+ return 1 - mapBoundToRange(progress, 0, 0.5f, 0, 1, LINEAR);
+ }
+ };
+ }
+
+ /**
+ * Returns the associated view on the workspace matching one of the launch cookies, or the app
+ * associated with the running task.
+ */
+ @Nullable
+ private View findWorkspaceView(ArrayList<IBinder> launchCookies, TaskView runningTaskView) {
+ if (mIsSwipingPipToHome) {
+ // Disable if swiping to PIP
+ return null;
+ }
+ if (runningTaskView == null || runningTaskView.getTask() == null
+ || runningTaskView.getTask().key.getComponent() == null) {
+ // Disable if it's an invalid task
+ return null;
+ }
+
+ // Find the associated item info for the launch cookie (if available), note that predicted
+ // apps actually have an id of -1, so use another default id here
+ int launchCookieItemId = NO_MATCHING_ID;
+ for (IBinder cookie : launchCookies) {
+ Integer itemId = ObjectWrapper.unwrap(cookie);
+ if (itemId != null) {
+ launchCookieItemId = itemId;
+ break;
+ }
+ }
+
+ return mActivity.getWorkspace().getFirstMatchForAppClose(launchCookieItemId,
+ runningTaskView.getTask().key.getComponent().getPackageName(),
+ UserHandle.of(runningTaskView.getTask().key.userId));
+ }
+
+ @Override
+ protected void finishRecentsControllerToHome(Runnable callback) {
+ mRecentsAnimationController.finish(
+ true /* toRecents */, callback, true /* sendUserLeaveHint */);
+ }
+
+ private class FloatingViewHomeAnimationFactory extends LauncherHomeAnimationFactory {
+
+ private final float mTransY;
+ private final FloatingView mFloatingView;
+ private ValueAnimator mBounceBackAnimator;
+ private final AnimatorSet mWorkspaceReveal;
+
+ FloatingViewHomeAnimationFactory(FloatingView floatingView) {
+ mFloatingView = floatingView;
+
+ ResourceProvider rp = DynamicResource.provider(mActivity);
+ mTransY = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp));
+
+ mWorkspaceReveal = PROTOTYPE_APP_CLOSE.get()
+ ? new WorkspaceRevealAnim(mActivity, true /* animateScrim */).getAnimators()
+ : null;
+ }
+
+ @Override
+ public @NonNull RectF getWindowTargetRect() {
+ if (PROTOTYPE_APP_CLOSE.get()) {
+ // We want the target rect to be at this offset position, so that all
+ // launcher content can spring back upwards.
+ mFloatingView.setPositionOffsetY(mTransY);
+ }
+ return super.getWindowTargetRect();
+ }
+
+ @Override
+ public boolean shouldPlayAtomicWorkspaceReveal() {
+ return false;
+ }
+
+ @Override
+ public void update(@Nullable AppCloseConfig config, RectF currentRect, float progress,
+ float radius) {
+ if (config != null && PROTOTYPE_APP_CLOSE.get()) {
+ DragLayer dl = mActivity.getDragLayer();
+ float translationY = config.getWorkspaceTransY();
+ dl.setTranslationY(translationY);
+
+ long duration = mWorkspaceReveal.getDuration();
+ long playTime = boundToRange(round(duration * progress), 0, duration);
+ mWorkspaceReveal.setCurrentPlayTime(playTime);
+ }
+ }
+
+ protected void bounceBackToRestingPosition() {
+ final float startValue = mTransY;
+ final float endValue = 0;
+ // Ensures the velocity is always aligned with the direction.
+ float pixelPerSecond = Math.abs(mSwipeVelocity) * Math.signum(endValue - mTransY);
+
+ DragLayer dl = mActivity.getDragLayer();
+ Workspace workspace = mActivity.getWorkspace();
+ Hotseat hotseat = mActivity.getHotseat();
+
+ ResourceProvider rp = DynamicResource.provider(mActivity);
+ ValueAnimator springTransY = new SpringAnimationBuilder(dl.getContext())
+ .setStiffness(rp.getFloat(R.dimen.swipe_up_trans_y_stiffness))
+ .setDampingRatio(rp.getFloat(R.dimen.swipe_up_trans_y_damping))
+ .setMinimumVisibleChange(1f)
+ .setStartValue(startValue)
+ .setEndValue(endValue)
+ .setStartVelocity(pixelPerSecond)
+ .build(dl, VIEW_TRANSLATE_Y);
+ springTransY.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ dl.setTranslationY(0f);
+ dl.setAlpha(1f);
+ SCALE_PROPERTY.set(workspace, 1f);
+ SCALE_PROPERTY.set(hotseat, 1f);
+ }
+ });
+
+ mBounceBackAnimator = springTransY;
+ mBounceBackAnimator.start();
+ }
+
+ @Override
+ public void setAnimation(RectFSpringAnim anim) {
+ if (PROTOTYPE_APP_CLOSE.get()) {
+ // Use a spring to put drag layer translation back to 0.
+ anim.addAnimatorListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mFloatingView.setPositionOffsetY(0);
+ bounceBackToRestingPosition();
+ }
+ });
+
+ // Will be updated manually below so that the two animations are in sync.
+ mWorkspaceReveal.start();
+ mWorkspaceReveal.pause();
+
+ anim.addAnimatorListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mWorkspaceReveal.end();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ mFloatingView.fastFinish();
+ if (mBounceBackAnimator != null) {
+ mBounceBackAnimator.cancel();
+ }
+ }
+ }
+
+ private class LauncherHomeAnimationFactory extends HomeAnimationFactory {
+
+ /**
+ * Returns a view which should be excluded from the Workspace animation, or null if there
+ * is no view to exclude.
+ */
+ @Nullable
+ protected View getViewIgnoredInWorkspaceRevealAnimation() {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public AnimatorPlaybackController createActivityAnimationToHome() {
+ // Return an empty APC here since we have an non-user controlled animation
+ // to home.
+ long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
+ return mActivity.getStateManager().createAnimationToNewWorkspace(
+ NORMAL, accuracy, StateAnimationConfig.SKIP_ALL_ANIMATIONS);
+ }
+
+ @Override
+ public void playAtomicAnimation(float velocity) {
+ if (!PROTOTYPE_APP_CLOSE.get()) {
+ new StaggeredWorkspaceAnim(mActivity, velocity, true /* animateOverviewScrim */,
+ getViewIgnoredInWorkspaceRevealAnimation())
+ .start();
+ } else if (shouldPlayAtomicWorkspaceReveal()) {
+ new WorkspaceRevealAnim(mActivity, true).start();
+ }
+ }
+
+ @Override
+ public boolean supportSwipePipToHome() {
+ return true;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 1081548..35a851a 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -30,16 +30,17 @@
import android.graphics.Point;
import android.graphics.RectF;
import android.util.Log;
-import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.Surface;
import com.android.launcher3.R;
import com.android.launcher3.ResourceUtils;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController.Info;
import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
/**
* Maintains state for supporting nav bars and tracking their gestures in multiple orientations.
@@ -51,19 +52,60 @@
*/
class OrientationTouchTransformer {
+ private static class CurrentDisplay {
+ public Point size;
+ public int rotation;
+
+ CurrentDisplay() {
+ this.size = new Point(0, 0);
+ this.rotation = 0;
+ }
+
+ CurrentDisplay(Point size, int rotation) {
+ this.size = size;
+ this.rotation = rotation;
+ }
+
+ @Override
+ public String toString() {
+ return "CurrentDisplay:"
+ + " rotation: " + rotation
+ + " size: " + size;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CurrentDisplay display = (CurrentDisplay) o;
+ if (rotation != display.rotation) return false;
+
+ return Objects.equals(size, display.size);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(size, rotation);
+ }
+ };
+
private static final String TAG = "OrientationTouchTransformer";
private static final boolean DEBUG = false;
- private static final int MAX_ORIENTATIONS = 4;
private static final int QUICKSTEP_ROTATION_UNINITIALIZED = -1;
private final Matrix mTmpMatrix = new Matrix();
private final float[] mTmpPoint = new float[2];
- private SparseArray<OrientationRectF> mSwipeTouchRegions = new SparseArray<>(MAX_ORIENTATIONS);
+ private final Map<CurrentDisplay, OrientationRectF> mSwipeTouchRegions =
+ new HashMap<CurrentDisplay, OrientationRectF>();
private final RectF mAssistantLeftRegion = new RectF();
private final RectF mAssistantRightRegion = new RectF();
- private int mCurrentDisplayRotation;
+ private final RectF mOneHandedModeRegion = new RectF();
+ private CurrentDisplay mCurrentDisplay = new CurrentDisplay();
+ private int mNavBarGesturalHeight;
+ private final int mNavBarLargerGesturalHeight;
private boolean mEnableMultipleRegions;
private Resources mResources;
private OrientationRectF mLastRectTouched;
@@ -83,7 +125,7 @@
* QUICKSTEP_ROTATION_UNINITIALIZED, then user has not tapped on an active nav region.
* Otherwise it will be the rotation of the display when the user first interacted with the
* active nav bar region.
- * The "session" ends when {@link #enableMultipleRegions(boolean, DefaultDisplay.Info)} is
+ * The "session" ends when {@link #enableMultipleRegions(boolean, Info)} is
* called - usually from a timeout or if user starts interacting w/ the foreground app.
*
* This is different than {@link #mLastRectTouched} as it can get reset by the system whereas
@@ -103,18 +145,38 @@
mResources = resources;
mMode = mode;
mContractInfo = contractInfo;
+ mNavBarGesturalHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+ mNavBarLargerGesturalHeight = ResourceUtils.getDimenByName(
+ ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, resources,
+ mNavBarGesturalHeight);
}
- void setNavigationMode(SysUINavigationMode.Mode newMode, DefaultDisplay.Info info) {
+ private void refreshTouchRegion(Info info, Resources newRes) {
+ // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
+ // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
+ // It tries to cache and reuse swipe regions whenever possible based only on rotation
+ mResources = newRes;
+ mSwipeTouchRegions.clear();
+ resetSwipeRegions(info);
+ }
+
+ void setNavigationMode(SysUINavigationMode.Mode newMode, Info info, Resources newRes) {
+ if (DEBUG) {
+ Log.d(TAG, "setNavigationMode new: " + newMode + " oldMode: " + mMode + " " + this);
+ }
if (mMode == newMode) {
return;
}
this.mMode = newMode;
- // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
- // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
- // It tries to cache and reuse swipe regions whenever possible based only on rotation
- mSwipeTouchRegions.clear();
- resetSwipeRegions(info);
+ refreshTouchRegion(info, newRes);
+ }
+
+ void setGesturalHeight(int newGesturalHeight, Info info, Resources newRes) {
+ if (mNavBarGesturalHeight == newGesturalHeight) {
+ return;
+ }
+ mNavBarGesturalHeight = newGesturalHeight;
+ refreshTouchRegion(info, newRes);
}
/**
@@ -123,24 +185,25 @@
* alongside other regions.
* Ok to call multiple times
*
- * @see #enableMultipleRegions(boolean, DefaultDisplay.Info)
+ * @see #enableMultipleRegions(boolean, Info)
*/
- void createOrAddTouchRegion(DefaultDisplay.Info info) {
- mCurrentDisplayRotation = info.rotation;
+ void createOrAddTouchRegion(Info info) {
+ mCurrentDisplay = new CurrentDisplay(info.currentSize, info.rotation);
+
if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED
- && mCurrentDisplayRotation == mQuickStepStartingRotation) {
+ && mCurrentDisplay.rotation == mQuickStepStartingRotation) {
// User already was swiping and the current screen is same rotation as the starting one
// Remove active nav bars in other rotations except for the one we started out in
resetSwipeRegions(info);
return;
}
- OrientationRectF region = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+ OrientationRectF region = mSwipeTouchRegions.get(mCurrentDisplay);
if (region != null) {
return;
}
if (mEnableMultipleRegions) {
- mSwipeTouchRegions.put(mCurrentDisplayRotation, createRegionForDisplay(info));
+ mSwipeTouchRegions.put(mCurrentDisplay, createRegionForDisplay(info));
} else {
resetSwipeRegions(info);
}
@@ -153,7 +216,7 @@
* @param enableMultipleRegions Set to true to start tracking multiple nav bar regions
* @param info The current displayInfo which will be the start of the quickswitch gesture
*/
- void enableMultipleRegions(boolean enableMultipleRegions, DefaultDisplay.Info info) {
+ void enableMultipleRegions(boolean enableMultipleRegions, Info info) {
mEnableMultipleRegions = enableMultipleRegions &&
mMode != SysUINavigationMode.Mode.TWO_BUTTONS;
if (mEnableMultipleRegions) {
@@ -174,7 +237,7 @@
*
* @param displayInfo The display whos rotation will be used as the current active rotation
*/
- void setSingleActiveRegion(DefaultDisplay.Info displayInfo) {
+ void setSingleActiveRegion(Info displayInfo) {
mActiveTouchRotation = displayInfo.rotation;
resetSwipeRegions(displayInfo);
}
@@ -185,60 +248,64 @@
* To be called whenever we want to stop tracking more than one swipe region.
* Ok to call multiple times.
*/
- private void resetSwipeRegions(DefaultDisplay.Info region) {
+ private void resetSwipeRegions(Info region) {
if (DEBUG) {
- Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplayRotation);
+ Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplay.rotation);
}
- mCurrentDisplayRotation = region.rotation;
- OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+ mCurrentDisplay = new CurrentDisplay(region.currentSize, region.rotation);
+ OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay);
if (regionToKeep == null) {
regionToKeep = createRegionForDisplay(region);
}
mSwipeTouchRegions.clear();
- mSwipeTouchRegions.put(mCurrentDisplayRotation, regionToKeep);
+ mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep);
updateAssistantRegions(regionToKeep);
}
private void resetSwipeRegions() {
- OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+ OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay);
mSwipeTouchRegions.clear();
if (regionToKeep != null) {
- mSwipeTouchRegions.put(mCurrentDisplayRotation, regionToKeep);
+ mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep);
updateAssistantRegions(regionToKeep);
}
}
- private OrientationRectF createRegionForDisplay(DefaultDisplay.Info display) {
+ private OrientationRectF createRegionForDisplay(Info display) {
if (DEBUG) {
- Log.d(TAG, "creating rotation region for: " + mCurrentDisplayRotation);
+ Log.d(TAG, "creating rotation region for: " + mCurrentDisplay.rotation
+ + " with mode: " + mMode + " displayRotation: " + display.rotation);
}
- Point size = display.realSize;
+ Point size = display.currentSize;
int rotation = display.rotation;
+ int touchHeight = mNavBarGesturalHeight;
OrientationRectF orientationRectF =
new OrientationRectF(0, 0, size.x, size.y, rotation);
if (mMode == SysUINavigationMode.Mode.NO_BUTTON) {
- int touchHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
orientationRectF.top = orientationRectF.bottom - touchHeight;
updateAssistantRegions(orientationRectF);
} else {
mAssistantLeftRegion.setEmpty();
mAssistantRightRegion.setEmpty();
+ int navbarSize = getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
switch (rotation) {
case Surface.ROTATION_90:
orientationRectF.left = orientationRectF.right
- - getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+ - navbarSize;
break;
case Surface.ROTATION_270:
orientationRectF.right = orientationRectF.left
- + getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
+ + navbarSize;
break;
default:
- orientationRectF.top = orientationRectF.bottom
- - getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+ orientationRectF.top = orientationRectF.bottom - touchHeight;
}
}
+ // One handed gestural only active on portrait mode
+ mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
+ size.x, size.y);
return orientationRectF;
}
@@ -264,14 +331,18 @@
}
+ boolean touchInOneHandedModeRegion(MotionEvent ev) {
+ return mOneHandedModeRegion.contains(ev.getX(), ev.getY());
+ }
+
private int getNavbarSize(String resName) {
return ResourceUtils.getNavbarSize(resName, mResources);
}
boolean touchInValidSwipeRegions(float x, float y) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_SWIPE_TO_HOME, "touchInValidSwipeRegions " + x + "," + y + " in "
- + mLastRectTouched);
+ if (DEBUG) {
+ Log.d(TAG, "touchInValidSwipeRegions " + x + "," + y + " in "
+ + mLastRectTouched + " this: " + this);
}
if (mLastRectTouched != null) {
return mLastRectTouched.contains(x, y);
@@ -312,22 +383,15 @@
return;
}
- for (int i = 0; i < MAX_ORIENTATIONS; i++) {
- OrientationRectF rect = mSwipeTouchRegions.get(i);
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_SWIPE_TO_HOME, "transform:DOWN, rect=" + rect);
- }
+ for (OrientationRectF rect : mSwipeTouchRegions.values()) {
if (rect == null) {
continue;
}
if (rect.applyTransform(event, false)) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_SWIPE_TO_HOME, "setting mLastRectTouched");
- }
mLastRectTouched = rect;
mActiveTouchRotation = rect.mRotation;
if (mEnableMultipleRegions
- && mCurrentDisplayRotation == mActiveTouchRotation) {
+ && mCurrentDisplay.rotation == mActiveTouchRotation) {
// TODO(b/154580671) might make this block unnecessary
// Start a touch session for the default nav region for the display
mQuickStepStartingRotation = mLastRectTouched.mRotation;
@@ -350,11 +414,14 @@
pw.println(" lastTouchedRegion=" + mLastRectTouched);
pw.println(" multipleRegionsEnabled=" + mEnableMultipleRegions);
StringBuilder regions = new StringBuilder(" currentTouchableRotations=");
- for(int i = 0; i < mSwipeTouchRegions.size(); i++) {
- OrientationRectF rectF = mSwipeTouchRegions.get(mSwipeTouchRegions.keyAt(i));
- regions.append(rectF.mRotation).append(" ");
+ for (CurrentDisplay key: mSwipeTouchRegions.keySet()) {
+ OrientationRectF rectF = mSwipeTouchRegions.get(key);
+ regions.append(rectF).append(" ");
}
pw.println(regions.toString());
+ pw.println(" mNavBarGesturalHeight=" + mNavBarGesturalHeight);
+ pw.println(" mNavBarLargerGesturalHeight=" + mNavBarLargerGesturalHeight);
+ pw.println(" mOneHandedModeRegion=" + mOneHandedModeRegion);
}
private class OrientationRectF extends RectF {
@@ -386,13 +453,14 @@
boolean applyTransform(MotionEvent event, boolean forceTransform) {
mTmpMatrix.reset();
- postDisplayRotation(deltaRotation(mCurrentDisplayRotation, mRotation),
+ postDisplayRotation(deltaRotation(mCurrentDisplay.rotation, mRotation),
mHeight, mWidth, mTmpMatrix);
if (forceTransform) {
if (DEBUG) {
Log.d(TAG, "Transforming rotation due to forceTransform, "
- + "mCurrentRotation: " + mCurrentDisplayRotation
- + "mRotation: " + mRotation);
+ + "mCurrentRotation: " + mCurrentDisplay.rotation
+ + "mRotation: " + mRotation
+ + " this: " + this);
}
event.transform(mTmpMatrix);
return true;
@@ -403,9 +471,10 @@
if (DEBUG) {
Log.d(TAG, "original: " + event.getX() + ", " + event.getY()
- + " new: " + mTmpPoint[0] + ", " + mTmpPoint[1]
- + " rect: " + this + " forceTransform: " + forceTransform
- + " contains: " + contains(mTmpPoint[0], mTmpPoint[1]));
+ + " new: " + mTmpPoint[0] + ", " + mTmpPoint[1]
+ + " rect: " + this + " forceTransform: " + forceTransform
+ + " contains: " + contains(mTmpPoint[0], mTmpPoint[1])
+ + " this: " + this);
}
if (contains(mTmpPoint[0], mTmpPoint[1])) {
diff --git a/quickstep/src/com/android/quickstep/OverscrollPluginFactory.java b/quickstep/src/com/android/quickstep/OverscrollPluginFactory.java
new file mode 100644
index 0000000..4c261ab
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/OverscrollPluginFactory.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.systemui.plugins.OverscrollPlugin;
+
+/**
+ * Resource overrideable factory for forcing a local overscroll plugin.
+ * Override {@link R.string#overscroll_plugin_factory_class} to set a different class.
+ */
+public class OverscrollPluginFactory implements ResourceBasedOverride {
+ public static final MainThreadInitializedObject<OverscrollPluginFactory> INSTANCE = forOverride(
+ OverscrollPluginFactory.class,
+ R.string.overscroll_plugin_factory_class);
+
+ /**
+ * Get the plugin that is defined locally in launcher, as opposed to a dynamic side loaded one.
+ */
+ public OverscrollPlugin getLocalOverscrollPlugin() {
+ return null;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
new file mode 100644
index 0000000..5d1f908
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -0,0 +1,276 @@
+/*
+ * 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.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.graphics.PointF;
+import android.os.Build;
+import android.os.SystemClock;
+import android.os.Trace;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.util.RunnableList;
+import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
+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.InteractionJankMonitorWrapper;
+
+import java.util.ArrayList;
+
+/**
+ * Helper class to handle various atomic commands for switching between Overview.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class OverviewCommandHelper {
+
+ public static final int TYPE_SHOW = 1;
+ public static final int TYPE_SHOW_NEXT_FOCUS = 2;
+ public static final int TYPE_HIDE = 3;
+ public static final int TYPE_TOGGLE = 4;
+
+ private static final String TRANSITION_NAME = "Transition:toOverview";
+
+ private final TouchInteractionService mService;
+ private final OverviewComponentObserver mOverviewComponentObserver;
+ private final TaskAnimationManager mTaskAnimationManager;
+ private final ArrayList<CommandInfo> mPendingCommands = new ArrayList<>();
+
+ public OverviewCommandHelper(TouchInteractionService service,
+ OverviewComponentObserver observer,
+ TaskAnimationManager taskAnimationManager) {
+ mService = service;
+ mOverviewComponentObserver = observer;
+ mTaskAnimationManager = taskAnimationManager;
+ }
+
+ /**
+ * Called when the command finishes execution.
+ */
+ private void scheduleNextTask(CommandInfo command) {
+ if (!mPendingCommands.isEmpty() && mPendingCommands.get(0) == command) {
+ mPendingCommands.remove(0);
+ executeNext();
+ }
+ }
+
+ /**
+ * Executes the next command from the queue. If the command finishes immediately (returns true),
+ * it continues to execute the next command, until the queue is empty of a command defer's its
+ * completion (returns false).
+ */
+ @UiThread
+ private void executeNext() {
+ if (mPendingCommands.isEmpty()) {
+ return;
+ }
+ CommandInfo cmd = mPendingCommands.get(0);
+ if (executeCommand(cmd)) {
+ scheduleNextTask(cmd);
+ }
+ }
+
+ @UiThread
+ private void addCommand(CommandInfo cmd) {
+ boolean wasEmpty = mPendingCommands.isEmpty();
+ mPendingCommands.add(cmd);
+ if (wasEmpty) {
+ executeNext();
+ }
+ }
+
+ /**
+ * Adds a command to be executed next, after all pending tasks are completed
+ */
+ @BinderThread
+ public void addCommand(int type) {
+ CommandInfo cmd = new CommandInfo(type);
+ MAIN_EXECUTOR.execute(() -> addCommand(cmd));
+ }
+
+ @UiThread
+ public void clearPendingCommands() {
+ mPendingCommands.clear();
+ }
+
+ private TaskView getNextTask(RecentsView view) {
+ final TaskView runningTaskView = view.getRunningTaskView();
+
+ if (runningTaskView == null) {
+ return view.getTaskViewCount() > 0 ? view.getTaskViewAt(0) : null;
+ } else {
+ final TaskView nextTask = view.getNextTaskView();
+ return nextTask != null ? nextTask : runningTaskView;
+ }
+ }
+
+ private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) {
+ RunnableList callbackList = null;
+ if (taskView != null) {
+ taskView.setEndQuickswitchCuj(true);
+ callbackList = taskView.launchTaskAnimated();
+ }
+
+ if (callbackList != null) {
+ callbackList.add(() -> scheduleNextTask(cmd));
+ return false;
+ } else {
+ recents.startHome();
+ return true;
+ }
+ }
+
+ /**
+ * Executes the task and returns true if next task can be executed. If false, then the next
+ * task is deferred until {@link #scheduleNextTask} is called
+ */
+ private <T extends StatefulActivity<?>> boolean executeCommand(CommandInfo cmd) {
+ BaseActivityInterface<?, T> activityInterface =
+ mOverviewComponentObserver.getActivityInterface();
+ RecentsView recents = activityInterface.getVisibleRecentsView();
+ if (recents == null) {
+ if (cmd.type == TYPE_HIDE) {
+ // already hidden
+ return true;
+ }
+ } else {
+ switch (cmd.type) {
+ case TYPE_SHOW:
+ // already visible
+ return true;
+ case TYPE_HIDE: {
+ int currentPage = recents.getNextPage();
+ TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount())
+ ? (TaskView) recents.getPageAt(currentPage)
+ : null;
+ return launchTask(recents, tv, cmd);
+ }
+ case TYPE_TOGGLE:
+ return launchTask(recents, getNextTask(recents), cmd);
+ }
+ }
+
+ if (activityInterface.switchToRecentsIfVisible(() -> scheduleNextTask(cmd))) {
+ // If successfully switched, wait until animation finishes
+ return false;
+ }
+
+ final T activity = activityInterface.getCreatedActivity();
+ if (activity != null) {
+ InteractionJankMonitorWrapper.begin(
+ activity.getRootView(),
+ InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+ }
+
+ GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE);
+ gestureState.setHandlingAtomicEvent(true);
+ AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory()
+ .newHandler(gestureState, cmd.createTime);
+ interactionHandler.setGestureEndCallback(
+ () -> onTransitionComplete(cmd, interactionHandler));
+ interactionHandler.initWhenReady();
+
+ RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() {
+ @Override
+ public void onRecentsAnimationStart(RecentsAnimationController controller,
+ RecentsAnimationTargets targets) {
+ interactionHandler.onGestureEnded(0, new PointF(), new PointF());
+ cmd.removeListener(this);
+ }
+
+ @Override
+ public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+ interactionHandler.onGestureCancelled();
+ cmd.removeListener(this);
+
+ RecentsView createdRecents =
+ activityInterface.getCreatedActivity().getOverviewPanel();
+ if (createdRecents != null) {
+ createdRecents.onRecentsAnimationComplete();
+ }
+ }
+ };
+
+ if (mTaskAnimationManager.isRecentsAnimationRunning()) {
+ cmd.mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(gestureState);
+ cmd.mActiveCallbacks.addListener(interactionHandler);
+ mTaskAnimationManager.notifyRecentsAnimationState(interactionHandler);
+ interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/);
+
+ cmd.mActiveCallbacks.addListener(recentAnimListener);
+ mTaskAnimationManager.notifyRecentsAnimationState(recentAnimListener);
+ } else {
+ Intent intent = new Intent(interactionHandler.getLaunchIntent());
+ intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, gestureState.getGestureId());
+ cmd.mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(
+ gestureState, intent, interactionHandler);
+ interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/);
+ cmd.mActiveCallbacks.addListener(recentAnimListener);
+ }
+
+ Trace.beginAsyncSection(TRANSITION_NAME, 0);
+ return false;
+ }
+
+ private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
+ cmd.removeListener(handler);
+ Trace.endAsyncSection(TRANSITION_NAME, 0);
+
+ if (cmd.type == TYPE_SHOW_NEXT_FOCUS) {
+ RecentsView rv =
+ mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+ if (rv != null) {
+ // Ensure that recents view has focus so that it receives the followup key inputs
+ TaskView taskView = rv.getNextTaskView();
+ if (taskView == null) {
+ if (rv.getTaskViewCount() > 0) {
+ taskView = rv.getTaskViewAt(0);
+ taskView.requestFocus();
+ } else {
+ rv.requestFocus();
+ }
+ } else {
+ taskView.requestFocus();
+ }
+ }
+ }
+ scheduleNextTask(cmd);
+ }
+
+ private static class CommandInfo {
+ public final long createTime = SystemClock.elapsedRealtime();
+ public final int type;
+ RecentsAnimationCallbacks mActiveCallbacks;
+
+ CommandInfo(int type) {
+ this.type = type;
+ }
+
+ void removeListener(RecentsAnimationListener listener) {
+ if (mActiveCallbacks != null) {
+ mActiveCallbacks.removeListener(listener);
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 07f838b..0efe666 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -20,10 +20,10 @@
import static android.content.Intent.ACTION_PACKAGE_CHANGED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+import static com.android.launcher3.Utilities.createHomeIntent;
import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -35,6 +35,8 @@
import android.content.pm.ResolveInfo;
import android.util.SparseIntArray;
+import com.android.launcher3.tracing.OverviewComponentObserverProto;
+import com.android.launcher3.tracing.TouchInteractionServiceProto;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.systemui.shared.system.PackageManagerWrapper;
@@ -73,9 +75,7 @@
public OverviewComponentObserver(Context context, RecentsAnimationDeviceState deviceState) {
mContext = context;
mDeviceState = deviceState;
- mCurrentHomeIntent = new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mCurrentHomeIntent = createHomeIntent();
mMyHomeIntent = new Intent(mCurrentHomeIntent).setPackage(mContext.getPackageName());
ResolveInfo info = context.getPackageManager().resolveActivity(mMyHomeIntent, 0);
ComponentName myHomeComponent =
@@ -111,16 +111,17 @@
if (mDeviceState.isHomeDisabled() != mIsHomeDisabled) {
updateOverviewTargets();
}
+
+ // Notify ALL_APPS touch controller when one handed mode state activated or deactivated
+ if (mDeviceState.isOneHandedModeEnabled()) {
+ mActivityInterface.onOneHandedModeStateChanged(mDeviceState.isOneHandedModeActive());
+ }
}
private void updateOverviewTargets(Intent unused) {
updateOverviewTargets();
}
- public boolean assistantGestureIsConstrained() {
- return (mDeviceState.getSystemUiStateFlags() & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0;
- }
-
/**
* Update overview intent and {@link BaseActivityInterface} based off the current launcher home
* component.
@@ -262,4 +263,17 @@
pw.println(" overviewIntent=" + mOverviewIntent);
pw.println(" homeIntent=" + mCurrentHomeIntent);
}
+
+ /**
+ * Used for winscope tracing, see launcher_trace.proto
+ * @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
+ * @param serviceProto The parent of this proto message.
+ */
+ public void writeToProto(TouchInteractionServiceProto.Builder serviceProto) {
+ OverviewComponentObserverProto.Builder overviewComponentObserver =
+ OverviewComponentObserverProto.newBuilder();
+ overviewComponentObserver.setOverviewActivityStarted(mActivityInterface.isStarted());
+ overviewComponentObserver.setOverviewActivityResumed(mActivityInterface.isResumed());
+ serviceProto.setOverviewComponentObvserver(overviewComponentObserver);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 32268a4..65847f1 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -15,21 +15,32 @@
*/
package com.android.quickstep;
+import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.UserManager;
import android.util.Log;
import com.android.launcher3.BuildConfig;
import com.android.launcher3.MainProcessInitializer;
+import com.android.launcher3.util.Executors;
+import com.android.quickstep.logging.SettingsChangeLogger;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.ThreadedRendererCompat;
@SuppressWarnings("unused")
+@TargetApi(Build.VERSION_CODES.R)
public class QuickstepProcessInitializer extends MainProcessInitializer {
private static final String TAG = "QuickstepProcessInitializer";
+ private static final int SETUP_DELAY_MILLIS = 5000;
- public QuickstepProcessInitializer(Context context) { }
+ public QuickstepProcessInitializer(Context context) {
+ // Fake call to create an instance of InteractionJankMonitor to avoid binder calls during
+ // its initialization during transitions.
+ InteractionJankMonitorWrapper.cancel(-1);
+ }
@Override
protected void init(Context context) {
@@ -51,5 +62,9 @@
// Elevate GPU priority for Quickstep and Remote animations.
ThreadedRendererCompat.setContextPriority(
ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
+
+ // Initialize settings logger after a default timeout
+ Executors.MAIN_EXECUTOR.getHandler()
+ .postDelayed(() -> new SettingsChangeLogger(context), SETUP_DELAY_MILLIS);
}
}
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
new file mode 100644
index 0000000..39af0db
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -0,0 +1,88 @@
+package com.android.quickstep;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.testing.TestInformationHandler;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
+import com.android.quickstep.util.LayoutUtils;
+
+public class QuickstepTestInformationHandler extends TestInformationHandler {
+
+ protected final Context mContext;
+
+ public QuickstepTestInformationHandler(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public Bundle call(String method) {
+ final Bundle response = new Bundle();
+ switch (method) {
+ case TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT: {
+ return getLauncherUIProperty(Bundle::putInt, l -> {
+ final float progress = LauncherState.OVERVIEW.getVerticalProgress(l)
+ - LauncherState.ALL_APPS.getVerticalProgress(l);
+ final float distance = l.getAllAppsController().getShiftRange() * progress;
+ return (int) distance;
+ });
+ }
+
+ case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
+ final float swipeHeight =
+ LayoutUtils.getDefaultSwipeHeight(mContext, mDeviceProfile);
+ response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
+ return response;
+ }
+
+ case TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT: {
+ final float swipeHeight =
+ LayoutUtils.getShelfTrackingDistance(mContext, mDeviceProfile,
+ PagedOrientationHandler.PORTRAIT);
+ response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
+ return response;
+ }
+
+ case TestProtocol.REQUEST_HOTSEAT_TOP: {
+ return getLauncherUIProperty(
+ Bundle::putInt, PortraitStatesTouchController::getHotseatTop);
+ }
+
+ case TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED: {
+ response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+ FeatureFlags.ENABLE_OVERVIEW_SHARE.get());
+ return response;
+ }
+
+ case TestProtocol.REQUEST_OVERVIEW_CONTENT_PUSH_ENABLED: {
+ response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+ FeatureFlags.ENABLE_OVERVIEW_CONTENT_PUSH.get());
+ return response;
+ }
+ }
+
+ return super.call(method);
+ }
+
+ @Override
+ protected Activity getCurrentActivity() {
+ RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
+ OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
+ try {
+ return observer.getActivityInterface().getCreatedActivity();
+ } finally {
+ observer.onDestroy();
+ rads.destroy();
+ }
+ }
+
+ @Override
+ protected boolean isLauncherInitialized() {
+ return super.isLauncherInitialized() && TouchInteractionService.isInitialized();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 70b4f20..3080f04 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -22,15 +22,18 @@
import android.app.ActivityManager;
import android.os.Build;
import android.os.Process;
+import android.util.Log;
import android.util.SparseBooleanArray;
import androidx.annotation.VisibleForTesting;
+import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.LooperExecutor;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.KeyguardManagerCompat;
import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import java.util.ArrayList;
import java.util.Collections;
@@ -51,6 +54,9 @@
// The list change id, increments as the task list changes in the system
private int mChangeId;
+ // Whether we are currently updating the tasks in the background (up to when the result is
+ // posted back on the main thread)
+ private boolean mLoadingTasksInBackground;
private TaskLoadResult mResultsBg = INVALID_RESULT;
private TaskLoadResult mResultsUi = INVALID_RESULT;
@@ -61,7 +67,12 @@
mKeyguardManager = keyguardManager;
mChangeId = 1;
mActivityManagerWrapper = activityManagerWrapper;
- mActivityManagerWrapper.registerTaskStackListener(this);
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+ }
+
+ @VisibleForTesting
+ public boolean isLoadingTasksInBackground() {
+ return mLoadingTasksInBackground;
}
/**
@@ -90,19 +101,23 @@
if (callback != null) {
// Copy synchronously as the changeId might change by next frame
ArrayList<Task> result = copyOf(mResultsUi);
- mMainThreadExecutor.post(() -> callback.accept(result));
+ mMainThreadExecutor.post(() -> {
+ callback.accept(result);
+ });
}
return requestLoadId;
}
// Kick off task loading in the background
+ mLoadingTasksInBackground = true;
UI_HELPER_EXECUTOR.execute(() -> {
if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
}
TaskLoadResult loadResult = mResultsBg;
mMainThreadExecutor.execute(() -> {
+ mLoadingTasksInBackground = false;
mResultsUi = loadResult;
if (callback != null) {
ArrayList<Task> result = copyOf(mResultsUi);
@@ -191,6 +206,7 @@
} else {
task = new Task(taskKey);
}
+ task.setLastSnapshotData(rawTask);
allTasks.add(task);
}
@@ -200,9 +216,7 @@
private ArrayList<Task> copyOf(ArrayList<Task> tasks) {
ArrayList<Task> newTasks = new ArrayList<>();
for (int i = 0; i < tasks.size(); i++) {
- Task t = tasks.get(i);
- newTasks.add(new Task(t.key, t.colorPrimary, t.colorBackground, t.isDockable,
- t.isLocked, t.taskDescription, t.topActivity));
+ newTasks.add(new Task(tasks.get(i)));
}
return newTasks;
}
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
new file mode 100644
index 0000000..9dfcd12
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
+import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
+
+import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_PRE_DELAY;
+import static com.android.launcher3.Utilities.createHomeIntent;
+import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
+import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
+import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
+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.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.SurfaceControl.Transaction;
+import android.view.View;
+import android.window.SplashScreen;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAnimationRunner;
+import com.android.launcher3.LauncherAnimationRunner.AnimationResult;
+import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.ActivityTracker;
+import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.SystemUiController;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.views.ScrimView;
+import com.android.quickstep.fallback.FallbackRecentsStateController;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsDragLayer;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.RecentsAtomicAnimationFactory;
+import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.views.OverviewActionsView;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.SplitPlaceholderView;
+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.RemoteAnimationTargetCompat;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * A recents activity that shows the recently launched tasks as swipable task cards.
+ * See {@link com.android.quickstep.views.RecentsView}.
+ */
+public final class RecentsActivity extends StatefulActivity<RecentsState> {
+
+ public static final ActivityTracker<RecentsActivity> ACTIVITY_TRACKER =
+ new ActivityTracker<>();
+
+ private Handler mUiHandler = new Handler(Looper.getMainLooper());
+
+ private static final long HOME_APPEAR_DURATION = 250;
+
+ private RecentsDragLayer mDragLayer;
+ private ScrimView mScrimView;
+ private FallbackRecentsView mFallbackRecentsView;
+ private OverviewActionsView mActionsView;
+
+ private Configuration mOldConfig;
+
+ private StateManager<RecentsState> mStateManager;
+
+ // Strong refs to runners which are cleared when the activity is destroyed
+ private RemoteAnimationFactory mActivityLaunchAnimationRunner;
+
+ /**
+ * Init drag layer and overview panel views.
+ */
+ protected void setupViews() {
+ inflateRootView(R.layout.fallback_recents_activity);
+ setContentView(getRootView());
+ mDragLayer = findViewById(R.id.drag_layer);
+ mScrimView = findViewById(R.id.scrim_view);
+ mFallbackRecentsView = findViewById(R.id.overview_panel);
+ mActionsView = findViewById(R.id.overview_actions_view);
+ SYSUI_PROGRESS.set(getRootView().getSysUiScrim(), 0f);
+
+ SplitPlaceholderView splitPlaceholderView = findViewById(R.id.split_placeholder);
+ splitPlaceholderView.init(
+ new SplitSelectStateController(mUiHandler, SystemUiProxy.INSTANCE.get(this))
+ );
+
+ mDragLayer.recreateControllers();
+ mFallbackRecentsView.init(mActionsView, splitPlaceholderView);
+ }
+
+ @Override
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
+ onHandleConfigChanged();
+ super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ ACTIVITY_TRACKER.handleNewIntent(this);
+ }
+
+ /**
+ * Logic for when device configuration changes (rotation, screen size change, multi-window,
+ * etc.)
+ */
+ protected void onHandleConfigChanged() {
+ initDeviceProfile();
+
+ AbstractFloatingView.closeOpenViews(this, true,
+ AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
+ dispatchDeviceProfileChanged();
+
+ reapplyUi();
+ mDragLayer.recreateControllers();
+ }
+
+ /**
+ * Generate the device profile to use in this activity.
+ * @return device profile
+ */
+ protected DeviceProfile createDeviceProfile() {
+ DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this);
+
+ // In case we are reusing IDP, create a copy so that we don't conflict with Launcher
+ // activity.
+ return (mDragLayer != null) && isInMultiWindowMode()
+ ? dp.getMultiWindowProfile(this, getMultiWindowDisplaySize())
+ : dp.copy(this);
+ }
+
+ @Override
+ public BaseDragLayer getDragLayer() {
+ return mDragLayer;
+ }
+
+ public ScrimView getScrimView() {
+ return mScrimView;
+ }
+
+ @Override
+ public <T extends View> T getOverviewPanel() {
+ return (T) mFallbackRecentsView;
+ }
+
+ public OverviewActionsView getActionsView() {
+ return mActionsView;
+ }
+
+ @Override
+ public void returnToHomescreen() {
+ super.returnToHomescreen();
+ // TODO(b/137318995) This should go home, but doing so removes freeform windows
+ }
+
+ @Override
+ public ActivityOptionsWrapper getActivityLaunchOptions(final View v, @Nullable ItemInfo item) {
+ if (!(v instanceof TaskView)) {
+ return super.getActivityLaunchOptions(v, item);
+ }
+
+ final TaskView taskView = (TaskView) v;
+ RunnableList onEndCallback = new RunnableList();
+
+ mActivityLaunchAnimationRunner = new RemoteAnimationFactory() {
+ @Override
+ public void onCreateAnimation(int transit, RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets, AnimationResult result) {
+ AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
+ wallpaperTargets, nonAppTargets);
+ anim.addListener(resetStateListener());
+ result.setAnimation(anim, RecentsActivity.this, onEndCallback::executeAllAndDestroy,
+ true /* skipFirstFrame */);
+ }
+
+ @Override
+ public void onAnimationCancelled() {
+ onEndCallback.executeAllAndDestroy();
+ }
+ };
+
+ final LauncherAnimationRunner wrapper = new LauncherAnimationRunner(
+ mUiHandler, mActivityLaunchAnimationRunner, true /* startAtFrontOfQueue */);
+ RemoteAnimationAdapterCompat adapterCompat = new RemoteAnimationAdapterCompat(
+ wrapper, RECENTS_LAUNCH_DURATION,
+ RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
+ - STATUS_BAR_TRANSITION_PRE_DELAY);
+ final ActivityOptionsWrapper activityOptions = new ActivityOptionsWrapper(
+ ActivityOptionsCompat.makeRemoteAnimation(adapterCompat),
+ onEndCallback);
+ activityOptions.options.setSplashscreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
+ return activityOptions;
+ }
+
+ /**
+ * Composes the animations for a launch from the recents list if possible.
+ */
+ private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets) {
+ AnimatorSet target = new AnimatorSet();
+ boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
+ PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
+ createRecentsWindowAnimator(taskView, !activityClosing, appTargets,
+ wallpaperTargets, nonAppTargets, null /* depthController */, pa);
+ target.play(pa.buildAnim());
+
+ // Found a visible recents task that matches the opening app, lets launch the app from there
+ if (activityClosing) {
+ Animator adjacentAnimation = mFallbackRecentsView
+ .createAdjacentPageAnimForTaskLaunch(taskView);
+ adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+ adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION);
+ adjacentAnimation.addListener(resetStateListener());
+ target.play(adjacentAnimation);
+ }
+ return target;
+ }
+
+ @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();
+ mFallbackRecentsView.updateLocusId();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ // Workaround for b/78520668, explicitly trim memory once UI is hidden
+ onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
+ mFallbackRecentsView.updateLocusId();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ AccessibilityManagerCompat.sendStateEventToTest(getBaseContext(), OVERVIEW_STATE_ORDINAL);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mStateManager = new StateManager<>(this, RecentsState.DEFAULT);
+
+ mOldConfig = new Configuration(getResources().getConfiguration());
+ initDeviceProfile();
+ setupViews();
+
+ getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
+ Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
+ ACTIVITY_TRACKER.handleCreate(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 onStateSetEnd(RecentsState state) {
+ super.onStateSetEnd(state);
+
+ if (state == RecentsState.DEFAULT) {
+ AccessibilityManagerCompat.sendStateEventToTest(getBaseContext(),
+ OVERVIEW_STATE_ORDINAL);
+ }
+ }
+
+ /**
+ * Initialize/update the device profile.
+ */
+ private void initDeviceProfile() {
+ mDeviceProfile = createDeviceProfile();
+ onDeviceProfileInitiated();
+ }
+
+ @Override
+ public void onEnterAnimationComplete() {
+ super.onEnterAnimationComplete();
+ // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
+ // as a part of quickstep, so that high-res thumbnails can load the next time we enter
+ // overview
+ RecentsModel.INSTANCE.get(this).getThumbnailCache()
+ .getHighResLoadingState().setVisible(true);
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ super.onTrimMemory(level);
+ RecentsModel.INSTANCE.get(this).onTrimMemory(level);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ ACTIVITY_TRACKER.onActivityDestroyed(this);
+ mActivityLaunchAnimationRunner = null;
+ }
+
+ @Override
+ public void onBackPressed() {
+ // TODO: Launch the task we came from
+ startHome();
+ }
+
+ public void startHome() {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ RecentsView recentsView = getOverviewPanel();
+ recentsView.switchToScreenshot(() -> recentsView.finishRecentsAnimation(true,
+ this::startHomeInternal));
+ } else {
+ startHomeInternal();
+ }
+ }
+
+ private void startHomeInternal() {
+ LauncherAnimationRunner runner = new LauncherAnimationRunner(
+ getMainThreadHandler(), mAnimationToHomeFactory, true);
+ RemoteAnimationAdapterCompat adapterCompat =
+ new RemoteAnimationAdapterCompat(runner, HOME_APPEAR_DURATION, 0);
+ startActivity(createHomeIntent(),
+ ActivityOptionsCompat.makeRemoteAnimation(adapterCompat).toBundle());
+ }
+
+ private final RemoteAnimationFactory mAnimationToHomeFactory =
+ new RemoteAnimationFactory() {
+ @Override
+ public void onCreateAnimation(int transit, RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets, AnimationResult result) {
+ AnimatorPlaybackController controller = getStateManager()
+ .createAnimationToNewWorkspace(RecentsState.BG_LAUNCHER, HOME_APPEAR_DURATION);
+ controller.dispatchOnStart();
+
+ RemoteAnimationTargets targets = new RemoteAnimationTargets(
+ appTargets, wallpaperTargets, nonAppTargets, MODE_OPENING);
+ for (RemoteAnimationTargetCompat app : targets.apps) {
+ new Transaction().setAlpha(app.leash.getSurfaceControl(), 1).apply();
+ }
+ AnimatorSet anim = new AnimatorSet();
+ anim.play(controller.getAnimationPlayer());
+ anim.setDuration(HOME_APPEAR_DURATION);
+ result.setAnimation(anim, RecentsActivity.this,
+ () -> getStateManager().goToState(RecentsState.HOME, false),
+ true /* skipFirstFrame */);
+ }
+ };
+
+ @Override
+ protected void collectStateHandlers(List<StateHandler> out) {
+ out.add(new FallbackRecentsStateController(this));
+ }
+
+ @Override
+ public StateManager<RecentsState> getStateManager() {
+ return mStateManager;
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.println(prefix + "Misc:");
+ dumpMisc(prefix + "\t", writer);
+ }
+
+ @Override
+ public AtomicAnimationFactory<RecentsState> createAtomicAnimationFactory() {
+ return new RecentsAtomicAnimationFactory<>(this);
+ }
+
+ private AnimatorListenerAdapter resetStateListener() {
+ return new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mFallbackRecentsView.resetTaskVisuals();
+ mStateManager.reapplyState();
+ }
+ };
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 4e9aa61..53b6675 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -15,49 +15,38 @@
*/
package com.android.quickstep;
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_UP;
-
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
+import android.view.IRecentsAnimationController;
+import android.view.SurfaceControl;
+import android.window.PictureInPictureSurfaceTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.RunnableList;
import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import java.util.function.Consumer;
-import java.util.function.Supplier;
/**
* Wrapper around RecentsAnimationControllerCompat to help with some synchronization
*/
public class RecentsAnimationController {
- private static final String TAG = "RecentsAnimationController";
-
private final RecentsAnimationControllerCompat mController;
private final Consumer<RecentsAnimationController> mOnFinishedListener;
private final boolean mAllowMinimizeSplitScreen;
- private InputConsumerController mInputConsumerController;
- private Supplier<InputConsumer> mInputProxySupplier;
- private InputConsumer mInputConsumer;
private boolean mUseLauncherSysBarFlags = false;
private boolean mSplitScreenMinimized = false;
- private boolean mTouchInProgress;
- private boolean mDisableInputProxyPending;
+ private boolean mFinishRequested = false;
+ private RunnableList mPendingFinishCallbacks = new RunnableList();
public RecentsAnimationController(RecentsAnimationControllerCompat controller,
boolean allowMinimizeSplitScreen,
@@ -108,40 +97,22 @@
}
/**
- * Notifies the controller that we want to defer cancel until the next app transition starts.
- * If {@param screenshot} is set, then we will receive a screenshot on the next
- * {@link RecentsAnimationCallbacks#onAnimationCanceled(ThumbnailData)} and we must also call
- * {@link #cleanupScreenshot()} when that screenshot is no longer used.
- */
- public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
- mController.setDeferCancelUntilNextTransition(defer, screenshot);
- }
-
- /**
- * Cleans up the screenshot previously returned from
- * {@link RecentsAnimationCallbacks#onAnimationCanceled(ThumbnailData)}.
- */
- public void cleanupScreenshot() {
- UI_HELPER_EXECUTOR.execute(() -> mController.cleanupScreenshot());
- }
-
- /**
* Remove task remote animation target from
* {@link RecentsAnimationCallbacks#onTaskAppeared(RemoteAnimationTargetCompat)}}.
*/
@UiThread
- public boolean removeTaskTarget(@NonNull RemoteAnimationTargetCompat target) {
- return mController.removeTask(target.taskId);
+ public void removeTaskTarget(@NonNull RemoteAnimationTargetCompat target) {
+ UI_HELPER_EXECUTOR.execute(() -> mController.removeTask(target.taskId));
}
@UiThread
public void finishAnimationToHome() {
- finishAndDisableInputProxy(true /* toRecents */, null, false /* sendUserLeaveHint */);
+ finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
}
@UiThread
public void finishAnimationToApp() {
- finishAndDisableInputProxy(false /* toRecents */, null, false /* sendUserLeaveHint */);
+ finishController(false /* toRecents */, null, false /* sendUserLeaveHint */);
}
/** See {@link #finish(boolean, Runnable, boolean)} */
@@ -160,34 +131,79 @@
@UiThread
public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
Preconditions.assertUIThread();
- if (toRecents && mTouchInProgress) {
- // Finish the controller as requested, but don't disable input proxy yet.
- mDisableInputProxyPending = true;
- finishController(toRecents, onFinishComplete, sendUserLeaveHint);
- } else {
- finishAndDisableInputProxy(toRecents, onFinishComplete, sendUserLeaveHint);
- }
- }
-
- private void finishAndDisableInputProxy(boolean toRecents, Runnable onFinishComplete,
- boolean sendUserLeaveHint) {
- disableInputProxy();
finishController(toRecents, onFinishComplete, sendUserLeaveHint);
}
@UiThread
public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
+ if (mFinishRequested) {
+ // If finishing, add to pending finish callbacks, otherwise, if finished, adding to the
+ // destroyed RunnableList will just trigger the callback to be called immediately
+ mPendingFinishCallbacks.add(callback);
+ return;
+ }
+
+ // Finish not yet requested
+ mFinishRequested = true;
mOnFinishedListener.accept(this);
+ mPendingFinishCallbacks.add(callback);
UI_HELPER_EXECUTOR.execute(() -> {
- mController.setInputConsumerEnabled(false);
mController.finish(toRecents, sendUserLeaveHint);
- if (callback != null) {
- MAIN_EXECUTOR.execute(callback);
- }
+ InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+ InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
+ MAIN_EXECUTOR.execute(mPendingFinishCallbacks::executeAllAndDestroy);
});
}
/**
+ * @see IRecentsAnimationController#cleanupScreenshot()
+ */
+ @UiThread
+ public void cleanupScreenshot() {
+ UI_HELPER_EXECUTOR.execute(() -> mController.cleanupScreenshot());
+ }
+
+ /**
+ * @see RecentsAnimationControllerCompat#detachNavigationBarFromApp
+ */
+ @UiThread
+ public void detachNavigationBarFromApp(boolean moveHomeToTop) {
+ UI_HELPER_EXECUTOR.execute(() -> mController.detachNavigationBarFromApp(moveHomeToTop));
+ }
+
+ /**
+ * @see IRecentsAnimationController#animateNavigationBarToApp(long)
+ */
+ @UiThread
+ public void animateNavigationBarToApp(long duration) {
+ UI_HELPER_EXECUTOR.execute(() -> mController.animateNavigationBarToApp(duration));
+ }
+
+ /**
+ * @see IRecentsAnimationController#setWillFinishToHome(boolean)
+ */
+ @UiThread
+ public void setWillFinishToHome(boolean willFinishToHome) {
+ UI_HELPER_EXECUTOR.execute(() -> mController.setWillFinishToHome(willFinishToHome));
+ }
+
+ /**
+ * Sets the final surface transaction on a Task. This is used by Launcher to notify the system
+ * that animating Activity to PiP has completed and the associated task surface should be
+ * updated accordingly. This should be called before `finish`
+ * @param taskId for which the leash should be updated
+ * @param finishTransaction the transaction to transfer to the task surface control after the
+ * leash is removed
+ * @param overlay the surface control for an overlay being shown above the pip (can be null)
+ */
+ public void setFinishTaskTransaction(int taskId,
+ PictureInPictureSurfaceTransaction finishTransaction,
+ SurfaceControl overlay) {
+ UI_HELPER_EXECUTOR.execute(
+ () -> mController.setFinishTaskTransaction(taskId, finishTransaction, overlay));
+ }
+
+ /**
* Enables the input consumer to start intercepting touches in the app window.
*/
public void enableInputConsumer() {
@@ -197,75 +213,8 @@
});
}
- public void enableInputProxy(InputConsumerController inputConsumerController,
- Supplier<InputConsumer> inputProxySupplier) {
- mInputProxySupplier = inputProxySupplier;
- mInputConsumerController = inputConsumerController;
- mInputConsumerController.setInputListener(this::onInputConsumerEvent);
- }
-
/** @return wrapper controller. */
public RecentsAnimationControllerCompat getController() {
return mController;
}
-
- private void disableInputProxy() {
- if (mInputConsumer != null && mTouchInProgress) {
- long now = SystemClock.uptimeMillis();
- MotionEvent dummyCancel = MotionEvent.obtain(now, now, ACTION_CANCEL, 0, 0, 0);
- mInputConsumer.onMotionEvent(dummyCancel);
- dummyCancel.recycle();
- }
- if (mInputConsumerController != null) {
- mInputConsumerController.setInputListener(null);
- }
- mInputProxySupplier = null;
- }
-
- private boolean onInputConsumerEvent(InputEvent ev) {
- if (ev instanceof MotionEvent) {
- onInputConsumerMotionEvent((MotionEvent) ev);
- } else if (ev instanceof KeyEvent) {
- if (mInputConsumer == null) {
- mInputConsumer = mInputProxySupplier.get();
- }
- mInputConsumer.onKeyEvent((KeyEvent) ev);
- return true;
- }
- return false;
- }
-
- private boolean onInputConsumerMotionEvent(MotionEvent ev) {
- int action = ev.getAction();
-
- // Just to be safe, verify that ACTION_DOWN comes before any other action,
- // and ignore any ACTION_DOWN after the first one (though that should not happen).
- if (!mTouchInProgress && action != ACTION_DOWN) {
- Log.w(TAG, "Received non-down motion before down motion: " + action);
- return false;
- }
- if (mTouchInProgress && action == ACTION_DOWN) {
- Log.w(TAG, "Received down motion while touch was already in progress");
- return false;
- }
-
- if (action == ACTION_DOWN) {
- mTouchInProgress = true;
- if (mInputConsumer == null) {
- mInputConsumer = mInputProxySupplier.get();
- }
- } else if (action == ACTION_CANCEL || action == ACTION_UP) {
- // Finish any pending actions
- mTouchInProgress = false;
- if (mDisableInputProxyPending) {
- mDisableInputProxyPending = false;
- disableInputProxy();
- }
- }
- if (mInputConsumer != null) {
- mInputConsumer.onMotionEvent(ev);
- }
-
- return true;
- }
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 0a70bd6..444d77a 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -15,28 +15,35 @@
*/
package com.android.quickstep;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.Intent.ACTION_USER_UNLOCKED;
-import static android.view.Surface.ROTATION_0;
-import static com.android.launcher3.util.DefaultDisplay.CHANGE_ALL;
-import static com.android.launcher3.util.DefaultDisplay.CHANGE_FRAME_DELAY;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
+import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
+import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED;
+import static com.android.launcher3.util.SettingsCache.ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED;
import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -44,28 +51,36 @@
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.Region;
+import android.net.Uri;
import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemProperties;
import android.os.UserManager;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
import android.view.MotionEvent;
-import android.view.OrientationEventListener;
+import android.view.Surface;
import androidx.annotation.BinderThread;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
+import com.android.launcher3.util.DisplayController.Info;
+import com.android.launcher3.util.SettingsCache;
import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.SysUINavigationMode.OneHandedModeChangeListener;
import com.android.quickstep.util.NavBarPosition;
-import com.android.quickstep.util.RecentsOrientedState;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -77,13 +92,18 @@
*/
public class RecentsAnimationDeviceState implements
NavigationModeChangeListener,
- DefaultDisplay.DisplayInfoChangeListener {
+ DisplayInfoChangeListener,
+ OneHandedModeChangeListener {
+
+ static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
private final Context mContext;
private final SysUINavigationMode mSysUiNavMode;
- private final DefaultDisplay mDefaultDisplay;
+ private final DisplayController mDisplayController;
private final int mDisplayId;
- private int mDisplayRotation;
+ private final RotationTouchHelper mRotationTouchHelper;
+ private final TaskStackChangeListener mPipListener;
+ private final List<ComponentName> mGestureBlockedActivities;
private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
@@ -94,6 +114,11 @@
private final Region mDeferredGestureRegion = new Region();
private boolean mAssistantAvailable;
private float mAssistantVisibility;
+ private boolean mIsUserSetupComplete;
+ private boolean mIsOneHandedModeEnabled;
+ private boolean mIsSwipeToNotificationEnabled;
+ private final boolean mIsOneHandedModeSupported;
+ private boolean mPipIsActive;
private boolean mIsUserUnlocked;
private final ArrayList<Runnable> mUserUnlockedActions = new ArrayList<>();
@@ -107,85 +132,31 @@
}
};
- private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
- @Override
- public void onRecentTaskListFrozenChanged(boolean frozen) {
- mTaskListFrozen = frozen;
- if (frozen || mInOverview) {
- return;
- }
- enableMultipleRegions(false);
- }
-
- @Override
- public void onActivityRotation(int displayId) {
- super.onActivityRotation(displayId);
- // This always gets called before onDisplayInfoChanged() so we know how to process
- // the rotation in that method. This is done to avoid having a race condition between
- // the sensor readings and onDisplayInfoChanged() call
- if (displayId != mDisplayId) {
- return;
- }
-
- mPrioritizeDeviceRotation = true;
- if (mInOverview) {
- // reset, launcher must be rotating
- mExitOverviewRunnable.run();
- }
- }
- };
-
- private Runnable mExitOverviewRunnable = new Runnable() {
- @Override
- public void run() {
- mInOverview = false;
- enableMultipleRegions(false);
- }
- };
-
- private OrientationTouchTransformer mOrientationTouchTransformer;
- /**
- * Used to listen for when the device rotates into the orientation of the current
- * foreground app. For example, if a user quickswitches from a portrait to a fixed landscape
- * app and then rotates rotates the device to match that orientation, this triggers calls to
- * sysui to adjust the navbar.
- */
- private OrientationEventListener mOrientationListener;
- private int mSensorRotation = ROTATION_0;
- /**
- * This is the configuration of the foreground app or the app that will be in the foreground
- * once a quickstep gesture finishes.
- */
- private int mCurrentAppRotation = -1;
- /**
- * This flag is set to true when the device physically changes orientations. When true,
- * we will always report the current rotation of the foreground app whenever the display
- * changes, as it would indicate the user's intention to rotate the foreground app.
- */
- private boolean mPrioritizeDeviceRotation = false;
-
private Region mExclusionRegion;
private SystemGestureExclusionListenerCompat mExclusionListener;
- private final List<ComponentName> mGestureBlockedActivities;
- private Runnable mOnDestroyFrozenTaskRunnable;
- /**
- * Set to true when user swipes to recents. In recents, we ignore the state of the recents
- * task list being frozen or not to allow the user to keep interacting with nav bar rotation
- * they went into recents with as opposed to defaulting to the default display rotation.
- * TODO: (b/156984037) For when user rotates after entering overview
- */
- private boolean mInOverview;
- private boolean mTaskListFrozen;
-
- private boolean mIsUserSetupComplete;
-
public RecentsAnimationDeviceState(Context context) {
+ this(context, false);
+ }
+
+ /**
+ * @param isInstanceForTouches {@code true} if this is the persistent instance being used for
+ * gesture touch handling
+ */
+ public RecentsAnimationDeviceState(Context context, boolean isInstanceForTouches) {
mContext = context;
+ mDisplayController = DisplayController.INSTANCE.get(context);
mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
- mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
- mDisplayId = mDefaultDisplay.getInfo().id;
- runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
+ mDisplayId = mDisplayController.getInfo().id;
+ mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
+ runOnDestroy(() -> mDisplayController.removeChangeListener(this));
+ mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
+ if (isInstanceForTouches) {
+ // rotationTouchHelper doesn't get initialized after being destroyed, so only destroy
+ // if primary TouchInteractionService instance needs to be destroyed.
+ mRotationTouchHelper.init();
+ runOnDestroy(mRotationTouchHelper::destroy);
+ }
// Register for user unlocked if necessary
mIsUserUnlocked = context.getSystemService(UserManager.class)
@@ -207,10 +178,6 @@
};
runOnDestroy(mExclusionListener::unregister);
- Resources resources = mContext.getResources();
- mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
- () -> QuickStepContract.getWindowCornerRadius(resources));
-
// Register for navigation mode changes
onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
@@ -231,48 +198,54 @@
}
}
- SecureSettingsObserver userSetupObserver = new SecureSettingsObserver(
- context.getContentResolver(),
- e -> mIsUserSetupComplete = e,
- Settings.Secure.USER_SETUP_COMPLETE,
- 0);
- mIsUserSetupComplete = userSetupObserver.getValue();
- if (!mIsUserSetupComplete) {
- userSetupObserver.register();
- runOnDestroy(userSetupObserver::unregister);
+ SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
+ if (mIsOneHandedModeSupported) {
+ Uri oneHandedUri = Settings.Secure.getUriFor(ONE_HANDED_ENABLED);
+ SettingsCache.OnChangeListener onChangeListener =
+ enabled -> mIsOneHandedModeEnabled = enabled;
+ settingsCache.register(oneHandedUri, onChangeListener);
+ mIsOneHandedModeEnabled = settingsCache.getValue(oneHandedUri);
+ runOnDestroy(() -> settingsCache.unregister(oneHandedUri, onChangeListener));
+ } else {
+ mIsOneHandedModeEnabled = false;
}
- mOrientationListener = new OrientationEventListener(context) {
+ Uri swipeBottomNotificationUri =
+ Settings.Secure.getUriFor(ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED);
+ SettingsCache.OnChangeListener onChangeListener =
+ enabled -> mIsSwipeToNotificationEnabled = enabled;
+ settingsCache.register(swipeBottomNotificationUri, onChangeListener);
+ mIsSwipeToNotificationEnabled = settingsCache.getValue(swipeBottomNotificationUri);
+ runOnDestroy(() -> settingsCache.unregister(swipeBottomNotificationUri, onChangeListener));
+
+ Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
+ mIsUserSetupComplete = settingsCache.getValue(setupCompleteUri, 0);
+ if (!mIsUserSetupComplete) {
+ SettingsCache.OnChangeListener userSetupChangeListener = e -> mIsUserSetupComplete = e;
+ settingsCache.register(setupCompleteUri, userSetupChangeListener);
+ runOnDestroy(() -> settingsCache.unregister(setupCompleteUri, userSetupChangeListener));
+ }
+
+ try {
+ mPipIsActive = ActivityTaskManager.getService().getRootTaskInfo(
+ WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED) != null;
+ } catch (RemoteException e) {
+ // Do nothing
+ }
+ mPipListener = new TaskStackChangeListener() {
@Override
- public void onOrientationChanged(int degrees) {
- int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
- mSensorRotation);
- if (newRotation == mSensorRotation) {
- return;
- }
+ public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
+ mPipIsActive = true;
+ }
- mSensorRotation = newRotation;
- mPrioritizeDeviceRotation = true;
-
- if (newRotation == mCurrentAppRotation) {
- // When user rotates device to the orientation of the foreground app after
- // quickstepping
- toggleSecondaryNavBarsForRotation();
- }
+ @Override
+ public void onActivityUnpinned() {
+ mPipIsActive = false;
}
};
- }
-
- private void setupOrientationSwipeHandler() {
- ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
- mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
- .unregisterTaskStackListener(mFrozenTaskListener);
- runOnDestroy(mOnDestroyFrozenTaskRunnable);
- }
-
- private void destroyOrientationSwipeHandlerCallback() {
- ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
- mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mPipListener);
+ runOnDestroy(() ->
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mPipListener));
}
private void runOnDestroy(Runnable action) {
@@ -297,11 +270,20 @@
runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(listener));
}
+ /**
+ * Adds a listener for the one handed mode change,
+ * guaranteed to be called after the device state's mode has changed.
+ */
+ public void addOneHandedModeChangedCallback(OneHandedModeChangeListener listener) {
+ listener.onOneHandedModeChanged(mSysUiNavMode.addOneHandedOverlayChangeListener(listener));
+ runOnDestroy(() -> mSysUiNavMode.removeOneHandedOverlayChangeListener(listener));
+ }
+
@Override
public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
- mDefaultDisplay.removeChangeListener(this);
- mDefaultDisplay.addChangeListener(this);
- onDisplayInfoChanged(mDefaultDisplay.getInfo(), CHANGE_ALL);
+ mDisplayController.removeChangeListener(this);
+ mDisplayController.addChangeListener(this);
+ onDisplayInfoChanged(mContext, mDisplayController.getInfo(), CHANGE_ALL);
if (newMode == NO_BUTTON) {
mExclusionListener.register();
@@ -309,47 +291,20 @@
mExclusionListener.unregister();
}
- mNavBarPosition = new NavBarPosition(newMode, mDefaultDisplay.getInfo());
-
- mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo());
- if (!mMode.hasGestures && newMode.hasGestures) {
- setupOrientationSwipeHandler();
- } else if (mMode.hasGestures && !newMode.hasGestures){
- destroyOrientationSwipeHandlerCallback();
- }
-
+ mNavBarPosition = new NavBarPosition(newMode, mDisplayController.getInfo());
mMode = newMode;
}
@Override
- public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
- if (info.id != getDisplayId() || flags == CHANGE_FRAME_DELAY) {
- // ignore displays that aren't running launcher and frame refresh rate changes
- return;
+ public void onDisplayInfoChanged(Context context, Info info, int flags) {
+ if ((flags & CHANGE_ROTATION) != 0) {
+ mNavBarPosition = new NavBarPosition(mMode, info);
}
+ }
- mDisplayRotation = info.rotation;
-
- if (!mMode.hasGestures) {
- return;
- }
- mNavBarPosition = new NavBarPosition(mMode, info);
- updateGestureTouchRegions();
- mOrientationTouchTransformer.createOrAddTouchRegion(info);
- mCurrentAppRotation = mDisplayRotation;
-
- /* Update nav bars on the following:
- * a) if this is coming from an activity rotation OR
- * aa) we launch an app in the orientation that user is already in
- * b) We're not in overview, since overview will always be portrait (w/o home rotation)
- * c) We're actively in quickswitch mode
- */
- if ((mPrioritizeDeviceRotation
- || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
- && !mInOverview
- && mTaskListFrozen) {
- toggleSecondaryNavBarsForRotation();
- }
+ @Override
+ public void onOneHandedModeChanged(int newGesturalHeight) {
+ mRotationTouchHelper.setGesturalHeight(newGesturalHeight);
}
/**
@@ -377,7 +332,14 @@
* @return whether the current nav mode has some gestures (either 2 or 0 button mode).
*/
public boolean isGesturalNavMode() {
- return mMode == TWO_BUTTONS || mMode == NO_BUTTON;
+ return mMode.hasGestures;
+ }
+
+ /**
+ * @return whether the current nav mode is 2-button-based.
+ */
+ public boolean isTwoButtonNavMode() {
+ return mMode == TWO_BUTTONS;
}
/**
@@ -455,7 +417,7 @@
* @return the system ui state flags.
*/
// TODO(141886704): See if we can remove this
- public @SystemUiStateFlags int getSystemUiStateFlags() {
+ public int getSystemUiStateFlags() {
return mSystemUiStateFlags;
}
@@ -464,10 +426,12 @@
*/
public boolean canStartSystemGesture() {
boolean canStartWithNavHidden = (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
- || mTaskListFrozen;
+ || (mSystemUiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0
+ || mRotationTouchHelper.isTaskListFrozen();
return canStartWithNavHidden
&& (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
&& (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
+ && (mSystemUiStateFlags & SYSUI_STATE_MAGNIFICATION_OVERLAP) == 0
&& ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
|| (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0);
}
@@ -488,6 +452,13 @@
}
/**
+ * @return whether assistant gesture is constraint
+ */
+ public boolean isAssistantGestureIsConstrained() {
+ return (mSystemUiStateFlags & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0;
+ }
+
+ /**
* @return whether the bubble stack is expanded
*/
public boolean isBubblesExpanded() {
@@ -537,30 +508,10 @@
}
/**
- * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
+ * @return whether one-handed mode is enabled and active
*/
- public void updateGestureTouchRegions() {
- if (!mMode.hasGestures) {
- return;
- }
-
- mOrientationTouchTransformer.createOrAddTouchRegion(mDefaultDisplay.getInfo());
- }
-
- /**
- * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
- */
- public boolean isInSwipeUpTouchRegion(MotionEvent event) {
- return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
- }
-
- /**
- * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
- * is in the swipe up gesture region.
- */
- public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
- return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
- event.getY(pointerIndex));
+ public boolean isOneHandedModeActive() {
+ return (mSystemUiStateFlags & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0;
}
/**
@@ -620,101 +571,46 @@
public boolean canTriggerAssistantAction(MotionEvent ev, ActivityManager.RunningTaskInfo task) {
return mAssistantAvailable
&& !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
- && mOrientationTouchTransformer.touchInAssistantRegion(ev)
+ && mRotationTouchHelper.touchInAssistantRegion(ev)
&& !isLockToAppActive()
&& !isGestureBlockedActivity(task);
}
/**
- * *May* apply a transform on the motion event if it lies in the nav bar region for another
- * orientation that is currently being tracked as a part of quickstep
+ * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode
+ *
+ * @param ev The touch screen motion event.
+ * @return whether the given motion event can trigger the one handed mode.
*/
- void setOrientationTransformIfNeeded(MotionEvent event) {
- // negative coordinates bug b/143901881
- if (event.getX() < 0 || event.getY() < 0) {
- event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
+ public boolean canTriggerOneHandedAction(MotionEvent ev) {
+ if (!mIsOneHandedModeSupported) {
+ return false;
}
- mOrientationTouchTransformer.transform(event);
- }
- private void enableMultipleRegions(boolean enable) {
- mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
- notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
- if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
- // Clear any previous state from sensor manager
- mSensorRotation = mCurrentAppRotation;
- mOrientationListener.enable();
- } else {
- mOrientationListener.disable();
+ if (mIsOneHandedModeEnabled) {
+ final Info displayInfo = mDisplayController.getInfo();
+ return (mRotationTouchHelper.touchInOneHandedModeRegion(ev)
+ && displayInfo.rotation != Surface.ROTATION_90
+ && displayInfo.rotation != Surface.ROTATION_270
+ && displayInfo.densityDpi < DisplayMetrics.DENSITY_600);
}
+ return false;
}
- public void onStartGesture() {
- if (mTaskListFrozen) {
- // Prioritize whatever nav bar user touches once in quickstep
- // This case is specifically when user changes what nav bar they are using mid
- // quickswitch session before tasks list is unfrozen
- notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
- }
+ public boolean isOneHandedModeEnabled() {
+ return mIsOneHandedModeEnabled;
}
- void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
- BaseActivityInterface activityInterface) {
- if (endTarget == GestureState.GestureEndTarget.RECENTS) {
- mInOverview = true;
- if (!mTaskListFrozen) {
- // If we're in landscape w/o ever quickswitching, show the navbar in landscape
- enableMultipleRegions(true);
- }
- activityInterface.onExitOverview(this, mExitOverviewRunnable);
- } else if (endTarget == GestureState.GestureEndTarget.HOME) {
- enableMultipleRegions(false);
- } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
- if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
- // First gesture to start quickswitch
- enableMultipleRegions(true);
- } else {
- notifySysuiOfCurrentRotation(
- mOrientationTouchTransformer.getCurrentActiveRotation());
- }
-
- // A new gesture is starting, reset the current device rotation
- // This is done under the assumption that the user won't rotate the phone and then
- // quickswitch in the old orientation.
- mPrioritizeDeviceRotation = false;
- } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
- if (!mTaskListFrozen) {
- // touched nav bar but didn't go anywhere and not quickswitching, do nothing
- return;
- }
- notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
- }
+ public boolean isSwipeToNotificationEnabled() {
+ return mIsSwipeToNotificationEnabled;
}
- private void notifySysuiOfCurrentRotation(int rotation) {
- UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
- .onQuickSwitchToNewTask(rotation));
+ public boolean isPipActive() {
+ return mPipIsActive;
}
- /**
- * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
- * notifies system UI of the primary rotation the user is interacting with
- */
- private void toggleSecondaryNavBarsForRotation() {
- mOrientationTouchTransformer.setSingleActiveRegion(mDefaultDisplay.getInfo());
- notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
- }
-
- public int getCurrentActiveRotation() {
- if (!mMode.hasGestures) {
- // touch rotation should always match that of display for 3 button
- return mDisplayRotation;
- }
- return mOrientationTouchTransformer.getCurrentActiveRotation();
- }
-
- public int getDisplayRotation() {
- return mDisplayRotation;
+ public RotationTouchHelper getRotationTouchHelper() {
+ return mRotationTouchHelper;
}
public void dump(PrintWriter pw) {
@@ -726,9 +622,11 @@
pw.println(" assistantAvailable=" + mAssistantAvailable);
pw.println(" assistantDisabled="
+ QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
- pw.println(" currentActiveRotation=" + getCurrentActiveRotation());
- pw.println(" displayRotation=" + getDisplayRotation());
pw.println(" isUserUnlocked=" + mIsUserUnlocked);
- mOrientationTouchTransformer.dump(pw);
+ pw.println(" isOneHandedModeEnabled=" + mIsOneHandedModeEnabled);
+ pw.println(" isSwipeToNotificationEnabled=" + mIsSwipeToNotificationEnabled);
+ pw.println(" deferredGestureRegion=" + mDeferredGestureRegion);
+ pw.println(" pipIsActive=" + mPipIsActive);
+ mRotationTouchHelper.dump(pw);
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index 718c5ba..3861bab 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -33,7 +33,7 @@
public RecentsAnimationTargets(RemoteAnimationTargetCompat[] apps,
RemoteAnimationTargetCompat[] wallpapers, Rect homeContentInsets,
Rect minimizedHomeBounds) {
- super(apps, wallpapers, MODE_CLOSING);
+ super(apps, wallpapers, new RemoteAnimationTargetCompat[0], MODE_CLOSING);
this.homeContentInsets = homeContentInsets;
this.minimizedHomeBounds = minimizedHomeBounds;
}
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 517501a..1e82c8c 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -18,7 +18,6 @@
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.createAndStartNewLooper;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
import android.annotation.TargetApi;
@@ -26,32 +25,41 @@
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.os.Build;
-import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.icons.IconProvider.IconChangeListener;
+import com.android.launcher3.util.Executors.SimpleThreadFactory;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.KeyguardManagerCompat;
import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
import java.util.function.Consumer;
/**
* Singleton class to load and manage recents model.
*/
@TargetApi(Build.VERSION_CODES.O)
-public class RecentsModel extends TaskStackChangeListener {
+public class RecentsModel extends TaskStackChangeListener implements IconChangeListener {
// We do not need any synchronization for this variable as its only written on UI thread.
public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
new MainThreadInitializedObject<>(RecentsModel::new);
+ private static final Executor RECENTS_MODEL_EXECUTOR = Executors.newSingleThreadExecutor(
+ new SimpleThreadFactory("TaskThumbnailIconCache-", THREAD_PRIORITY_BACKGROUND));
+
private final List<TaskVisualsChangeListener> mThumbnailChangeListeners = new ArrayList<>();
private final Context mContext;
@@ -61,16 +69,15 @@
private RecentsModel(Context context) {
mContext = context;
- Looper looper =
- createAndStartNewLooper("TaskThumbnailIconCache", THREAD_PRIORITY_BACKGROUND);
mTaskList = new RecentTasksList(MAIN_EXECUTOR,
new KeyguardManagerCompat(context), ActivityManagerWrapper.getInstance());
- mIconCache = new TaskIconCache(context, looper);
- mThumbnailCache = new TaskThumbnailCache(context, looper);
- ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
- IconProvider.registerIconChangeListener(context,
- this::onPackageIconChanged, MAIN_EXECUTOR.getHandler());
+ IconProvider iconProvider = new IconProvider(context);
+ mIconCache = new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider);
+ mThumbnailCache = new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR);
+
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+ iconProvider.registerIconChangeListener(this, MAIN_EXECUTOR.getHandler());
}
public TaskIconCache getIconCache() {
@@ -93,15 +100,6 @@
}
/**
- * @return The task id of the running task, or -1 if there is no current running task.
- */
- public static int getRunningTaskId() {
- ActivityManager.RunningTaskInfo runningTask =
- ActivityManagerWrapper.getInstance().getRunningTask();
- return runningTask != null ? runningTask.id : -1;
- }
-
- /**
* @return Whether the provided {@param changeId} is the latest recent tasks list id.
*/
public boolean isTaskListValid(int changeId) {
@@ -109,20 +107,27 @@
}
/**
- * Finds and returns the task key associated with the given task id.
- *
- * @param callback The callback to receive the task key if it is found or null. This is always
- * called on the UI thread.
+ * @return Whether the task list is currently updating in the background
*/
- public void findTaskWithId(int taskId, Consumer<Task.TaskKey> callback) {
+ @VisibleForTesting
+ public boolean isLoadingTasksInBackground() {
+ return mTaskList.isLoadingTasksInBackground();
+ }
+
+ /**
+ * Checks if a task has been removed or not.
+ *
+ * @param callback Receives true if task is removed, false otherwise
+ */
+ public void isTaskRemoved(int taskId, Consumer<Boolean> callback) {
mTaskList.getTasks(true /* loadKeysOnly */, (tasks) -> {
for (Task task : tasks) {
if (task.key.id == taskId) {
- callback.accept(task.key);
+ callback.accept(false);
return;
}
}
- callback.accept(null);
+ callback.accept(true);
});
}
@@ -140,7 +145,9 @@
}
// Keep the cache up to date with the latest thumbnails
- int runningTaskId = RecentsModel.getRunningTaskId();
+ ActivityManager.RunningTaskInfo runningTask =
+ ActivityManagerWrapper.getInstance().getRunningTask();
+ int runningTaskId = runningTask != null ? runningTask.id : -1;
mTaskList.getTaskKeys(mThumbnailCache.getCacheSize(), tasks -> {
for (Task task : tasks) {
if (task.key.id == runningTaskId) {
@@ -167,9 +174,9 @@
@Override
public void onTaskRemoved(int taskId) {
- Task.TaskKey dummyKey = new Task.TaskKey(taskId, 0, null, null, 0, 0);
- mThumbnailCache.remove(dummyKey);
- mIconCache.onTaskRemoved(dummyKey);
+ Task.TaskKey stubKey = new Task.TaskKey(taskId, 0, null, null, 0, 0);
+ mThumbnailCache.remove(stubKey);
+ mIconCache.onTaskRemoved(stubKey);
}
public void onTrimMemory(int level) {
@@ -179,17 +186,23 @@
if (level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
// Clear everything once we reach a low-mem situation
mThumbnailCache.clear();
- mIconCache.clear();
+ mIconCache.clearCache();
}
}
- private void onPackageIconChanged(String pkg, UserHandle user) {
- mIconCache.invalidateCacheEntries(pkg, user);
+ @Override
+ public void onAppIconChanged(String packageName, UserHandle user) {
+ mIconCache.invalidateCacheEntries(packageName, user);
for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
- mThumbnailChangeListeners.get(i).onTaskIconChanged(pkg, user);
+ mThumbnailChangeListeners.get(i).onTaskIconChanged(packageName, user);
}
}
+ @Override
+ public void onSystemIconStateChanged(String iconState) {
+ mIconCache.clearCache();
+ }
+
/**
* Adds a listener for visuals changes
*/
diff --git a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
index ab5e3ba..c032889 100644
--- a/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RemoteAnimationTargets.java
@@ -15,6 +15,10 @@
*/
package com.android.quickstep;
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
+
+import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
+
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import java.util.ArrayList;
@@ -30,13 +34,15 @@
public final RemoteAnimationTargetCompat[] unfilteredApps;
public final RemoteAnimationTargetCompat[] apps;
public final RemoteAnimationTargetCompat[] wallpapers;
+ public final RemoteAnimationTargetCompat[] nonApps;
public final int targetMode;
public final boolean hasRecents;
private boolean mReleased = false;
public RemoteAnimationTargets(RemoteAnimationTargetCompat[] apps,
- RemoteAnimationTargetCompat[] wallpapers, int targetMode) {
+ RemoteAnimationTargetCompat[] wallpapers, RemoteAnimationTargetCompat[] nonApps,
+ int targetMode) {
ArrayList<RemoteAnimationTargetCompat> filteredApps = new ArrayList<>();
boolean hasRecents = false;
if (apps != null) {
@@ -55,6 +61,7 @@
this.wallpapers = wallpapers;
this.targetMode = targetMode;
this.hasRecents = hasRecents;
+ this.nonApps = nonApps;
}
public RemoteAnimationTargetCompat findTask(int taskId) {
@@ -66,6 +73,29 @@
return null;
}
+ /**
+ * Gets the navigation bar remote animation target if exists.
+ */
+ public RemoteAnimationTargetCompat getNavBarRemoteAnimationTarget() {
+ for (RemoteAnimationTargetCompat target : nonApps) {
+ if (target.windowType == TYPE_NAVIGATION_BAR) {
+ return target;
+ }
+ }
+ return null;
+ }
+
+ /** Returns the first opening app target. */
+ public RemoteAnimationTargetCompat getFirstAppTarget() {
+ return apps.length > 0 ? apps[0] : null;
+ }
+
+ /** Returns the task id of the first opening app target, or -1 if none is found. */
+ public int getFirstAppTargetTaskId() {
+ RemoteAnimationTargetCompat target = getFirstAppTarget();
+ return target == null ? -1 : target.taskId;
+ }
+
public boolean isAnimatingHome() {
for (RemoteAnimationTargetCompat target : unfilteredApps) {
if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
@@ -80,6 +110,10 @@
}
public void release() {
+ if (ENABLE_SHELL_TRANSITIONS) {
+ mReleaseChecks.clear();
+ return;
+ }
if (mReleased) {
return;
}
@@ -98,6 +132,9 @@
for (RemoteAnimationTargetCompat target : wallpapers) {
target.release();
}
+ for (RemoteAnimationTargetCompat target : nonApps) {
+ target.release();
+ }
}
/**
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
new file mode 100644
index 0000000..678b176
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
+import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
+import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
+import com.android.launcher3.util.DisplayController.Info;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class RotationTouchHelper implements
+ SysUINavigationMode.NavigationModeChangeListener,
+ DisplayInfoChangeListener {
+
+ public static final MainThreadInitializedObject<RotationTouchHelper> INSTANCE =
+ new MainThreadInitializedObject<>(RotationTouchHelper::new);
+
+ private OrientationTouchTransformer mOrientationTouchTransformer;
+ private DisplayController mDisplayController;
+ private SysUINavigationMode mSysUiNavMode;
+ private int mDisplayId;
+ private int mDisplayRotation;
+
+ private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
+
+ private SysUINavigationMode.Mode mMode = THREE_BUTTONS;
+
+ private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
+ @Override
+ public void onRecentTaskListFrozenChanged(boolean frozen) {
+ mTaskListFrozen = frozen;
+ if (frozen || mInOverview) {
+ return;
+ }
+ enableMultipleRegions(false);
+ }
+
+ @Override
+ public void onActivityRotation(int displayId) {
+ super.onActivityRotation(displayId);
+ // This always gets called before onDisplayInfoChanged() so we know how to process
+ // the rotation in that method. This is done to avoid having a race condition between
+ // the sensor readings and onDisplayInfoChanged() call
+ if (displayId != mDisplayId) {
+ return;
+ }
+
+ mPrioritizeDeviceRotation = true;
+ if (mInOverview) {
+ // reset, launcher must be rotating
+ mExitOverviewRunnable.run();
+ }
+ }
+ };
+
+ private Runnable mExitOverviewRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mInOverview = false;
+ enableMultipleRegions(false);
+ }
+ };
+
+ /**
+ * Used to listen for when the device rotates into the orientation of the current foreground
+ * app. For example, if a user quickswitches from a portrait to a fixed landscape app and then
+ * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust
+ * the navbar.
+ */
+ private OrientationEventListener mOrientationListener;
+ private int mSensorRotation = ROTATION_0;
+ /**
+ * This is the configuration of the foreground app or the app that will be in the foreground
+ * once a quickstep gesture finishes.
+ */
+ private int mCurrentAppRotation = -1;
+ /**
+ * This flag is set to true when the device physically changes orientations. When true, we will
+ * always report the current rotation of the foreground app whenever the display changes, as it
+ * would indicate the user's intention to rotate the foreground app.
+ */
+ private boolean mPrioritizeDeviceRotation = false;
+ private Runnable mOnDestroyFrozenTaskRunnable;
+ /**
+ * Set to true when user swipes to recents. In recents, we ignore the state of the recents
+ * task list being frozen or not to allow the user to keep interacting with nav bar rotation
+ * they went into recents with as opposed to defaulting to the default display rotation.
+ * TODO: (b/156984037) For when user rotates after entering overview
+ */
+ private boolean mInOverview;
+ private boolean mTaskListFrozen;
+ private final Context mContext;
+
+ /**
+ * Keeps track of whether destroy has been called for this instance. Mainly used for TAPL tests
+ * where multiple instances of RotationTouchHelper are being created. b/177316094
+ */
+ private boolean mNeedsInit = true;
+
+ private RotationTouchHelper(Context context) {
+ mContext = context;
+ if (mNeedsInit) {
+ init();
+ }
+ }
+
+ public void init() {
+ if (!mNeedsInit) {
+ return;
+ }
+ mDisplayController = DisplayController.INSTANCE.get(mContext);
+ Resources resources = mContext.getResources();
+ mSysUiNavMode = SysUINavigationMode.INSTANCE.get(mContext);
+ mDisplayId = mDisplayController.getInfo().id;
+
+ mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
+ () -> QuickStepContract.getWindowCornerRadius(resources));
+
+ // Register for navigation mode changes
+ SysUINavigationMode.Mode newMode = mSysUiNavMode.addModeChangeListener(this);
+ onNavModeChangedInternal(newMode, newMode.hasGestures);
+ runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
+
+ mOrientationListener = new OrientationEventListener(mContext) {
+ @Override
+ public void onOrientationChanged(int degrees) {
+ int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
+ mSensorRotation);
+ if (newRotation == mSensorRotation) {
+ return;
+ }
+
+ mSensorRotation = newRotation;
+ mPrioritizeDeviceRotation = true;
+
+ if (newRotation == mCurrentAppRotation) {
+ // When user rotates device to the orientation of the foreground app after
+ // quickstepping
+ toggleSecondaryNavBarsForRotation();
+ }
+ }
+ };
+ mNeedsInit = false;
+ }
+
+ private void setupOrientationSwipeHandler() {
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mFrozenTaskListener);
+ mOnDestroyFrozenTaskRunnable = () -> TaskStackChangeListeners.getInstance()
+ .unregisterTaskStackListener(mFrozenTaskListener);
+ runOnDestroy(mOnDestroyFrozenTaskRunnable);
+ }
+
+ private void destroyOrientationSwipeHandlerCallback() {
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
+ mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
+ }
+
+ private void runOnDestroy(Runnable action) {
+ mOnDestroyActions.add(action);
+ }
+
+ /**
+ * Cleans up all the registered listeners and receivers.
+ */
+ public void destroy() {
+ for (Runnable r : mOnDestroyActions) {
+ r.run();
+ }
+ mNeedsInit = true;
+ }
+
+ public boolean isTaskListFrozen() {
+ return mTaskListFrozen;
+ }
+
+ public boolean touchInAssistantRegion(MotionEvent ev) {
+ return mOrientationTouchTransformer.touchInAssistantRegion(ev);
+ }
+
+ public boolean touchInOneHandedModeRegion(MotionEvent ev) {
+ return mOrientationTouchTransformer.touchInOneHandedModeRegion(ev);
+ }
+
+ /**
+ * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
+ */
+ public void updateGestureTouchRegions() {
+ if (!mMode.hasGestures) {
+ return;
+ }
+
+ mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo());
+ }
+
+ /**
+ * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
+ */
+ public boolean isInSwipeUpTouchRegion(MotionEvent event) {
+ return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
+ }
+
+ /**
+ * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
+ * is in the swipe up gesture region.
+ */
+ public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
+ return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
+ event.getY(pointerIndex));
+ }
+
+
+ @Override
+ public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+ onNavModeChangedInternal(newMode, false);
+ }
+
+ /**
+ * @param forceRegister if {@code true}, this will register {@link #mFrozenTaskListener} via
+ * {@link #setupOrientationSwipeHandler()}
+ */
+ private void onNavModeChangedInternal(SysUINavigationMode.Mode newMode, boolean forceRegister) {
+ mDisplayController.removeChangeListener(this);
+ mDisplayController.addChangeListener(this);
+ onDisplayInfoChanged(mContext, mDisplayController.getInfo(), CHANGE_ALL);
+
+ mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
+ mContext.getResources());
+
+ if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) {
+ setupOrientationSwipeHandler();
+ } else if (mMode.hasGestures && !newMode.hasGestures){
+ destroyOrientationSwipeHandlerCallback();
+ }
+
+ mMode = newMode;
+ }
+
+ public int getDisplayRotation() {
+ return mDisplayRotation;
+ }
+
+ @Override
+ public void onDisplayInfoChanged(Context context, Info info, int flags) {
+ if ((flags & (CHANGE_ROTATION | CHANGE_ACTIVE_SCREEN)) == 0) {
+ return;
+ }
+
+ mDisplayRotation = info.rotation;
+
+ if (!mMode.hasGestures) {
+ return;
+ }
+ updateGestureTouchRegions();
+ mOrientationTouchTransformer.createOrAddTouchRegion(info);
+ mCurrentAppRotation = mDisplayRotation;
+
+ /* Update nav bars on the following:
+ * a) if this is coming from an activity rotation OR
+ * aa) we launch an app in the orientation that user is already in
+ * b) We're not in overview, since overview will always be portrait (w/o home rotation)
+ * c) We're actively in quickswitch mode
+ */
+ if ((mPrioritizeDeviceRotation
+ || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
+ && !mInOverview
+ && mTaskListFrozen) {
+ toggleSecondaryNavBarsForRotation();
+ }
+ }
+
+ /**
+ * Sets the gestural height.
+ */
+ void setGesturalHeight(int newGesturalHeight) {
+ mOrientationTouchTransformer.setGesturalHeight(
+ newGesturalHeight, mDisplayController.getInfo(), mContext.getResources());
+ }
+
+ /**
+ * *May* apply a transform on the motion event if it lies in the nav bar region for another
+ * orientation that is currently being tracked as a part of quickstep
+ */
+ void setOrientationTransformIfNeeded(MotionEvent event) {
+ // negative coordinates bug b/143901881
+ if (event.getX() < 0 || event.getY() < 0) {
+ event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
+ }
+ mOrientationTouchTransformer.transform(event);
+ }
+
+ private void enableMultipleRegions(boolean enable) {
+ mOrientationTouchTransformer.enableMultipleRegions(enable, mDisplayController.getInfo());
+ notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
+ if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
+ // Clear any previous state from sensor manager
+ mSensorRotation = mCurrentAppRotation;
+ mOrientationListener.enable();
+ } else {
+ mOrientationListener.disable();
+ }
+ }
+
+ public void onStartGesture() {
+ if (mTaskListFrozen) {
+ // Prioritize whatever nav bar user touches once in quickstep
+ // This case is specifically when user changes what nav bar they are using mid
+ // quickswitch session before tasks list is unfrozen
+ notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+ }
+ }
+
+ void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
+ BaseActivityInterface activityInterface) {
+ if (endTarget == GestureState.GestureEndTarget.RECENTS) {
+ mInOverview = true;
+ if (!mTaskListFrozen) {
+ // If we're in landscape w/o ever quickswitching, show the navbar in landscape
+ enableMultipleRegions(true);
+ }
+ activityInterface.onExitOverview(this, mExitOverviewRunnable);
+ } else if (endTarget == GestureState.GestureEndTarget.HOME) {
+ enableMultipleRegions(false);
+ } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
+ if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
+ // First gesture to start quickswitch
+ enableMultipleRegions(true);
+ } else {
+ notifySysuiOfCurrentRotation(
+ mOrientationTouchTransformer.getCurrentActiveRotation());
+ }
+
+ // A new gesture is starting, reset the current device rotation
+ // This is done under the assumption that the user won't rotate the phone and then
+ // quickswitch in the old orientation.
+ mPrioritizeDeviceRotation = false;
+ } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
+ if (!mTaskListFrozen) {
+ // touched nav bar but didn't go anywhere and not quickswitching, do nothing
+ return;
+ }
+ notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+ }
+ }
+
+ private void notifySysuiOfCurrentRotation(int rotation) {
+ UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
+ .notifyPrioritizedRotation(rotation));
+ }
+
+ /**
+ * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
+ * notifies system UI of the primary rotation the user is interacting with
+ */
+ private void toggleSecondaryNavBarsForRotation() {
+ mOrientationTouchTransformer.setSingleActiveRegion(mDisplayController.getInfo());
+ notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+ }
+
+ public int getCurrentActiveRotation() {
+ if (!mMode.hasGestures) {
+ // touch rotation should always match that of display for 3 button
+ return mDisplayRotation;
+ }
+ return mOrientationTouchTransformer.getCurrentActiveRotation();
+ }
+
+ public void dump(PrintWriter pw) {
+ pw.println("RotationTouchHelper:");
+ pw.println(" currentActiveRotation=" + getCurrentActiveRotation());
+ pw.println(" displayRotation=" + getDisplayRotation());
+ mOrientationTouchTransformer.dump(pw);
+ }
+
+ public OrientationTouchTransformer getOrientationTouchTransformer() {
+ return mOrientationTouchTransformer;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
new file mode 100644
index 0000000..4495455
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.PROTOTYPE_APP_CLOSE;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.AppCloseConfig;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.RectFSpringAnim2;
+import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
+
+public abstract class SwipeUpAnimationLogic {
+
+ protected static final Rect TEMP_RECT = new Rect();
+
+ protected DeviceProfile mDp;
+
+ protected final Context mContext;
+ protected final RecentsAnimationDeviceState mDeviceState;
+ protected final GestureState mGestureState;
+ protected final TaskViewSimulator mTaskViewSimulator;
+
+ protected final TransformParams mTransformParams;
+
+ // 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.
+ protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+
+ // The distance needed to drag to reach the task size in recents.
+ protected int mTransitionDragLength;
+ // How much further we can drag past recents, as a factor of mTransitionDragLength.
+ protected float mDragLengthFactor = 1;
+
+ protected AnimatorControllerWithResistance mWindowTransitionController;
+
+ public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
+ GestureState gestureState, TransformParams transformParams) {
+ mContext = context;
+ mDeviceState = deviceState;
+ mGestureState = gestureState;
+ mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
+ mTransformParams = transformParams;
+
+ mTaskViewSimulator.getOrientationState().update(
+ mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
+ mDeviceState.getRotationTouchHelper().getDisplayRotation());
+ }
+
+ protected void initTransitionEndpoints(DeviceProfile dp) {
+ mDp = dp;
+
+ mTaskViewSimulator.setDp(dp);
+ mTransitionDragLength = mGestureState.getActivityInterface().getSwipeUpDestinationAndLength(
+ dp, mContext, TEMP_RECT,
+ mTaskViewSimulator.getOrientationState().getOrientationHandler());
+ mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
+
+ PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
+ mTaskViewSimulator.addAppToOverviewAnim(pa, LINEAR);
+ AnimatorPlaybackController normalController = pa.createPlaybackController();
+ mWindowTransitionController = AnimatorControllerWithResistance.createForRecents(
+ normalController, mContext, mTaskViewSimulator.getOrientationState(),
+ mDp, mTaskViewSimulator.recentsViewScale, AnimatedFloat.VALUE,
+ mTaskViewSimulator.recentsViewSecondaryTranslation, AnimatedFloat.VALUE);
+ }
+
+ @UiThread
+ public void updateDisplacement(float displacement) {
+ // We are moving in the negative x/y direction
+ displacement = -displacement;
+ float shift;
+ if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
+ shift = mDragLengthFactor;
+ } else {
+ float translation = Math.max(displacement, 0);
+ shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
+ }
+
+ mCurrentShift.updateValue(shift);
+ }
+
+ /**
+ * Called when the value of {@link #mCurrentShift} changes
+ */
+ @UiThread
+ public abstract void updateFinalShift();
+
+ protected PagedOrientationHandler getOrientationHandler() {
+ return mTaskViewSimulator.getOrientationState().getOrientationHandler();
+ }
+
+ protected abstract class HomeAnimationFactory {
+ protected float mSwipeVelocity;
+
+ public @NonNull RectF getWindowTargetRect() {
+ PagedOrientationHandler orientationHandler = getOrientationHandler();
+ DeviceProfile dp = mDp;
+ final int halfIconSize = dp.iconSizePx / 2;
+ float primaryDimension = orientationHandler
+ .getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx);
+ float secondaryDimension = orientationHandler
+ .getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx);
+ final float targetX = primaryDimension / 2f;
+ final float targetY = secondaryDimension - dp.hotseatBarSizePx;
+ // Fallback to animate to center of screen.
+ return new RectF(targetX - halfIconSize, targetY - halfIconSize,
+ targetX + halfIconSize, targetY + halfIconSize);
+ }
+
+ /** Returns the corner radius of the window at the end of the animation. */
+ public float getEndRadius(RectF cropRectF) {
+ return cropRectF.width() / 2f;
+ }
+
+ public abstract @NonNull AnimatorPlaybackController createActivityAnimationToHome();
+
+ public void setSwipeVelocity(float velocity) {
+ mSwipeVelocity = velocity;
+ }
+
+ public void playAtomicAnimation(float velocity) {
+ // No-op
+ }
+
+ public boolean shouldPlayAtomicWorkspaceReveal() {
+ return true;
+ }
+
+ public void setAnimation(RectFSpringAnim anim) { }
+
+ public boolean keepWindowOpaque() { return false; }
+
+ public void update(@Nullable AppCloseConfig config, RectF currentRect, float progress,
+ float radius) { }
+
+ public void onCancel() { }
+
+ /**
+ * @return {@code true} if this factory supports animating an Activity to PiP window on
+ * swiping up to home.
+ */
+ public boolean supportSwipePipToHome() {
+ return false;
+ }
+
+ /**
+ * @param progress The progress of the animation to the home screen.
+ * @return The current alpha to set on the animating app window.
+ */
+ protected float getWindowAlpha(float progress) {
+ // Alpha interpolates between [1, 0] between progress values [start, end]
+ final float start = 0f;
+ final float end = 0.85f;
+
+ if (progress <= start) {
+ return 1f;
+ }
+ if (progress >= end) {
+ return 0f;
+ }
+ return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
+ }
+ }
+
+ /**
+ * Update with start progress for window animation to home.
+ * @param outMatrix {@link Matrix} to map a rect in Launcher space to window space.
+ * @param startProgress The progress of {@link #mCurrentShift} to start thw window from.
+ * @return {@link RectF} represents the bounds as starting point in window space.
+ */
+ protected RectF updateProgressForStartRect(Matrix outMatrix, float startProgress) {
+ mCurrentShift.updateValue(startProgress);
+ mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
+ RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
+
+ mTaskViewSimulator.applyWindowToHomeRotation(outMatrix);
+
+ final RectF startRect = new RectF(cropRectF);
+ mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
+ return startRect;
+ }
+
+ /**
+ * Creates an animation that transforms the current app window into the home app.
+ * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+ * @param homeAnimationFactory The home animation factory.
+ */
+ protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
+ HomeAnimationFactory homeAnimationFactory) {
+ final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
+
+ Matrix homeToWindowPositionMap = new Matrix();
+ final RectF startRect = updateProgressForStartRect(homeToWindowPositionMap, startProgress);
+ RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
+
+ // Move the startRect to Launcher space as floatingIconView runs in Launcher
+ Matrix windowToHomePositionMap = new Matrix();
+ homeToWindowPositionMap.invert(windowToHomePositionMap);
+ windowToHomePositionMap.mapRect(startRect);
+
+ RectFSpringAnim anim;
+ if (PROTOTYPE_APP_CLOSE.get()) {
+ anim = new RectFSpringAnim2(startRect, targetRect, mContext,
+ mTaskViewSimulator.getCurrentCornerRadius(),
+ homeAnimationFactory.getEndRadius(cropRectF));
+ } else {
+ anim = new RectFSpringAnim(startRect, targetRect, mContext);
+ }
+ homeAnimationFactory.setAnimation(anim);
+
+ SpringAnimationRunner runner = new SpringAnimationRunner(
+ homeAnimationFactory, cropRectF, homeToWindowPositionMap);
+ anim.addOnUpdateListener(runner);
+ anim.addAnimatorListener(runner);
+ return anim;
+ }
+
+ protected class SpringAnimationRunner extends AnimationSuccessListener
+ implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
+
+ final Rect mCropRect = new Rect();
+ final Matrix mMatrix = new Matrix();
+
+ final RectF mWindowCurrentRect = new RectF();
+ final Matrix mHomeToWindowPositionMap;
+ final HomeAnimationFactory mAnimationFactory;
+
+ final AnimatorPlaybackController mHomeAnim;
+ final RectF mCropRectF;
+
+ final float mStartRadius;
+ final float mEndRadius;
+
+ SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
+ Matrix homeToWindowPositionMap) {
+ mAnimationFactory = factory;
+ mHomeAnim = factory.createActivityAnimationToHome();
+ mCropRectF = cropRectF;
+ mHomeToWindowPositionMap = homeToWindowPositionMap;
+
+ cropRectF.roundOut(mCropRect);
+
+ // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
+ // rounding at the end of the animation.
+ mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
+ mEndRadius = factory.getEndRadius(cropRectF);
+ }
+
+ @Override
+ public void onUpdate(@Nullable AppCloseConfig config, RectF currentRect, float progress) {
+ mHomeAnim.setPlayFraction(progress);
+ mHomeToWindowPositionMap.mapRect(mWindowCurrentRect, currentRect);
+
+ mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
+ float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius);
+ float alpha = mAnimationFactory.getWindowAlpha(progress);
+ if (config != null && PROTOTYPE_APP_CLOSE.get()) {
+ alpha = config.getWindowAlpha();
+ cornerRadius = config.getCornerRadius();
+ }
+ if (mAnimationFactory.keepWindowOpaque()) {
+ alpha = 1f;
+ }
+ mTransformParams
+ .setTargetAlpha(alpha)
+ .setCornerRadius(cornerRadius);
+ mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
+ mAnimationFactory.update(config, currentRect, progress,
+ mMatrix.mapRadius(cornerRadius));
+ }
+
+ @Override
+ public void onBuildTargetParams(
+ Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
+ builder.withMatrix(mMatrix)
+ .withWindowCrop(mCropRect)
+ .withCornerRadius(params.getCornerRadius());
+ }
+
+ @Override
+ public void onCancel() {
+ mAnimationFactory.onCancel();
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mHomeAnim.dispatchOnStart();
+ }
+
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ mHomeAnim.getAnimationPlayer().end();
+ }
+ }
+
+ public interface RunningWindowAnim {
+ void end();
+
+ void cancel();
+
+ static RunningWindowAnim wrap(Animator animator) {
+ return new RunningWindowAnim() {
+ @Override
+ public void end() {
+ animator.end();
+ }
+
+ @Override
+ public void cancel() {
+ animator.cancel();
+ }
+ };
+ }
+
+ static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
+ return new RunningWindowAnim() {
+ @Override
+ public void end() {
+ rectFSpringAnim.end();
+ }
+
+ @Override
+ public void cancel() {
+ rectFSpringAnim.cancel();
+ }
+ };
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index 05ce2a2..74f4bea 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -16,20 +16,25 @@
package com.android.quickstep;
+import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_2_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_3_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON;
import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.util.Log;
-import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
import com.android.launcher3.util.MainThreadInitializedObject;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* Observer for the resource config that specifies the navigation bar mode.
@@ -37,16 +42,18 @@
public class SysUINavigationMode {
public enum Mode {
- THREE_BUTTONS(false, 0),
- TWO_BUTTONS(true, 1),
- NO_BUTTON(true, 2);
+ THREE_BUTTONS(false, 0, LAUNCHER_NAVIGATION_MODE_3_BUTTON),
+ TWO_BUTTONS(true, 1, LAUNCHER_NAVIGATION_MODE_2_BUTTON),
+ NO_BUTTON(true, 2, LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON);
public final boolean hasGestures;
public final int resValue;
+ public final LauncherEvent launcherEvent;
- Mode(boolean hasGestures, int resValue) {
+ Mode(boolean hasGestures, int resValue, LauncherEvent launcherEvent) {
this.hasGestures = hasGestures;
this.resValue = resValue;
+ this.launcherEvent = launcherEvent;
}
}
@@ -58,15 +65,21 @@
new MainThreadInitializedObject<>(SysUINavigationMode::new);
private static final String TAG = "SysUINavigationMode";
-
private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
private static final String NAV_BAR_INTERACTION_MODE_RES_NAME =
"config_navBarInteractionMode";
+ private static final String TARGET_OVERLAY_PACKAGE = "android";
private final Context mContext;
private Mode mMode;
- private final List<NavigationModeChangeListener> mChangeListeners = new ArrayList<>();
+ private int mNavBarGesturalHeight;
+ private int mNavBarLargerGesturalHeight;
+
+ private final List<NavigationModeChangeListener> mChangeListeners =
+ new CopyOnWriteArrayList<>();
+ private final List<OneHandedModeChangeListener> mOneHandedOverlayChangeListeners =
+ new ArrayList<>();
public SysUINavigationMode(Context context) {
mContext = context;
@@ -76,8 +89,9 @@
@Override
public void onReceive(Context context, Intent intent) {
updateMode();
+ updateGesturalHeight();
}
- }, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
+ }, getPackageFilter(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED));
}
/** Updates navigation mode when needed. */
@@ -89,9 +103,49 @@
}
}
+ private void updateGesturalHeight() {
+ int newGesturalHeight = ResourceUtils.getDimenByName(
+ ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(),
+ INVALID_RESOURCE_HANDLE);
+
+ if (newGesturalHeight == INVALID_RESOURCE_HANDLE) {
+ Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
+ return;
+ }
+
+ if (mNavBarGesturalHeight != newGesturalHeight) {
+ mNavBarGesturalHeight = newGesturalHeight;
+ }
+
+ int newLargerGesturalHeight = ResourceUtils.getDimenByName(
+ ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, mContext.getResources(),
+ INVALID_RESOURCE_HANDLE);
+ if (newLargerGesturalHeight == INVALID_RESOURCE_HANDLE) {
+ Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
+ return;
+ }
+ if (mNavBarLargerGesturalHeight != newLargerGesturalHeight) {
+ mNavBarLargerGesturalHeight = newLargerGesturalHeight;
+ dispatchOneHandedOverlayChange();
+ }
+ }
+
private void initializeMode() {
- int modeInt = getSystemIntegerRes(mContext, NAV_BAR_INTERACTION_MODE_RES_NAME);
- for(Mode m : Mode.values()) {
+ int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME,
+ mContext.getResources(), INVALID_RESOURCE_HANDLE);
+ mNavBarGesturalHeight = ResourceUtils.getDimenByName(
+ ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(),
+ INVALID_RESOURCE_HANDLE);
+ mNavBarLargerGesturalHeight = ResourceUtils.getDimenByName(
+ ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, mContext.getResources(),
+ mNavBarGesturalHeight);
+
+ if (modeInt == INVALID_RESOURCE_HANDLE) {
+ Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
+ return;
+ }
+
+ for (Mode m : Mode.values()) {
if (m.resValue == modeInt) {
mMode = m;
}
@@ -104,6 +158,12 @@
}
}
+ private void dispatchOneHandedOverlayChange() {
+ for (OneHandedModeChangeListener listener : mOneHandedOverlayChangeListeners) {
+ listener.onOneHandedModeChanged(mNavBarLargerGesturalHeight);
+ }
+ }
+
public Mode addModeChangeListener(NavigationModeChangeListener listener) {
mChangeListeners.add(listener);
return mMode;
@@ -113,41 +173,30 @@
mChangeListeners.remove(listener);
}
+ public int addOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) {
+ mOneHandedOverlayChangeListeners.add(listener);
+ return mNavBarLargerGesturalHeight;
+ }
+
+ public void removeOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) {
+ mOneHandedOverlayChangeListeners.remove(listener);
+ }
+
public Mode getMode() {
return mMode;
}
- private static int getSystemIntegerRes(Context context, String resName) {
- Resources res = context.getResources();
- int resId = res.getIdentifier(resName, "integer", "android");
-
- if (resId != 0) {
- return res.getInteger(resId);
- } else {
- Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
- return -1;
- }
- }
-
- /** @return Whether we can remove the shelf from overview. */
- public static boolean removeShelfFromOverview(Context context) {
- // The shelf is core to the two-button mode model, so we need to continue supporting it.
- return getMode(context) != Mode.TWO_BUTTONS;
- }
-
- public static boolean hideShelfInTwoButtonLandscape(Context context,
- PagedOrientationHandler pagedOrientationHandler) {
- return getMode(context) == Mode.TWO_BUTTONS &&
- !pagedOrientationHandler.isLayoutNaturalToLauncher();
- }
-
public void dump(PrintWriter pw) {
pw.println("SysUINavigationMode:");
pw.println(" mode=" + mMode.name());
+ pw.println(" mNavBarGesturalHeight=:" + mNavBarGesturalHeight);
}
public interface NavigationModeChangeListener {
-
void onNavigationModeChanged(Mode newMode);
}
-}
\ No newline at end of file
+
+ public interface OneHandedModeChangeListener {
+ void onOneHandedModeChanged(int newGesturalHeight);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 299e9e5..d040904 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,7 +17,12 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import android.app.PendingIntent;
+import android.app.PictureInPictureParams;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Rect;
@@ -25,39 +30,95 @@
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.util.Log;
import android.view.MotionEvent;
+import android.view.SurfaceControl;
import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
+import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.RemoteTransitionCompat;
+import com.android.systemui.shared.system.smartspace.ISmartspaceCallback;
+import com.android.systemui.shared.system.smartspace.ISmartspaceTransitionController;
+import com.android.wm.shell.onehanded.IOneHanded;
+import com.android.wm.shell.pip.IPip;
+import com.android.wm.shell.pip.IPipAnimationListener;
+import com.android.wm.shell.splitscreen.ISplitScreen;
+import com.android.wm.shell.splitscreen.ISplitScreenListener;
+import com.android.wm.shell.startingsurface.IStartingWindow;
+import com.android.wm.shell.startingsurface.IStartingWindowListener;
+import com.android.wm.shell.transition.IShellTransitions;
/**
* Holds the reference to SystemUI.
*/
-public class SystemUiProxy implements ISystemUiProxy {
+public class SystemUiProxy implements ISystemUiProxy,
+ SysUINavigationMode.NavigationModeChangeListener {
private static final String TAG = SystemUiProxy.class.getSimpleName();
public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
new MainThreadInitializedObject<>(SystemUiProxy::new);
private ISystemUiProxy mSystemUiProxy;
+ private IPip mPip;
+ private ISmartspaceTransitionController mSmartspaceTransitionController;
+ private ISplitScreen mSplitScreen;
+ private IOneHanded mOneHanded;
+ private IShellTransitions mShellTransitions;
+ private IStartingWindow mStartingWindow;
private final DeathRecipient mSystemUiProxyDeathRecipient = () -> {
- MAIN_EXECUTOR.execute(() -> setProxy(null));
+ MAIN_EXECUTOR.execute(() -> clearProxy());
};
+ // Save the listeners passed into the proxy since when set/register these listeners,
+ // setProxy may not have been called, eg. OverviewProxyService is not connected yet.
+ private IPipAnimationListener mPendingPipAnimationListener;
+ private ISplitScreenListener mPendingSplitScreenListener;
+ private IStartingWindowListener mPendingStartingWindowListener;
+ private ISmartspaceCallback mPendingSmartspaceCallback;
+
// Used to dedupe calls to SystemUI
private int mLastShelfHeight;
private boolean mLastShelfVisible;
- private float mLastBackButtonAlpha;
- private boolean mLastBackButtonAnimate;
+ private float mLastNavButtonAlpha;
+ private boolean mLastNavButtonAnimate;
+ private boolean mHasNavButtonAlphaBeenSet = false;
// TODO(141886704): Find a way to remove this
private int mLastSystemUiStateFlags;
public SystemUiProxy(Context context) {
- // Do nothing
+ SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this);
+ }
+
+ @Override
+ public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+ // Whenever the nav mode changes, force reset the nav button alpha
+ setNavBarButtonAlpha(1f, false);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mSystemUiProxy != null) {
+ try {
+ mSystemUiProxy.onBackPressed();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call onBackPressed", e);
+ }
+ }
+ }
+
+ @Override
+ public void setHomeRotationEnabled(boolean enabled) {
+ if (mSystemUiProxy != null) {
+ try {
+ mSystemUiProxy.setHomeRotationEnabled(enabled);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call onBackPressed", e);
+ }
+ }
}
@Override
@@ -66,10 +127,40 @@
return null;
}
- public void setProxy(ISystemUiProxy proxy) {
+ public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen,
+ IOneHanded oneHanded, IShellTransitions shellTransitions,
+ IStartingWindow startingWindow,
+ ISmartspaceTransitionController smartSpaceTransitionController) {
unlinkToDeath();
mSystemUiProxy = proxy;
+ mPip = pip;
+ mSplitScreen = splitScreen;
+ mOneHanded = oneHanded;
+ mShellTransitions = shellTransitions;
+ mStartingWindow = startingWindow;
+ mSmartspaceTransitionController = smartSpaceTransitionController;
linkToDeath();
+ // re-attach the listeners once missing due to setProxy has not been initialized yet.
+ if (mPendingPipAnimationListener != null && mPip != null) {
+ setPinnedStackAnimationListener(mPendingPipAnimationListener);
+ mPendingPipAnimationListener = null;
+ }
+ if (mPendingSplitScreenListener != null && mSplitScreen != null) {
+ registerSplitScreenListener(mPendingSplitScreenListener);
+ mPendingSplitScreenListener = null;
+ }
+ if (mPendingStartingWindowListener != null && mStartingWindow != null) {
+ setStartingWindowListener(mPendingStartingWindowListener);
+ mPendingStartingWindowListener = null;
+ }
+ if (mPendingSmartspaceCallback != null && mSmartspaceTransitionController != null) {
+ setSmartspaceCallback(mPendingSmartspaceCallback);
+ mPendingSmartspaceCallback = null;
+ }
+ }
+
+ public void clearProxy() {
+ setProxy(null, null, null, null, null, null, null);
}
// TODO(141886704): Find a way to remove this
@@ -114,17 +205,6 @@
}
@Override
- public void onSplitScreenInvoked() {
- if (mSystemUiProxy != null) {
- try {
- mSystemUiProxy.onSplitScreenInvoked();
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call onSplitScreenInvoked", e);
- }
- }
- }
-
- @Override
public void onOverviewShown(boolean fromHome) {
onOverviewShown(fromHome, TAG);
}
@@ -151,28 +231,19 @@
return null;
}
- @Override
- public void setBackButtonAlpha(float alpha, boolean animate) {
- boolean changed = Float.compare(alpha, mLastBackButtonAlpha) != 0
- || animate != mLastBackButtonAnimate;
- if (mSystemUiProxy != null && changed) {
- mLastBackButtonAlpha = alpha;
- mLastBackButtonAnimate = animate;
- try {
- mSystemUiProxy.setBackButtonAlpha(alpha, animate);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call setBackButtonAlpha", e);
- }
- }
- }
-
- public float getLastBackButtonAlpha() {
- return mLastBackButtonAlpha;
+ public float getLastNavButtonAlpha() {
+ return mLastNavButtonAlpha;
}
@Override
public void setNavBarButtonAlpha(float alpha, boolean animate) {
- if (mSystemUiProxy != null) {
+ boolean changed = Float.compare(alpha, mLastNavButtonAlpha) != 0
+ || animate != mLastNavButtonAnimate
+ || !mHasNavButtonAlphaBeenSet;
+ if (mSystemUiProxy != null && changed) {
+ mLastNavButtonAlpha = alpha;
+ mLastNavButtonAnimate = animate;
+ mHasNavButtonAlphaBeenSet = true;
try {
mSystemUiProxy.setNavBarButtonAlpha(alpha, animate);
} catch (RemoteException e) {
@@ -226,18 +297,6 @@
}
@Override
- public Bundle monitorGestureInput(String name, int displayId) {
- if (mSystemUiProxy != null) {
- try {
- return mSystemUiProxy.monitorGestureInput(name, displayId);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call monitorGestureInput: " + name, e);
- }
- }
- return null;
- }
-
- @Override
public void notifyAccessibilityButtonClicked(int displayId) {
if (mSystemUiProxy != null) {
try {
@@ -271,21 +330,6 @@
}
@Override
- public void setShelfHeight(boolean visible, int shelfHeight) {
- boolean changed = visible != mLastShelfVisible || shelfHeight != mLastShelfHeight;
- if (mSystemUiProxy != null && changed) {
- mLastShelfVisible = visible;
- mLastShelfHeight = shelfHeight;
- try {
- mSystemUiProxy.setShelfHeight(visible, shelfHeight);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call setShelfHeight visible: " + visible
- + " height: " + shelfHeight, e);
- }
- }
- }
-
- @Override
public void handleImageAsScreenshot(Bitmap bitmap, Rect rect, Insets insets, int i) {
if (mSystemUiProxy != null) {
try {
@@ -307,6 +351,17 @@
}
}
+ @Override
+ public void notifySwipeUpGestureStarted() {
+ if (mSystemUiProxy != null) {
+ try {
+ mSystemUiProxy.notifySwipeUpGestureStarted();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call notifySwipeUpGestureStarted", e);
+ }
+ }
+ }
+
/**
* Notifies that swipe-to-home action is finished.
*/
@@ -316,32 +371,18 @@
try {
mSystemUiProxy.notifySwipeToHomeFinished();
} catch (RemoteException e) {
- Log.w(TAG, "Failed call setPinnedStackAnimationType", e);
- }
- }
- }
-
- /**
- * Sets listener to get pinned stack animation callbacks.
- */
- @Override
- public void setPinnedStackAnimationListener(IPinnedStackAnimationListener listener) {
- if (mSystemUiProxy != null) {
- try {
- mSystemUiProxy.setPinnedStackAnimationListener(listener);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
+ Log.w(TAG, "Failed call notifySwipeToHomeFinished", e);
}
}
}
@Override
- public void onQuickSwitchToNewTask(int rotation) {
+ public void notifyPrioritizedRotation(int rotation) {
if (mSystemUiProxy != null) {
try {
- mSystemUiProxy.onQuickSwitchToNewTask(rotation);
+ mSystemUiProxy.notifyPrioritizedRotation(rotation);
} catch (RemoteException e) {
- Log.w(TAG, "Failed call onQuickSwitchToNewTask with arg: " + rotation, e);
+ Log.w(TAG, "Failed call notifyPrioritizedRotation with arg: " + rotation, e);
}
}
}
@@ -352,10 +393,280 @@
if (mSystemUiProxy != null) {
try {
mSystemUiProxy.handleImageBundleAsScreenshot(screenImageBundle, locationInScreen,
- visibleInsets, task);
+ visibleInsets, task);
} catch (RemoteException e) {
Log.w(TAG, "Failed call handleImageBundleAsScreenshot");
}
}
}
+
+ @Override
+ public void expandNotificationPanel() {
+ if (mSystemUiProxy != null) {
+ try {
+ mSystemUiProxy.expandNotificationPanel();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call expandNotificationPanel", e);
+ }
+ }
+ }
+
+ //
+ // Pip
+ //
+
+ /**
+ * Sets the shelf height.
+ */
+ public void setShelfHeight(boolean visible, int shelfHeight) {
+ boolean changed = visible != mLastShelfVisible || shelfHeight != mLastShelfHeight;
+ if (mPip != null && changed) {
+ mLastShelfVisible = visible;
+ mLastShelfHeight = shelfHeight;
+ try {
+ mPip.setShelfHeight(visible, shelfHeight);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call setShelfHeight visible: " + visible
+ + " height: " + shelfHeight, e);
+ }
+ }
+ }
+
+ /**
+ * Sets listener to get pinned stack animation callbacks.
+ */
+ public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
+ if (mPip != null) {
+ try {
+ mPip.setPinnedStackAnimationListener(listener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
+ }
+ } else {
+ mPendingPipAnimationListener = listener;
+ }
+ }
+
+ public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
+ PictureInPictureParams pictureInPictureParams, int launcherRotation, int shelfHeight) {
+ if (mPip != null) {
+ try {
+ return mPip.startSwipePipToHome(componentName, activityInfo,
+ pictureInPictureParams, launcherRotation, shelfHeight);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call startSwipePipToHome", e);
+ }
+ }
+ return null;
+ }
+
+ public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds,
+ SurfaceControl overlay) {
+ if (mPip != null) {
+ try {
+ mPip.stopSwipePipToHome(componentName, destinationBounds, overlay);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call stopSwipePipToHome");
+ }
+ }
+ }
+
+ //
+ // Splitscreen
+ //
+
+ public void registerSplitScreenListener(ISplitScreenListener listener) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.registerSplitScreenListener(listener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call registerSplitScreenListener");
+ }
+ } else {
+ mPendingSplitScreenListener = listener;
+ }
+ }
+
+ public void unregisterSplitScreenListener(ISplitScreenListener listener) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.unregisterSplitScreenListener(listener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call unregisterSplitScreenListener");
+ }
+ }
+ mPendingSplitScreenListener = null;
+ }
+
+ public void setSideStageVisibility(boolean visible) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.setSideStageVisibility(visible);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call setSideStageVisibility");
+ }
+ }
+ }
+
+ public void exitSplitScreen() {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.exitSplitScreen();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call exitSplitScreen");
+ }
+ }
+ }
+
+ public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.exitSplitScreenOnHide(exitSplitScreenOnHide);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call exitSplitScreen");
+ }
+ }
+ }
+
+ public void startTask(int taskId, int stage, int position, Bundle options) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.startTask(taskId, stage, position, options);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call startTask");
+ }
+ }
+ }
+
+ /** Start multiple tasks in split-screen simultaneously. */
+ public void startTasks(int mainTaskId, Bundle mainOptions, int sideTaskId, Bundle sideOptions,
+ @SplitConfigurationOptions.StagePosition int sidePosition,
+ RemoteTransitionCompat remoteTransition) {
+ if (mSystemUiProxy != null) {
+ try {
+ mSplitScreen.startTasks(mainTaskId, mainOptions, sideTaskId, sideOptions,
+ sidePosition, remoteTransition.getTransition());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call startTask");
+ }
+ }
+ }
+
+ public void startShortcut(String packageName, String shortcutId, int stage, int position,
+ Bundle options, UserHandle user) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.startShortcut(packageName, shortcutId, stage, position, options,
+ user);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call startShortcut");
+ }
+ }
+ }
+
+ public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
+ Bundle options) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.startIntent(intent, fillInIntent, stage, position, options);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call startIntent");
+ }
+ }
+ }
+
+ public void removeFromSideStage(int taskId) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.removeFromSideStage(taskId);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call removeFromSideStage");
+ }
+ }
+ }
+
+ //
+ // One handed
+ //
+
+ public void startOneHandedMode() {
+ if (mOneHanded != null) {
+ try {
+ mOneHanded.startOneHanded();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call startOneHandedMode", e);
+ }
+ }
+ }
+
+ public void stopOneHandedMode() {
+ if (mOneHanded != null) {
+ try {
+ mOneHanded.stopOneHanded();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call stopOneHandedMode", e);
+ }
+ }
+ }
+
+ //
+ // Remote transitions
+ //
+
+ public void registerRemoteTransition(RemoteTransitionCompat remoteTransition) {
+ if (mShellTransitions != null) {
+ try {
+ mShellTransitions.registerRemote(remoteTransition.getFilter(),
+ remoteTransition.getTransition());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call registerRemoteTransition");
+ }
+ }
+ }
+
+ public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
+ if (mShellTransitions != null) {
+ try {
+ mShellTransitions.unregisterRemote(remoteTransition.getTransition());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call registerRemoteTransition");
+ }
+ }
+ }
+
+ //
+ // Starting window
+ //
+
+ /**
+ * Sets listener to get callbacks when launching a task.
+ */
+ public void setStartingWindowListener(IStartingWindowListener listener) {
+ if (mStartingWindow != null) {
+ try {
+ mStartingWindow.setStartingWindowListener(listener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call setStartingWindowListener", e);
+ }
+ } else {
+ mPendingStartingWindowListener = listener;
+ }
+ }
+
+
+ //
+ // SmartSpace transitions
+ //
+
+ public void setSmartspaceCallback(ISmartspaceCallback callback) {
+ if (mSmartspaceTransitionController != null) {
+ try {
+ mSmartspaceTransitionController.setSmartspace(callback);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call setStartingWindowListener", e);
+ }
+ } else {
+ mPendingSmartspaceCallback = callback;
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index cad51f4..67bd85f 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -15,23 +15,35 @@
*/
package com.android.quickstep;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
+import android.app.ActivityManager;
+import android.content.Context;
import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemProperties;
import android.util.Log;
import androidx.annotation.UiThread;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
+import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.RemoteTransitionCompat;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener {
+ public static final boolean ENABLE_SHELL_TRANSITIONS =
+ SystemProperties.getBoolean("persist.debug.shell_transit", false);
private RecentsAnimationController mController;
private RecentsAnimationCallbacks mCallbacks;
@@ -39,14 +51,41 @@
// Temporary until we can hook into gesture state events
private GestureState mLastGestureState;
private RemoteAnimationTargetCompat mLastAppearedTaskTarget;
+ private Runnable mLiveTileCleanUpHandler;
+ private Context mCtx;
+ private final TaskStackChangeListener mLiveTileRestartListener = new TaskStackChangeListener() {
+ @Override
+ public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+ boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
+ if (mLastGestureState == null) {
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+ mLiveTileRestartListener);
+ return;
+ }
+ BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface();
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityInterface.isInLiveTileMode()
+ && activityInterface.getCreatedActivity() != null) {
+ RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel();
+ if (recentsView != null) {
+ recentsView.launchSideTaskInLiveTileModeForRestartedApp(task.taskId);
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+ mLiveTileRestartListener);
+ }
+ }
+ }
+ };
+
+ TaskAnimationManager(Context ctx) {
+ mCtx = ctx;
+ }
/**
* Preloads the recents animation.
*/
public void preloadRecentsAnimation(Intent intent) {
// Pass null animation handler to indicate this start is for preloading
UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
- .startRecentsActivity(intent, null, null, null, null));
+ .startRecentsActivity(intent, 0, null, null, null));
}
/**
@@ -88,22 +127,30 @@
@Override
public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
- if (thumbnailData != null) {
- // If a screenshot is provided, switch to the screenshot before cleaning up
- activityInterface.switchRunningTaskViewToScreenshot(thumbnailData,
- () -> cleanUpRecentsAnimation(thumbnailData));
- } else {
- cleanUpRecentsAnimation(null /* canceledThumbnail */);
- }
+ cleanUpRecentsAnimation();
}
@Override
public void onRecentsAnimationFinished(RecentsAnimationController controller) {
- cleanUpRecentsAnimation(null /* canceledThumbnail */);
+ cleanUpRecentsAnimation();
}
@Override
public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+ BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface();
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityInterface.isInLiveTileMode()
+ && activityInterface.getCreatedActivity() != null) {
+ RecentsView recentsView =
+ activityInterface.getCreatedActivity().getOverviewPanel();
+ if (recentsView != null) {
+ RemoteAnimationTargetCompat[] apps = new RemoteAnimationTargetCompat[1];
+ apps[0] = appearedTaskTarget;
+ recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId, apps,
+ new RemoteAnimationTargetCompat[0] /* wallpaper */,
+ new RemoteAnimationTargetCompat[0] /* nonApps */);
+ return;
+ }
+ }
if (mController != null) {
if (mLastAppearedTaskTarget == null
|| appearedTaskTarget.taskId != mLastAppearedTaskTarget.taskId) {
@@ -116,10 +163,20 @@
}
}
});
+ final long eventTime = gestureState.getSwipeUpStartTimeMs();
mCallbacks.addListener(gestureState);
mCallbacks.addListener(listener);
- UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
- .startRecentsActivity(intent, null, mCallbacks, null, null));
+
+ if (ENABLE_SHELL_TRANSITIONS) {
+ RemoteTransitionCompat transition = new RemoteTransitionCompat(mCallbacks,
+ mController != null ? mController.getController() : null);
+ Bundle options = ActivityOptionsCompat.makeRemoteTransition(transition)
+ .setTransientLaunch().toBundle();
+ mCtx.startActivity(intent, options);
+ } else {
+ UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
+ .startRecentsActivity(intent, eventTime, mCallbacks, null, null));
+ }
gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED);
return mCallbacks;
}
@@ -137,6 +194,14 @@
return mCallbacks;
}
+ public void setLiveTileCleanUpHandler(Runnable cleanUpHandler) {
+ mLiveTileCleanUpHandler = cleanUpHandler;
+ }
+
+ public void enableLiveTileRestartListener() {
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mLiveTileRestartListener);
+ }
+
/**
* Finishes the running recents animation.
*/
@@ -146,7 +211,7 @@
Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
? mController::finishAnimationToHome
: mController::finishAnimationToApp);
- cleanUpRecentsAnimation(null /* canceledThumbnail */);
+ cleanUpRecentsAnimation();
}
}
@@ -173,11 +238,12 @@
/**
* Cleans up the recents animation entirely.
*/
- private void cleanUpRecentsAnimation(ThumbnailData canceledThumbnail) {
- // Clean up the screenshot if necessary
- if (mController != null && canceledThumbnail != null) {
- mController.cleanupScreenshot();
+ private void cleanUpRecentsAnimation() {
+ if (mLiveTileCleanUpHandler != null) {
+ mLiveTileCleanUpHandler.run();
+ mLiveTileCleanUpHandler = null;
}
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mLiveTileRestartListener);
// Release all the target leashes
if (mTargets != null) {
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 7ff799e..fa61fff 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,49 +15,50 @@
*/
package com.android.quickstep;
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import android.app.ActivityManager.TaskDescription;
import android.content.Context;
import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.SparseArray;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.WorkerThread;
-import com.android.launcher3.FastBitmapDrawable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.IconProvider;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.icons.cache.HandlerRunnable;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
+import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.CancellableTask;
import com.android.quickstep.util.TaskKeyLruCache;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.PackageManagerWrapper;
import com.android.systemui.shared.system.TaskDescriptionCompat;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* Manages the caching of task icons and related data.
*/
-public class TaskIconCache {
+public class TaskIconCache implements DisplayInfoChangeListener {
- private final Handler mBackgroundHandler;
+ private final Executor mBgExecutor;
private final AccessibilityManager mAccessibilityManager;
private final Context mContext;
@@ -65,15 +66,27 @@
private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
private final IconProvider mIconProvider;
- public TaskIconCache(Context context, Looper backgroundLooper) {
+ private BaseIconFactory mIconFactory;
+
+ public TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider) {
mContext = context;
- mBackgroundHandler = new Handler(backgroundLooper);
+ mBgExecutor = bgExecutor;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ mIconProvider = iconProvider;
Resources res = context.getResources();
int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
+
mIconCache = new TaskKeyLruCache<>(cacheSize);
- mIconProvider = new IconProvider(context);
+
+ DisplayController.INSTANCE.get(mContext).addChangeListener(this);
+ }
+
+ @Override
+ public void onDisplayInfoChanged(Context context, Info info, int flags) {
+ if ((flags & CHANGE_DENSITY) != 0) {
+ clearCache();
+ }
}
/**
@@ -83,36 +96,35 @@
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
- public IconLoadRequest updateIconInBackground(Task task, Consumer<Task> callback) {
+ public CancellableTask updateIconInBackground(Task task, Consumer<Task> callback) {
Preconditions.assertUIThread();
if (task.icon != null) {
// Nothing to load, the icon is already loaded
callback.accept(task);
return null;
}
-
- IconLoadRequest request = new IconLoadRequest(mBackgroundHandler) {
+ CancellableTask<TaskCacheEntry> request = new CancellableTask<TaskCacheEntry>() {
@Override
- public void run() {
- TaskCacheEntry entry = getCacheEntry(task);
- if (isCanceled()) {
- // We don't call back to the provided callback in this case
- return;
- }
- MAIN_EXECUTOR.execute(() -> {
- task.icon = entry.icon;
- task.titleDescription = entry.contentDescription;
- callback.accept(task);
- onEnd();
- });
+ public TaskCacheEntry getResultOnBg() {
+ return getCacheEntry(task);
+ }
+
+ @Override
+ public void handleResult(TaskCacheEntry result) {
+ task.icon = result.icon;
+ task.titleDescription = result.contentDescription;
+ callback.accept(task);
}
};
- Utilities.postAsyncCallback(mBackgroundHandler, request);
+ mBgExecutor.execute(request);
return request;
}
- public void clear() {
- mIconCache.evictAll();
+ /**
+ * Clears the icon cache
+ */
+ public void clearCache() {
+ mBgExecutor.execute(this::resetFactory);
}
void onTaskRemoved(TaskKey taskKey) {
@@ -120,9 +132,8 @@
}
void invalidateCacheEntries(String pkg, UserHandle handle) {
- Utilities.postAsyncCallback(mBackgroundHandler,
- () -> mIconCache.removeAll(key ->
- pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
+ mBgExecutor.execute(() -> mIconCache.removeAll(key ->
+ pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
}
@WorkerThread
@@ -143,21 +154,22 @@
// TODO: Load icon resource (b/143363444)
Bitmap icon = TaskDescriptionCompat.getIcon(desc, key.userId);
if (icon != null) {
- entry.icon = new FastBitmapDrawable(getBitmapInfo(
+ /* isInstantApp */
+ entry.icon = getBitmapInfo(
new BitmapDrawable(mContext.getResources(), icon),
key.userId,
desc.getPrimaryColor(),
- false /* isInstantApp */));
+ false /* isInstantApp */).newIcon(mContext);
} else {
activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
key.getComponent(), key.userId);
if (activityInfo != null) {
BitmapInfo bitmapInfo = getBitmapInfo(
- mIconProvider.getIcon(activityInfo, UserHandle.of(key.userId)),
+ mIconProvider.getIcon(activityInfo),
key.userId,
desc.getPrimaryColor(),
activityInfo.applicationInfo.isInstantApp());
- entry.icon = newIcon(mContext, bitmapInfo);
+ entry.icon = bitmapInfo.newIcon(mContext);
} else {
entry.icon = getDefaultIcon(key.userId);
}
@@ -171,9 +183,8 @@
key.getComponent(), key.userId);
}
if (activityInfo != null) {
- entry.contentDescription = ActivityManagerWrapper.getInstance()
- .getBadgedContentDescription(activityInfo, task.key.userId,
- task.taskDescription);
+ entry.contentDescription = getBadgedContentDescription(
+ activityInfo, task.key.userId, task.taskDescription);
}
}
@@ -181,37 +192,62 @@
return entry;
}
+ private String getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td) {
+ PackageManager pm = mContext.getPackageManager();
+ String taskLabel = td == null ? null : Utilities.trim(td.getLabel());
+ if (TextUtils.isEmpty(taskLabel)) {
+ taskLabel = Utilities.trim(info.loadLabel(pm));
+ }
+
+ String applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(pm));
+ String badgedApplicationLabel = userId != UserHandle.myUserId()
+ ? pm.getUserBadgedLabel(applicationLabel, UserHandle.of(userId)).toString()
+ : applicationLabel;
+ return applicationLabel.equals(taskLabel)
+ ? badgedApplicationLabel : badgedApplicationLabel + " " + taskLabel;
+ }
+
@WorkerThread
private Drawable getDefaultIcon(int userId) {
synchronized (mDefaultIcons) {
BitmapInfo info = mDefaultIcons.get(userId);
if (info == null) {
- try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
- info = la.makeDefaultIcon(UserHandle.of(userId));
+ try (BaseIconFactory bif = getIconFactory()) {
+ info = bif.makeDefaultIcon(UserHandle.of(userId));
}
mDefaultIcons.put(userId, info);
}
- return new FastBitmapDrawable(info);
+ return info.newIcon(mContext);
}
}
@WorkerThread
private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
int primaryColor, boolean isInstantApp) {
- try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
- la.disableColorExtraction();
- la.setWrapperBackgroundColor(primaryColor);
+ try (BaseIconFactory bif = getIconFactory()) {
+ bif.disableColorExtraction();
+ bif.setWrapperBackgroundColor(primaryColor);
// User version code O, so that the icon is always wrapped in an adaptive icon container
- return la.createBadgedIconBitmap(drawable, UserHandle.of(userId),
+ return bif.createBadgedIconBitmap(drawable, UserHandle.of(userId),
Build.VERSION_CODES.O, isInstantApp);
}
}
- public static abstract class IconLoadRequest extends HandlerRunnable {
- IconLoadRequest(Handler handler) {
- super(handler, null);
+ @WorkerThread
+ private BaseIconFactory getIconFactory() {
+ if (mIconFactory == null) {
+ mIconFactory = new BaseIconFactory(mContext,
+ DisplayController.INSTANCE.get(mContext).getInfo().densityDpi,
+ mContext.getResources().getDimensionPixelSize(R.dimen.taskbar_icon_size));
}
+ return mIconFactory;
+ }
+
+ @WorkerThread
+ private void resetFactory() {
+ mIconFactory = null;
+ mIconCache.evictAll();
}
private static class TaskCacheEntry {
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
new file mode 100644
index 0000000..e75d751
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -0,0 +1,334 @@
+/*
+ * 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.Surface.ROTATION_0;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.quickstep.TaskShortcutFactory.SplitSelectSystemShortcut;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.views.OverviewActionsView;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Factory class to create and add an overlays on the TaskView
+ */
+public class TaskOverlayFactory implements ResourceBasedOverride {
+
+ public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView,
+ DeviceProfile deviceProfile) {
+ final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
+ final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
+ for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
+ SystemShortcut shortcut = menuOption.getShortcut(activity, taskView);
+ if (menuOption == TaskShortcutFactory.SPLIT_SCREEN &&
+ FeatureFlags.ENABLE_SPLIT_SELECT.get()) {
+ addSplitOptions(shortcuts, activity, taskView, deviceProfile);
+ continue;
+ }
+
+ if (shortcut != null) {
+ shortcuts.add(shortcut);
+ }
+ }
+ RecentsOrientedState orientedState = taskView.getRecentsView().getPagedViewOrientedState();
+ boolean canLauncherRotate = orientedState.canRecentsActivityRotate();
+ boolean isInLandscape = orientedState.getTouchRotation() != ROTATION_0;
+
+ // Add overview actions to the menu when in in-place rotate landscape mode.
+ if (!canLauncherRotate && isInLandscape) {
+ // Add screenshot action to task menu.
+ SystemShortcut screenshotShortcut = TaskShortcutFactory.SCREENSHOT
+ .getShortcut(activity, taskView);
+ if (screenshotShortcut != null) {
+ screenshotShortcut.setHasFinishRecentsInAction(true);
+ shortcuts.add(screenshotShortcut);
+ }
+
+ // Add modal action only if display orientation is the same as the device orientation.
+ if (orientedState.getDisplayRotation() == ROTATION_0) {
+ SystemShortcut modalShortcut = TaskShortcutFactory.MODAL
+ .getShortcut(activity, taskView);
+ if (modalShortcut != null) {
+ modalShortcut.setHasFinishRecentsInAction(true);
+ shortcuts.add(modalShortcut);
+ }
+ }
+ }
+ return shortcuts;
+ }
+
+
+ public static void addSplitOptions(List<SystemShortcut> outShortcuts,
+ BaseDraggingActivity activity, TaskView taskView, DeviceProfile deviceProfile) {
+ PagedOrientationHandler orientationHandler =
+ taskView.getRecentsView().getPagedOrientationHandler();
+ List<SplitPositionOption> positions =
+ orientationHandler.getSplitPositionOptions(deviceProfile);
+ for (SplitPositionOption option : positions) {
+ outShortcuts.add(new SplitSelectSystemShortcut(activity, taskView, option));
+ }
+ }
+
+ public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
+ return new TaskOverlay(thumbnailView);
+ }
+
+ /**
+ * Subclasses can attach any system listeners in this method, must be paired with
+ * {@link #removeListeners()}
+ */
+ public void initListeners() { }
+
+ /**
+ * Subclasses should remove any system listeners in this method, must be paired with
+ * {@link #initListeners()}
+ */
+ public void removeListeners() { }
+
+ /** Note that these will be shown in order from top to bottom, if available for the task. */
+ private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
+ TaskShortcutFactory.APP_INFO,
+ TaskShortcutFactory.SPLIT_SCREEN,
+ TaskShortcutFactory.PIN,
+ TaskShortcutFactory.INSTALL,
+ TaskShortcutFactory.FREE_FORM,
+ TaskShortcutFactory.WELLBEING
+ };
+
+ /**
+ * Overlay on each task handling Overview Action Buttons.
+ */
+ public static class TaskOverlay<T extends OverviewActionsView> {
+
+ protected final Context mApplicationContext;
+ protected final TaskThumbnailView mThumbnailView;
+
+ private T mActionsView;
+ protected ImageActionsApi mImageApi;
+
+ protected TaskOverlay(TaskThumbnailView taskThumbnailView) {
+ mApplicationContext = taskThumbnailView.getContext().getApplicationContext();
+ mThumbnailView = taskThumbnailView;
+ mImageApi = new ImageActionsApi(
+ mApplicationContext, mThumbnailView::getThumbnail);
+ }
+
+ protected T getActionsView() {
+ if (mActionsView == null) {
+ mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById(
+ R.id.overview_actions_view);
+ }
+ return mActionsView;
+ }
+
+ /**
+ * Called when the current task is interactive for the user
+ */
+ public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+ boolean rotated) {
+ getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
+
+ if (thumbnail != null) {
+ getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+ boolean isAllowedByPolicy = mThumbnailView.isRealSnapshot();
+ getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
+ }
+ }
+
+ /**
+ * End rendering live tile in Overview.
+ *
+ * @param callback callback to run, after switching to screenshot
+ */
+ public void endLiveTileMode(@NonNull Runnable callback) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView();
+ recentsView.switchToScreenshot(
+ () -> recentsView.finishRecentsAnimation(true /* toRecents */,
+ false /* shouldPip */, callback));
+ } else {
+ callback.run();
+ }
+ }
+
+ /**
+ * Called to save screenshot of the task thumbnail.
+ */
+ @SuppressLint("NewApi")
+ protected void saveScreenshot(Task task) {
+ if (mThumbnailView.isRealSnapshot()) {
+ mImageApi.saveScreenshot(mThumbnailView.getThumbnail(),
+ getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+
+ /**
+ * Called when the overlay is no longer used.
+ */
+ public void reset() {
+ }
+
+ /**
+ * Called when the system wants to reset the modal visuals.
+ */
+ public void resetModalVisuals() {
+ }
+
+ /**
+ * Gets the modal state system shortcut.
+ */
+ public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo) {
+ return null;
+ }
+
+ /**
+ * Sets full screen progress to the task overlay.
+ */
+ public void setFullscreenProgress(float progress) {
+ }
+
+ /**
+ * Gets the system shortcut for the screenshot that will be added to the task menu.
+ */
+ public SystemShortcut getScreenshotShortcut(BaseDraggingActivity activity,
+ ItemInfo iteminfo) {
+ return new ScreenshotSystemShortcut(activity, iteminfo);
+ }
+ /**
+ * Gets the task snapshot as it is displayed on the screen.
+ *
+ * @return the bounds of the snapshot in screen coordinates.
+ */
+ public Rect getTaskSnapshotBounds() {
+ int[] location = new int[2];
+ mThumbnailView.getLocationOnScreen(location);
+
+ return new Rect(location[0], location[1], mThumbnailView.getWidth() + location[0],
+ mThumbnailView.getHeight() + location[1]);
+ }
+
+ /**
+ * Gets the insets that the snapshot is drawn with.
+ *
+ * @return the insets in screen coordinates.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ public Insets getTaskSnapshotInsets() {
+ return mThumbnailView.getScaledInsets();
+ }
+
+ /**
+ * Called when the device rotated.
+ */
+ public void updateOrientationState(RecentsOrientedState state) {
+ }
+
+ protected void showBlockedByPolicyMessage() {
+ Toast.makeText(
+ mThumbnailView.getContext(),
+ R.string.blocked_by_policy,
+ Toast.LENGTH_LONG).show();
+ }
+
+ private class ScreenshotSystemShortcut extends SystemShortcut {
+
+ private final BaseDraggingActivity mActivity;
+
+ ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo) {
+ super(R.drawable.ic_screenshot, R.string.action_screenshot, activity, itemInfo);
+ mActivity = activity;
+ }
+
+ @Override
+ public void onClick(View view) {
+ saveScreenshot(mThumbnailView.getTaskView().getTask());
+ dismissTaskMenuView(mActivity);
+ }
+ }
+
+ protected class OverlayUICallbacksImpl implements OverlayUICallbacks {
+ protected final boolean mIsAllowedByPolicy;
+ protected final Task mTask;
+
+ public OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task) {
+ mIsAllowedByPolicy = isAllowedByPolicy;
+ mTask = task;
+ }
+
+ public void onShare() {
+ if (mIsAllowedByPolicy) {
+ endLiveTileMode(() -> mImageApi.startShareActivity(null));
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public void onScreenshot() {
+ endLiveTileMode(() -> saveScreenshot(mTask));
+ }
+ }
+ }
+
+ /**
+ * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
+ * controller.
+ */
+ public interface OverlayUICallbacks {
+ /** User has indicated they want to share the current task. */
+ void onShare();
+
+ /** User has indicated they want to screenshot the current task. */
+ void onScreenshot();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
new file mode 100644
index 0000000..a078bf3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -0,0 +1,340 @@
+/*
+ * 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.Display.DEFAULT_DISPLAY;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SELECTIONS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.window.SplashScreen;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
+import com.android.launcher3.model.WellbeingModel;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.popup.SystemShortcut.AppInfo;
+import com.android.launcher3.util.InstantAppResolver;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
+import com.android.systemui.shared.recents.view.RecentsTransition;
+import com.android.systemui.shared.system.ActivityCompat;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a system shortcut that can be shown for a recent task.
+ */
+public interface TaskShortcutFactory {
+ SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view);
+
+ TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, view.getItemInfo());
+
+ abstract class MultiWindowFactory implements TaskShortcutFactory {
+
+ private final int mIconRes;
+ private final int mTextRes;
+ private final LauncherEvent mLauncherEvent;
+
+ MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent) {
+ mIconRes = iconRes;
+ mTextRes = textRes;
+ mLauncherEvent = launcherEvent;
+ }
+
+ protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
+ protected abstract ActivityOptions makeLaunchOptions(Activity activity);
+ protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
+
+ @Override
+ public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
+ final Task task = taskView.getTask();
+ if (!task.isDockable) {
+ return null;
+ }
+ if (!isAvailable(activity, task.key.displayId)) {
+ return null;
+ }
+ return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this,
+ mLauncherEvent);
+ }
+ }
+
+ class SplitSelectSystemShortcut extends SystemShortcut {
+ private final TaskView mTaskView;
+ private SplitPositionOption mSplitPositionOption;
+ public SplitSelectSystemShortcut(BaseDraggingActivity target, TaskView taskView,
+ SplitPositionOption option) {
+ super(option.mIconResId, option.mTextResId, target, taskView.getItemInfo());
+ mTaskView = taskView;
+ mSplitPositionOption = option;
+ setEnabled(taskView.getRecentsView().getTaskViewCount() > 1);
+ }
+
+ @Override
+ public void onClick(View view) {
+ mTaskView.initiateSplitSelect(mSplitPositionOption);
+ }
+ }
+
+ class MultiWindowSystemShortcut extends SystemShortcut {
+
+ private Handler mHandler;
+
+ private final RecentsView mRecentsView;
+ private final TaskThumbnailView mThumbnailView;
+ private final TaskView mTaskView;
+ private final MultiWindowFactory mFactory;
+ private final LauncherEvent mLauncherEvent;
+
+ public MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
+ TaskView taskView, MultiWindowFactory factory, LauncherEvent launcherEvent) {
+ super(iconRes, textRes, activity, taskView.getItemInfo());
+ mLauncherEvent = launcherEvent;
+ mHandler = new Handler(Looper.getMainLooper());
+ mTaskView = taskView;
+ mRecentsView = activity.getOverviewPanel();
+ mThumbnailView = taskView.getThumbnail();
+ mFactory = factory;
+ }
+
+ @Override
+ public void onClick(View view) {
+ Task.TaskKey taskKey = mTaskView.getTask().key;
+ final int taskId = taskKey.id;
+
+ final View.OnLayoutChangeListener onLayoutChangeListener =
+ new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int l, int t, int r, int b,
+ int oldL, int oldT, int oldR, int oldB) {
+ mTaskView.getRootView().removeOnLayoutChangeListener(this);
+ mRecentsView.clearIgnoreResetTask(taskId);
+
+ // Start animating in the side pages once launcher has been resized
+ mRecentsView.dismissTask(mTaskView, false, false);
+ }
+ };
+
+ final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
+ new DeviceProfile.OnDeviceProfileChangeListener() {
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile dp) {
+ mTarget.removeOnDeviceProfileChangeListener(this);
+ if (dp.isMultiWindowMode) {
+ mTaskView.getRootView().addOnLayoutChangeListener(
+ onLayoutChangeListener);
+ }
+ }
+ };
+
+ dismissTaskMenuView(mTarget);
+
+ ActivityOptions options = mFactory.makeLaunchOptions(mTarget);
+ if (options != null) {
+ options.setSplashscreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
+ }
+ if (options != null
+ && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
+ options)) {
+ if (!mFactory.onActivityStarted(mTarget)) {
+ return;
+ }
+ // Add a device profile change listener to kick off animating the side tasks
+ // once we enter multiwindow mode and relayout
+ mTarget.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
+
+ final Runnable animStartedListener = () -> {
+ // Hide the task view and wait for the window to be resized
+ // TODO: Consider animating in launcher and do an in-place start activity
+ // afterwards
+ mRecentsView.setIgnoreResetTask(taskId);
+ mTaskView.setAlpha(0f);
+ };
+
+ final int[] position = new int[2];
+ mThumbnailView.getLocationOnScreen(position);
+ final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
+ final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
+ final Rect taskBounds = new Rect(position[0], position[1],
+ position[0] + width, position[1] + height);
+
+ // Take the thumbnail of the task without a scrim and apply it back after
+ float alpha = mThumbnailView.getDimAlpha();
+ mThumbnailView.setDimAlpha(0);
+ Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
+ taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
+ Color.BLACK);
+ mThumbnailView.setDimAlpha(alpha);
+
+ AppTransitionAnimationSpecsFuture future =
+ new AppTransitionAnimationSpecsFuture(mHandler) {
+ @Override
+ public List<AppTransitionAnimationSpecCompat> composeSpecs() {
+ return Collections.singletonList(new AppTransitionAnimationSpecCompat(
+ taskId, thumbnail, taskBounds));
+ }
+ };
+ WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
+ future, animStartedListener, mHandler, true /* scaleUp */,
+ taskKey.displayId);
+ mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
+ .log(mLauncherEvent);
+ }
+ }
+ }
+
+ TaskShortcutFactory SPLIT_SCREEN = new MultiWindowFactory(R.drawable.ic_split_screen,
+ R.string.recent_task_option_split_screen, LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP) {
+
+ @Override
+ protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+ // Don't show menu-item if already in multi-window and the task is from
+ // the secondary display.
+ // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
+ // implementation is enabled
+ return !activity.getDeviceProfile().isMultiWindowMode
+ && (displayId == -1 || displayId == DEFAULT_DISPLAY);
+ }
+
+ @Override
+ public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
+ SystemShortcut shortcut = super.getShortcut(activity, taskView);
+ if (shortcut != null && FeatureFlags.ENABLE_SPLIT_SELECT.get()) {
+ // Disable if there's only one recent app for split screen
+ shortcut.setEnabled(taskView.getRecentsView().getTaskViewCount() > 1);
+ }
+ return shortcut;
+ }
+
+ @Override
+ protected ActivityOptions makeLaunchOptions(Activity activity) {
+ final ActivityCompat act = new ActivityCompat(activity);
+ final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
+ act.getDisplayId());
+ if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
+ return null;
+ }
+ boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
+ return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
+ }
+
+ @Override
+ protected boolean onActivityStarted(BaseDraggingActivity activity) {
+ return true;
+ }
+ };
+
+ TaskShortcutFactory FREE_FORM = new MultiWindowFactory(R.drawable.ic_split_screen,
+ R.string.recent_task_option_freeform, LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP) {
+
+ @Override
+ protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+ return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
+ }
+
+ @Override
+ protected ActivityOptions makeLaunchOptions(Activity activity) {
+ ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
+ // Arbitrary bounds only because freeform is in dev mode right now
+ Rect r = new Rect(50, 50, 200, 200);
+ activityOptions.setLaunchBounds(r);
+ return activityOptions;
+ }
+
+ @Override
+ protected boolean onActivityStarted(BaseDraggingActivity activity) {
+ activity.returnToHomescreen();
+ return true;
+ }
+ };
+
+ TaskShortcutFactory PIN = (activity, tv) -> {
+ if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
+ return null;
+ }
+ if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
+ return null;
+ }
+ if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
+ // We shouldn't be able to pin while an app is locked.
+ return null;
+ }
+ return new PinSystemShortcut(activity, tv);
+ };
+
+ class PinSystemShortcut extends SystemShortcut {
+
+ private static final String TAG = "PinSystemShortcut";
+
+ private final TaskView mTaskView;
+
+ public PinSystemShortcut(BaseDraggingActivity target, TaskView tv) {
+ super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, tv.getItemInfo());
+ mTaskView = tv;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mTaskView.launchTaskAnimated() != null) {
+ SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(mTaskView.getTask().key.id);
+ }
+ dismissTaskMenuView(mTarget);
+ mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
+ .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
+ }
+ }
+
+ TaskShortcutFactory INSTALL = (activity, view) ->
+ InstantAppResolver.newInstance(activity).isInstantApp(activity,
+ view.getTask().getTopComponent().getPackageName())
+ ? new SystemShortcut.Install(activity, view.getItemInfo()) : null;
+
+ TaskShortcutFactory WELLBEING = (activity, view) ->
+ WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, view.getItemInfo());
+
+ TaskShortcutFactory SCREENSHOT = (activity, tv) -> tv.getThumbnail().getTaskOverlay()
+ .getScreenshotShortcut(activity, tv.getItemInfo());
+
+ TaskShortcutFactory MODAL = (activity, tv) -> {
+ if (ENABLE_OVERVIEW_SELECTIONS.get()) {
+ return tv.getThumbnail().getTaskOverlay().getModalStateSystemShortcut(tv.getItemInfo());
+ }
+ return null;
+ };
+}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 2b7a8ec..a8a0219 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -15,17 +15,12 @@
*/
package com.android.quickstep;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
import android.content.Context;
import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Looper;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.CancellableTask;
import com.android.quickstep.util.TaskKeyLruCache;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
@@ -33,11 +28,12 @@
import com.android.systemui.shared.system.ActivityManagerWrapper;
import java.util.ArrayList;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
public class TaskThumbnailCache {
- private final Handler mBackgroundHandler;
+ private final Executor mBgExecutor;
private final int mCacheSize;
private final TaskKeyLruCache<ThumbnailData> mCache;
@@ -94,8 +90,8 @@
}
}
- public TaskThumbnailCache(Context context, Looper backgroundLooper) {
- mBackgroundHandler = new Handler(backgroundLooper);
+ public TaskThumbnailCache(Context context, Executor bgExecutor) {
+ mBgExecutor = bgExecutor;
mHighResLoadingState = new HighResLoadingState(context);
Resources res = context.getResources();
@@ -130,7 +126,7 @@
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
- public ThumbnailLoadRequest updateThumbnailInBackground(
+ public CancellableTask updateThumbnailInBackground(
Task task, Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
@@ -142,14 +138,13 @@
return null;
}
-
return updateThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), t -> {
task.thumbnail = t;
callback.accept(t);
});
}
- private ThumbnailLoadRequest updateThumbnailInBackground(TaskKey key, boolean lowResolution,
+ private CancellableTask updateThumbnailInBackground(TaskKey key, boolean lowResolution,
Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
@@ -160,26 +155,20 @@
return null;
}
- ThumbnailLoadRequest request = new ThumbnailLoadRequest(mBackgroundHandler,
- lowResolution) {
+ CancellableTask<ThumbnailData> request = new CancellableTask<ThumbnailData>() {
@Override
- public void run() {
- ThumbnailData thumbnail = ActivityManagerWrapper.getInstance().getTaskThumbnail(
+ public ThumbnailData getResultOnBg() {
+ return ActivityManagerWrapper.getInstance().getTaskThumbnail(
key.id, lowResolution);
+ }
- MAIN_EXECUTOR.execute(() -> {
- if (isCanceled()) {
- // We don't call back to the provided callback in this case
- return;
- }
-
- mCache.put(key, thumbnail);
- callback.accept(thumbnail);
- onEnd();
- });
+ @Override
+ public void handleResult(ThumbnailData result) {
+ mCache.put(key, result);
+ callback.accept(result);
}
};
- Utilities.postAsyncCallback(mBackgroundHandler, request);
+ mBgExecutor.execute(request);
return request;
}
@@ -218,15 +207,6 @@
return mEnableTaskSnapshotPreloading && mHighResLoadingState.mVisible;
}
- public static abstract class ThumbnailLoadRequest extends HandlerRunnable {
- public final boolean mLowResolution;
-
- ThumbnailLoadRequest(Handler handler, boolean lowResolution) {
- super(handler, null);
- mLowResolution = lowResolution;
- }
- }
-
/**
* @return Whether device supports low-res thumbnails. Low-res files are an optimization
* for faster load times of snapshots. Devices can optionally disable low-res files so that
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 04b488d..c9db153 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -16,6 +16,8 @@
package com.android.quickstep;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -27,6 +29,7 @@
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import java.util.List;
@@ -86,4 +89,12 @@
}
return false;
}
+
+ /**
+ * Requests that the system close any open system windows (including other SystemUI).
+ */
+ public static void closeSystemWindowsAsync(String reason) {
+ UI_HELPER_EXECUTOR.execute(
+ () -> ActivityManagerWrapper.getInstance().closeSystemWindows(reason));
+ }
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
new file mode 100644
index 0000000..37fda73
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_DELAY_NAV_FADE_IN;
+import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_NAV_FADE_IN_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_NAV_FADE_OUT_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.NAV_FADE_IN_INTERPOLATOR;
+import static com.android.launcher3.QuickstepTransitionManager.NAV_FADE_OUT_INTERPOLATOR;
+import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
+import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR_ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.statehandlers.DepthController.DEPTH;
+import static com.android.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.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.util.MultiValueUpdateListener;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+/**
+ * Utility class for helpful methods related to {@link TaskView} objects and their tasks.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public final class TaskViewUtils {
+
+ private TaskViewUtils() {}
+
+ /**
+ * 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(
+ RecentsView recentsView, View v, RemoteAnimationTargetCompat[] targets) {
+ if (v instanceof TaskView) {
+ TaskView taskView = (TaskView) v;
+ return recentsView.isTaskViewVisible(taskView) ? taskView : null;
+ }
+
+ // 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.getTaskViewCount(); i++) {
+ TaskView taskView = recentsView.getTaskViewAt(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;
+ }
+
+ public static void createRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets, DepthController depthController,
+ PendingAnimation out) {
+ boolean isRunningTask = v.isRunningTask();
+ TransformParams params = null;
+ TaskViewSimulator tsv = null;
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask) {
+ params = v.getRecentsView().getLiveTileParams();
+ tsv = v.getRecentsView().getLiveTileTaskViewSimulator();
+ }
+ createRecentsWindowAnimator(v, skipViewChanges, appTargets, wallpaperTargets, nonAppTargets,
+ depthController, out, params, tsv);
+ }
+
+ /**
+ * Creates an animation that controls the window of the opening targets for the recents launch
+ * animation.
+ */
+ public static void createRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets, DepthController depthController,
+ PendingAnimation out, @Nullable TransformParams params,
+ @Nullable TaskViewSimulator tsv) {
+ boolean isQuickSwitch = v.isEndQuickswitchCuj();
+ v.setEndQuickswitchCuj(false);
+
+ boolean inLiveTileMode =
+ ENABLE_QUICKSTEP_LIVE_TILE.get() && v.getRecentsView().getRunningTaskIndex() != -1;
+ final RemoteAnimationTargets targets =
+ new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets,
+ inLiveTileMode ? MODE_CLOSING : MODE_OPENING);
+ final RemoteAnimationTargetCompat navBarTarget = targets.getNavBarRemoteAnimationTarget();
+
+ if (params == null) {
+ SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
+ targets.addReleaseCheck(applier);
+
+ params = new TransformParams()
+ .setSyncTransactionApplier(applier)
+ .setTargetSet(targets);
+ }
+
+ final RecentsView recentsView = v.getRecentsView();
+ int taskIndex = recentsView.indexOfChild(v);
+ Context context = v.getContext();
+ DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+ boolean showAsGrid = dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
+ boolean parallaxCenterAndAdjacentTask =
+ taskIndex != recentsView.getCurrentPage() && !showAsGrid;
+ float gridTranslationSecondary = recentsView.getGridTranslationSecondary(taskIndex);
+ int startScroll = recentsView.getScrollOffset(taskIndex);
+
+ TaskViewSimulator topMostSimulator = null;
+
+ if (tsv == null && targets.apps.length > 0) {
+ tsv = new TaskViewSimulator(context, recentsView.getSizeStrategy());
+ tsv.setDp(dp);
+
+ // RecentsView never updates the display rotation until swipe-up so the value may
+ // be stale. Use the display value instead.
+ int displayRotation = DisplayController.INSTANCE.get(context).getInfo().rotation;
+ tsv.getOrientationState().update(displayRotation, displayRotation);
+
+ tsv.setPreview(targets.apps[targets.apps.length - 1]);
+ tsv.fullScreenProgress.value = 0;
+ tsv.recentsViewScale.value = 1;
+ if (showAsGrid) {
+ tsv.taskSecondaryTranslation.value = gridTranslationSecondary;
+ }
+ tsv.setScroll(startScroll);
+
+ // Fade in the task during the initial 20% of the animation
+ out.addFloat(params, TransformParams.TARGET_ALPHA, 0, 1,
+ clampToProgress(LINEAR, 0, 0.2f));
+ }
+
+ if (tsv != null) {
+ out.setFloat(tsv.fullScreenProgress,
+ AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
+ out.setFloat(tsv.recentsViewScale,
+ AnimatedFloat.VALUE, tsv.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
+ if (showAsGrid) {
+ out.setFloat(tsv.taskSecondaryTranslation, AnimatedFloat.VALUE, 0,
+ TOUCH_RESPONSE_INTERPOLATOR_ACCEL_DEACCEL);
+ }
+ out.setFloat(tsv.recentsViewScroll, AnimatedFloat.VALUE, 0,
+ TOUCH_RESPONSE_INTERPOLATOR);
+
+ TaskViewSimulator finalTsv = tsv;
+ TransformParams finalParams = params;
+ out.addOnFrameCallback(() -> finalTsv.apply(finalParams));
+ if (navBarTarget != null) {
+ final Rect cropRect = new Rect();
+ out.addOnFrameListener(new MultiValueUpdateListener() {
+ FloatProp mNavFadeOut = new FloatProp(1f, 0f, 0,
+ ANIMATION_NAV_FADE_OUT_DURATION, NAV_FADE_OUT_INTERPOLATOR);
+ FloatProp mNavFadeIn = new FloatProp(0f, 1f, ANIMATION_DELAY_NAV_FADE_IN,
+ ANIMATION_NAV_FADE_IN_DURATION, NAV_FADE_IN_INTERPOLATOR);
+
+ @Override
+ public void onUpdate(float percent, boolean initOnly) {
+ final SurfaceParams.Builder navBuilder =
+ new SurfaceParams.Builder(navBarTarget.leash);
+ if (mNavFadeIn.value > mNavFadeIn.getStartValue()) {
+ finalTsv.getCurrentCropRect().round(cropRect);
+ navBuilder.withMatrix(finalTsv.getCurrentMatrix())
+ .withWindowCrop(cropRect)
+ .withAlpha(mNavFadeIn.value);
+ } else {
+ navBuilder.withAlpha(mNavFadeOut.value);
+ }
+ finalParams.applySurfaceParams(navBuilder.build());
+ }
+ });
+ } else if (inLiveTileMode) {
+ // There is no transition animation for app launch from recent in live tile mode so
+ // we have to trigger the navigation bar animation from system here.
+ final RecentsAnimationController controller =
+ recentsView.getRecentsAnimationController();
+ if (controller != null) {
+ controller.animateNavigationBarToApp(RECENTS_LAUNCH_DURATION);
+ }
+ }
+ topMostSimulator = tsv;
+ }
+
+ if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulator != null) {
+ out.addFloat(v, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
+
+ TaskViewSimulator simulatorToCopy = topMostSimulator;
+ simulatorToCopy.apply(params);
+
+ // Mt represents the overall transformation on the thumbnailView relative to the
+ // Launcher's rootView
+ // K(t) represents transformation on the running window by the taskViewSimulator at
+ // any time t.
+ // at t = 0, we know that the simulator matches the thumbnailView. So if we apply K(0)`
+ // on the Launcher's rootView, the thumbnailView would match the full running task
+ // window. If we apply "K(0)` K(t)" thumbnailView will match the final transformed
+ // window at any time t. This gives the overall matrix on thumbnailView to be:
+ // Mt K(0)` K(t)
+ // During animation we apply transformation on the thumbnailView (and not the rootView)
+ // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is:
+ // Mt K(0)` K(t) Mt`
+ TaskThumbnailView ttv = v.getThumbnail();
+ RectF tvBounds = new RectF(0, 0, ttv.getWidth(), ttv.getHeight());
+ float[] tvBoundsMapped = new float[]{0, 0, ttv.getWidth(), ttv.getHeight()};
+ getDescendantCoordRelativeToAncestor(ttv, ttv.getRootView(), tvBoundsMapped, false);
+ RectF tvBoundsInRoot = new RectF(
+ tvBoundsMapped[0], tvBoundsMapped[1],
+ tvBoundsMapped[2], tvBoundsMapped[3]);
+
+ Matrix mt = new Matrix();
+ mt.setRectToRect(tvBounds, tvBoundsInRoot, ScaleToFit.FILL);
+
+ Matrix mti = new Matrix();
+ mt.invert(mti);
+
+ Matrix k0i = new Matrix();
+ simulatorToCopy.getCurrentMatrix().invert(k0i);
+
+ Matrix animationMatrix = new Matrix();
+ out.addOnFrameCallback(() -> {
+ animationMatrix.set(mt);
+ animationMatrix.postConcat(k0i);
+ animationMatrix.postConcat(simulatorToCopy.getCurrentMatrix());
+ animationMatrix.postConcat(mti);
+ ttv.setAnimationMatrix(animationMatrix);
+ });
+
+ out.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ttv.setAnimationMatrix(null);
+ }
+ });
+ }
+
+ out.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (isQuickSwitch) {
+ InteractionJankMonitorWrapper.end(
+ InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ targets.release();
+ super.onAnimationEnd(animation);
+ }
+ });
+
+ if (depthController != null) {
+ out.setFloat(depthController, DEPTH, BACKGROUND_APP.getDepth(context),
+ TOUCH_RESPONSE_INTERPOLATOR);
+ }
+ }
+
+ /**
+ * TODO: This doesn't animate at present. Feel free to blow out everyhing in this method
+ * if needed
+ *
+ * We could manually try to animate the just the bounds for the leashes we get back, but we try
+ * to do it through TaskViewSimulator(TVS) since that handles a lot of the recents UI stuff for
+ * us.
+ *
+ * First you have to call TVS#setPreview() to indicate which leash it will operate one
+ * Then operations happen in TVS#apply() on each frame callback.
+ *
+ * TVS uses DeviceProfile to try to figure out things like task height and such based on if the
+ * device is in multiWindowMode or not. It's unclear given the two calls to startTask() when the
+ * device is considered in multiWindowMode and things like insets and stuff change
+ * and calculations have to be adjusted in the animations for that
+ */
+ public static void composeRecentsSplitLaunchAnimator(@NonNull TaskView initialView,
+ @NonNull TaskView v, @NonNull TransitionInfo transitionInfo,
+ SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
+
+ final TransitionInfo.Change[] splitRoots = new TransitionInfo.Change[2];
+ for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
+ final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
+ final int taskId = change.getTaskInfo() != null ? change.getTaskInfo().taskId : -1;
+ final int mode = change.getMode();
+ // Find the target tasks' root tasks since those are the split stages that need to
+ // be animated (the tasks themselves are children and thus inherit animation).
+ if (taskId == initialView.getTask().key.id || taskId == v.getTask().key.id) {
+ if (!(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
+ throw new IllegalStateException(
+ "Expected task to be showing, but it is " + mode);
+ }
+ if (change.getParent() == null) {
+ throw new IllegalStateException("Initiating multi-split launch but the split"
+ + "root of " + taskId + " is already visible or has broken hierarchy.");
+ }
+ splitRoots[taskId == initialView.getTask().key.id ? 0 : 1] =
+ transitionInfo.getChange(change.getParent());
+ }
+ }
+
+ // This is where we should animate the split roots. For now, though, just make them visible.
+ for (int i = 0; i < 2; ++i) {
+ t.show(splitRoots[i].getLeash());
+ t.setAlpha(splitRoots[i].getLeash(), 1.f);
+ }
+
+ // This contains the initial state (before animation), so apply this at the beginning of
+ // the animation.
+ t.apply();
+
+ // Once there is an animation, this should be called AFTER the animation completes.
+ finishCallback.run();
+ }
+
+ /** Legacy version (until shell transitions are enabled) */
+ public static void composeRecentsSplitLaunchAnimatorLegacy(@NonNull AnimatorSet anim,
+ @NonNull TaskView v, @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+ @NonNull RemoteAnimationTargetCompat[] nonAppTargets, boolean launcherClosing,
+ @NonNull StateManager stateManager, @NonNull DepthController depthController,
+ int targetStage) {
+ PendingAnimation out = new PendingAnimation(RECENTS_LAUNCH_DURATION);
+ boolean isRunningTask = v.isRunningTask();
+ TransformParams params = null;
+ TaskViewSimulator tvs = null;
+ RecentsView recentsView = v.getRecentsView();
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask) {
+ params = recentsView.getLiveTileParams();
+ tvs = recentsView.getLiveTileTaskViewSimulator();
+ }
+
+ boolean inLiveTileMode =
+ ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1;
+ final RemoteAnimationTargets targets =
+ new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets,
+ inLiveTileMode ? MODE_CLOSING : MODE_OPENING);
+
+ if (params == null) {
+ SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
+ targets.addReleaseCheck(applier);
+
+ params = new TransformParams()
+ .setSyncTransactionApplier(applier)
+ .setTargetSet(targets);
+ }
+
+ Rect crop = new Rect();
+ Context context = v.getContext();
+ DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+ if (tvs == null && targets.apps.length > 0) {
+ tvs = new TaskViewSimulator(recentsView.getContext(), recentsView.getSizeStrategy());
+ tvs.setDp(dp);
+
+ // RecentsView never updates the display rotation until swipe-up so the value may
+ // be stale. Use the display value instead.
+ int displayRotation = DisplayController.INSTANCE.get(recentsView.getContext())
+ .getInfo().rotation;
+ tvs.getOrientationState().update(displayRotation, displayRotation);
+
+ tvs.setPreview(targets.apps[targets.apps.length - 1]);
+ tvs.fullScreenProgress.value = 0;
+ tvs.recentsViewScale.value = 1;
+// tvs.setScroll(startScroll);
+
+ // Fade in the task during the initial 20% of the animation
+ out.addFloat(params, TransformParams.TARGET_ALPHA, 0, 1,
+ clampToProgress(LINEAR, 0, 0.2f));
+ }
+
+ TaskViewSimulator topMostSimulator = null;
+
+ if (tvs != null) {
+ out.setFloat(tvs.fullScreenProgress,
+ AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
+ out.setFloat(tvs.recentsViewScale,
+ AnimatedFloat.VALUE, tvs.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
+ out.setFloat(tvs.recentsViewScroll,
+ AnimatedFloat.VALUE, 0, TOUCH_RESPONSE_INTERPOLATOR);
+
+ TaskViewSimulator finalTsv = tvs;
+ TransformParams finalParams = params;
+ out.addOnFrameCallback(() -> finalTsv.apply(finalParams));
+ topMostSimulator = tvs;
+ }
+
+ anim.play(out.buildAnim());
+ }
+
+ public static void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
+ @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+ @NonNull RemoteAnimationTargetCompat[] nonAppTargets, boolean launcherClosing,
+ @NonNull StateManager stateManager, @NonNull RecentsView recentsView,
+ @NonNull DepthController depthController) {
+ boolean skipLauncherChanges = !launcherClosing;
+
+ TaskView taskView = findTaskViewToLaunch(recentsView, v, appTargets);
+ PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
+ createRecentsWindowAnimator(taskView, skipLauncherChanges, appTargets, wallpaperTargets,
+ nonAppTargets, depthController, pa);
+
+ 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) {
+ Context context = v.getContext();
+ DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+ launcherAnim = dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()
+ ? ObjectAnimator.ofFloat(recentsView, RecentsView.CONTENT_ALPHA, 0)
+ : recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
+ launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+ launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
+
+ // Make sure recents gets fixed up by resetting task alphas and scales, etc.
+ windowAnimEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ recentsView.finishRecentsAnimation(false /* toRecents */, () -> {
+ recentsView.post(() -> {
+ stateManager.moveToRestState();
+ stateManager.reapplyState();
+ });
+ });
+ }
+ };
+ } else {
+ AnimatorPlaybackController controller =
+ stateManager.createAnimationToNewWorkspace(NORMAL, RECENTS_LAUNCH_DURATION);
+ controller.dispatchOnStart();
+ childStateAnimation = controller.getTarget();
+ launcherAnim = controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION);
+ windowAnimEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ recentsView.finishRecentsAnimation(false /* toRecents */,
+ () -> stateManager.goToState(NORMAL, false));
+ }
+ };
+ }
+ pa.add(launcherAnim);
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1) {
+ pa.addOnFrameCallback(recentsView::redrawLiveTile);
+ }
+ anim.play(pa.buildAnim());
+
+ // 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).
+ stateManager.setCurrentAnimation(anim, childStateAnimation);
+ anim.addListener(windowAnimEndListener);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
new file mode 100644
index 0000000..db5c93c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -0,0 +1,1011 @@
+/*
+ * 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.Intent.ACTION_CHOOSER;
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.GestureState.DEFAULT_STATE;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SPLIT_SCREEN;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_STARTING_WINDOW;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SMARTSPACE_TRANSITION_CONTROLLER;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Icon;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.Display;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.tracing.LauncherTraceProto;
+import com.android.launcher3.tracing.TouchInteractionServiceProto;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.util.WindowBounds;
+import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
+import com.android.quickstep.inputconsumers.AssistantInputConsumer;
+import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
+import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
+import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
+import com.android.quickstep.inputconsumers.OverscrollInputConsumer;
+import com.android.quickstep.inputconsumers.OverviewInputConsumer;
+import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
+import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
+import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
+import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.AssistantUtilities;
+import com.android.quickstep.util.ProtoTracer;
+import com.android.quickstep.util.SplitScreenBounds;
+import com.android.systemui.plugins.OverscrollPlugin;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputChannelCompat;
+import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.InputMonitorCompat;
+import com.android.systemui.shared.system.smartspace.ISmartspaceTransitionController;
+import com.android.systemui.shared.tracing.ProtoTraceable;
+import com.android.wm.shell.onehanded.IOneHanded;
+import com.android.wm.shell.pip.IPip;
+import com.android.wm.shell.splitscreen.ISplitScreen;
+import com.android.wm.shell.startingsurface.IStartingWindow;
+import com.android.wm.shell.transition.IShellTransitions;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.LinkedList;
+
+/**
+ * Service connected by system-UI for handling touch interaction.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin>,
+ ProtoTraceable<LauncherTraceProto.Builder> {
+
+ private static final String TAG = "TouchInteractionService";
+
+ private static final String KEY_BACK_NOTIFICATION_COUNT = "backNotificationCount";
+ private static final String NOTIFY_ACTION_BACK = "com.android.quickstep.action.BACK_GESTURE";
+ private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
+ private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
+
+ /**
+ * System Action ID to show all apps.
+ * TODO: Use AccessibilityService's corresponding global action constant in S
+ */
+ private static final int SYSTEM_ACTION_ID_ALL_APPS = 14;
+
+ public static final boolean ENABLE_PER_WINDOW_INPUT_ROTATION =
+ SystemProperties.getBoolean("persist.debug.per_window_input_rotation", false);
+
+ private int mBackGestureNotificationCounter = -1;
+ @Nullable
+ private OverscrollPlugin mOverscrollPlugin;
+
+ /**
+ * Local IOverviewProxy implementation with some methods for local components
+ */
+ public class TISBinder extends IOverviewProxy.Stub {
+
+ @BinderThread
+ public void onInitialize(Bundle bundle) {
+ ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
+ bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
+ IPip pip = IPip.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_PIP));
+ ISplitScreen splitscreen = ISplitScreen.Stub.asInterface(bundle.getBinder(
+ KEY_EXTRA_SHELL_SPLIT_SCREEN));
+ IOneHanded onehanded = IOneHanded.Stub.asInterface(
+ bundle.getBinder(KEY_EXTRA_SHELL_ONE_HANDED));
+ IShellTransitions shellTransitions = IShellTransitions.Stub.asInterface(
+ bundle.getBinder(KEY_EXTRA_SHELL_SHELL_TRANSITIONS));
+ IStartingWindow startingWindow = IStartingWindow.Stub.asInterface(
+ bundle.getBinder(KEY_EXTRA_SHELL_STARTING_WINDOW));
+ ISmartspaceTransitionController smartspaceTransitionController =
+ ISmartspaceTransitionController.Stub.asInterface(
+ bundle.getBinder(KEY_EXTRA_SMARTSPACE_TRANSITION_CONTROLLER));
+ MAIN_EXECUTOR.execute(() -> {
+ SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
+ splitscreen, onehanded, shellTransitions, startingWindow,
+ smartspaceTransitionController);
+ TouchInteractionService.this.initInputMonitor();
+ preloadOverview(true /* fromInit */);
+ mDeviceState.runOnUserUnlocked(() -> {
+ final BaseActivityInterface ai =
+ mOverviewComponentObserver.getActivityInterface();
+ if (ai == null) return;
+ ai.onOverviewServiceBound();
+ });
+ });
+ sIsInitialized = true;
+ }
+
+ @BinderThread
+ public void onOverviewToggle() {
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
+ // If currently screen pinning, do not enter overview
+ if (mDeviceState.isScreenPinningActive()) {
+ return;
+ }
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_TOGGLE);
+ }
+
+ @BinderThread
+ @Override
+ public void onOverviewShown(boolean triggeredFromAltTab) {
+ if (triggeredFromAltTab) {
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW_NEXT_FOCUS);
+ } else {
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
+ if (triggeredFromAltTab && !triggeredFromHomeKey) {
+ // onOverviewShownFromAltTab hides the overview and ends at the target app
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HIDE);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void onTip(int actionType, int viewType) {
+ // Please delete this method from the interface
+ }
+
+ @BinderThread
+ @Override
+ public void onAssistantAvailable(boolean available) {
+ MAIN_EXECUTOR.execute(() -> {
+ mDeviceState.setAssistantAvailable(available);
+ TouchInteractionService.this.onAssistantVisibilityChanged();
+ });
+ }
+
+ @BinderThread
+ @Override
+ public void onAssistantVisibilityChanged(float visibility) {
+ MAIN_EXECUTOR.execute(() -> {
+ mDeviceState.setAssistantVisibility(visibility);
+ TouchInteractionService.this.onAssistantVisibilityChanged();
+ });
+ }
+
+ @BinderThread
+ public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
+ boolean gestureSwipeLeft) {
+ // Remove this method from the interface
+ }
+
+ @BinderThread
+ public void onSystemUiStateChanged(int stateFlags) {
+ MAIN_EXECUTOR.execute(() -> {
+ int lastFlags = mDeviceState.getSystemUiStateFlags();
+ mDeviceState.setSystemUiFlags(stateFlags);
+ TouchInteractionService.this.onSystemUiFlagsChanged(lastFlags);
+ });
+ }
+
+ @BinderThread
+ public void onActiveNavBarRegionChanges(Region region) {
+ MAIN_EXECUTOR.execute(() -> mDeviceState.setDeferredGestureRegion(region));
+ }
+
+ @Override
+ public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
+ WindowBounds wb = new WindowBounds(bounds, insets);
+ MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
+ }
+
+ @Override
+ public void onImeWindowStatusChanged(int displayId, IBinder token, int vis,
+ int backDisposition, boolean showImeSwitcher) {
+ MAIN_EXECUTOR.execute(() -> mTaskbarManager.updateImeStatus(
+ displayId, vis, backDisposition, showImeSwitcher));
+ }
+
+ public TaskbarManager getTaskbarManager() {
+ return mTaskbarManager;
+ }
+
+ public OverviewCommandHelper getOverviewCommandHelper() {
+ return mOverviewCommandHelper;
+ }
+ }
+
+ private static boolean sConnected = false;
+ private static boolean sIsInitialized = false;
+ private RotationTouchHelper mRotationTouchHelper;
+
+ public static boolean isConnected() {
+ return sConnected;
+ }
+
+
+ public static boolean isInitialized() {
+ return sIsInitialized;
+ }
+
+ private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
+ this::createLauncherSwipeHandler;
+ private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
+ this::createFallbackSwipeHandler;
+
+ private ActivityManagerWrapper mAM;
+ private OverviewCommandHelper mOverviewCommandHelper;
+ private OverviewComponentObserver mOverviewComponentObserver;
+ private InputConsumerController mInputConsumer;
+ private RecentsAnimationDeviceState mDeviceState;
+ private TaskAnimationManager mTaskAnimationManager;
+
+ private @NonNull InputConsumer mUncheckedConsumer = InputConsumer.NO_OP;
+ private @NonNull InputConsumer mConsumer = InputConsumer.NO_OP;
+ private Choreographer mMainChoreographer;
+ private @Nullable ResetGestureInputConsumer mResetGestureInputConsumer;
+ private GestureState mGestureState = DEFAULT_STATE;
+
+ private InputMonitorCompat mInputMonitorCompat;
+ private InputEventReceiver mInputEventReceiver;
+
+ private DisplayManager mDisplayManager;
+
+ private TaskbarManager mTaskbarManager;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ // Initialize anything here that is needed in direct boot mode.
+ // Everything else should be initialized in onUserUnlocked() below.
+ mMainChoreographer = Choreographer.getInstance();
+ mAM = ActivityManagerWrapper.getInstance();
+ mDeviceState = new RecentsAnimationDeviceState(this, true);
+ mDisplayManager = getSystemService(DisplayManager.class);
+ mTaskbarManager = new TaskbarManager(this);
+
+ mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+ mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
+ mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged);
+ mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
+ mDeviceState.runOnUserUnlocked(mTaskbarManager::onUserUnlocked);
+ ProtoTracer.INSTANCE.get(this).add(this);
+ sConnected = true;
+ }
+
+ private void disposeEventHandlers() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitorCompat != null) {
+ mInputMonitorCompat.dispose();
+ mInputMonitorCompat = null;
+ }
+ }
+
+ private void initInputMonitor() {
+ disposeEventHandlers();
+
+ if (mDeviceState.isButtonNavMode()) {
+ return;
+ }
+
+ mInputMonitorCompat = new InputMonitorCompat("swipe-up", mDeviceState.getDisplayId());
+ mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
+ mMainChoreographer, this::onInputEvent);
+
+ mRotationTouchHelper.updateGestureTouchRegions();
+ }
+
+ /**
+ * Called when the navigation mode changes, guaranteed to be after the device state has updated.
+ */
+ private void onNavigationModeChanged(SysUINavigationMode.Mode mode) {
+ initInputMonitor();
+ resetHomeBounceSeenOnQuickstepEnabledFirstTime();
+ }
+
+ /**
+ * Called when the one handed mode overlay package changes, to recreate touch region.
+ */
+ private void onOneHandedModeOverlayChanged(int newGesturalHeight) {
+ initInputMonitor();
+ }
+
+ @UiThread
+ public void onUserUnlocked() {
+ mTaskAnimationManager = new TaskAnimationManager(this);
+ mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
+ mOverviewCommandHelper = new OverviewCommandHelper(this,
+ mOverviewComponentObserver, mTaskAnimationManager);
+ mResetGestureInputConsumer = new ResetGestureInputConsumer(mTaskAnimationManager);
+ mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
+ mInputConsumer.registerInputConsumer();
+ onSystemUiFlagsChanged(mDeviceState.getSystemUiStateFlags());
+ onAssistantVisibilityChanged();
+
+ // Temporarily disable model preload
+ // new ModelPreload().start(this);
+ mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this)
+ .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT));
+ resetHomeBounceSeenOnQuickstepEnabledFirstTime();
+
+ PluginManagerWrapper.INSTANCE.get(getBaseContext()).addPluginListener(this,
+ OverscrollPlugin.class, false /* allowMultiple */);
+
+ mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
+ onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame());
+ }
+
+ public OverviewCommandHelper getOverviewCommandHelper() {
+ return mOverviewCommandHelper;
+ }
+
+ private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
+ if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) {
+ // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation
+ // mode doesn't have gestures
+ return;
+ }
+
+ // Reset home bounce seen on quick step enabled for first time
+ SharedPreferences sharedPrefs = Utilities.getPrefs(this);
+ if (!sharedPrefs.getBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)) {
+ sharedPrefs.edit()
+ .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)
+ .putBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN, false)
+ .apply();
+ }
+ }
+
+ private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
+ AccessibilityManager am = getSystemService(AccessibilityManager.class);
+
+ if (isHomeAndOverviewSame) {
+ Intent intent = new Intent(mOverviewComponentObserver.getHomeIntent())
+ .setAction(Intent.ACTION_ALL_APPS);
+ RemoteAction allAppsAction = new RemoteAction(
+ Icon.createWithResource(this, R.drawable.ic_apps),
+ getString(R.string.all_apps_label),
+ getString(R.string.all_apps_label),
+ PendingIntent.getActivity(this, SYSTEM_ACTION_ID_ALL_APPS, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+ am.registerSystemAction(allAppsAction, SYSTEM_ACTION_ID_ALL_APPS);
+ } else {
+ am.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
+ }
+ }
+
+ @UiThread
+ private void onSystemUiFlagsChanged(int lastSysUIFlags) {
+ if (mDeviceState.isUserUnlocked()) {
+ int systemUiStateFlags = mDeviceState.getSystemUiStateFlags();
+ SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags);
+ mOverviewComponentObserver.onSystemUiStateChanged();
+ mTaskbarManager.onSystemUiFlagsChanged(systemUiStateFlags);
+
+ if ((lastSysUIFlags & SYSUI_STATE_TRACING_ENABLED) !=
+ (systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED)) {
+ // Update the tracing state
+ if ((systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED) != 0) {
+ Log.d(TAG, "Starting tracing.");
+ ProtoTracer.INSTANCE.get(this).start();
+ } else {
+ Log.d(TAG, "Stopping tracing. Dumping to file="
+ + ProtoTracer.INSTANCE.get(this).getTraceFile());
+ ProtoTracer.INSTANCE.get(this).stop();
+ }
+ }
+ }
+ }
+
+ @UiThread
+ private void onAssistantVisibilityChanged() {
+ if (mDeviceState.isUserUnlocked()) {
+ mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged(
+ mDeviceState.getAssistantVisibility());
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "Touch service destroyed: user=" + getUserId());
+ sIsInitialized = false;
+ if (mDeviceState.isUserUnlocked()) {
+ mInputConsumer.unregisterInputConsumer();
+ mOverviewComponentObserver.onDestroy();
+ PluginManagerWrapper.INSTANCE.get(getBaseContext()).removePluginListener(this);
+ }
+ disposeEventHandlers();
+ mDeviceState.destroy();
+ SystemUiProxy.INSTANCE.get(this).clearProxy();
+ ProtoTracer.INSTANCE.get(this).stop();
+ ProtoTracer.INSTANCE.get(this).remove(this);
+
+ getSystemService(AccessibilityManager.class)
+ .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
+
+ mTaskbarManager.destroy();
+ sConnected = false;
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.d(TAG, "Touch service connected: user=" + getUserId());
+ return new TISBinder();
+ }
+
+ private void onInputEvent(InputEvent ev) {
+ if (!(ev instanceof MotionEvent)) {
+ Log.e(TAG, "Unknown event " + ev);
+ return;
+ }
+ MotionEvent event = (MotionEvent) ev;
+ if (ENABLE_PER_WINDOW_INPUT_ROTATION) {
+ final Display display = mDisplayManager.getDisplay(mDeviceState.getDisplayId());
+ int rotation = display.getRotation();
+ Point sz = new Point();
+ display.getRealSize(sz);
+ if (rotation != Surface.ROTATION_0) {
+ event.transform(InputChannelCompat.createRotationMatrix(rotation, sz.x, sz.y));
+ }
+ }
+
+ TestLogging.recordMotionEvent(
+ TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
+
+ if (!mDeviceState.isUserUnlocked()) {
+ return;
+ }
+
+ Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride(
+ TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
+
+ final int action = event.getAction();
+ if (action == ACTION_DOWN) {
+ mRotationTouchHelper.setOrientationTransformIfNeeded(event);
+
+ if (!mDeviceState.isOneHandedModeActive()
+ && mRotationTouchHelper.isInSwipeUpTouchRegion(event)) {
+ // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
+ // onConsumerInactive and wipe the previous gesture state
+ GestureState prevGestureState = new GestureState(mGestureState);
+ GestureState newGestureState = createGestureState(mGestureState);
+ newGestureState.setSwipeUpStartTimeMs(SystemClock.uptimeMillis());
+ mConsumer.onConsumerAboutToBeSwitched();
+ mGestureState = newGestureState;
+ mConsumer = newConsumer(prevGestureState, mGestureState, event);
+
+ ActiveGestureLog.INSTANCE.addLog("setInputConsumer: " + mConsumer.getName());
+ mUncheckedConsumer = mConsumer;
+ } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode()) {
+ mGestureState = createGestureState(mGestureState);
+ ActivityManager.RunningTaskInfo runningTask = mGestureState.getRunningTask();
+ if (mDeviceState.canTriggerAssistantAction(event, runningTask)) {
+ // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
+ // should not interrupt it. QuickSwitch assumes that interruption can only
+ // happen if the next gesture is also quick switch.
+ mUncheckedConsumer = new AssistantInputConsumer(
+ this,
+ mGestureState,
+ InputConsumer.NO_OP, mInputMonitorCompat,
+ mDeviceState,
+ event);
+ } else if (mDeviceState.canTriggerOneHandedAction(event)) {
+ // Consume gesture event for triggering one handed feature.
+ mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
+ InputConsumer.NO_OP, mInputMonitorCompat);
+ } else {
+ mUncheckedConsumer = InputConsumer.NO_OP;
+ }
+ } else if (mDeviceState.canTriggerOneHandedAction(event)) {
+ // Consume gesture event for triggering one handed feature.
+ mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
+ InputConsumer.NO_OP, mInputMonitorCompat);
+ } else {
+ mUncheckedConsumer = InputConsumer.NO_OP;
+ }
+ } else {
+ // Other events
+ if (mUncheckedConsumer != InputConsumer.NO_OP) {
+ // Only transform the event if we are handling it in a proper consumer
+ mRotationTouchHelper.setOrientationTransformIfNeeded(event);
+ }
+ }
+
+ if (mUncheckedConsumer != InputConsumer.NO_OP) {
+ switch (event.getActionMasked()) {
+ case ACTION_DOWN:
+ case ACTION_UP:
+ ActiveGestureLog.INSTANCE.addLog("onMotionEvent("
+ + (int) event.getRawX() + ", " + (int) event.getRawY() + ")",
+ event.getActionMasked());
+ break;
+ default:
+ ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
+ break;
+ }
+ }
+
+ boolean cancelGesture = mGestureState.getActivityInterface() != null
+ && mGestureState.getActivityInterface().shouldCancelCurrentGesture();
+ boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL || cancelGesture)
+ && mConsumer != null
+ && !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
+ if (cancelGesture) {
+ event.setAction(ACTION_CANCEL);
+ }
+ mUncheckedConsumer.onMotionEvent(event);
+
+ if (cleanUpConsumer) {
+ reset();
+ }
+ TraceHelper.INSTANCE.endFlagsOverride(traceToken);
+ ProtoTracer.INSTANCE.get(this).scheduleFrameUpdate();
+ }
+
+ public GestureState createGestureState(GestureState previousGestureState) {
+ GestureState gestureState = new GestureState(mOverviewComponentObserver,
+ ActiveGestureLog.INSTANCE.generateAndSetLogId());
+ if (mTaskAnimationManager.isRecentsAnimationRunning()) {
+ gestureState.updateRunningTask(previousGestureState.getRunningTask());
+ gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId());
+ gestureState.updatePreviouslyAppearedTaskIds(
+ previousGestureState.getPreviouslyAppearedTaskIds());
+ } else {
+ gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.0",
+ () -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
+ }
+ return gestureState;
+ }
+
+ private InputConsumer newConsumer(GestureState previousGestureState,
+ GestureState newGestureState, MotionEvent event) {
+ boolean canStartSystemGesture = mDeviceState.canStartSystemGesture();
+
+ if (!mDeviceState.isUserUnlocked()) {
+ if (canStartSystemGesture) {
+ // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
+ // launched while device is locked even after exiting direct boot mode (e.g. camera).
+ return createDeviceLockedInputConsumer(newGestureState);
+ } else {
+ return getDefaultInputConsumer();
+ }
+ }
+
+ // When there is an existing recents animation running, bypass systemState check as this is
+ // a followup gesture and the first gesture started in a valid system state.
+ InputConsumer base = canStartSystemGesture
+ || previousGestureState.isRecentsAnimationRunning()
+ ? newBaseConsumer(previousGestureState, newGestureState, event)
+ : getDefaultInputConsumer();
+ if (mDeviceState.isGesturalNavMode()) {
+ handleOrientationSetup(base);
+ }
+ if (mDeviceState.isFullyGesturalNavMode()) {
+ if (mDeviceState.canTriggerAssistantAction(event, newGestureState.getRunningTask())) {
+ base = new AssistantInputConsumer(this, newGestureState, base, mInputMonitorCompat,
+ mDeviceState, event);
+ }
+
+ if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
+ OverscrollPlugin plugin = null;
+ if (FeatureFlags.FORCE_LOCAL_OVERSCROLL_PLUGIN.get()) {
+ plugin = OverscrollPluginFactory.INSTANCE.get(
+ getApplicationContext()).getLocalOverscrollPlugin();
+ }
+
+ // If not local plugin was forced, use the actual overscroll plugin if available.
+ if (plugin == null && mOverscrollPlugin != null && mOverscrollPlugin.isActive()) {
+ plugin = mOverscrollPlugin;
+ }
+
+ if (plugin != null) {
+ // Put the overscroll gesture as higher priority than the Assistant or base
+ // gestures
+ base = new OverscrollInputConsumer(this, newGestureState, base,
+ mInputMonitorCompat, plugin);
+ }
+ }
+
+ // If Bubbles is expanded, use the overlay input consumer, which will close Bubbles
+ // instead of going all the way home when a swipe up is detected.
+ if (mDeviceState.isBubblesExpanded() || mDeviceState.isGlobalActionsShowing()) {
+ base = new SysUiOverlayInputConsumer(
+ getBaseContext(), mDeviceState, mInputMonitorCompat);
+ }
+
+ if (mDeviceState.isScreenPinningActive()) {
+ // Note: we only allow accessibility to wrap this, and it replaces the previous
+ // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
+ base = new ScreenPinnedInputConsumer(this, newGestureState);
+ }
+
+ if (mDeviceState.canTriggerOneHandedAction(event)) {
+ base = new OneHandedModeInputConsumer(this, mDeviceState, base,
+ mInputMonitorCompat);
+ }
+
+ if (mDeviceState.isAccessibilityMenuAvailable()) {
+ base = new AccessibilityInputConsumer(this, mDeviceState, base,
+ mInputMonitorCompat);
+ }
+ } else {
+ if (mDeviceState.isScreenPinningActive()) {
+ base = getDefaultInputConsumer();
+ }
+
+ if (mDeviceState.canTriggerOneHandedAction(event)) {
+ base = new OneHandedModeInputConsumer(this, mDeviceState, base,
+ mInputMonitorCompat);
+ }
+ }
+ return base;
+ }
+
+ private void handleOrientationSetup(InputConsumer baseInputConsumer) {
+ baseInputConsumer.notifyOrientationSetup();
+ }
+
+ private InputConsumer newBaseConsumer(GestureState previousGestureState,
+ GestureState gestureState, MotionEvent event) {
+ if (mDeviceState.isKeyguardShowingOccluded()) {
+ // This handles apps showing over the lockscreen (e.g. camera)
+ return createDeviceLockedInputConsumer(gestureState);
+ }
+
+ // Use overview input consumer for sharesheets on top of home.
+ boolean forceOverviewInputConsumer = gestureState.getActivityInterface().isStarted()
+ && gestureState.getRunningTask() != null
+ && ACTION_CHOOSER.equals(gestureState.getRunningTask().baseIntent.getAction());
+ if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) {
+ // In the case where we are in the excluded assistant state, ignore it and treat the
+ // running activity as the task behind the assistant
+ gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.assistant",
+ () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
+ ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
+ ComponentName runningComponent =
+ gestureState.getRunningTask().baseIntent.getComponent();
+ forceOverviewInputConsumer =
+ runningComponent != null && runningComponent.equals(homeComponent);
+ }
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()
+ && gestureState.getActivityInterface().isInLiveTileMode()) {
+ return createOverviewInputConsumer(
+ previousGestureState, gestureState, event, forceOverviewInputConsumer);
+ } else if (gestureState.getRunningTask() == null) {
+ return getDefaultInputConsumer();
+ } else if (previousGestureState.isRunningAnimationToLauncher()
+ || gestureState.getActivityInterface().isResumed()
+ || forceOverviewInputConsumer) {
+ return createOverviewInputConsumer(
+ previousGestureState, gestureState, event, forceOverviewInputConsumer);
+ } else if (mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask())) {
+ return getDefaultInputConsumer();
+ } else {
+ return createOtherActivityInputConsumer(gestureState, event);
+ }
+ }
+
+ public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
+ return !mOverviewComponentObserver.isHomeAndOverviewSame()
+ ? mFallbackSwipeHandlerFactory : mLauncherSwipeHandlerFactory;
+ }
+
+ private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
+ MotionEvent event) {
+
+ final AbsSwipeUpHandler.Factory factory = getSwipeUpHandlerFactory();
+ final boolean shouldDefer = !mOverviewComponentObserver.isHomeAndOverviewSame()
+ || gestureState.getActivityInterface().deferStartingActivity(mDeviceState, event);
+ final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
+ return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager,
+ gestureState, shouldDefer, this::onConsumerInactive,
+ mInputMonitorCompat, mInputEventReceiver, disableHorizontalSwipe, factory);
+ }
+
+ private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState) {
+ if (mDeviceState.isFullyGesturalNavMode() && gestureState.getRunningTask() != null) {
+ return new DeviceLockedInputConsumer(this, mDeviceState, mTaskAnimationManager,
+ gestureState, mInputMonitorCompat);
+ } else {
+ return getDefaultInputConsumer();
+ }
+ }
+
+ public InputConsumer createOverviewInputConsumer(GestureState previousGestureState,
+ GestureState gestureState, MotionEvent event,
+ boolean forceOverviewInputConsumer) {
+ StatefulActivity activity = gestureState.getActivityInterface().getCreatedActivity();
+ if (activity == null) {
+ return getDefaultInputConsumer();
+ }
+
+ if (activity.getRootView().hasWindowFocus()
+ || previousGestureState.isRunningAnimationToLauncher()
+ || (ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+ && forceOverviewInputConsumer)
+ || (ENABLE_QUICKSTEP_LIVE_TILE.get()
+ && gestureState.getActivityInterface().isInLiveTileMode())) {
+ return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
+ false /* startingInActivityBounds */);
+ } else {
+ final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
+ return new OverviewWithoutFocusInputConsumer(activity, mDeviceState, gestureState,
+ mInputMonitorCompat, disableHorizontalSwipe);
+ }
+ }
+
+ /**
+ * To be called by the consumer when it's no longer active. This can be called by any consumer
+ * in the hierarchy at any point during the gesture (ie. if a delegate consumer starts
+ * intercepting touches, the base consumer can try to call this).
+ */
+ private void onConsumerInactive(InputConsumer caller) {
+ if (mConsumer != null && mConsumer.getActiveConsumerInHierarchy() == caller) {
+ reset();
+ }
+ }
+
+ private void reset() {
+ mConsumer = mUncheckedConsumer = getDefaultInputConsumer();
+ mGestureState = DEFAULT_STATE;
+ // By default, use batching of the input events, but check receiver before using in the rare
+ // case that the monitor was disposed before the swipe settled
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.setBatchingEnabled(true);
+ }
+ }
+
+ /**
+ * Returns the {@link ResetGestureInputConsumer} if user is unlocked, else NO_OP.
+ */
+ private @NonNull InputConsumer getDefaultInputConsumer() {
+ if (mResetGestureInputConsumer != null) {
+ return mResetGestureInputConsumer;
+ } else {
+ // mResetGestureInputConsumer isn't initialized until onUserUnlocked(), so reset to
+ // NO_OP until then (we never want these to be null).
+ return InputConsumer.NO_OP;
+ }
+ }
+
+ private void preloadOverview(boolean fromInit) {
+ if (!mDeviceState.isUserUnlocked()) {
+ return;
+ }
+
+ if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+ // Prevent the overview from being started before the real home on first boot.
+ return;
+ }
+
+ if (RestoreDbTask.isPending(this) || !mDeviceState.isUserSetupComplete()) {
+ // Preloading while a restore is pending may cause launcher to start the restore
+ // too early.
+ return;
+ }
+
+ final BaseActivityInterface activityInterface =
+ mOverviewComponentObserver.getActivityInterface();
+ final Intent overviewIntent = new Intent(
+ mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
+ if (activityInterface.getCreatedActivity() != null && fromInit) {
+ // The activity has been created before the initialization of overview service. It is
+ // usually happens when booting or launcher is the top activity, so we should already
+ // have the latest state.
+ return;
+ }
+
+ mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (!mDeviceState.isUserUnlocked()) {
+ return;
+ }
+ final BaseActivityInterface activityInterface =
+ mOverviewComponentObserver.getActivityInterface();
+ final BaseDraggingActivity activity = activityInterface.getCreatedActivity();
+ if (activity == null || activity.isStarted()) {
+ // We only care about the existing background activity.
+ return;
+ }
+ if (mOverviewComponentObserver.canHandleConfigChanges(activity.getComponentName(),
+ activity.getResources().getConfiguration().diff(newConfig))) {
+ // Since navBar gestural height are different between portrait and landscape,
+ // can handle orientation changes and refresh navigation gestural region through
+ // onOneHandedModeChanged()
+ int newGesturalHeight = ResourceUtils.getNavbarSize(
+ ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
+ getApplicationContext().getResources());
+ mDeviceState.onOneHandedModeChanged(newGesturalHeight);
+ return;
+ }
+
+ preloadOverview(false /* fromInit */);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] rawArgs) {
+ if (rawArgs.length > 0 && Utilities.IS_DEBUG_DEVICE) {
+ LinkedList<String> args = new LinkedList(Arrays.asList(rawArgs));
+ switch (args.pollFirst()) {
+ case "cmd":
+ if (args.peekFirst() == null) {
+ printAvailableCommands(pw);
+ } else {
+ onCommand(pw, args);
+ }
+ break;
+ }
+ } else {
+ // Dump everything
+ FeatureFlags.dump(pw);
+ if (mDeviceState.isUserUnlocked()) {
+ PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw);
+ }
+ mDeviceState.dump(pw);
+ if (mOverviewComponentObserver != null) {
+ mOverviewComponentObserver.dump(pw);
+ }
+ if (mGestureState != null) {
+ mGestureState.dump(pw);
+ }
+ pw.println("Input state:");
+ pw.println(" mInputMonitorCompat=" + mInputMonitorCompat);
+ pw.println(" mInputEventReceiver=" + mInputEventReceiver);
+ SysUINavigationMode.INSTANCE.get(this).dump(pw);
+ pw.println("TouchState:");
+ BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
+ : mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
+ boolean resumed = mOverviewComponentObserver != null
+ && mOverviewComponentObserver.getActivityInterface().isResumed();
+ pw.println(" createdOverviewActivity=" + createdOverviewActivity);
+ pw.println(" resumed=" + resumed);
+ pw.println(" mConsumer=" + mConsumer.getName());
+ ActiveGestureLog.INSTANCE.dump("", pw);
+ pw.println("ProtoTrace:");
+ pw.println(" file=" + ProtoTracer.INSTANCE.get(this).getTraceFile());
+ }
+ }
+
+ private void printAvailableCommands(PrintWriter pw) {
+ pw.println("Available commands:");
+ pw.println(" clear-touch-log: Clears the touch interaction log");
+ }
+
+ private void onCommand(PrintWriter pw, LinkedList<String> args) {
+ switch (args.pollFirst()) {
+ case "clear-touch-log":
+ ActiveGestureLog.INSTANCE.clear();
+ break;
+ }
+ }
+
+ private AbsSwipeUpHandler createLauncherSwipeHandler(
+ GestureState gestureState, long touchTimeMs) {
+ return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
+ gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
+ mInputConsumer);
+ }
+
+ private AbsSwipeUpHandler createFallbackSwipeHandler(
+ GestureState gestureState, long touchTimeMs) {
+ return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+ gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
+ mInputConsumer);
+ }
+
+ protected boolean shouldNotifyBackGesture() {
+ return mBackGestureNotificationCounter > 0 &&
+ !mDeviceState.getGestureBlockedActivityPackages().isEmpty();
+ }
+
+ @WorkerThread
+ protected void tryNotifyBackGesture() {
+ if (shouldNotifyBackGesture()) {
+ mBackGestureNotificationCounter--;
+ Utilities.getDevicePrefs(this).edit()
+ .putInt(KEY_BACK_NOTIFICATION_COUNT, mBackGestureNotificationCounter).apply();
+ mDeviceState.getGestureBlockedActivityPackages().forEach(blockedPackage ->
+ sendBroadcast(new Intent(NOTIFY_ACTION_BACK).setPackage(blockedPackage)));
+ }
+ }
+
+ @Override
+ public void onPluginConnected(OverscrollPlugin overscrollPlugin, Context context) {
+ mOverscrollPlugin = overscrollPlugin;
+ }
+
+ @Override
+ public void onPluginDisconnected(OverscrollPlugin overscrollPlugin) {
+ mOverscrollPlugin = null;
+ }
+
+ @Override
+ public void writeToProto(LauncherTraceProto.Builder proto) {
+ TouchInteractionServiceProto.Builder serviceProto =
+ TouchInteractionServiceProto.newBuilder();
+ serviceProto.setServiceConnected(true);
+
+ if (mOverviewComponentObserver != null) {
+ mOverviewComponentObserver.writeToProto(serviceProto);
+ }
+ mConsumer.writeToProto(serviceProto);
+
+ proto.setTouchInteractionService(serviceProto);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/ViewUtils.java b/quickstep/src/com/android/quickstep/ViewUtils.java
new file mode 100644
index 0000000..184ab17
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/ViewUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.os.Handler;
+import android.view.View;
+
+import com.android.launcher3.Utilities;
+import com.android.systemui.shared.system.ViewRootImplCompat;
+
+import java.util.function.BooleanSupplier;
+import java.util.function.LongConsumer;
+
+/**
+ * Utility class for helpful methods related to {@link View} objects.
+ */
+public class ViewUtils {
+
+ /** See {@link #postFrameDrawn(View, Runnable, BooleanSupplier)}} */
+ public static boolean postFrameDrawn(View view, Runnable onFinishRunnable) {
+ return postFrameDrawn(view, onFinishRunnable, () -> false);
+ }
+
+ /**
+ * Inject some addition logic in order to make sure that the view is updated smoothly post
+ * draw, and allow addition task to be run after view update.
+ *
+ * @param onFinishRunnable runnable to be run right after the view finishes drawing.
+ */
+ public static boolean postFrameDrawn(
+ View view, Runnable onFinishRunnable, BooleanSupplier canceled) {
+ return new FrameHandler(view, onFinishRunnable, canceled).schedule();
+ }
+
+ private static class FrameHandler implements LongConsumer {
+
+ final ViewRootImplCompat mViewRoot;
+ final Runnable mFinishCallback;
+ final BooleanSupplier mCancelled;
+ final Handler mHandler;
+
+ int mDeferFrameCount = 1;
+
+ FrameHandler(View view, Runnable finishCallback, BooleanSupplier cancelled) {
+ mViewRoot = new ViewRootImplCompat(view);
+ mFinishCallback = finishCallback;
+ mCancelled = cancelled;
+ mHandler = new Handler();
+ }
+
+ @Override
+ public void accept(long l) {
+ Utilities.postAsyncCallback(mHandler, this::onFrame);
+ }
+
+ private void onFrame() {
+ if (mCancelled.getAsBoolean()) {
+ return;
+ }
+
+ if (mDeferFrameCount > 0) {
+ mDeferFrameCount--;
+ schedule();
+ return;
+ }
+
+ if (mFinishCallback != null) {
+ mFinishCallback.run();
+ }
+ }
+
+ private boolean schedule() {
+ if (mViewRoot.isValid()) {
+ mViewRoot.registerRtFrameCallback(this);
+ mViewRoot.getView().invalidate();
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
new file mode 100644
index 0000000..9d9ef94
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+
+/**
+ * In 0-button mode, intercepts swipe up from the nav bar on FallbackRecentsView to go home.
+ */
+public class FallbackNavBarTouchController implements TouchController,
+ TriggerSwipeUpTouchTracker.OnSwipeUpListener {
+
+ private final RecentsActivity mActivity;
+ @Nullable
+ private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+
+ public FallbackNavBarTouchController(RecentsActivity activity) {
+ mActivity = activity;
+ SysUINavigationMode.Mode sysUINavigationMode = SysUINavigationMode.getMode(mActivity);
+ if (sysUINavigationMode == SysUINavigationMode.Mode.NO_BUTTON) {
+ NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode,
+ DisplayController.INSTANCE.get(mActivity).getInfo());
+ mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
+ true /* disableHorizontalSwipe */, navBarPosition,
+ null /* onInterceptTouch */, this);
+ } else {
+ mTriggerSwipeUpTracker = null;
+ }
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
+ if (cameFromNavBar && mTriggerSwipeUpTracker != null) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mTriggerSwipeUpTracker.init();
+ }
+ onControllerTouchEvent(ev);
+ return mTriggerSwipeUpTracker.interceptedTouch();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onControllerTouchEvent(MotionEvent ev) {
+ if (mTriggerSwipeUpTracker != null) {
+ mTriggerSwipeUpTracker.onMotionEvent(ev);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
+ mActivity.<FallbackRecentsView>getOverviewPanel().startHome();
+ }
+
+ @Override
+ public void onSwipeUpCancelled() {}
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
new file mode 100644
index 0000000..f0364eb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
+import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
+
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.views.ClearAllButton;
+
+/**
+ * State controller for fallback recents activity
+ */
+public class FallbackRecentsStateController implements StateHandler<RecentsState> {
+
+ private final StateAnimationConfig mNoConfig = new StateAnimationConfig();
+ private final RecentsActivity mActivity;
+ private final FallbackRecentsView mRecentsView;
+
+ public FallbackRecentsStateController(RecentsActivity activity) {
+ mActivity = activity;
+ mRecentsView = activity.getOverviewPanel();
+ }
+
+ @Override
+ public void setState(RecentsState state) {
+ mRecentsView.updateEmptyMessage();
+ mRecentsView.resetTaskVisuals();
+ setProperties(state, mNoConfig, PropertySetter.NO_ANIM_PROPERTY_SETTER);
+ }
+
+ @Override
+ public void setStateWithAnimation(RecentsState toState, StateAnimationConfig config,
+ PendingAnimation setter) {
+ if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
+ return;
+ }
+ // While animating into recents, update the visible task data as needed
+ setter.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
+ mRecentsView.updateEmptyMessage();
+
+ setProperties(toState, config, setter);
+ }
+
+ private void setProperties(RecentsState state, StateAnimationConfig config,
+ PropertySetter setter) {
+ float clearAllButtonAlpha = state.hasClearAllButton() ? 1 : 0;
+ setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
+ clearAllButtonAlpha, LINEAR);
+ float overviewButtonAlpha =
+ state.hasOverviewActions() && mRecentsView.shouldShowOverviewActionsForState(state)
+ ? 1 : 0;
+ setter.setFloat(mActivity.getActionsView().getVisibilityAlpha(),
+ MultiValueAlpha.VALUE, overviewButtonAlpha, LINEAR);
+
+ float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
+ setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
+ config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
+ setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1],
+ config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
+ setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
+ config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
+
+ setter.setFloat(mRecentsView, TASK_MODALNESS, state.getOverviewModalness(),
+ config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
+ setter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, state.isFullScreen() ? 1 : 0, LINEAR);
+ setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
+ state.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()) ? 1f : 0f, LINEAR);
+
+ setter.setViewBackgroundColor(mActivity.getScrimView(), state.getScrimColor(mActivity),
+ config.getInterpolator(ANIM_SCRIM_FADE, LINEAR));
+ }
+}
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 0000000..ac3fb27
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -0,0 +1,208 @@
+/*
+ * 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 static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
+import static com.android.quickstep.fallback.RecentsState.DEFAULT;
+import static com.android.quickstep.fallback.RecentsState.HOME;
+import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
+
+import android.animation.AnimatorSet;
+import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.quickstep.FallbackActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.views.OverviewActionsView;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.SplitPlaceholderView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+
+import java.util.ArrayList;
+
+@TargetApi(Build.VERSION_CODES.R)
+public class FallbackRecentsView extends RecentsView<RecentsActivity, RecentsState>
+ implements StateListener<RecentsState> {
+
+ private RunningTaskInfo mHomeTaskInfo;
+
+ public FallbackRecentsView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr, FallbackActivityInterface.INSTANCE);
+ mActivity.getStateManager().addStateListener(this);
+ }
+
+ @Override
+ public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
+ super.init(actionsView, splitPlaceholderView);
+ setOverviewStateEnabled(true);
+ setOverlayEnabled(true);
+ }
+
+ @Override
+ public void startHome() {
+ mActivity.startHome();
+ }
+
+ /**
+ * When starting gesture interaction from home, we add a temporary invisible tile corresponding
+ * to the home task. This allows us to handle quick-switch similarly to a quick-switching
+ * from a foreground task.
+ */
+ public void onGestureAnimationStartOnHome(RunningTaskInfo homeTaskInfo) {
+ mHomeTaskInfo = homeTaskInfo;
+ onGestureAnimationStart(homeTaskInfo);
+ }
+
+ /**
+ * When the gesture ends and we're going to recents view, we also remove the temporary
+ * invisible tile added for the home task. This also pushes the remaining tiles back
+ * to the center.
+ */
+ @Override
+ public void onPrepareGestureEndAnimation(
+ @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget) {
+ super.onPrepareGestureEndAnimation(animatorSet, endTarget);
+ if (mHomeTaskInfo != null && endTarget == RECENTS && animatorSet != null) {
+ TaskView tv = getTaskView(mHomeTaskInfo.taskId);
+ if (tv != null) {
+ PendingAnimation pa = createTaskDismissAnimation(tv, true, false, 150);
+ pa.addEndListener(e -> setCurrentTask(-1));
+ AnimatorPlaybackController controller = pa.createPlaybackController();
+ controller.dispatchOnStart();
+ animatorSet.play(controller.getAnimationPlayer());
+ }
+ }
+ }
+
+ @Override
+ public void onGestureAnimationEnd() {
+ if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.HOME) {
+ // Clean-up logic that occurs when recents is no longer in use/visible.
+ reset();
+ }
+ super.onGestureAnimationEnd();
+ }
+
+ @Override
+ public void setCurrentTask(int runningTaskId) {
+ super.setCurrentTask(runningTaskId);
+ if (mHomeTaskInfo != null && mHomeTaskInfo.taskId != runningTaskId) {
+ mHomeTaskInfo = null;
+ setRunningTaskHidden(false);
+ }
+ }
+
+ @Override
+ protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
+ if (mHomeTaskInfo != null && runningTaskInfo != null &&
+ mHomeTaskInfo.taskId == runningTaskInfo.taskId
+ && getTaskViewCount() == 0) {
+ // Do not add a stub task if we are running over home with empty recents, so that we
+ // show the empty recents message instead of showing a stub task and later removing it.
+ return false;
+ }
+ return super.shouldAddStubTaskView(runningTaskInfo);
+ }
+
+ @Override
+ protected void applyLoadPlan(ArrayList<Task> tasks) {
+ // When quick-switching on 3p-launcher, we add a "stub" tile corresponding to Launcher
+ // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
+ // track the index of the next task appropriately, as if we are switching on any other app.
+ if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId && !tasks.isEmpty()) {
+ // Check if the task list has running task
+ boolean found = false;
+ for (Task t : tasks) {
+ if (t.key.id == mRunningTaskId) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ ArrayList<Task> newList = new ArrayList<>(tasks.size() + 1);
+ newList.addAll(tasks);
+ newList.add(Task.from(new TaskKey(mHomeTaskInfo), mHomeTaskInfo, false));
+ tasks = newList;
+ }
+ }
+ super.applyLoadPlan(tasks);
+ }
+
+ @Override
+ public void setRunningTaskHidden(boolean isHidden) {
+ if (mHomeTaskInfo != null) {
+ // Always keep the home task hidden
+ isHidden = true;
+ }
+ super.setRunningTaskHidden(isHidden);
+ }
+
+ @Override
+ public void setModalStateEnabled(boolean isModalState) {
+ super.setModalStateEnabled(isModalState);
+ if (isModalState) {
+ mActivity.getStateManager().goToState(RecentsState.MODAL_TASK);
+ } else {
+ if (mActivity.isInState(RecentsState.MODAL_TASK)) {
+ mActivity.getStateManager().goToState(DEFAULT);
+ }
+ }
+ }
+
+ @Override
+ public void onStateTransitionStart(RecentsState toState) {
+ setOverviewStateEnabled(true);
+ setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
+ setOverviewFullscreenEnabled(toState.isFullScreen());
+ setFreezeViewVisibility(true);
+ }
+
+ @Override
+ public void onStateTransitionComplete(RecentsState finalState) {
+ if (finalState == HOME) {
+ // Clean-up logic that occurs when recents is no longer in use/visible.
+ reset();
+ }
+ setOverlayEnabled(finalState == DEFAULT || finalState == MODAL_TASK);
+ setFreezeViewVisibility(false);
+ }
+
+ @Override
+ public void setOverviewStateEnabled(boolean enabled) {
+ super.setOverviewStateEnabled(enabled);
+ if (enabled) {
+ RecentsState state = mActivity.getStateManager().getState();
+ setDisallowScrollToClearAll(!state.hasClearAllButton());
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
new file mode 100644
index 0000000..29c3dc8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
@@ -0,0 +1,41 @@
+/*
+ * 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.util.AttributeSet;
+
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.RecentsActivity;
+
+/**
+ * Drag layer for fallback recents activity
+ */
+public class RecentsDragLayer extends BaseDragLayer<RecentsActivity> {
+
+ public RecentsDragLayer(Context context, AttributeSet attrs) {
+ super(context, attrs, 1 /* alphaChannelCount */);
+ }
+
+ @Override
+ public void recreateControllers() {
+ mControllers = new TouchController[] {
+ new RecentsTaskController(mActivity),
+ new FallbackNavBarTouchController(mActivity),
+ };
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
new file mode 100644
index 0000000..b6cfdce
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
+import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.RecentsActivity;
+
+/**
+ * State definition for Fallback recents
+ */
+public class RecentsState implements BaseState<RecentsState> {
+
+ private static final int FLAG_MODAL = BaseState.getFlag(0);
+ private static final int FLAG_CLEAR_ALL_BUTTON = BaseState.getFlag(1);
+ private static final int FLAG_FULL_SCREEN = BaseState.getFlag(2);
+ private static final int FLAG_OVERVIEW_ACTIONS = BaseState.getFlag(3);
+ private static final int FLAG_SHOW_AS_GRID = BaseState.getFlag(4);
+ private static final int FLAG_SCRIM = BaseState.getFlag(5);
+ private static final int FLAG_LIVE_TILE = BaseState.getFlag(6);
+
+ public static final RecentsState DEFAULT = new RecentsState(0,
+ FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID | FLAG_SCRIM
+ | FLAG_LIVE_TILE);
+ public static final RecentsState MODAL_TASK = new ModalState(1,
+ FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_MODAL
+ | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE);
+ public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2,
+ FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN);
+ public static final RecentsState HOME = new RecentsState(3, 0);
+ public static final RecentsState BG_LAUNCHER = new LauncherState(4, 0);
+
+ public final int ordinal;
+ private final int mFlags;
+
+ private static final float NO_OFFSET = 0;
+ private static final float NO_SCALE = 1;
+
+ public RecentsState(int id, int flags) {
+ this.ordinal = id;
+ this.mFlags = flags;
+ }
+
+
+ @Override
+ public String toString() {
+ return "Ordinal-" + ordinal;
+ }
+
+ @Override
+ public final boolean hasFlag(int mask) {
+ return (mFlags & mask) != 0;
+ }
+
+ @Override
+ public int getTransitionDuration(Context context) {
+ return 250;
+ }
+
+ @Override
+ public RecentsState getHistoryForState(RecentsState previousState) {
+ return DEFAULT;
+ }
+
+ /**
+ * For this state, how modal should over view been shown. 0 modalness means all tasks drawn,
+ * 1 modalness means the current task is show on its own.
+ */
+ public float getOverviewModalness() {
+ return hasFlag(FLAG_MODAL) ? 1 : 0;
+ }
+
+ public boolean isFullScreen() {
+ return hasFlag(FLAG_FULL_SCREEN);
+ }
+
+ /**
+ * For this state, whether clear all button should be shown.
+ */
+ public boolean hasClearAllButton() {
+ return hasFlag(FLAG_CLEAR_ALL_BUTTON);
+ }
+
+ /**
+ * For this state, whether overview actions should be shown.
+ */
+ public boolean hasOverviewActions() {
+ return hasFlag(FLAG_OVERVIEW_ACTIONS);
+ }
+
+ /**
+ * For this state, whether live tile should be shown.
+ */
+ public boolean hasLiveTile() {
+ return hasFlag(FLAG_LIVE_TILE);
+ }
+
+ /**
+ * For this state, what color scrim should be drawn behind overview.
+ */
+ public int getScrimColor(RecentsActivity activity) {
+ return hasFlag(FLAG_SCRIM) ? Themes.getAttrColor(activity, R.attr.overviewScrimColor)
+ : Color.TRANSPARENT;
+ }
+
+ public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
+ return new float[] { NO_SCALE, NO_OFFSET };
+ }
+
+ /**
+ * For this state, whether tasks should layout as a grid rather than a list.
+ */
+ public boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) {
+ return hasFlag(FLAG_SHOW_AS_GRID) && showAsGrid(deviceProfile);
+ }
+
+ private boolean showAsGrid(DeviceProfile deviceProfile) {
+ return deviceProfile.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
+ }
+
+ private static class ModalState extends RecentsState {
+
+ public ModalState(int id, int flags) {
+ super(id, flags);
+ }
+
+ @Override
+ public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
+ return getOverviewScaleAndOffsetForModalState(activity);
+ }
+ }
+
+ private static class BackgroundAppState extends RecentsState {
+ public BackgroundAppState(int id, int flags) {
+ super(id, flags);
+ }
+
+ @Override
+ public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
+ return getOverviewScaleAndOffsetForBackgroundState(activity);
+ }
+ }
+
+ private static class LauncherState extends RecentsState {
+ LauncherState(int id, int flags) {
+ super(id, flags);
+ }
+
+ @Override
+ public float[] getOverviewScaleAndOffset(RecentsActivity activity) {
+ return new float[] { NO_SCALE, 1 };
+ }
+ }
+}
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 0000000..eca61bb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
@@ -0,0 +1,39 @@
+/*
+ * 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 static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+
+import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
+import com.android.quickstep.RecentsActivity;
+
+public class RecentsTaskController extends TaskViewTouchController<RecentsActivity> {
+
+ public RecentsTaskController(RecentsActivity activity) {
+ super(activity);
+ }
+
+ @Override
+ protected boolean isRecentsInteractive() {
+ return mActivity.hasWindowFocus() || (ENABLE_QUICKSTEP_LIVE_TILE.get()
+ && mActivity.getStateManager().getState().hasLiveTile());
+ }
+
+ @Override
+ protected boolean isRecentsModal() {
+ return false;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
new file mode 100644
index 0000000..0c2c92c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+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 android.content.Context;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.R;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Input consumer for two finger swipe actions for accessibility actions
+ */
+public class AccessibilityInputConsumer extends DelegateInputConsumer {
+
+ private static final String TAG = "A11yInputConsumer";
+
+ private final Context mContext;
+ private final VelocityTracker mVelocityTracker;
+ private final MotionPauseDetector mMotionPauseDetector;
+ private final RecentsAnimationDeviceState mDeviceState;
+
+ private final float mMinGestureDistance;
+ private final float mMinFlingVelocity;
+
+ private int mActivePointerId = -1;
+ private float mDownY;
+ private float mTotalY;
+
+ public AccessibilityInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+ InputConsumer delegate, InputMonitorCompat inputMonitor) {
+ super(delegate, inputMonitor);
+ mContext = context;
+ mVelocityTracker = VelocityTracker.obtain();
+ mMinGestureDistance = context.getResources()
+ .getDimension(R.dimen.accessibility_gesture_min_swipe_distance);
+ mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+ mDeviceState = deviceState;
+
+ mMotionPauseDetector = new MotionPauseDetector(context);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_ACCESSIBILITY | mDelegate.getType();
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mState != STATE_DELEGATE_ACTIVE) {
+ mVelocityTracker.addMovement(ev);
+ }
+
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ break;
+ }
+ case ACTION_POINTER_UP: {
+ if (mState == STATE_ACTIVE) {
+ int pointerIndex = ev.getActionIndex();
+ int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ final int newPointerIdx = pointerIndex == 0 ? 1 : 0;
+
+ mTotalY += (ev.getY(pointerIndex) - mDownY);
+ mDownY = ev.getY(newPointerIdx);
+ mActivePointerId = ev.getPointerId(newPointerIdx);
+ }
+ }
+ break;
+ }
+ case ACTION_POINTER_DOWN: {
+ if (mState == STATE_INACTIVE) {
+ int pointerIndex = ev.getActionIndex();
+ if (mDeviceState.getRotationTouchHelper()
+ .isInSwipeUpTouchRegion(ev, pointerIndex)
+ && mDelegate.allowInterceptByParent()) {
+ setActive(ev);
+
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ mDownY = ev.getY(pointerIndex);
+ } else {
+ mState = STATE_DELEGATE_ACTIVE;
+ }
+ }
+ break;
+ }
+ case ACTION_MOVE: {
+ if (mState == STATE_ACTIVE && mDeviceState.isAccessibilityMenuShortcutAvailable()) {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ break;
+ }
+ mMotionPauseDetector.addPosition(ev, pointerIndex);
+ }
+ break;
+ }
+ case ACTION_UP:
+ if (mState == STATE_ACTIVE) {
+ if (mDeviceState.isAccessibilityMenuShortcutAvailable()
+ && mMotionPauseDetector.isPaused()) {
+ SystemUiProxy.INSTANCE.get(mContext).notifyAccessibilityButtonLongClicked();
+ } else {
+ mTotalY += (ev.getY() - mDownY);
+ mVelocityTracker.computeCurrentVelocity(1000);
+
+ if ((-mTotalY) > mMinGestureDistance
+ || (-mVelocityTracker.getYVelocity()) > mMinFlingVelocity) {
+ SystemUiProxy.INSTANCE.get(mContext).notifyAccessibilityButtonClicked(
+ Display.DEFAULT_DISPLAY);
+ }
+ }
+ }
+ // Follow through
+ case ACTION_CANCEL: {
+ mVelocityTracker.recycle();
+ mMotionPauseDetector.clear();
+ break;
+ }
+ }
+
+ if (mState != STATE_ACTIVE) {
+ mDelegate.onMotionEvent(ev);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
new file mode 100644
index 0000000..510820a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
@@ -0,0 +1,281 @@
+
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.inputconsumers;
+
+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.launcher3.Utilities.squaredHypot;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import java.util.function.Consumer;
+
+/**
+ * Touch consumer for handling events to launch assistant from launcher
+ */
+public class AssistantInputConsumer extends DelegateInputConsumer {
+
+ private static final String TAG = "AssistantInputConsumer";
+ private static final long RETRACT_ANIMATION_DURATION_MS = 300;
+
+ // From //java/com/google/android/apps/gsa/search/shared/util/OpaContract.java.
+ private static final String OPA_BUNDLE_TRIGGER = "triggered_by";
+ // From //java/com/google/android/apps/gsa/assistant/shared/proto/opa_trigger.proto.
+ private static final int OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE = 83;
+ private static final String INVOCATION_TYPE_KEY = "invocation_type";
+ private static final int INVOCATION_TYPE_GESTURE = 1;
+
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+ private final PointF mStartDragPos = new PointF();
+
+ private int mActivePointerId = -1;
+ private boolean mPassedSlop;
+ private boolean mLaunchedAssistant;
+ private float mDistance;
+ private float mTimeFraction;
+ private long mDragTime;
+ private float mLastProgress;
+ private BaseActivityInterface mActivityInterface;
+
+ private final float mDragDistThreshold;
+ private final float mFlingDistThreshold;
+ private final long mTimeThreshold;
+ private final int mAngleThreshold;
+ private final float mSquaredSlop;
+ private final Context mContext;
+ private final Consumer<MotionEvent> mGestureDetector;
+
+ public AssistantInputConsumer(
+ Context context,
+ GestureState gestureState,
+ InputConsumer delegate,
+ InputMonitorCompat inputMonitor,
+ RecentsAnimationDeviceState deviceState,
+ MotionEvent startEvent) {
+ super(delegate, inputMonitor);
+ final Resources res = context.getResources();
+ mContext = context;
+ mDragDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
+ mFlingDistThreshold = res.getDimension(R.dimen.gestures_assistant_fling_threshold);
+ mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
+ mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+
+ float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+ mSquaredSlop = slop * slop;
+ mActivityInterface = gestureState.getActivityInterface();
+
+ boolean flingDisabled = deviceState.isAssistantGestureIsConstrained()
+ || deviceState.isInDeferredGestureRegion(startEvent);
+ mGestureDetector = flingDisabled
+ ? ev -> { }
+ : new GestureDetector(context, new AssistantGestureListener())::onTouchEvent;
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_ASSISTANT | mDelegate.getType();
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ // TODO add logging
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ mActivePointerId = ev.getPointerId(0);
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+ mTimeFraction = 0;
+ break;
+ }
+ case ACTION_POINTER_DOWN: {
+ if (mState != STATE_ACTIVE) {
+ mState = STATE_DELEGATE_ACTIVE;
+ }
+ 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: {
+ if (mState == STATE_DELEGATE_ACTIVE) {
+ break;
+ }
+ if (!mDelegate.allowInterceptByParent()) {
+ mState = STATE_DELEGATE_ACTIVE;
+ break;
+ }
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ break;
+ }
+ mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+ if (!mPassedSlop) {
+ // Normal gesture, ensure we pass the slop before we start tracking the gesture
+ if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+ > mSquaredSlop) {
+
+ mPassedSlop = true;
+ mStartDragPos.set(mLastPos.x, mLastPos.y);
+ mDragTime = SystemClock.uptimeMillis();
+
+ if (isValidAssistantGestureAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) {
+ setActive(ev);
+ } else {
+ mState = STATE_DELEGATE_ACTIVE;
+ }
+ }
+ } else {
+ // Movement
+ mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
+ mLastPos.y - mStartDragPos.y);
+ if (mDistance >= 0) {
+ final long diff = SystemClock.uptimeMillis() - mDragTime;
+ mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
+ updateAssistantProgress();
+ }
+ }
+ break;
+ }
+ case ACTION_CANCEL:
+ case ACTION_UP:
+ if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) {
+ ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0)
+ .setDuration(RETRACT_ANIMATION_DURATION_MS);
+ animator.addUpdateListener(valueAnimator -> {
+ float progress = (float) valueAnimator.getAnimatedValue();
+ SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(progress);
+ });
+ // Ensure that we always send a zero at the end to clear the invocation state.
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(0f);
+ }
+ });
+ animator.setInterpolator(Interpolators.DEACCEL_2);
+ animator.start();
+ }
+ mPassedSlop = false;
+ mState = STATE_INACTIVE;
+ break;
+ }
+
+ mGestureDetector.accept(ev);
+
+ if (mState != STATE_ACTIVE) {
+ mDelegate.onMotionEvent(ev);
+ }
+ }
+
+ private void updateAssistantProgress() {
+ if (!mLaunchedAssistant) {
+ mLastProgress = Math.min(mDistance * 1f / mDragDistThreshold, 1) * mTimeFraction;
+ if (mDistance >= mDragDistThreshold && mTimeFraction >= 1) {
+ SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion(0);
+ startAssistantInternal();
+ } else {
+ SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(mLastProgress);
+ }
+ }
+ }
+
+ private void startAssistantInternal() {
+ BaseDraggingActivity launcherActivity = mActivityInterface.getCreatedActivity();
+ if (launcherActivity != null) {
+ launcherActivity.getRootView().performHapticFeedback(
+ 13, // HapticFeedbackConstants.GESTURE_END
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+
+ Bundle args = new Bundle();
+ args.putInt(OPA_BUNDLE_TRIGGER, OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE);
+ args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
+ SystemUiProxy.INSTANCE.get(mContext).startAssistant(args);
+ mLaunchedAssistant = true;
+ }
+
+ /**
+ * Determine if angle is larger than threshold for assistant detection
+ */
+ private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) {
+ float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+
+ // normalize so that angle is measured clockwise from horizontal in the bottom right corner
+ // and counterclockwise from horizontal in the bottom left corner
+ angle = angle > 90 ? 180 - angle : angle;
+ return (angle > mAngleThreshold && angle < 90);
+ }
+
+ private class AssistantGestureListener extends SimpleOnGestureListener {
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (isValidAssistantGestureAngle(velocityX, -velocityY)
+ && mDistance >= mFlingDistThreshold
+ && !mLaunchedAssistant
+ && mState != STATE_DELEGATE_ACTIVE) {
+ mLastProgress = 1;
+ SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion(
+ (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY));
+ startAssistantInternal();
+ }
+ return true;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
new file mode 100644
index 0000000..8da2fd3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -0,0 +1,62 @@
+package com.android.quickstep.inputconsumers;
+
+import android.view.MotionEvent;
+
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.tracing.InputConsumerProto;
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+public abstract class DelegateInputConsumer implements InputConsumer {
+
+ protected static final int STATE_INACTIVE = 0;
+ protected static final int STATE_ACTIVE = 1;
+ protected static final int STATE_DELEGATE_ACTIVE = 2;
+
+ protected final InputConsumer mDelegate;
+ protected final InputMonitorCompat mInputMonitor;
+
+ protected int mState;
+
+ public DelegateInputConsumer(InputConsumer delegate, InputMonitorCompat inputMonitor) {
+ mDelegate = delegate;
+ mInputMonitor = inputMonitor;
+ mState = STATE_INACTIVE;
+ }
+
+ @Override
+ public InputConsumer getActiveConsumerInHierarchy() {
+ if (mState == STATE_ACTIVE) {
+ return this;
+ }
+ return mDelegate.getActiveConsumerInHierarchy();
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return mDelegate.allowInterceptByParent() && mState != STATE_ACTIVE;
+ }
+
+ @Override
+ public void onConsumerAboutToBeSwitched() {
+ mDelegate.onConsumerAboutToBeSwitched();
+ }
+
+ protected void setActive(MotionEvent ev) {
+ mState = STATE_ACTIVE;
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
+ mInputMonitor.pilferPointers();
+
+ // Send cancel event
+ MotionEvent event = MotionEvent.obtain(ev);
+ event.setAction(MotionEvent.ACTION_CANCEL);
+ mDelegate.onMotionEvent(event);
+ event.recycle();
+ }
+
+ @Override
+ public void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {
+ mDelegate.writeToProtoInternal(inputConsumerProto);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
new file mode 100644
index 0000000..fcc0217
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.createHomeIntent;
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
+import static com.android.quickstep.AbsSwipeUpHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.MultiStateCallback;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputMonitorCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
+
+/**
+ * A placeholder input consumer used when the device is still locked, e.g. from secure camera.
+ */
+public class DeviceLockedInputConsumer implements InputConsumer,
+ RecentsAnimationCallbacks.RecentsAnimationListener, BuilderProxy {
+
+ private static final String[] STATE_NAMES = DEBUG_STATES ? new String[2] : null;
+ private static int getFlagForIndex(int index, String name) {
+ if (DEBUG_STATES) {
+ STATE_NAMES[index] = name;
+ }
+ return 1 << index;
+ }
+
+ private static final int STATE_TARGET_RECEIVED =
+ getFlagForIndex(0, "STATE_TARGET_RECEIVED");
+ private static final int STATE_HANDLER_INVALIDATED =
+ getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
+
+ private final Context mContext;
+ private final RecentsAnimationDeviceState mDeviceState;
+ private final TaskAnimationManager mTaskAnimationManager;
+ private final GestureState mGestureState;
+ private final float mTouchSlopSquared;
+ private final InputMonitorCompat mInputMonitorCompat;
+
+ private final PointF mTouchDown = new PointF();
+ private final TransformParams mTransformParams;
+ private final MultiStateCallback mStateCallback;
+
+ private final Point mDisplaySize;
+ private final Matrix mMatrix = new Matrix();
+ private final float mMaxTranslationY;
+
+ private VelocityTracker mVelocityTracker;
+ private final AnimatedFloat mProgress = new AnimatedFloat(this::applyTransform);
+
+ private boolean mThresholdCrossed = false;
+ private boolean mHomeLaunched = false;
+
+ private RecentsAnimationController mRecentsAnimationController;
+
+ public DeviceLockedInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+ TaskAnimationManager taskAnimationManager, GestureState gestureState,
+ InputMonitorCompat inputMonitorCompat) {
+ mContext = context;
+ mDeviceState = deviceState;
+ mTaskAnimationManager = taskAnimationManager;
+ mGestureState = gestureState;
+ mTouchSlopSquared = squaredTouchSlop(context);
+ mTransformParams = new TransformParams();
+ mInputMonitorCompat = inputMonitorCompat;
+ mMaxTranslationY = context.getResources().getDimensionPixelSize(
+ R.dimen.device_locked_y_offset);
+
+ // Do not use DeviceProfile as the user data might be locked
+ mDisplaySize = DisplayController.INSTANCE.get(context).getInfo().currentSize;
+
+ // Init states
+ mStateCallback = new MultiStateCallback(STATE_NAMES);
+ mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
+ this::endRemoteAnimation);
+
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_DEVICE_LOCKED;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+ mVelocityTracker.addMovement(ev);
+
+ float x = ev.getX();
+ float y = ev.getY();
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mTouchDown.set(x, y);
+ break;
+ case ACTION_POINTER_DOWN: {
+ if (!mThresholdCrossed) {
+ // Cancel interaction in case of multi-touch interaction
+ int ptrIdx = ev.getActionIndex();
+ if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx)) {
+ int action = ev.getAction();
+ ev.setAction(ACTION_CANCEL);
+ finishTouchTracking(ev);
+ ev.setAction(action);
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (!mThresholdCrossed) {
+ if (squaredHypot(x - mTouchDown.x, y - mTouchDown.y) > mTouchSlopSquared) {
+ startRecentsTransition();
+ }
+ } else {
+ float dy = Math.max(mTouchDown.y - y, 0);
+ mProgress.updateValue(dy / mDisplaySize.y);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ finishTouchTracking(ev);
+ break;
+ }
+ }
+
+ /**
+ * 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 (mThresholdCrossed && ev.getAction() == ACTION_UP) {
+ mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
+
+ float velocityY = mVelocityTracker.getYVelocity();
+ float flingThreshold = mContext.getResources()
+ .getDimension(R.dimen.quickstep_fling_threshold_speed);
+
+ boolean dismissTask;
+ if (Math.abs(velocityY) > flingThreshold) {
+ // Is fling
+ dismissTask = velocityY < 0;
+ } else {
+ dismissTask = mProgress.value >= (1 - MIN_PROGRESS_FOR_OVERVIEW);
+ }
+
+ // Animate back to fullscreen before finishing
+ ObjectAnimator animator = mProgress.animateToValue(mProgress.value, 0);
+ animator.setDuration(100);
+ animator.setInterpolator(Interpolators.ACCEL);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (dismissTask) {
+ // For now, just start the home intent so user is prompted to unlock the device.
+ mContext.startActivity(createHomeIntent());
+ mHomeLaunched = true;
+ }
+ mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+ }
+ });
+ animator.start();
+ } else {
+ mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ private void startRecentsTransition() {
+ mThresholdCrossed = true;
+ mHomeLaunched = false;
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
+ mInputMonitorCompat.pilferPointers();
+
+ Intent intent = mGestureState.getHomeIntent()
+ .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
+ mTaskAnimationManager.startRecentsAnimation(mGestureState, intent, this);
+ }
+
+ @Override
+ public void onRecentsAnimationStart(RecentsAnimationController controller,
+ RecentsAnimationTargets targets) {
+ mRecentsAnimationController = controller;
+ mTransformParams.setTargetSet(targets);
+ applyTransform();
+ mStateCallback.setState(STATE_TARGET_RECEIVED);
+ }
+
+ @Override
+ public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+ mRecentsAnimationController = null;
+ mTransformParams.setTargetSet(null);
+ }
+
+ private void endRemoteAnimation() {
+ if (mHomeLaunched) {
+ ActivityManagerWrapper.getInstance().cancelRecentsAnimation(false);
+ } else if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.finishController(
+ false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
+ }
+ }
+
+ private void applyTransform() {
+ mTransformParams.setProgress(mProgress.value);
+ if (mTransformParams.getTargetSet() != null) {
+ mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
+ }
+ }
+
+ @Override
+ public void onBuildTargetParams(
+ Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
+ mMatrix.setTranslate(0, mProgress.value * mMaxTranslationY);
+ builder.withMatrix(mMatrix);
+ }
+
+ @Override
+ public void onConsumerAboutToBeSwitched() {
+ mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mThresholdCrossed;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
new file mode 100644
index 0000000..bc20902
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.inputconsumers;
+
+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_UP;
+
+import static com.android.launcher3.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE;
+import static com.android.launcher3.Utilities.squaredHypot;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Touch consumer for handling gesture event to launch one handed
+ * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode
+ */
+public class OneHandedModeInputConsumer extends DelegateInputConsumer {
+
+ private static final int ANGLE_MAX = 150;
+ private static final int ANGLE_MIN = 30;
+
+ private final Context mContext;
+ private final Point mDisplaySize;
+ private final RecentsAnimationDeviceState mDeviceState;
+
+ private final float mDragDistThreshold;
+ private final float mSquaredSlop;
+
+ private final int mNavBarSize;
+
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+
+ private boolean mPassedSlop;
+ private boolean mIsStopGesture;
+
+ public OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+ InputConsumer delegate, InputMonitorCompat inputMonitor) {
+ super(delegate, inputMonitor);
+ mContext = context;
+ mDeviceState = deviceState;
+ mDragDistThreshold = context.getResources().getDimensionPixelSize(
+ R.dimen.gestures_onehanded_drag_threshold);
+ mSquaredSlop = Utilities.squaredTouchSlop(context);
+ mDisplaySize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
+ mNavBarSize = ResourceUtils.getNavbarSize(NAVBAR_BOTTOM_GESTURE_SIZE,
+ mContext.getResources());
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_ONE_HANDED | mDelegate.getType();
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+ break;
+ }
+ case ACTION_MOVE: {
+ if (mState == STATE_DELEGATE_ACTIVE) {
+ break;
+ }
+ if (!mDelegate.allowInterceptByParent()) {
+ mState = STATE_DELEGATE_ACTIVE;
+ break;
+ }
+
+ mLastPos.set(ev.getX(), ev.getY());
+ if (!mPassedSlop) {
+ if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+ > mSquaredSlop) {
+ if ((!mDeviceState.isOneHandedModeActive() && isValidStartAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))
+ || (mDeviceState.isOneHandedModeActive() && isValidExitAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))) {
+ // To avoid mis-trigger when motion not touch system gesture region.
+ mPassedSlop = isInSystemGestureRegion(mLastPos);
+ setActive(ev);
+ } else {
+ mState = STATE_DELEGATE_ACTIVE;
+ }
+ }
+ } else {
+ float distance = (float) Math.hypot(mLastPos.x - mDownPos.x,
+ mLastPos.y - mDownPos.y);
+ if (distance > mDragDistThreshold && mPassedSlop) {
+ mIsStopGesture = true;
+ }
+ }
+ break;
+ }
+ case ACTION_UP: {
+ if (mLastPos.y >= mDownPos.y && mPassedSlop) {
+ onStartGestureDetected();
+ } else if (mIsStopGesture) {
+ onStopGestureDetected();
+ }
+ clearState();
+ break;
+ }
+ case ACTION_CANCEL:
+ clearState();
+ break;
+ }
+
+ if (mState != STATE_ACTIVE) {
+ mDelegate.onMotionEvent(ev);
+ }
+ }
+
+ private void clearState() {
+ mPassedSlop = false;
+ mState = STATE_INACTIVE;
+ mIsStopGesture = false;
+ }
+
+ private void onStartGestureDetected() {
+ if (mDeviceState.isSwipeToNotificationEnabled()) {
+ SystemUiProxy.INSTANCE.get(mContext).expandNotificationPanel();
+ } else if (!mDeviceState.isOneHandedModeActive()) {
+ SystemUiProxy.INSTANCE.get(mContext).startOneHandedMode();
+ }
+ }
+
+ private void onStopGestureDetected() {
+ if (!mDeviceState.isOneHandedModeEnabled() || !mDeviceState.isOneHandedModeActive()) {
+ return;
+ }
+
+ SystemUiProxy.INSTANCE.get(mContext).stopOneHandedMode();
+ }
+
+ private boolean isInSystemGestureRegion(PointF lastPos) {
+ final int navBarUpperBound = mDisplaySize.y - mNavBarSize;
+ return mDeviceState.isGesturalNavMode() && lastPos.y > navBarUpperBound;
+ }
+
+ private boolean isValidStartAngle(float deltaX, float deltaY) {
+ final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN);
+ }
+
+ private boolean isValidExitAngle(float deltaX, float deltaY) {
+ final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ return angle > ANGLE_MIN && angle < ANGLE_MAX;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
new file mode 100644
index 0000000..725c7c4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -0,0 +1,502 @@
+/*
+ * 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.inputconsumers;
+
+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 android.view.MotionEvent.INVALID_POINTER_ID;
+
+import static com.android.launcher3.PagedView.ACTION_MOVE_ALLOW_EASY_FLING;
+import static com.android.launcher3.PagedView.DEBUG_FAILED_QUICKSWITCH;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
+import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
+import static com.android.quickstep.GestureState.STATE_OVERSCROLL_WINDOW_CREATED;
+import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.graphics.PointF;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.R;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.tracing.InputConsumerProto;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.AbsSwipeUpHandler;
+import com.android.quickstep.AbsSwipeUpHandler.Factory;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RotationTouchHelper;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.CachedEventDispatcher;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import java.util.function.Consumer;
+
+/**
+ * Input consumer for handling events originating from an activity other than Launcher
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer {
+
+ public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN";
+ private static final String UP_EVT = "OtherActivityInputConsumer.UP";
+
+ // TODO: Move to quickstep contract
+ public static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 9;
+ public static final float QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL = 2;
+
+ private final RecentsAnimationDeviceState mDeviceState;
+ private final NavBarPosition mNavBarPosition;
+ private final TaskAnimationManager mTaskAnimationManager;
+ private final GestureState mGestureState;
+ private final RotationTouchHelper mRotationTouchHelper;
+ private RecentsAnimationCallbacks mActiveCallbacks;
+ private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
+ private final InputMonitorCompat mInputMonitorCompat;
+ private final InputEventReceiver mInputEventReceiver;
+ private final BaseActivityInterface mActivityInterface;
+
+ private final AbsSwipeUpHandler.Factory mHandlerFactory;
+
+ private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
+ private final MotionPauseDetector mMotionPauseDetector;
+ private final float mMotionPauseMinDisplacement;
+
+ private VelocityTracker mVelocityTracker;
+
+ private AbsSwipeUpHandler mInteractionHandler;
+
+ private final boolean mIsDeferredDownTarget;
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+ private int mActivePointerId = INVALID_POINTER_ID;
+
+ private int mLastRotation = -1;
+
+ // Distance after which we start dragging the window.
+ private final float mTouchSlop;
+
+ private final float mSquaredTouchSlop;
+ private final boolean mDisableHorizontalSwipe;
+
+ // Slop used to check when we start moving window.
+ private boolean mPassedWindowMoveSlop;
+ // Slop used to determine when we say that the gesture has started.
+ private boolean mPassedPilferInputSlop;
+ // Same as mPassedPilferInputSlop, except when continuing a gesture mPassedPilferInputSlop is
+ // initially true while this one is false.
+ private boolean mPassedSlopOnThisGesture;
+
+ // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
+ private float mStartDisplacement;
+
+ private final DisplayManager mDisplayManager;
+
+ private Handler mMainThreadHandler;
+ private Runnable mCancelRecentsAnimationRunnable = () -> {
+ ActivityManagerWrapper.getInstance().cancelRecentsAnimation(
+ true /* restoreHomeStackPosition */);
+ };
+
+ public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
+ TaskAnimationManager taskAnimationManager, GestureState gestureState,
+ boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
+ InputMonitorCompat inputMonitorCompat, InputEventReceiver inputEventReceiver,
+ boolean disableHorizontalSwipe, Factory handlerFactory) {
+ super(base);
+ mDeviceState = deviceState;
+ mNavBarPosition = mDeviceState.getNavBarPosition();
+ mTaskAnimationManager = taskAnimationManager;
+ mGestureState = gestureState;
+ mMainThreadHandler = new Handler(Looper.getMainLooper());
+ mHandlerFactory = handlerFactory;
+ mActivityInterface = mGestureState.getActivityInterface();
+
+ mMotionPauseDetector = new MotionPauseDetector(base, false,
+ mNavBarPosition.isLeftEdge() || mNavBarPosition.isRightEdge()
+ ? MotionEvent.AXIS_X : MotionEvent.AXIS_Y);
+ mMotionPauseMinDisplacement = base.getResources().getDimension(
+ R.dimen.motion_pause_detector_min_displacement_from_app);
+ mOnCompleteCallback = onCompleteCallback;
+ mVelocityTracker = VelocityTracker.obtain();
+ mInputMonitorCompat = inputMonitorCompat;
+ mInputEventReceiver = inputEventReceiver;
+
+ boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
+ mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
+
+ float slopMultiplier = mDeviceState.isFullyGesturalNavMode()
+ ? QUICKSTEP_TOUCH_SLOP_RATIO_GESTURAL
+ : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON;
+ mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+ mSquaredTouchSlop = slopMultiplier * mTouchSlop * mTouchSlop;
+
+ mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
+ mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
+ mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+ mDisplayManager = getSystemService(DisplayManager.class);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_OTHER_ACTIVITY;
+ }
+
+ @Override
+ public boolean isConsumerDetachedFromGesture() {
+ return true;
+ }
+
+ private void forceCancelGesture(MotionEvent ev) {
+ int action = ev.getAction();
+ ev.setAction(ACTION_CANCEL);
+ finishTouchTracking(ev);
+ ev.setAction(action);
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ if (TouchInteractionService.ENABLE_PER_WINDOW_INPUT_ROTATION) {
+ final Display display = mDisplayManager.getDisplay(mDeviceState.getDisplayId());
+ final int rotation = display.getRotation();
+ if (rotation != mLastRotation) {
+ // If rotation changes, reset tracking to avoid degenerate velocities.
+ mLastPos.set(ev.getX(), ev.getY());
+ mVelocityTracker.clear();
+ mLastRotation = rotation;
+ }
+ }
+
+ // Proxy events to recents view
+ if (mPassedWindowMoveSlop && mInteractionHandler != null
+ && !mRecentsViewDispatcher.hasConsumer()) {
+ mRecentsViewDispatcher.setConsumer(mInteractionHandler
+ .getRecentsViewDispatcher(mNavBarPosition.getRotation()));
+ int action = ev.getAction();
+ ev.setAction(ACTION_MOVE_ALLOW_EASY_FLING);
+ mRecentsViewDispatcher.dispatchEvent(ev);
+ ev.setAction(action);
+ }
+ int edgeFlags = ev.getEdgeFlags();
+ ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
+ mRecentsViewDispatcher.dispatchEvent(ev);
+ ev.setEdgeFlags(edgeFlags);
+
+ mVelocityTracker.addMovement(ev);
+ if (ev.getActionMasked() == ACTION_POINTER_UP) {
+ mVelocityTracker.clear();
+ mMotionPauseDetector.clear();
+ }
+
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ // Until we detect the gesture, handle events as we receive them
+ mInputEventReceiver.setBatchingEnabled(false);
+
+ Object traceToken = TraceHelper.INSTANCE.beginSection(DOWN_EVT,
+ FLAG_CHECK_FOR_RACE_CONDITIONS);
+ mActivePointerId = ev.getPointerId(0);
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+
+ // 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());
+ }
+
+ TraceHelper.INSTANCE.endSection(traceToken);
+ break;
+ }
+ case ACTION_POINTER_DOWN: {
+ if (!mPassedPilferInputSlop) {
+ // Cancel interaction in case of multi-touch interaction
+ int ptrIdx = ev.getActionIndex();
+ if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) {
+ forceCancelGesture(ev);
+ }
+ }
+ 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);
+ float displacementX = mLastPos.x - mDownPos.x;
+ float displacementY = mLastPos.y - mDownPos.y;
+
+ if (!mPassedWindowMoveSlop) {
+ if (!mIsDeferredDownTarget) {
+ // Normal gesture, ensure we pass the drag slop before we start tracking
+ // the gesture
+ if (Math.abs(displacement) > mTouchSlop) {
+ mPassedWindowMoveSlop = true;
+ mStartDisplacement = Math.min(displacement, -mTouchSlop);
+ }
+ }
+ }
+
+ float horizontalDist = Math.abs(displacementX);
+ float upDist = -displacement;
+ boolean passedSlop = squaredHypot(displacementX, displacementY)
+ >= mSquaredTouchSlop;
+ if (!mPassedSlopOnThisGesture && passedSlop) {
+ mPassedSlopOnThisGesture = true;
+ }
+ // Until passing slop, we don't know what direction we're going, so assume
+ // we're quick switching to avoid translating recents away when continuing
+ // the gesture (in which case mPassedPilferInputSlop starts as true).
+ boolean haveNotPassedSlopOnContinuedGesture =
+ !mPassedSlopOnThisGesture && mPassedPilferInputSlop;
+ boolean isLikelyToStartNewTask = haveNotPassedSlopOnContinuedGesture
+ || horizontalDist > upDist;
+
+ if (!mPassedPilferInputSlop) {
+ if (passedSlop) {
+ if (mDisableHorizontalSwipe
+ && Math.abs(displacementX) > Math.abs(displacementY)) {
+ // Horizontal gesture is not allowed in this region
+ forceCancelGesture(ev);
+ break;
+ }
+
+ mPassedPilferInputSlop = true;
+
+ if (mIsDeferredDownTarget) {
+ // Deferred gesture, start the animation and gesture tracking once
+ // we pass the actual touch slop
+ startTouchTrackingForWindowAnimation(ev.getEventTime());
+ }
+ if (!mPassedWindowMoveSlop) {
+ mPassedWindowMoveSlop = true;
+ mStartDisplacement = Math.min(displacement, -mTouchSlop);
+
+ }
+ notifyGestureStarted(isLikelyToStartNewTask);
+ }
+ }
+
+ if (mInteractionHandler != null) {
+ if (mPassedWindowMoveSlop) {
+ // Move
+ mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
+ }
+
+ if (mDeviceState.isFullyGesturalNavMode()) {
+ mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
+ || isLikelyToStartNewTask);
+ mMotionPauseDetector.addPosition(ev);
+ mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
+ }
+ }
+ break;
+ }
+ case ACTION_CANCEL:
+ case ACTION_UP: {
+ if (DEBUG_FAILED_QUICKSWITCH && !mPassedWindowMoveSlop) {
+ float displacementX = mLastPos.x - mDownPos.x;
+ float displacementY = mLastPos.y - mDownPos.y;
+ Log.d("Quickswitch", "mPassedWindowMoveSlop=false"
+ + " disp=" + squaredHypot(displacementX, displacementY)
+ + " slop=" + mSquaredTouchSlop);
+ }
+ finishTouchTracking(ev);
+ break;
+ }
+ }
+ }
+
+ private void notifyGestureStarted(boolean isLikelyToStartNewTask) {
+ ActiveGestureLog.INSTANCE.addLog("startQuickstep");
+ if (mInteractionHandler == null) {
+ return;
+ }
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
+ mInputMonitorCompat.pilferPointers();
+ // Once we detect the gesture, we can enable batching to reduce further updates
+ mInputEventReceiver.setBatchingEnabled(true);
+
+ // Notify the handler that the gesture has actually started
+ mInteractionHandler.onGestureStarted(isLikelyToStartNewTask);
+ }
+
+ private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
+ ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
+
+ mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs);
+ mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
+ mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener());
+ mInteractionHandler.initWhenReady();
+
+ if (mTaskAnimationManager.isRecentsAnimationRunning()) {
+ mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
+ mActiveCallbacks.addListener(mInteractionHandler);
+ mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
+ notifyGestureStarted(true /*isLikelyToStartNewTask*/);
+ } else {
+ Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
+ intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
+ mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
+ mInteractionHandler);
+ }
+ }
+
+ /**
+ * 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) {
+ Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT,
+ FLAG_CHECK_FOR_RACE_CONDITIONS);
+
+ if (mPassedWindowMoveSlop && mInteractionHandler != null) {
+ if (ev.getActionMasked() == ACTION_CANCEL) {
+ mInteractionHandler.onGestureCancelled();
+ } else {
+ mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
+ float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
+ float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
+ float velocity = mNavBarPosition.isRightEdge()
+ ? velocityX
+ : mNavBarPosition.isLeftEdge()
+ ? -velocityX
+ : velocityY;
+ mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
+ mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY),
+ mDownPos);
+ }
+ } else {
+ // Since we start touch tracking on DOWN, we may reach this state without actually
+ // starting the gesture. In that case, just cleanup immediately.
+ onConsumerAboutToBeSwitched();
+ onInteractionGestureFinished();
+
+ // Cancel the recents animation if SysUI happens to handle UP before we have a chance
+ // to start the recents animation. In addition, workaround for b/126336729 by delaying
+ // the cancel of the animation for a period, in case SysUI is slow to handle UP and we
+ // handle DOWN & UP and move the home stack before SysUI can start the activity
+ mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable);
+ mMainThreadHandler.postDelayed(mCancelRecentsAnimationRunnable, 100);
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ mMotionPauseDetector.clear();
+ TraceHelper.INSTANCE.endSection(traceToken);
+ }
+
+ @Override
+ public void notifyOrientationSetup() {
+ mRotationTouchHelper.onStartGesture();
+ }
+
+ @Override
+ public void onConsumerAboutToBeSwitched() {
+ Preconditions.assertUIThread();
+ mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable);
+ if (mInteractionHandler != null) {
+ // The consumer is being switched while we are active. Set up the shared state to be
+ // used by the next animation
+ removeListener();
+ mInteractionHandler.onConsumerAboutToBeSwitched();
+ }
+ }
+
+ @UiThread
+ private void onInteractionGestureFinished() {
+ Preconditions.assertUIThread();
+ removeListener();
+ mInteractionHandler = null;
+ mOnCompleteCallback.accept(this);
+ }
+
+ private void removeListener() {
+ if (mActiveCallbacks != null) {
+ mActiveCallbacks.removeListener(mInteractionHandler);
+ }
+ }
+
+ private float getDisplacement(MotionEvent ev) {
+ if (mNavBarPosition.isRightEdge()) {
+ return ev.getX() - mDownPos.x;
+ } else if (mNavBarPosition.isLeftEdge()) {
+ return mDownPos.x - ev.getX();
+ } else {
+ return ev.getY() - mDownPos.y;
+ }
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED);
+ }
+
+ @Override
+ public void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {
+ if (mInteractionHandler != null) {
+ mInteractionHandler.writeToProto(inputConsumerProto);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
new file mode 100644
index 0000000..b0df286
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.media.AudioManager;
+import android.media.session.MediaSessionManager;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Input consumer for handling touch on the recents/Launcher activity.
+ */
+public class OverviewInputConsumer<S extends BaseState<S>, T extends StatefulActivity<S>>
+ implements InputConsumer {
+
+ private final T mActivity;
+ private final BaseActivityInterface<?, T> mActivityInterface;
+ private final BaseDragLayer mTarget;
+ private final InputMonitorCompat mInputMonitor;
+
+ private final int[] mLocationOnScreen = new int[2];
+
+ private final boolean mStartingInActivityBounds;
+ private boolean mTargetHandledTouch;
+
+ public OverviewInputConsumer(GestureState gestureState, T activity,
+ @Nullable InputMonitorCompat inputMonitor, boolean startingInActivityBounds) {
+ mActivity = activity;
+ mInputMonitor = inputMonitor;
+ mStartingInActivityBounds = startingInActivityBounds;
+ mActivityInterface = gestureState.getActivityInterface();
+
+ mTarget = activity.getDragLayer();
+ mTarget.getLocationOnScreen(mLocationOnScreen);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_OVERVIEW;
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mTargetHandledTouch;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ int flags = ev.getEdgeFlags();
+ if (!mStartingInActivityBounds) {
+ ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR);
+ }
+ ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
+ boolean handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
+ ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
+ ev.setEdgeFlags(flags);
+
+ if (!mTargetHandledTouch && handled) {
+ mTargetHandledTouch = true;
+ if (!mStartingInActivityBounds) {
+ mActivityInterface.closeOverlay();
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ ActiveGestureLog.INSTANCE.addLog("startQuickstep");
+ }
+ if (mInputMonitor != null) {
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
+ mInputMonitor.pilferPointers();
+ }
+ }
+ }
+
+ @Override
+ public void onKeyEvent(KeyEvent ev) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ switch (ev.getKeyCode()) {
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ MediaSessionManager mgr = mActivity.getSystemService(MediaSessionManager.class);
+ mgr.dispatchVolumeKeyEventAsSystemService(ev,
+ AudioManager.USE_DEFAULT_STREAM_TYPE);
+ break;
+ default:
+ break;
+ }
+ mActivity.dispatchKeyEvent(ev);
+ }
+ }
+}
+
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
new file mode 100644
index 0000000..864e08d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import static com.android.launcher3.Utilities.createHomeIntent;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+public class OverviewWithoutFocusInputConsumer implements InputConsumer,
+ TriggerSwipeUpTouchTracker.OnSwipeUpListener {
+
+ private final Context mContext;
+ private final InputMonitorCompat mInputMonitor;
+ private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+ private final GestureState mGestureState;
+
+ public OverviewWithoutFocusInputConsumer(Context context,
+ RecentsAnimationDeviceState deviceState, GestureState gestureState,
+ InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
+ mContext = context;
+ mGestureState = gestureState;
+ mInputMonitor = inputMonitor;
+ mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
+ deviceState.getNavBarPosition(), this::onInterceptTouch, this);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_OVERVIEW_WITHOUT_FOCUS;
+ }
+
+ @Override
+ public boolean allowInterceptByParent() {
+ return !mTriggerSwipeUpTracker.interceptedTouch();
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ mTriggerSwipeUpTracker.onMotionEvent(ev);
+ }
+
+ private void onInterceptTouch() {
+ if (mInputMonitor != null) {
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
+ mInputMonitor.pilferPointers();
+ }
+ }
+
+ @Override
+ public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
+ try {
+ mContext.startActivity(mGestureState.getHomeIntent());
+ } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
+ mContext.startActivity(createHomeIntent());
+ }
+ ActiveGestureLog.INSTANCE.addLog("startQuickstep");
+ BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
+ int state = (mGestureState != null && mGestureState.getEndTarget() != null)
+ ? mGestureState.getEndTarget().containerType
+ : LAUNCHER_STATE_HOME;
+ activity.getStatsLogManager().logger()
+ .withSrcState(LAUNCHER_STATE_BACKGROUND)
+ .withDstState(state)
+ .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+ .setWorkspace(
+ LauncherAtom.WorkspaceContainer.newBuilder()
+ .setPageIndex(-1))
+ .build())
+ .log(LAUNCHER_HOME_GESTURE);
+ }
+
+ @Override
+ public void onSwipeUpCancelled() {}
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
new file mode 100644
index 0000000..a8bf333
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import android.content.Context;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.MotionPauseDetector;
+
+/**
+ * An input consumer that detects swipe up and hold to exit screen pinning mode.
+ */
+public class ScreenPinnedInputConsumer implements InputConsumer {
+
+ private static final String TAG = "ScreenPinnedConsumer";
+
+ private final float mMotionPauseMinDisplacement;
+ private final MotionPauseDetector mMotionPauseDetector;
+
+ private float mTouchDownY;
+
+ public ScreenPinnedInputConsumer(Context context, GestureState gestureState) {
+ mMotionPauseMinDisplacement = context.getResources().getDimension(
+ R.dimen.motion_pause_detector_min_displacement_from_app);
+ mMotionPauseDetector = new MotionPauseDetector(context, true /* makePauseHarderToTrigger*/);
+ mMotionPauseDetector.setOnMotionPauseListener(() -> {
+ SystemUiProxy.INSTANCE.get(context).stopScreenPinning();
+ BaseDraggingActivity launcherActivity = gestureState.getActivityInterface()
+ .getCreatedActivity();
+ if (launcherActivity != null) {
+ launcherActivity.getRootView().performHapticFeedback(
+ HapticFeedbackConstants.LONG_PRESS,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+ mMotionPauseDetector.clear();
+ });
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_SCREEN_PINNED;
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ float y = ev.getY();
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mTouchDownY = y;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ float displacement = mTouchDownY - y;
+ mMotionPauseDetector.setDisallowPause(displacement < mMotionPauseMinDisplacement);
+ mMotionPauseDetector.addPosition(ev);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ mMotionPauseDetector.clear();
+ break;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
new file mode 100644
index 0000000..4472bdc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+
+import java.net.URISyntaxException;
+
+/**
+ * A page shows after SUW flow to hint users to swipe up from the bottom of the screen to go home
+ * for the gestural system navigation.
+ */
+public class AllSetActivity extends Activity {
+
+ private static final String LOG_TAG = "AllSetActivity";
+ private static final String URI_SYSTEM_NAVIGATION_SETTING =
+ "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S.:settings:fragment_args_key=gesture_system_navigation_input_summary;S.:settings:show_fragment=com.android.settings.gestures.SystemNavigationGestureSettings;end";
+ private static final String EXTRA_ACCENT_COLOR_DARK_MODE = "suwColorAccentDark";
+ private static final String EXTRA_ACCENT_COLOR_LIGHT_MODE = "suwColorAccentLight";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_allset);
+
+ int mode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ boolean isDarkTheme = mode == Configuration.UI_MODE_NIGHT_YES;
+ int accentColor = getIntent().getIntExtra(
+ isDarkTheme ? EXTRA_ACCENT_COLOR_DARK_MODE : EXTRA_ACCENT_COLOR_LIGHT_MODE,
+ isDarkTheme ? Color.WHITE : Color.BLACK);
+
+ ((ImageView) findViewById(R.id.icon)).getDrawable().mutate().setTint(accentColor);
+
+ TextView tv = findViewById(R.id.navigation_settings);
+ tv.setTextColor(accentColor);
+ tv.setOnClickListener(v -> {
+ try {
+ startActivityForResult(
+ Intent.parseUri(URI_SYSTEM_NAVIGATION_SETTING, 0), 0);
+ } catch (URISyntaxException e) {
+ Log.e(LOG_TAG, "Failed to parse system nav settings intent", e);
+ }
+ finish();
+ });
+
+ findViewById(R.id.hint).setAccessibilityDelegate(new SkipButtonAccessibilityDelegate());
+ }
+
+ /**
+ * Accessibility delegate which exposes a click event without making the view
+ * clickable in touch mode
+ */
+ private class SkipButtonAccessibilityDelegate extends AccessibilityDelegate {
+
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(View host) {
+ AccessibilityNodeInfo info = super.createAccessibilityNodeInfo(host);
+ info.addAction(AccessibilityAction.ACTION_CLICK);
+ info.setClickable(true);
+ return info;
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == AccessibilityAction.ACTION_CLICK.getId()) {
+ startActivity(Utilities.createHomeIntent());
+ finish();
+ return true;
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
index 6862f07..957f776 100644
--- a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
@@ -18,7 +18,6 @@
import static com.android.quickstep.interaction.TutorialController.TutorialType.ASSISTANT_COMPLETE;
import android.graphics.PointF;
-import android.view.View;
import com.android.launcher3.R;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
@@ -33,38 +32,6 @@
}
@Override
- Integer getTitleStringId() {
- switch (mTutorialType) {
- case ASSISTANT:
- return R.string.assistant_gesture_tutorial_playground_title;
- case ASSISTANT_COMPLETE:
- return R.string.gesture_tutorial_confirm_title;
- }
- return null;
- }
-
- @Override
- Integer getSubtitleStringId() {
- if (mTutorialType == TutorialType.ASSISTANT) {
- return R.string.assistant_gesture_tutorial_playground_subtitle;
- }
- return null;
- }
-
- @Override
- Integer getActionButtonStringId() {
- if (mTutorialType == ASSISTANT_COMPLETE) {
- return R.string.gesture_tutorial_action_button_label_done;
- }
- return null;
- }
-
- @Override
- void onActionButtonClicked(View button) {
- mTutorialFragment.closeTutorial();
- }
-
- @Override
public void onBackGestureAttempted(BackGestureResult result) {
switch (mTutorialType) {
case ASSISTANT:
@@ -101,10 +68,9 @@
showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner);
break;
case ASSISTANT_COMPLETED:
- hideFeedback();
- hideHandCoachingAnimation();
- showRippleEffect(
- () -> mTutorialFragment.changeController(ASSISTANT_COMPLETE));
+ hideFeedback(true);
+ showRippleEffect(null);
+ showFeedback(R.string.assistant_gesture_tutorial_playground_subtitle);
break;
case ASSISTANT_NOT_STARTED_BAD_ANGLE:
showFeedback(R.string.assistant_gesture_feedback_swipe_not_diagonal);
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
index 70181fb..b797f0c 100644
--- a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
@@ -18,17 +18,11 @@
import android.view.MotionEvent;
import android.view.View;
-import com.android.launcher3.R;
import com.android.quickstep.interaction.TutorialController.TutorialType;
/** Shows the Home gesture interactive tutorial. */
public class AssistantGestureTutorialFragment extends TutorialFragment {
@Override
- int getHandAnimationResId() {
- return R.drawable.assistant_gesture;
- }
-
- @Override
TutorialController createController(TutorialType type) {
return new AssistantGestureTutorialController(this, type);
}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 921e568..3cb22f4 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -15,11 +15,12 @@
*/
package com.android.quickstep.interaction;
+import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION;
import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
-import static com.android.quickstep.interaction.TutorialController.TutorialType.LEFT_EDGE_BACK_NAVIGATION;
import android.graphics.PointF;
-import android.view.View;
+
+import androidx.appcompat.content.res.AppCompatResources;
import com.android.launcher3.R;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
@@ -33,65 +34,28 @@
}
@Override
- Integer getTitleStringId() {
- switch (mTutorialType) {
- case RIGHT_EDGE_BACK_NAVIGATION:
- return R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge;
- case LEFT_EDGE_BACK_NAVIGATION:
- return R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge;
- case BACK_NAVIGATION_COMPLETE:
- return R.string.gesture_tutorial_confirm_title;
- }
- return null;
+ public Integer getIntroductionTitle() {
+ return R.string.back_gesture_intro_title;
}
@Override
- Integer getSubtitleStringId() {
- switch (mTutorialType) {
- case RIGHT_EDGE_BACK_NAVIGATION:
- return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge;
- case LEFT_EDGE_BACK_NAVIGATION:
- return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge;
- case BACK_NAVIGATION_COMPLETE:
- return R.string.back_gesture_tutorial_confirm_subtitle;
- }
- return null;
+ public Integer getIntroductionSubtitle() {
+ return R.string.back_gesture_intro_subtitle;
}
@Override
- Integer getActionButtonStringId() {
- if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
- return R.string.gesture_tutorial_action_button_label_done;
- }
- return null;
- }
-
- @Override
- Integer getActionTextButtonStringId() {
- if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
- return R.string.gesture_tutorial_action_button_label_settings;
- }
- return null;
- }
-
- @Override
- void onActionButtonClicked(View button) {
- mTutorialFragment.closeTutorial();
- }
-
- @Override
- void onActionTextButtonClicked(View button) {
- mTutorialFragment.startSystemNavigationSetting();
+ protected int getMockAppTaskThumbnailResId(boolean forDarkMode) {
+ return R.drawable.mock_conversation;
}
@Override
public void onBackGestureAttempted(BackGestureResult result) {
+ if (mGestureCompleted) {
+ return;
+ }
switch (mTutorialType) {
- case RIGHT_EDGE_BACK_NAVIGATION:
- handleAttemptFromRight(result);
- break;
- case LEFT_EDGE_BACK_NAVIGATION:
- handleAttemptFromLeft(result);
+ case BACK_NAVIGATION:
+ handleBackAttempt(result);
break;
case BACK_NAVIGATION_COMPLETE:
if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
@@ -102,43 +66,25 @@
}
}
- private void handleAttemptFromRight(BackGestureResult result) {
- switch (result) {
- case BACK_COMPLETED_FROM_RIGHT:
- hideFeedback();
- hideHandCoachingAnimation();
- showRippleEffect(
- () -> mTutorialFragment.changeController(LEFT_EDGE_BACK_NAVIGATION));
- break;
- case BACK_CANCELLED_FROM_RIGHT:
- showFeedback(R.string.back_gesture_feedback_cancelled_right_edge);
- break;
- case BACK_COMPLETED_FROM_LEFT:
- case BACK_CANCELLED_FROM_LEFT:
- case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
- showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_right_edge);
- break;
- case BACK_NOT_STARTED_IN_NAV_BAR_REGION:
- showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
- break;
- }
- }
-
- private void handleAttemptFromLeft(BackGestureResult result) {
+ private void handleBackAttempt(BackGestureResult result) {
switch (result) {
case BACK_COMPLETED_FROM_LEFT:
- hideFeedback();
- hideHandCoachingAnimation();
- showRippleEffect(
- () -> mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE));
+ case BACK_COMPLETED_FROM_RIGHT:
+ mTutorialFragment.releaseGestureVideoView();
+ hideFeedback(true);
+ mFakeTaskView.setBackground(AppCompatResources.getDrawable(mContext,
+ R.drawable.mock_conversations_list));
+ int subtitleResId = mTutorialFragment.isAtFinalStep()
+ ? R.string.back_gesture_feedback_complete_without_follow_up
+ : R.string.back_gesture_feedback_complete_with_overview_follow_up;
+ showFeedback(subtitleResId, true);
break;
case BACK_CANCELLED_FROM_LEFT:
- showFeedback(R.string.back_gesture_feedback_cancelled_left_edge);
- break;
- case BACK_COMPLETED_FROM_RIGHT:
case BACK_CANCELLED_FROM_RIGHT:
+ showFeedback(R.string.back_gesture_feedback_cancelled);
+ break;
case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
- showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_left_edge);
+ showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_edge);
break;
case BACK_NOT_STARTED_IN_NAV_BAR_REGION:
showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
@@ -148,10 +94,15 @@
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+ if (mGestureCompleted) {
+ return;
+ }
if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
mTutorialFragment.closeTutorial();
}
+ } else if (mTutorialType == BACK_NAVIGATION) {
+ showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
}
}
}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index bef50ea..1740f68 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -18,14 +18,23 @@
import android.view.MotionEvent;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.R;
import com.android.quickstep.interaction.TutorialController.TutorialType;
/** Shows the Back gesture interactive tutorial. */
public class BackGestureTutorialFragment extends TutorialFragment {
+ @Nullable
@Override
- int getHandAnimationResId() {
- return R.drawable.back_gesture;
+ Integer getFeedbackVideoResId(boolean forDarkMode) {
+ return R.drawable.gesture_tutorial_motion_back;
+ }
+
+ @Nullable
+ @Override
+ Integer getGestureVideoResId() {
+ return R.drawable.gesture_tutorial_loop_back;
}
@Override
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
index e4b348e..c532f8e 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -30,6 +30,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.Utilities;
/**
* Utility class to handle edge swipes for back gestures.
@@ -47,9 +48,9 @@
private final Point mDisplaySize = new Point();
// The edge width where touch down is allowed
- private int mEdgeWidth;
+ private final int mEdgeWidth;
// The bottom gesture area height
- private int mBottomGestureHeight;
+ private final int mBottomGestureHeight;
// The slop to distinguish between horizontal and vertical motion
private final float mTouchSlop;
// Duration after which we consider the event as longpress.
@@ -97,7 +98,9 @@
mBottomGestureHeight =
ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res);
- mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res);
+ int systemBackRegion = ResourceUtils.getNavbarSize("config_backGestureInset", res);
+ // System back region is 0 if gesture nav is not currently enabled.
+ mEdgeWidth = systemBackRegion == 0 ? Utilities.dpToPx(18) : systemBackRegion;
}
void setViewGroupParent(@Nullable ViewGroup parent) {
@@ -143,6 +146,10 @@
return false;
}
+ boolean onInterceptTouch(MotionEvent motionEvent) {
+ return isWithinTouchRegion((int) motionEvent.getX(), (int) motionEvent.getY());
+ }
+
private boolean isWithinTouchRegion(int x, int y) {
// Disallow if too far from the edge
if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index f8d9d8d..7fb7d29 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -15,16 +15,16 @@
*/
package com.android.quickstep.interaction;
-import static com.android.quickstep.interaction.TutorialFragment.KEY_TUTORIAL_TYPE;
-
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
+import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.Window;
+import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import com.android.launcher3.R;
@@ -37,15 +37,26 @@
private static final String LOG_TAG = "GestureSandboxActivity";
+ private static final String KEY_TUTORIAL_STEPS = "tutorial_steps";
+ private static final String KEY_CURRENT_STEP = "current_step";
+
+ private TutorialType[] mTutorialSteps;
+ private TutorialType mCurrentTutorialStep;
private TutorialFragment mFragment;
+ private int mCurrentStep;
+ private int mNumSteps;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.gesture_tutorial_activity);
- mFragment = TutorialFragment.newInstance(getTutorialType(getIntent().getExtras()));
+ Bundle args = savedInstanceState == null ? getIntent().getExtras() : savedInstanceState;
+ mTutorialSteps = getTutorialSteps(args);
+ mCurrentTutorialStep = mTutorialSteps[mCurrentStep - 1];
+ mFragment = TutorialFragment.newInstance(mCurrentTutorialStep);
getSupportFragmentManager().beginTransaction()
.add(R.id.gesture_tutorial_fragment_container, mFragment)
.commit();
@@ -72,17 +83,98 @@
}
}
- private TutorialType getTutorialType(Bundle extras) {
- TutorialType defaultType = TutorialType.RIGHT_EDGE_BACK_NAVIGATION;
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
+ savedInstanceState.putStringArray(KEY_TUTORIAL_STEPS, getTutorialStepNames());
+ savedInstanceState.putInt(KEY_CURRENT_STEP, mCurrentStep);
+ super.onSaveInstanceState(savedInstanceState);
+ }
- if (extras == null || !extras.containsKey(KEY_TUTORIAL_TYPE)) {
- return defaultType;
+ /** Returns true iff there aren't anymore tutorial types to display to the user. */
+ public boolean isTutorialComplete() {
+ return mCurrentStep >= mNumSteps;
+ }
+
+ public int getCurrentStep() {
+ return mCurrentStep;
+ }
+
+ public int getNumSteps() {
+ return mNumSteps;
+ }
+
+ /**
+ * Closes the tutorial and this activity.
+ */
+ public void closeTutorial() {
+ mFragment.closeTutorial();
+ }
+
+ /**
+ * Replaces the current TutorialFragment, continuing to the next tutorial step if there is one.
+ *
+ * If there is no following step, the tutorial is closed.
+ */
+ public void continueTutorial() {
+ if (isTutorialComplete()) {
+ mFragment.closeTutorial();
+ return;
}
- try {
- return TutorialType.valueOf(extras.getString(KEY_TUTORIAL_TYPE, ""));
- } catch (IllegalArgumentException e) {
- return defaultType;
+ mCurrentTutorialStep = mTutorialSteps[mCurrentStep];
+ mFragment = TutorialFragment.newInstance(mCurrentTutorialStep);
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.gesture_tutorial_fragment_container, mFragment)
+ .runOnCommit(() -> mFragment.onAttachedToWindow())
+ .commit();
+ mCurrentStep++;
+ }
+
+ private String[] getTutorialStepNames() {
+ String[] tutorialStepNames = new String[mTutorialSteps.length];
+
+ int i = 0;
+ for (TutorialType tutorialStep : mTutorialSteps) {
+ tutorialStepNames[i++] = tutorialStep.name();
}
+
+ return tutorialStepNames;
+ }
+
+ private TutorialType[] getTutorialSteps(Bundle extras) {
+ TutorialType[] defaultSteps = new TutorialType[] {TutorialType.BACK_NAVIGATION};
+ mCurrentStep = 1;
+ mNumSteps = 1;
+
+ if (extras == null || !extras.containsKey(KEY_TUTORIAL_STEPS)) {
+ return defaultSteps;
+ }
+
+ Object savedSteps = extras.get(KEY_TUTORIAL_STEPS);
+ int currentStep = extras.getInt(KEY_CURRENT_STEP, -1);
+ String[] savedStepsNames;
+
+ if (savedSteps instanceof String) {
+ savedStepsNames = TextUtils.isEmpty((String) savedSteps)
+ ? null : ((String) savedSteps).split(",");
+ } else if (savedSteps instanceof String[]) {
+ savedStepsNames = (String[]) savedSteps;
+ } else {
+ return defaultSteps;
+ }
+
+ if (savedStepsNames == null) {
+ return defaultSteps;
+ }
+
+ TutorialType[] tutorialSteps = new TutorialType[savedStepsNames.length];
+ for (int i = 0; i < savedStepsNames.length; i++) {
+ tutorialSteps[i] = TutorialType.valueOf(savedStepsNames[i]);
+ }
+
+ mCurrentStep = Math.max(currentStep, 1);
+ mNumSteps = tutorialSteps.length;
+
+ return tutorialSteps;
}
private void hideSystemUI() {
@@ -91,6 +183,7 @@
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN);
getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
index 0edabd4..819c91c 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -15,12 +15,9 @@
*/
package com.android.quickstep.interaction;
-import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
-
import android.annotation.TargetApi;
import android.graphics.PointF;
import android.os.Build;
-import android.view.View;
import com.android.launcher3.R;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
@@ -35,39 +32,25 @@
}
@Override
- Integer getTitleStringId() {
- switch (mTutorialType) {
- case HOME_NAVIGATION:
- return R.string.home_gesture_tutorial_playground_title;
- case HOME_NAVIGATION_COMPLETE:
- return R.string.gesture_tutorial_confirm_title;
- }
- return null;
+ public Integer getIntroductionTitle() {
+ return R.string.home_gesture_intro_title;
}
@Override
- Integer getSubtitleStringId() {
- if (mTutorialType == TutorialType.HOME_NAVIGATION) {
- return R.string.home_gesture_tutorial_playground_subtitle;
- }
- return null;
+ public Integer getIntroductionSubtitle() {
+ return R.string.home_gesture_intro_subtitle;
}
@Override
- Integer getActionButtonStringId() {
- if (mTutorialType == HOME_NAVIGATION_COMPLETE) {
- return R.string.gesture_tutorial_action_button_label_done;
- }
- return null;
- }
-
- @Override
- void onActionButtonClicked(View button) {
- mTutorialFragment.closeTutorial();
+ protected int getMockAppTaskThumbnailResId(boolean forDarkMode) {
+ return forDarkMode ? R.drawable.mock_webpage_dark_mode : R.drawable.mock_webpage_light_mode;
}
@Override
public void onBackGestureAttempted(BackGestureResult result) {
+ if (mGestureCompleted) {
+ return;
+ }
switch (mTutorialType) {
case HOME_NAVIGATION:
switch (result) {
@@ -90,12 +73,19 @@
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+ if (mGestureCompleted) {
+ return;
+ }
switch (mTutorialType) {
case HOME_NAVIGATION:
switch (result) {
case HOME_GESTURE_COMPLETED: {
- animateFakeTaskViewHome(finalVelocity, () ->
- mTutorialFragment.changeController(HOME_NAVIGATION_COMPLETE));
+ mTutorialFragment.releaseGestureVideoView();
+ animateFakeTaskViewHome(finalVelocity, null);
+ int subtitleResId = mTutorialFragment.isAtFinalStep()
+ ? R.string.home_gesture_feedback_complete_without_follow_up
+ : R.string.home_gesture_feedback_complete_with_follow_up;
+ showFeedback(subtitleResId, true);
break;
}
case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
@@ -103,12 +93,12 @@
showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge);
break;
case OVERVIEW_GESTURE_COMPLETED:
- fadeOutFakeTaskView(true, () ->
+ fadeOutFakeTaskView(true, true, () ->
showFeedback(R.string.home_gesture_feedback_overview_detected));
break;
case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
case HOME_OR_OVERVIEW_CANCELLED:
- fadeOutFakeTaskView(false, null);
+ fadeOutFakeTaskView(false, true, null);
showFeedback(R.string.home_gesture_feedback_wrong_swipe_direction);
break;
}
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
index e2a9d12..9572637 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
@@ -15,14 +15,25 @@
*/
package com.android.quickstep.interaction;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.R;
import com.android.quickstep.interaction.TutorialController.TutorialType;
/** Shows the Home gesture interactive tutorial. */
public class HomeGestureTutorialFragment extends TutorialFragment {
+ @Nullable
@Override
- int getHandAnimationResId() {
- return R.drawable.home_gesture;
+ Integer getFeedbackVideoResId(boolean forDarkMode) {
+ return forDarkMode
+ ? R.drawable.gesture_tutorial_motion_home_dark_mode
+ : R.drawable.gesture_tutorial_motion_home_light_mode;
+ }
+
+ @Nullable
+ @Override
+ Integer getGestureVideoResId() {
+ return R.drawable.gesture_tutorial_loop_home;
}
@Override
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index 0e2312b..a9a9e2a 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -16,6 +16,7 @@
package com.android.quickstep.interaction;
import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
@@ -48,13 +49,14 @@
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.util.VibratorWrapper;
import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.MotionPauseDetector;
import com.android.quickstep.util.NavBarPosition;
import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
import com.android.systemui.shared.system.QuickStepContract;
/** Utility class to handle Home and Assistant gestures. */
public class NavBarGestureHandler implements OnTouchListener,
- TriggerSwipeUpTouchTracker.OnSwipeUpListener {
+ TriggerSwipeUpTouchTracker.OnSwipeUpListener, MotionPauseDetector.OnMotionPauseListener {
private static final String LOG_TAG = "NavBarGestureHandler";
private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300;
@@ -74,6 +76,7 @@
private final PointF mAssistantStartDragPos = new PointF();
private final PointF mDownPos = new PointF();
private final PointF mLastPos = new PointF();
+ private final MotionPauseDetector mMotionPauseDetector;
private boolean mTouchCameFromAssistantCorner;
private boolean mTouchCameFromNavBar;
private boolean mPassedAssistantSlop;
@@ -100,6 +103,7 @@
new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
new NavBarPosition(Mode.NO_BUTTON, displayRotation),
null /*onInterceptTouch*/, this);
+ mMotionPauseDetector = new MotionPauseDetector(context);
final Resources resources = context.getResources();
mBottomGestureHeight =
@@ -140,7 +144,6 @@
if (mGestureCallback == null || mAssistantGestureActive) {
return;
}
- finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000);
if (mTouchCameFromNavBar) {
mGestureCallback.onNavBarGestureAttempted(wasFling
? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED, finalVelocity);
@@ -177,12 +180,14 @@
}
mLaunchedAssistant = false;
mSwipeUpTouchTracker.init();
+ mMotionPauseDetector.clear();
+ mMotionPauseDetector.setOnMotionPauseListener(this);
break;
case MotionEvent.ACTION_MOVE:
+ mLastPos.set(event.getX(), event.getY());
if (!mAssistantGestureActive) {
break;
}
- mLastPos.set(event.getX(), event.getY());
if (!mPassedAssistantSlop) {
// Normal gesture, ensure we pass the slop before we start tracking the gesture
@@ -213,6 +218,7 @@
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
+ mMotionPauseDetector.clear();
if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
mGestureCallback.onNavBarGestureAttempted(
HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
@@ -239,9 +245,27 @@
}
mSwipeUpTouchTracker.onMotionEvent(event);
mAssistantGestureDetector.onTouchEvent(event);
+ mMotionPauseDetector.addPosition(event);
+ mMotionPauseDetector.setDisallowPause(mLastPos.y >= mDisplaySize.y - mBottomGestureHeight);
return intercepted;
}
+ boolean onInterceptTouch(MotionEvent event) {
+ return mAssistantLeftRegion.contains(event.getX(), event.getY())
+ || mAssistantRightRegion.contains(event.getX(), event.getY())
+ || event.getY() >= mDisplaySize.y - mBottomGestureHeight;
+ }
+
+ @Override
+ public void onMotionPauseChanged(boolean isPaused) {
+ mGestureCallback.onMotionPaused(isPaused);
+ }
+
+ @Override
+ public void onMotionPauseDetected() {
+ VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+ }
+
/**
* Determine if angle is larger than threshold for assistant detection
*/
@@ -293,6 +317,9 @@
/** Called whenever any touch is completed. */
void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity);
+ /** Called when a motion stops or resumes */
+ default void onMotionPaused(boolean isPaused) {}
+
/** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
default void setNavBarGestureProgress(@Nullable Float displacement) {}
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
index c636eba..77ddb2b 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
@@ -15,14 +15,17 @@
*/
package com.android.quickstep.interaction;
-import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import android.animation.AnimatorSet;
import android.annotation.TargetApi;
import android.graphics.PointF;
import android.os.Build;
-import android.view.View;
import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.SwipeUpAnimationLogic;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
@@ -36,39 +39,25 @@
}
@Override
- Integer getTitleStringId() {
- switch (mTutorialType) {
- case OVERVIEW_NAVIGATION:
- return R.string.overview_gesture_tutorial_playground_title;
- case OVERVIEW_NAVIGATION_COMPLETE:
- return R.string.gesture_tutorial_confirm_title;
- }
- return null;
+ public Integer getIntroductionTitle() {
+ return R.string.overview_gesture_intro_title;
}
@Override
- Integer getSubtitleStringId() {
- if (mTutorialType == TutorialType.OVERVIEW_NAVIGATION) {
- return R.string.overview_gesture_tutorial_playground_subtitle;
- }
- return null;
+ public Integer getIntroductionSubtitle() {
+ return R.string.overview_gesture_intro_subtitle;
}
@Override
- Integer getActionButtonStringId() {
- if (mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) {
- return R.string.gesture_tutorial_action_button_label_done;
- }
- return null;
- }
-
- @Override
- void onActionButtonClicked(View button) {
- mTutorialFragment.closeTutorial();
+ protected int getMockAppTaskThumbnailResId(boolean forDarkMode) {
+ return R.drawable.mock_conversations_list;
}
@Override
public void onBackGestureAttempted(BackGestureResult result) {
+ if (mGestureCompleted) {
+ return;
+ }
switch (mTutorialType) {
case OVERVIEW_NAVIGATION:
switch (result) {
@@ -91,12 +80,17 @@
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+ if (mGestureCompleted) {
+ return;
+ }
switch (mTutorialType) {
case OVERVIEW_NAVIGATION:
switch (result) {
case HOME_GESTURE_COMPLETED: {
- animateFakeTaskViewHome(finalVelocity, () ->
- showFeedback(R.string.overview_gesture_feedback_home_detected));
+ animateFakeTaskViewHome(finalVelocity, () -> {
+ resetFakeTaskView();
+ showFeedback(R.string.overview_gesture_feedback_home_detected);
+ });
break;
}
case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
@@ -104,12 +98,23 @@
showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge);
break;
case OVERVIEW_GESTURE_COMPLETED:
- fadeOutFakeTaskView(true, () ->
- mTutorialFragment.changeController(OVERVIEW_NAVIGATION_COMPLETE));
+ mTutorialFragment.releaseGestureVideoView();
+ PendingAnimation anim = new PendingAnimation(300);
+ anim.setFloat(mTaskViewSwipeUpAnimation
+ .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
+ AnimatorSet animset = anim.buildAnim();
+ animset.start();
+ mRunningWindowAnim = SwipeUpAnimationLogic.RunningWindowAnim.wrap(animset);
+ onMotionPaused(true /*arbitrary value*/);
+ int subtitleResId = mTutorialFragment.getNumSteps() > 1
+ && mTutorialFragment.isAtFinalStep()
+ ? R.string.overview_gesture_feedback_complete_with_follow_up
+ : R.string.overview_gesture_feedback_complete_without_follow_up;
+ showFeedback(subtitleResId, true);
break;
case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
case HOME_OR_OVERVIEW_CANCELLED:
- fadeOutFakeTaskView(false, null);
+ fadeOutFakeTaskView(false, true, null);
showFeedback(R.string.overview_gesture_feedback_wrong_swipe_direction);
break;
}
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
index 3357b70..d2ec327 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
@@ -15,14 +15,25 @@
*/
package com.android.quickstep.interaction;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.R;
import com.android.quickstep.interaction.TutorialController.TutorialType;
/** Shows the Overview gesture interactive tutorial. */
public class OverviewGestureTutorialFragment extends TutorialFragment {
+ @Nullable
@Override
- int getHandAnimationResId() {
- return R.drawable.overview_gesture;
+ Integer getFeedbackVideoResId(boolean forDarkMode) {
+ return forDarkMode
+ ? R.drawable.gesture_tutorial_motion_overview_dark_mode
+ : R.drawable.gesture_tutorial_motion_overview_light_mode;
+ }
+
+ @Nullable
+ @Override
+ Integer getGestureVideoResId() {
+ return R.drawable.gesture_tutorial_loop_overview;
}
@Override
diff --git a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java
new file mode 100644
index 0000000..db1afc2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.RelativeLayout;
+
+import androidx.fragment.app.FragmentManager;
+
+/** Root layout that TutorialFragment uses to intercept motion events. */
+public class RootSandboxLayout extends RelativeLayout {
+ public RootSandboxLayout(Context context) {
+ super(context);
+ }
+
+ public RootSandboxLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public RootSandboxLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent motionEvent) {
+ return ((TutorialFragment) FragmentManager.findFragment(this))
+ .onInterceptTouch(motionEvent);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
new file mode 100644
index 0000000..19b7933
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.graphics.PointF;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
+
+/** A {@link TutorialController} for the Sandbox Mode. */
+public class SandboxModeTutorialController extends SwipeUpGestureTutorialController {
+
+ SandboxModeTutorialController(SandboxModeTutorialFragment fragment, TutorialType tutorialType) {
+ super(fragment, tutorialType);
+ }
+
+ @Override
+ public void onBackGestureAttempted(BackGestureResult result) {
+ switch (result) {
+ case BACK_COMPLETED_FROM_LEFT:
+ case BACK_COMPLETED_FROM_RIGHT:
+ showRippleEffect(null);
+ showFeedback(R.string.sandbox_mode_back_gesture_feedback_successful);
+ break;
+ case BACK_CANCELLED_FROM_LEFT:
+ case BACK_CANCELLED_FROM_RIGHT:
+ showFeedback(R.string.back_gesture_feedback_cancelled);
+ break;
+ case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
+ showFeedback(R.string.sandbox_mode_back_gesture_feedback_swipe_too_far_from_edge);
+ break;
+ }
+ }
+
+ @Override
+ public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+ switch (result) {
+ case ASSISTANT_COMPLETED:
+ showRippleEffect(null);
+ showFeedback(R.string.sandbox_mode_assistant_gesture_feedback_successful);
+ break;
+ case HOME_GESTURE_COMPLETED:
+ animateFakeTaskViewHome(finalVelocity, () -> {
+ showFeedback(R.string.sandbox_mode_home_gesture_feedback_successful);
+ });
+ break;
+ case OVERVIEW_GESTURE_COMPLETED:
+ fadeOutFakeTaskView(true, true, () -> {
+ showFeedback(R.string.sandbox_mode_overview_gesture_feedback_successful);
+ });
+ break;
+ case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
+ case HOME_OR_OVERVIEW_CANCELLED:
+ case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
+ case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
+ showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge);
+ break;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialFragment.java
new file mode 100644
index 0000000..955a2f7
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialFragment.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+/** Shows the general navigation gesture sandbox environment. */
+public class SandboxModeTutorialFragment extends TutorialFragment {
+
+ @Override
+ TutorialController createController(TutorialType type) {
+ return new SandboxModeTutorialController(this, type);
+ }
+
+ @Override
+ Class<? extends TutorialController> getControllerClass() {
+ return SandboxModeTutorialController.class;
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) {
+ mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY());
+ }
+ return super.onTouch(view, motionEvent);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index 14e00dc..b2183d6 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -16,8 +16,9 @@
package com.android.quickstep.interaction;
import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.MAX_SWIPE_DURATION;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+import static com.android.quickstep.AbsSwipeUpHandler.MAX_SWIPE_DURATION;
import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
@@ -26,17 +27,15 @@
import android.animation.AnimatorSet;
import android.annotation.TargetApi;
import android.content.Context;
-import android.graphics.Insets;
import android.graphics.Outline;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
+import android.util.DisplayMetrics;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewOutlineProvider;
-import android.view.WindowInsets;
-import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -44,7 +43,7 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.quickstep.AnimatedFloat;
@@ -53,23 +52,52 @@
import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.SwipeUpAnimationLogic;
import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim;
+import com.android.quickstep.util.AppCloseConfig;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.TransformParams;
-import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
@TargetApi(Build.VERSION_CODES.R)
abstract class SwipeUpGestureTutorialController extends TutorialController {
- private final ViewSwipeUpAnimation mViewSwipeUpAnimation;
+
+ private static final int FAKE_PREVIOUS_TASK_MARGIN = Utilities.dpToPx(12);
+
+ final ViewSwipeUpAnimation mTaskViewSwipeUpAnimation;
private float mFakeTaskViewRadius;
private Rect mFakeTaskViewRect = new Rect();
- private RunningWindowAnim mRunningWindowAnim;
+ RunningWindowAnim mRunningWindowAnim;
+ private boolean mShowTasks = false;
+ private boolean mShowPreviousTasks = false;
+
+ private AnimatorListenerAdapter mResetTaskView = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mFakeHotseatView.setVisibility(View.INVISIBLE);
+ mFakeIconView.setVisibility(View.INVISIBLE);
+ if (mTutorialFragment.getActivity() != null) {
+ DisplayMetrics displayMetrics =
+ mTutorialFragment.getResources().getDisplayMetrics();
+ int height = displayMetrics.heightPixels;
+ int width = displayMetrics.widthPixels;
+ mFakeTaskViewRect.set(0, 0, width, height);
+ }
+ mFakeTaskViewRadius = 0;
+ mFakeTaskView.invalidateOutline();
+ mFakeTaskView.setVisibility(View.VISIBLE);
+ mFakeTaskView.setAlpha(1);
+ mFakePreviousTaskView.setVisibility(View.INVISIBLE);
+ mFakePreviousTaskView.setAlpha(1);
+ mShowTasks = false;
+ mShowPreviousTasks = false;
+ mRunningWindowAnim = null;
+ }
+ };
SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
super(tutorialFragment, tutorialType);
RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext);
OverviewComponentObserver observer = new OverviewComponentObserver(mContext, deviceState);
- mViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
+ mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
new GestureState(observer, -1));
observer.onDestroy();
deviceState.destroy();
@@ -77,21 +105,26 @@
DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext)
.getDeviceProfile(mContext)
.copy(mContext);
- Insets insets = mContext.getSystemService(WindowManager.class)
- .getCurrentWindowMetrics()
- .getWindowInsets()
- .getInsets(WindowInsets.Type.systemBars());
- dp.updateInsets(new Rect(insets.left, insets.top, insets.right, insets.bottom));
- mViewSwipeUpAnimation.initDp(dp);
+ mTaskViewSwipeUpAnimation.initDp(dp);
- mFakeTaskViewRadius = QuickStepContract.getWindowCornerRadius(mContext.getResources());
- mFakeTaskView.setClipToOutline(true);
- mFakeTaskView.setOutlineProvider(new ViewOutlineProvider() {
+ DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
+ int height = displayMetrics.heightPixels;
+ int width = displayMetrics.widthPixels;
+ mFakeTaskViewRect.set(0, 0, width, height);
+ mFakeTaskViewRadius = 0;
+
+ ViewOutlineProvider outlineProvider = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(mFakeTaskViewRect, mFakeTaskViewRadius);
}
- });
+ };
+
+ mFakeTaskView.setClipToOutline(true);
+ mFakeTaskView.setOutlineProvider(outlineProvider);
+
+ mFakePreviousTaskView.setClipToOutline(true);
+ mFakePreviousTaskView.setOutlineProvider(outlineProvider);
}
private void cancelRunningAnimation() {
@@ -102,27 +135,29 @@
}
/** Fades the task view, optionally after animating to a fake Overview. */
- void fadeOutFakeTaskView(boolean toOverviewFirst, @Nullable Runnable onEndRunnable) {
- hideFeedback();
- hideHandCoachingAnimation();
+ void fadeOutFakeTaskView(boolean toOverviewFirst, boolean reset,
+ @Nullable Runnable onEndRunnable) {
+ hideFeedback(true);
cancelRunningAnimation();
PendingAnimation anim = new PendingAnimation(300);
- AnimatorListenerAdapter resetTaskView = new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation, boolean isReverse) {
- mFakeTaskView.setVisibility(View.INVISIBLE);
- mFakeTaskView.setAlpha(1);
- mRunningWindowAnim = null;
- }
- };
if (toOverviewFirst) {
- anim.setFloat(mViewSwipeUpAnimation.getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
+ anim.setFloat(mTaskViewSwipeUpAnimation
+ .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
PendingAnimation fadeAnim = new PendingAnimation(300);
- fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL);
- fadeAnim.addListener(resetTaskView);
+ if (reset) {
+ fadeAnim.setFloat(mTaskViewSwipeUpAnimation
+ .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
+ fadeAnim.addListener(mResetTaskView);
+ } else {
+ fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+ fadeAnim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
+ }
+ if (onEndRunnable != null) {
+ fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable));
+ }
AnimatorSet animset = fadeAnim.buildAnim();
animset.setStartDelay(100);
animset.start();
@@ -130,39 +165,92 @@
}
});
} else {
- anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
- anim.addListener(resetTaskView);
- }
- if (onEndRunnable != null) {
- anim.addListener(AnimationSuccessListener.forRunnable(onEndRunnable));
+ if (reset) {
+ anim.setFloat(mTaskViewSwipeUpAnimation
+ .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
+ anim.addListener(mResetTaskView);
+ } else {
+ anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+ anim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
+ }
+ if (onEndRunnable != null) {
+ anim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable));
+ }
}
AnimatorSet animset = anim.buildAnim();
animset.start();
mRunningWindowAnim = RunningWindowAnim.wrap(animset);
}
+ void resetFakeTaskView() {
+ PendingAnimation anim = new PendingAnimation(300);
+ anim.setFloat(mTaskViewSwipeUpAnimation
+ .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
+ anim.setViewAlpha(mFakeTaskView, 1, ACCEL);
+ anim.addListener(mResetTaskView);
+ AnimatorSet animset = anim.buildAnim();
+ animset.start();
+ mRunningWindowAnim = RunningWindowAnim.wrap(animset);
+ }
+
void animateFakeTaskViewHome(PointF finalVelocity, @Nullable Runnable onEndRunnable) {
- hideFeedback();
- hideHandCoachingAnimation();
+ hideFeedback(true);
cancelRunningAnimation();
+ mFakePreviousTaskView.setVisibility(View.INVISIBLE);
+ mFakeHotseatView.setVisibility(View.VISIBLE);
+ mShowPreviousTasks = false;
RectFSpringAnim rectAnim =
- mViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
+ mTaskViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
// After home animation finishes, fade out and run onEndRunnable.
- rectAnim.addAnimatorListener(AnimationSuccessListener.forRunnable(
- () -> fadeOutFakeTaskView(false, onEndRunnable)));
+ PendingAnimation fadeAnim = new PendingAnimation(300);
+ fadeAnim.setViewAlpha(mFakeIconView, 0, ACCEL);
+ if (onEndRunnable != null) {
+ fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable));
+ }
+ AnimatorSet animset = fadeAnim.buildAnim();
+ rectAnim.addAnimatorListener(AnimatorListeners.forSuccessCallback(animset::start));
mRunningWindowAnim = RunningWindowAnim.wrap(rectAnim);
}
@Override
public void setNavBarGestureProgress(@Nullable Float displacement) {
- if (displacement == null || mTutorialType == HOME_NAVIGATION_COMPLETE
+ if (mGestureCompleted) {
+ return;
+ }
+ if (displacement != null) {
+ hideFeedback(true);
+ }
+ if (mTutorialType == HOME_NAVIGATION_COMPLETE
|| mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) {
mFakeTaskView.setVisibility(View.INVISIBLE);
+ mFakePreviousTaskView.setVisibility(View.INVISIBLE);
} else {
+ mShowTasks = true;
mFakeTaskView.setVisibility(View.VISIBLE);
- if (mRunningWindowAnim == null) {
- mViewSwipeUpAnimation.updateDisplacement(displacement);
+ if (mShowPreviousTasks) {
+ mFakePreviousTaskView.setVisibility(View.VISIBLE);
}
+ if (mRunningWindowAnim == null && displacement != null) {
+ mTaskViewSwipeUpAnimation.updateDisplacement(displacement);
+ }
+ }
+ }
+
+ @Override
+ public void onMotionPaused(boolean unused) {
+ if (mGestureCompleted) {
+ return;
+ }
+ if (mShowTasks) {
+ if (!mShowPreviousTasks) {
+ mFakePreviousTaskView.setTranslationX(
+ -(2 * mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN));
+ mFakePreviousTaskView.animate()
+ .setDuration(300)
+ .translationX(-(mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN))
+ .start();
+ }
+ mShowPreviousTasks = true;
}
}
@@ -181,8 +269,7 @@
@Override
public void updateFinalShift() {
- float progress = mCurrentShift.value / mDragLengthFactor;
- mWindowTransitionController.setPlayFraction(progress);
+ mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
mTaskViewSimulator.apply(mTransformParams);
}
@@ -202,7 +289,7 @@
// derivative of the scroll interpolator at zero, ie. 2.
long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
long duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
- HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory(null) {
+ HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory() {
@Override
public AnimatorPlaybackController createActivityAnimationToHome() {
return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
@@ -211,13 +298,33 @@
@NonNull
@Override
public RectF getWindowTargetRect() {
- int fakeHomeIconSizePx = mDp.allAppsIconSizePx;
- int fakeHomeIconLeft = (mDp.widthPx - fakeHomeIconSizePx) / 2;
- int fakeHomeIconTop = mDp.heightPx - (mDp.allAppsCellHeightPx * 3);
+ int fakeHomeIconSizePx = Utilities.dpToPx(60);
+ int fakeHomeIconLeft = mFakeHotseatView.getLeft();
+ int fakeHomeIconTop = mDp.heightPx - Utilities.dpToPx(216);
return new RectF(fakeHomeIconLeft, fakeHomeIconTop,
fakeHomeIconLeft + fakeHomeIconSizePx,
fakeHomeIconTop + fakeHomeIconSizePx);
}
+
+ @Override
+ public void update(@Nullable AppCloseConfig config, RectF rect, float progress,
+ float radius) {
+ mFakeIconView.setVisibility(View.VISIBLE);
+ mFakeIconView.update(rect, progress,
+ 1f - SHAPE_PROGRESS_DURATION /* shapeProgressStart */,
+ radius, 255,
+ false, /* isOpening */
+ mFakeIconView, mDp,
+ false /* isVerticalBarLayout */);
+ mFakeIconView.setAlpha(1);
+ mFakeTaskView.setAlpha(getWindowAlpha(progress));
+ mFakePreviousTaskView.setAlpha(getWindowAlpha(progress));
+ }
+
+ @Override
+ public void onCancel() {
+ mFakeIconView.setVisibility(View.INVISIBLE);
+ }
};
RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift, homeAnimFactory);
windowAnim.start(mContext, velocityPxPerMs);
@@ -238,9 +345,11 @@
public void applySurfaceParams(SurfaceParams[] params) {
SurfaceParams p = params[0];
mFakeTaskView.setAnimationMatrix(p.matrix);
+ mFakePreviousTaskView.setAnimationMatrix(p.matrix);
mFakeTaskViewRect.set(p.windowCrop);
mFakeTaskViewRadius = p.cornerRadius;
mFakeTaskView.invalidateOutline();
+ mFakePreviousTaskView.invalidateOutline();
}
}
}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index c1918c2..4b4e7e6 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -15,109 +15,328 @@
*/
package com.android.quickstep.interaction;
+import static android.view.View.GONE;
+
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
+import android.util.Log;
import android.view.View;
-import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
import android.widget.Button;
-import android.widget.ImageButton;
import android.widget.ImageView;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.CallSuper;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.content.res.AppCompatResources;
import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.views.ClipIconView;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
abstract class TutorialController implements BackGestureAttemptCallback,
NavBarGestureAttemptCallback {
- private static final int FEEDBACK_VISIBLE_MS = 3000;
- private static final int FEEDBACK_ANIMATION_MS = 500;
+ private static final String TAG = "TutorialController";
+
+ private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips";
+ private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips";
+
+ private static final int FEEDBACK_ANIMATION_MS = 250;
private static final int RIPPLE_VISIBLE_MS = 300;
+ private static final int GESTURE_ANIMATION_DELAY_MS = 1500;
+ private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 4000;
final TutorialFragment mTutorialFragment;
TutorialType mTutorialType;
final Context mContext;
- final ImageButton mCloseButton;
- final TextView mTitleTextView;
- final TextView mSubtitleTextView;
- final TextView mFeedbackView;
+ final TextView mCloseButton;
+ final ViewGroup mFeedbackView;
+ final TextView mFeedbackTitleView;
+ final ImageView mFeedbackVideoView;
+ final ImageView mGestureVideoView;
+ final RelativeLayout mFakeLauncherView;
+ final ImageView mFakeHotseatView;
+ final ClipIconView mFakeIconView;
final View mFakeTaskView;
+ final View mFakePreviousTaskView;
final View mRippleView;
final RippleDrawable mRippleDrawable;
- final TutorialHandAnimation mHandCoachingAnimation;
- final ImageView mHandCoachingView;
- final Button mActionTextButton;
final Button mActionButton;
- private final Runnable mHideFeedbackRunnable;
+ final TutorialStepIndicator mTutorialStepView;
+ private final AlertDialog mSkipTutorialDialog;
+
+ protected boolean mGestureCompleted = false;
+
+ // These runnables should be used when posting callbacks to their views and cleared from their
+ // views before posting new callbacks.
+ private final Runnable mTitleViewCallback;
+ @Nullable private Runnable mFeedbackViewCallback;
+ @Nullable private Runnable mFeedbackVideoViewCallback;
TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
mTutorialFragment = tutorialFragment;
mTutorialType = tutorialType;
mContext = mTutorialFragment.getContext();
- View rootView = tutorialFragment.getRootView();
+ RootSandboxLayout rootView = tutorialFragment.getRootView();
mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
- mCloseButton.setOnClickListener(button -> mTutorialFragment.closeTutorial());
- mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
- mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
+ mCloseButton.setOnClickListener(button -> showSkipTutorialDialog());
mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
+ mFeedbackTitleView = mFeedbackView.findViewById(
+ R.id.gesture_tutorial_fragment_feedback_title);
+ mFeedbackVideoView = rootView.findViewById(R.id.gesture_tutorial_feedback_video);
+ mGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_gesture_video);
+ mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view);
+ mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view);
+ mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
+ mFakePreviousTaskView =
+ rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view);
mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
- mHandCoachingAnimation = tutorialFragment.getHandAnimation();
- mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
- mHandCoachingView.bringToFront();
- mActionTextButton =
- rootView.findViewById(R.id.gesture_tutorial_fragment_action_text_button);
mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
+ mTutorialStepView =
+ rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step);
+ mSkipTutorialDialog = createSkipTutorialDialog();
- mHideFeedbackRunnable =
- () -> mFeedbackView.animate().alpha(0).setDuration(FEEDBACK_ANIMATION_MS)
- .withEndAction(this::showHandCoachingAnimation).start();
+ mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }
+
+ private void showSkipTutorialDialog() {
+ if (mSkipTutorialDialog != null) {
+ mSkipTutorialDialog.show();
+ }
}
void setTutorialType(TutorialType tutorialType) {
mTutorialType = tutorialType;
}
- @Nullable
- Integer getTitleStringId() {
+ @DrawableRes
+ protected int getMockHotseatResId() {
+ return R.drawable.default_sandbox_mock_launcher;
+ }
+
+ @DrawableRes
+ protected int getMockAppTaskThumbnailResId(boolean forDarkMode) {
+ return R.drawable.default_sandbox_app_task_thumbnail;
+ }
+
+ @DrawableRes
+ protected int getMockPreviousAppTaskThumbnailResId() {
+ return R.drawable.default_sandbox_app_previous_task_thumbnail;
+ }
+
+ @DrawableRes
+ public int getMockAppIconResId() {
+ return R.drawable.default_sandbox_app_icon;
+ }
+
+ @DrawableRes
+ public int getMockWallpaperResId() {
+ return R.drawable.default_sandbox_wallpaper;
+ }
+
+ void fadeTaskViewAndRun(Runnable r) {
+ mFakeTaskView.animate().alpha(0).setListener(AnimatorListeners.forSuccessCallback(r));
+ }
+
+ @StringRes
+ public Integer getIntroductionTitle() {
return null;
}
- @Nullable
- Integer getSubtitleStringId() {
+ @StringRes
+ public Integer getIntroductionSubtitle() {
return null;
}
- @Nullable
- Integer getActionButtonStringId() {
- return null;
+ void showFeedback() {
+ if (mGestureCompleted) {
+ mFeedbackView.setTranslationY(0);
+ return;
+ }
+ AnimatedVectorDrawable tutorialAnimation = mTutorialFragment.getTutorialAnimation();
+ AnimatedVectorDrawable gestureAnimation = mTutorialFragment.getGestureAnimation();
+
+ if (tutorialAnimation != null && gestureAnimation != null) {
+ TextView title = mFeedbackView.findViewById(
+ R.id.gesture_tutorial_fragment_feedback_title);
+
+ playFeedbackVideo(tutorialAnimation, gestureAnimation, () -> {
+ mFeedbackView.setTranslationY(0);
+ title.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }, true);
+ }
}
- @Nullable
- Integer getActionTextButtonStringId() {
- return null;
+ /**
+ * Show feedback reflecting a failed gesture attempt.
+ *
+ * @param subtitleResId Resource of the text to display.
+ **/
+ void showFeedback(int subtitleResId) {
+ showFeedback(subtitleResId, false);
}
- void showFeedback(int resId) {
- hideHandCoachingAnimation();
- mFeedbackView.setText(resId);
- mFeedbackView.animate().alpha(1).setDuration(FEEDBACK_ANIMATION_MS).start();
- mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
- mFeedbackView.postDelayed(mHideFeedbackRunnable, FEEDBACK_VISIBLE_MS);
+ /**
+ * Show feedback reflecting the result of a gesture attempt.
+ *
+ * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown.
+ **/
+ void showFeedback(int subtitleResId, boolean isGestureSuccessful) {
+ showFeedback(
+ isGestureSuccessful
+ ? R.string.gesture_tutorial_nice : R.string.gesture_tutorial_try_again,
+ subtitleResId,
+ isGestureSuccessful,
+ false);
}
- void hideFeedback() {
- mFeedbackView.setText(null);
- mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
+ void showFeedback(
+ int titleResId,
+ int subtitleResId,
+ boolean isGestureSuccessful,
+ boolean useGestureAnimationDelay) {
+ mFeedbackTitleView.setText(titleResId);
+ mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
+ TextView subtitle =
+ mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_subtitle);
+ subtitle.setText(subtitleResId);
+ if (isGestureSuccessful) {
+ hideCloseButton();
+ if (mTutorialFragment.isAtFinalStep()) {
+ showActionButton();
+ }
+
+ if (mFeedbackVideoViewCallback != null) {
+ mFeedbackVideoView.removeCallbacks(mFeedbackVideoViewCallback);
+ mFeedbackVideoViewCallback = null;
+ }
+ }
+ mGestureCompleted = isGestureSuccessful;
+
+ AnimatedVectorDrawable tutorialAnimation = mTutorialFragment.getTutorialAnimation();
+ AnimatedVectorDrawable gestureAnimation = mTutorialFragment.getGestureAnimation();
+ if (tutorialAnimation != null && gestureAnimation != null) {
+ if (!isGestureSuccessful) {
+ playFeedbackVideo(tutorialAnimation, gestureAnimation, () -> {
+ mFeedbackView.setTranslationY(
+ -mFeedbackView.getHeight() - mFeedbackView.getTop());
+ mFeedbackView.setVisibility(View.VISIBLE);
+ mFeedbackView.animate()
+ .setDuration(FEEDBACK_ANIMATION_MS)
+ .translationY(0)
+ .start();
+ mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS);
+ }, useGestureAnimationDelay);
+ return;
+ } else {
+ mTutorialFragment.releaseFeedbackVideoView();
+ }
+ }
+ mFeedbackView.setTranslationY(-mFeedbackView.getHeight() - mFeedbackView.getTop());
+ mFeedbackView.setVisibility(View.VISIBLE);
+ mFeedbackView.animate()
+ .setDuration(FEEDBACK_ANIMATION_MS)
+ .translationY(0)
+ .withEndAction(() -> {
+ if (isGestureSuccessful && !mTutorialFragment.isAtFinalStep()) {
+ if (mFeedbackViewCallback != null) {
+ mFeedbackView.removeCallbacks(mFeedbackViewCallback);
+ }
+ mFeedbackViewCallback = mTutorialFragment::continueTutorial;
+ mFeedbackView.postDelayed(mFeedbackViewCallback,
+ ADVANCE_TUTORIAL_TIMEOUT_MS);
+ }
+ })
+ .start();
+ mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS);
+ }
+
+ void hideFeedback(boolean releaseFeedbackVideo) {
mFeedbackView.clearAnimation();
- mFeedbackView.setAlpha(0);
+ mFeedbackView.setVisibility(View.INVISIBLE);
+ if (releaseFeedbackVideo) {
+ mTutorialFragment.releaseFeedbackVideoView();
+ }
+ }
+
+ private void playFeedbackVideo(
+ @NonNull AnimatedVectorDrawable tutorialAnimation,
+ @NonNull AnimatedVectorDrawable gestureAnimation,
+ @NonNull Runnable onStartRunnable,
+ boolean useGestureAnimationDelay) {
+
+ if (tutorialAnimation.isRunning()) {
+ tutorialAnimation.reset();
+ }
+ tutorialAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
+
+ @Override
+ public void onAnimationStart(Drawable drawable) {
+ super.onAnimationStart(drawable);
+
+ mGestureVideoView.setVisibility(GONE);
+ if (gestureAnimation.isRunning()) {
+ gestureAnimation.stop();
+ }
+
+ if (!useGestureAnimationDelay) {
+ onStartRunnable.run();
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Drawable drawable) {
+ super.onAnimationEnd(drawable);
+
+ mGestureVideoView.setVisibility(View.VISIBLE);
+ gestureAnimation.start();
+
+ tutorialAnimation.unregisterAnimationCallback(this);
+ }
+ });
+
+ if (mFeedbackViewCallback != null) {
+ mFeedbackVideoView.removeCallbacks(mFeedbackViewCallback);
+ mFeedbackViewCallback = null;
+ }
+ if (mFeedbackVideoViewCallback != null) {
+ mFeedbackVideoView.removeCallbacks(mFeedbackVideoViewCallback);
+ mFeedbackVideoViewCallback = null;
+ }
+ if (useGestureAnimationDelay) {
+ mFeedbackViewCallback = onStartRunnable;
+ mFeedbackVideoViewCallback = () -> {
+ mFeedbackVideoView.setVisibility(View.VISIBLE);
+ tutorialAnimation.start();
+ };
+
+ mFeedbackVideoView.setVisibility(View.GONE);
+ mFeedbackView.post(mFeedbackViewCallback);
+ mFeedbackVideoView.postDelayed(mFeedbackVideoViewCallback, GESTURE_ANIMATION_DELAY_MS);
+ } else {
+ mFeedbackVideoView.setVisibility(View.VISIBLE);
+ tutorialAnimation.start();
+ }
}
void setRippleHotspot(float x, float y) {
@@ -135,87 +354,147 @@
}, RIPPLE_VISIBLE_MS);
}
- void onActionButtonClicked(View button) {}
-
- void onActionTextButtonClicked(View button) {}
-
- void showHandCoachingAnimation() {
- if (isComplete()) {
- return;
- }
- mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
- }
-
- void hideHandCoachingAnimation() {
- mHandCoachingAnimation.stop();
- mHandCoachingView.setVisibility(View.INVISIBLE);
+ void onActionButtonClicked(View button) {
+ mTutorialFragment.continueTutorial();
}
@CallSuper
void transitToController() {
- hideFeedback();
- updateTitles();
- updateActionButtons();
+ hideFeedback(false);
+ hideActionButton();
+ updateSubtext();
+ updateDrawables();
- if (isComplete()) {
- hideHandCoachingAnimation();
- } else {
- showHandCoachingAnimation();
+ mGestureCompleted = false;
+ if (mFakeHotseatView != null) {
+ mFakeHotseatView.setVisibility(View.INVISIBLE);
}
}
- private void updateTitles() {
- updateTitleView(mTitleTextView, getTitleStringId(),
- R.style.TextAppearance_GestureTutorial_Title);
- updateTitleView(mSubtitleTextView, getSubtitleStringId(),
- R.style.TextAppearance_GestureTutorial_Subtitle);
+ void hideCloseButton() {
+ mCloseButton.setVisibility(GONE);
}
- private void updateTitleView(TextView textView, @Nullable Integer stringId, int styleId) {
- if (stringId == null) {
- textView.setVisibility(View.GONE);
- return;
+ void showCloseButton() {
+ mCloseButton.setVisibility(View.VISIBLE);
+ mCloseButton.setTextAppearance(Utilities.isDarkTheme(mContext)
+ ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext
+ : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark);
+ }
+
+ void hideActionButton() {
+ showCloseButton();
+ // Invisible to maintain the layout.
+ mActionButton.setVisibility(View.INVISIBLE);
+ mActionButton.setOnClickListener(null);
+ }
+
+ void showActionButton() {
+ hideCloseButton();
+ mActionButton.setVisibility(View.VISIBLE);
+ mActionButton.setOnClickListener(this::onActionButtonClicked);
+ }
+
+ private void updateSubtext() {
+ mTutorialStepView.setTutorialProgress(
+ mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps());
+ }
+
+ private void updateDrawables() {
+ if (mContext != null) {
+ mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
+ mContext, getMockWallpaperResId()));
+ mTutorialFragment.updateFeedbackVideo();
+ mFakeLauncherView.setBackgroundColor(
+ mContext.getColor(Utilities.isDarkTheme(mContext)
+ ? R.color.fake_wallpaper_color_dark_mode
+ : R.color.fake_wallpaper_color_light_mode));
+ mFakeHotseatView.setImageDrawable(AppCompatResources.getDrawable(
+ mContext, getMockHotseatResId()));
+ mFakeTaskView.setBackground(AppCompatResources.getDrawable(
+ mContext, getMockAppTaskThumbnailResId(Utilities.isDarkTheme(mContext))));
+ mFakeTaskView.animate().alpha(1).setListener(
+ AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
+ mFakePreviousTaskView.setBackground(AppCompatResources.getDrawable(
+ mContext, getMockPreviousAppTaskThumbnailResId()));
+ mFakeIconView.setBackground(AppCompatResources.getDrawable(
+ mContext, getMockAppIconResId()));
+ }
+ }
+
+ private AlertDialog createSkipTutorialDialog() {
+ if (mContext instanceof GestureSandboxActivity) {
+ GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext;
+ View contentView = View.inflate(
+ sandboxActivity, R.layout.gesture_tutorial_dialog, null);
+ AlertDialog tutorialDialog = new AlertDialog
+ .Builder(sandboxActivity, R.style.Theme_AppCompat_Dialog_Alert)
+ .setView(contentView)
+ .create();
+
+ PackageManager packageManager = mContext.getPackageManager();
+ CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME;
+
+ try {
+ tipsAppName = packageManager.getApplicationLabel(
+ packageManager.getApplicationInfo(
+ PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG,
+ "Could not find app label for package name: "
+ + PIXEL_TIPS_APP_PACKAGE_NAME
+ + ". Defaulting to 'Pixel Tips.'",
+ e);
+ }
+
+ TextView subtitleTextView = (TextView) contentView.findViewById(
+ R.id.gesture_tutorial_dialog_subtitle);
+ if (subtitleTextView != null) {
+ subtitleTextView.setText(
+ mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName));
+ } else {
+ Log.w(TAG, "No subtitle view in the skip tutorial dialog to update.");
+ }
+
+ Button cancelButton = (Button) contentView.findViewById(
+ R.id.gesture_tutorial_dialog_cancel_button);
+ if (cancelButton != null) {
+ cancelButton.setOnClickListener(
+ v -> tutorialDialog.dismiss());
+ } else {
+ Log.w(TAG, "No cancel button in the skip tutorial dialog to update.");
+ }
+
+ Button confirmButton = contentView.findViewById(
+ R.id.gesture_tutorial_dialog_confirm_button);
+ if (confirmButton != null) {
+ confirmButton.setOnClickListener(v -> {
+ sandboxActivity.closeTutorial();
+ tutorialDialog.dismiss();
+ });
+ } else {
+ Log.w(TAG, "No confirm button in the skip tutorial dialog to update.");
+ }
+
+ tutorialDialog.getWindow().setBackgroundDrawable(
+ new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent)));
+
+ return tutorialDialog;
}
- textView.setVisibility(View.VISIBLE);
- textView.setText(stringId);
- textView.setTextAppearance(styleId);
- }
-
- private void updateActionButtons() {
- updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
- updateButton(
- mActionTextButton, getActionTextButtonStringId(), this::onActionTextButtonClicked);
- }
-
- private void updateButton(Button button, @Nullable Integer stringId, OnClickListener listener) {
- if (stringId == null) {
- button.setVisibility(View.INVISIBLE);
- return;
- }
-
- button.setVisibility(View.VISIBLE);
- button.setText(stringId);
- button.setOnClickListener(listener);
- }
-
- private boolean isComplete() {
- return mTutorialType == TutorialType.BACK_NAVIGATION_COMPLETE
- || mTutorialType == TutorialType.HOME_NAVIGATION_COMPLETE
- || mTutorialType == TutorialType.OVERVIEW_NAVIGATION_COMPLETE
- || mTutorialType == TutorialType.ASSISTANT_COMPLETE;
+ return null;
}
/** Denotes the type of the tutorial. */
enum TutorialType {
- RIGHT_EDGE_BACK_NAVIGATION,
- LEFT_EDGE_BACK_NAVIGATION,
+ BACK_NAVIGATION,
BACK_NAVIGATION_COMPLETE,
HOME_NAVIGATION,
HOME_NAVIGATION_COMPLETE,
OVERVIEW_NAVIGATION,
OVERVIEW_NAVIGATION_COMPLETE,
ASSISTANT,
- ASSISTANT_COMPLETE
+ ASSISTANT_COMPLETE,
+ SANDBOX_MODE
}
}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 9a8264d..7637450 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -15,8 +15,13 @@
*/
package com.android.quickstep.interaction;
+import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.graphics.Insets;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -25,6 +30,7 @@
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.WindowInsets;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,6 +38,7 @@
import androidx.fragment.app.FragmentActivity;
import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
import com.android.quickstep.interaction.TutorialController.TutorialType;
abstract class TutorialFragment extends Fragment implements OnTouchListener {
@@ -41,17 +48,25 @@
TutorialType mTutorialType;
@Nullable TutorialController mTutorialController = null;
- View mRootView;
- TutorialHandAnimation mHandCoachingAnimation;
+ RootSandboxLayout mRootView;
EdgeBackGestureHandler mEdgeBackGestureHandler;
NavBarGestureHandler mNavBarGestureHandler;
+ private ImageView mFeedbackVideoView;
+ private ImageView mGestureVideoView;
+
+ @Nullable private AnimatedVectorDrawable mTutorialAnimation = null;
+ @Nullable private AnimatedVectorDrawable mGestureAnimation = null;
+ private boolean mIntroductionShown = false;
+
+ private boolean mFragmentStopped = false;
public static TutorialFragment newInstance(TutorialType tutorialType) {
TutorialFragment fragment = getFragmentForTutorialType(tutorialType);
if (fragment == null) {
fragment = new BackGestureTutorialFragment();
- tutorialType = TutorialType.RIGHT_EDGE_BACK_NAVIGATION;
+ tutorialType = TutorialType.BACK_NAVIGATION;
}
+
Bundle args = new Bundle();
args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
fragment.setArguments(args);
@@ -61,8 +76,7 @@
@Nullable
private static TutorialFragment getFragmentForTutorialType(TutorialType tutorialType) {
switch (tutorialType) {
- case RIGHT_EDGE_BACK_NAVIGATION:
- case LEFT_EDGE_BACK_NAVIGATION:
+ case BACK_NAVIGATION:
case BACK_NAVIGATION_COMPLETE:
return new BackGestureTutorialFragment();
case HOME_NAVIGATION:
@@ -74,13 +88,31 @@
case ASSISTANT:
case ASSISTANT_COMPLETE:
return new AssistantGestureTutorialFragment();
+ case SANDBOX_MODE:
+ return new SandboxModeTutorialFragment();
default:
Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name());
}
return null;
}
- abstract int getHandAnimationResId();
+ @Nullable Integer getFeedbackVideoResId(boolean forDarkMode) {
+ return null;
+ }
+
+ @Nullable Integer getGestureVideoResId() {
+ return null;
+ }
+
+ @Nullable
+ AnimatedVectorDrawable getTutorialAnimation() {
+ return mTutorialAnimation;
+ }
+
+ @Nullable
+ AnimatedVectorDrawable getGestureAnimation() {
+ return mGestureAnimation;
+ }
abstract TutorialController createController(TutorialType type);
@@ -107,39 +139,142 @@
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
- mRootView = inflater.inflate(R.layout.gesture_tutorial_fragment, container, false);
+ mRootView = (RootSandboxLayout) inflater.inflate(
+ R.layout.gesture_tutorial_fragment, container, false);
mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
return insets;
});
mRootView.setOnTouchListener(this);
- mHandCoachingAnimation = new TutorialHandAnimation(getContext(), mRootView,
- getHandAnimationResId());
+ mFeedbackVideoView = mRootView.findViewById(R.id.gesture_tutorial_feedback_video);
+ mGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_gesture_video);
return mRootView;
}
@Override
+ public void onStop() {
+ super.onStop();
+ releaseFeedbackVideoView();
+ releaseGestureVideoView();
+ mFragmentStopped = true;
+ }
+
+ void initializeFeedbackVideoView() {
+ if (!updateFeedbackVideo()) {
+ return;
+ }
+
+ if (!mIntroductionShown && mTutorialController != null) {
+ Integer introTileStringResId = mTutorialController.getIntroductionTitle();
+ Integer introSubtitleResId = mTutorialController.getIntroductionSubtitle();
+ if (introTileStringResId != null && introSubtitleResId != null) {
+ mTutorialController.showFeedback(
+ introTileStringResId, introSubtitleResId, false, true);
+ mIntroductionShown = true;
+ }
+ }
+ }
+
+ boolean updateFeedbackVideo() {
+ if (getContext() == null) {
+ return false;
+ }
+ Integer feedbackVideoResId = getFeedbackVideoResId(Utilities.isDarkTheme(getContext()));
+
+ if (feedbackVideoResId == null || !updateGestureVideo()) {
+ return false;
+ }
+ mTutorialAnimation = (AnimatedVectorDrawable) getContext().getDrawable(feedbackVideoResId);
+
+ if (mTutorialAnimation != null) {
+ mTutorialAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
+
+ @Override
+ public void onAnimationStart(Drawable drawable) {
+ super.onAnimationStart(drawable);
+
+ mFeedbackVideoView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Drawable drawable) {
+ super.onAnimationEnd(drawable);
+
+ releaseFeedbackVideoView();
+ }
+ });
+ }
+ mFeedbackVideoView.setImageDrawable(mTutorialAnimation);
+
+ return true;
+ }
+
+ boolean updateGestureVideo() {
+ Integer gestureVideoResId = getGestureVideoResId();
+ if (gestureVideoResId == null || getContext() == null) {
+ return false;
+ }
+ mGestureAnimation = (AnimatedVectorDrawable) getContext().getDrawable(gestureVideoResId);
+
+ if (mGestureAnimation != null) {
+ mGestureAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
+
+ @Override
+ public void onAnimationEnd(Drawable drawable) {
+ super.onAnimationEnd(drawable);
+
+ mGestureAnimation.start();
+ }
+ });
+ }
+ mGestureVideoView.setImageDrawable(mGestureAnimation);
+
+ return true;
+ }
+
+ void releaseFeedbackVideoView() {
+ if (mTutorialAnimation != null && mTutorialAnimation.isRunning()) {
+ mTutorialAnimation.stop();
+ }
+
+ mFeedbackVideoView.setVisibility(View.GONE);
+ }
+
+ void releaseGestureVideoView() {
+ if (mGestureAnimation != null && mGestureAnimation.isRunning()) {
+ mGestureAnimation.stop();
+ }
+
+ mGestureVideoView.setVisibility(View.GONE);
+ }
+
+ @Override
public void onResume() {
super.onResume();
- changeController(mTutorialType);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- mHandCoachingAnimation.stop();
+ if (mFragmentStopped && mTutorialController != null) {
+ mTutorialController.showFeedback();
+ mFragmentStopped = false;
+ } else {
+ changeController(mTutorialType);
+ }
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
- // Note: Using logical or to ensure both functions get called.
+ // Note: Using logical-or to ensure both functions get called.
return mEdgeBackGestureHandler.onTouch(view, motionEvent)
| mNavBarGestureHandler.onTouch(view, motionEvent);
}
+ boolean onInterceptTouch(MotionEvent motionEvent) {
+ // Note: Using logical-or to ensure both functions get called.
+ return mEdgeBackGestureHandler.onInterceptTouch(motionEvent)
+ | mNavBarGestureHandler.onInterceptTouch(motionEvent);
+ }
+
void onAttachedToWindow() {
- mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
+ mEdgeBackGestureHandler.setViewGroupParent(getRootView());
}
void onDetachedFromWindow() {
@@ -149,13 +284,15 @@
void changeController(TutorialType tutorialType) {
if (getControllerClass().isInstance(mTutorialController)) {
mTutorialController.setTutorialType(tutorialType);
+ mTutorialController.fadeTaskViewAndRun(mTutorialController::transitToController);
} else {
mTutorialController = createController(tutorialType);
+ mTutorialController.transitToController();
}
- mTutorialController.transitToController();
mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController);
mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController);
mTutorialType = tutorialType;
+ initializeFeedbackVideoView();
}
@Override
@@ -164,17 +301,24 @@
super.onSaveInstanceState(savedInstanceState);
}
- View getRootView() {
+ RootSandboxLayout getRootView() {
return mRootView;
}
- TutorialHandAnimation getHandAnimation() {
- return mHandCoachingAnimation;
+ void continueTutorial() {
+ GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
+
+ if (gestureSandboxActivity == null) {
+ closeTutorial();
+ return;
+ }
+ gestureSandboxActivity.continueTutorial();
}
void closeTutorial() {
FragmentActivity activity = getActivity();
if (activity != null) {
+ activity.setResult(Activity.RESULT_OK);
activity.finish();
}
}
@@ -182,4 +326,27 @@
void startSystemNavigationSetting() {
startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS"));
}
+
+ int getCurrentStep() {
+ GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
+
+ return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getCurrentStep();
+ }
+
+ int getNumSteps() {
+ GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
+
+ return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getNumSteps();
+ }
+
+ boolean isAtFinalStep() {
+ return getCurrentStep() == getNumSteps();
+ }
+
+ @Nullable
+ private GestureSandboxActivity getGestureSandboxActivity() {
+ Context context = getContext();
+
+ return context instanceof GestureSandboxActivity ? (GestureSandboxActivity) context : null;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
deleted file mode 100644
index c810e43..0000000
--- a/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import android.content.Context;
-import android.graphics.drawable.Animatable2;
-import android.graphics.drawable.AnimatedVectorDrawable;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-import android.widget.ImageView;
-
-import androidx.core.content.ContextCompat;
-
-import com.android.launcher3.R;
-import com.android.quickstep.interaction.TutorialController.TutorialType;
-
-import java.time.Duration;
-
-/** Hand coaching animation. */
-final class TutorialHandAnimation {
-
- // A delay for waiting the Activity fully launches.
- private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
-
- private final ImageView mHandCoachingView;
- private final AnimatedVectorDrawable mGestureAnimation;
-
- TutorialHandAnimation(Context context, View rootView, int resId) {
- mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
- mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context, resId);
- }
-
- /** [Re]starts animation for the given tutorial. */
- void startLoopedAnimation(TutorialType tutorialType) {
- mHandCoachingView.setVisibility(View.VISIBLE);
- if (mGestureAnimation.isRunning()) {
- stop();
- }
-
- mGestureAnimation.clearAnimationCallbacks();
- mGestureAnimation.registerAnimationCallback(
- new Animatable2.AnimationCallback() {
- @Override
- public void onAnimationEnd(Drawable drawable) {
- super.onAnimationEnd(drawable);
- mGestureAnimation.start();
- }
- });
- start(tutorialType);
- }
-
- private void start(TutorialType tutorialType) {
- // Because the gesture animation has only the right side form.
- // The left side form of the gesture animation is made from flipping the View.
- float rotationY = tutorialType == TutorialType.LEFT_EDGE_BACK_NAVIGATION ? 180f : 0f;
- mHandCoachingView.setRotationY(rotationY);
- mHandCoachingView.setImageDrawable(mGestureAnimation);
- mHandCoachingView.postDelayed(mGestureAnimation::start, ANIMATION_START_DELAY.toMillis());
- }
-
- void stop() {
- mGestureAnimation.clearAnimationCallbacks();
- mGestureAnimation.stop();
- }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
new file mode 100644
index 0000000..d880d74
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.GraphicsUtils;
+
+/** Indicator displaying the current progress through the gesture navigation tutorial. */
+public class TutorialStepIndicator extends LinearLayout {
+
+ private static final String LOG_TAG = "TutorialStepIndicator";
+
+ private int mCurrentStep = -1;
+ private int mTotalSteps = -1;
+
+ public TutorialStepIndicator(Context context) {
+ super(context);
+ }
+
+ public TutorialStepIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TutorialStepIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public TutorialStepIndicator(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Updates this indicator to display totalSteps indicator pills, with the first currentStep
+ * pills highlighted.
+ */
+ public void setTutorialProgress(int currentStep, int totalSteps) {
+ if (currentStep <= 0) {
+ Log.w(LOG_TAG, "Current step number invalid: " + currentStep + ". Assuming step 1.");
+ currentStep = 1;
+ }
+ if (totalSteps <= 0) {
+ Log.w(LOG_TAG, "Total number of steps invalid: " + totalSteps + ". Assuming 1 step.");
+ totalSteps = 1;
+ }
+ if (currentStep > totalSteps) {
+ Log.w(LOG_TAG, "Current step number greater than the total number of steps. Assuming"
+ + " final step.");
+ currentStep = totalSteps;
+ }
+ if (totalSteps < 2) {
+ setVisibility(GONE);
+ return;
+ }
+ setVisibility(VISIBLE);
+ mCurrentStep = currentStep;
+ mTotalSteps = totalSteps;
+
+ initializeStepIndicators();
+ }
+
+ private void initializeStepIndicators() {
+ for (int i = mTotalSteps; i < getChildCount(); i++) {
+ removeViewAt(i);
+ }
+ int stepIndicatorColor = GraphicsUtils.getAttrColor(
+ getContext(), android.R.attr.textColorPrimary);
+ for (int i = 0; i < mTotalSteps; i++) {
+ Drawable pageIndicatorPillDrawable = AppCompatResources.getDrawable(
+ getContext(), R.drawable.tutorial_step_indicator_pill);
+
+ if (i >= getChildCount()) {
+ ImageView pageIndicatorPill = new ImageView(getContext());
+ pageIndicatorPill.setImageDrawable(pageIndicatorPillDrawable);
+
+ LinearLayout.LayoutParams lp = new LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+
+ lp.setMarginStart(Utilities.dpToPx(3));
+ lp.setMarginEnd(Utilities.dpToPx(3));
+
+ addView(pageIndicatorPill, lp);
+ }
+ if (pageIndicatorPillDrawable != null) {
+ if (i < mCurrentStep) {
+ pageIndicatorPillDrawable.setTint(stepIndicatorColor);
+ } else {
+ pageIndicatorPillDrawable.setTint(
+ ColorUtils.setAlphaComponent(stepIndicatorColor, 0x22));
+ }
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
new file mode 100644
index 0000000..7eca360
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.logging;
+
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
+import static com.android.launcher3.Utilities.getDevicePrefs;
+import static com.android.launcher3.Utilities.getPrefs;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_2;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_3;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_4;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_5;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_DISABLED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_ENABLED;
+import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
+import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.TypedArray;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.launcher3.AutoInstallsLayout;
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.InstanceIdSequence;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.util.SettingsCache;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Utility class to log launcher settings changes
+ */
+public class SettingsChangeLogger implements
+ NavigationModeChangeListener, OnSharedPreferenceChangeListener {
+
+ private static final String TAG = "SettingsChangeLogger";
+ private static final String ROOT_TAG = "androidx.preference.PreferenceScreen";
+ private static final String BOOLEAN_PREF = "SwitchPreference";
+
+ private final Context mContext;
+ private final ArrayMap<String, LoggablePref> mLoggablePrefs;
+
+ private Mode mNavMode;
+ private boolean mNotificationDotsEnabled;
+
+ public SettingsChangeLogger(Context context) {
+ mContext = context;
+ mLoggablePrefs = loadPrefKeys(context);
+ mNavMode = SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this);
+
+ getPrefs(context).registerOnSharedPreferenceChangeListener(this);
+ getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
+
+ SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context);
+ mSettingsCache.register(NOTIFICATION_BADGING_URI,
+ this::onNotificationDotsChanged);
+ onNotificationDotsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI));
+ }
+
+ private static ArrayMap<String, LoggablePref> loadPrefKeys(Context context) {
+ XmlPullParser parser = context.getResources().getXml(R.xml.launcher_preferences);
+ ArrayMap<String, LoggablePref> result = new ArrayMap<>();
+
+ try {
+ AutoInstallsLayout.beginDocument(parser, ROOT_TAG);
+ final int depth = parser.getDepth();
+ int type;
+ while (((type = parser.next()) != XmlPullParser.END_TAG
+ || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+ if (BOOLEAN_PREF.equals(parser.getName())) {
+ TypedArray a = context.obtainStyledAttributes(
+ Xml.asAttributeSet(parser), R.styleable.LoggablePref);
+ String key = a.getString(R.styleable.LoggablePref_android_key);
+ LoggablePref pref = new LoggablePref();
+ pref.defaultValue =
+ a.getBoolean(R.styleable.LoggablePref_android_defaultValue, true);
+ pref.eventIdOn = a.getInt(R.styleable.LoggablePref_logIdOn, 0);
+ pref.eventIdOff = a.getInt(R.styleable.LoggablePref_logIdOff, 0);
+ if (pref.eventIdOff > 0 && pref.eventIdOn > 0) {
+ result.put(key, pref);
+ }
+ }
+ }
+ } catch (XmlPullParserException | IOException e) {
+ Log.e(TAG, "Error parsing preference xml", e);
+ }
+ return result;
+ }
+
+ private void onNotificationDotsChanged(boolean isDotsEnabled) {
+ mNotificationDotsEnabled = isDotsEnabled;
+ dispatchUserEvent();
+ }
+
+ @Override
+ public void onNavigationModeChanged(Mode newMode) {
+ mNavMode = newMode;
+ dispatchUserEvent();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ if (LAST_PREDICTION_ENABLED_STATE.equals(key) || KEY_MIGRATION_SRC_HOTSEAT_COUNT.equals(key)
+ || mLoggablePrefs.containsKey(key)) {
+ dispatchUserEvent();
+ }
+ }
+
+ private void dispatchUserEvent() {
+ StatsLogger logger = StatsLogManager.newInstance(mContext).logger()
+ .withInstanceId(new InstanceIdSequence().newInstanceId());
+
+ logger.log(mNotificationDotsEnabled
+ ? LAUNCHER_NOTIFICATION_DOT_ENABLED
+ : LAUNCHER_NOTIFICATION_DOT_DISABLED);
+ logger.log(mNavMode.launcherEvent);
+ logger.log(getDevicePrefs(mContext).getBoolean(LAST_PREDICTION_ENABLED_STATE, true)
+ ? LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED
+ : LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED);
+
+ SharedPreferences prefs = getPrefs(mContext);
+ StatsLogManager.LauncherEvent gridSizeChangedEvent = null;
+ // TODO(b/184981523): This doesn't work for 2-panel grid, which has 6 hotseat icons
+ switch (prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1)) {
+ case 5:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_5;
+ break;
+ case 4:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_4;
+ break;
+ case 3:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_3;
+ break;
+ case 2:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_2;
+ break;
+ default:
+ // Ignore illegal input.
+ break;
+ }
+ if (gridSizeChangedEvent != null) {
+ logger.log(gridSizeChangedEvent);
+ }
+
+ if (FeatureFlags.ENABLE_THEMED_ICONS.get()) {
+ logger.log(prefs.getBoolean(KEY_THEMED_ICONS, false)
+ ? LAUNCHER_THEMED_ICON_ENABLED
+ : LAUNCHER_THEMED_ICON_DISABLED);
+ }
+
+ mLoggablePrefs.forEach((key, lp) -> logger.log(() ->
+ prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff));
+ }
+
+ private static class LoggablePref {
+ public boolean defaultValue;
+ public int eventIdOn;
+ public int eventIdOff;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index eac45e9..6575996 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -16,8 +16,13 @@
package com.android.quickstep.logging;
+import static androidx.core.util.Preconditions.checkNotNull;
+import static androidx.core.util.Preconditions.checkState;
+
+import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.EXTENDED_CONTAINERS;
import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER;
import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER;
+import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT;
import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__ALLAPPS;
import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND;
@@ -26,122 +31,80 @@
import android.content.Context;
import android.util.Log;
+import android.view.View;
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.slice.SliceItem;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.Utilities;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase;
import com.android.launcher3.logger.LauncherAtom.FolderIcon;
import com.android.launcher3.logger.LauncherAtom.FromState;
import com.android.launcher3.logger.LauncherAtom.ToState;
+import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer;
+import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers;
import com.android.launcher3.logging.InstanceId;
-import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BaseModelUpdateTask;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.LogConfig;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.SysUiStatsLog;
-import java.util.ArrayList;
import java.util.Optional;
import java.util.OptionalInt;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* This class calls StatsLog compile time generated methods.
*
* To see if the logs are properly sent to statsd, execute following command.
+ * <ul>
* $ wwdebug (to turn on the logcat printout)
* $ wwlogcat (see logcat with grep filter on)
* $ statsd_testdrive (see how ww is writing the proto to statsd buffer)
+ * </ul>
*/
public class StatsLogCompatManager extends StatsLogManager {
private static final String TAG = "StatsLog";
private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG);
-
- private static Context sContext;
-
private static final InstanceId DEFAULT_INSTANCE_ID = InstanceId.fakeInstanceId(0);
// LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher proto migrates
// from nano to lite, bake constant to prevent robo test failure.
private static final int DEFAULT_PAGE_INDEX = -2;
private static final int FOLDER_HIERARCHY_OFFSET = 100;
private static final int SEARCH_RESULT_HIERARCHY_OFFSET = 200;
+ private static final int EXTENDED_CONTAINERS_HIERARCHY_OFFSET = 300;
+ private static final int ATTRIBUTE_MULTIPLIER = 100;
+
+ public static final CopyOnWriteArrayList<StatsLogConsumer> LOGS_CONSUMER =
+ new CopyOnWriteArrayList<>();
+
+ private final Context mContext;
public StatsLogCompatManager(Context context) {
- sContext = context;
+ mContext = context;
}
@Override
- public StatsLogger logger() {
- return new StatsCompatLogger();
+ protected StatsLogger createLogger() {
+ return new StatsCompatLogger(mContext);
}
/**
- * Logs a ranking event and accompanying {@link InstanceId} and package name.
+ * Synchronously writes an itemInfo to stats log
*/
- @Override
- public void log(EventEnum rankingEvent, InstanceId instanceId, @Nullable String packageName,
- int position) {
- SysUiStatsLog.write(SysUiStatsLog.RANKING_SELECTED,
- rankingEvent.getId() /* event_id = 1; */,
- packageName /* package_name = 2; */,
- instanceId.getId() /* instance_id = 3; */,
- position /* position_picked = 4; */);
- }
-
- /**
- * Logs the workspace layout information on the model thread.
- */
- @Override
- public void logSnapshot() {
- LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
- new SnapshotWorker());
- }
-
- private class SnapshotWorker extends BaseModelUpdateTask {
- private final InstanceId mInstanceId;
- SnapshotWorker() {
- mInstanceId = new InstanceIdSequence(
- 1 << 20 /*InstanceId.INSTANCE_ID_MAX*/).newInstanceId();
- }
-
- @Override
- public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
- IntSparseArrayMap<FolderInfo> folders = dataModel.folders.clone();
- ArrayList<ItemInfo> workspaceItems = (ArrayList) dataModel.workspaceItems.clone();
- ArrayList<LauncherAppWidgetInfo> appWidgets = (ArrayList) dataModel.appWidgets.clone();
- for (ItemInfo info : workspaceItems) {
- LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
- writeSnapshot(atomInfo, mInstanceId);
- }
- for (FolderInfo fInfo : folders) {
- try {
- ArrayList<WorkspaceItemInfo> folderContents =
- (ArrayList) Executors.MAIN_EXECUTOR.submit(fInfo.contents::clone).get();
- for (ItemInfo info : folderContents) {
- LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo);
- writeSnapshot(atomInfo, mInstanceId);
- }
- } catch (Exception e) { }
- }
- for (ItemInfo info : appWidgets) {
- LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
- writeSnapshot(atomInfo, mInstanceId);
- }
- }
- }
-
- private static void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
+ @WorkerThread
+ public static void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
if (IS_VERBOSE) {
Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info));
}
@@ -150,7 +113,8 @@
}
SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_SNAPSHOT,
LAUNCHER_WORKSPACE_SNAPSHOT.getId() /* event_id */,
- info.getItemCase().getNumber() /* target_id */,
+ info.getAttribute().getNumber() * ATTRIBUTE_MULTIPLIER
+ + info.getItemCase().getNumber() /* target_id */,
instanceId.getId() /* instance_id */,
0 /* uid */,
getPackageName(info) /* package_name */,
@@ -166,7 +130,8 @@
info.getAttribute().getNumber() /* origin */,
getCardinality(info) /* cardinality */,
info.getWidget().getSpanX(),
- info.getWidget().getSpanY());
+ info.getWidget().getSpanY(),
+ getFeatures(info));
}
/**
@@ -175,6 +140,8 @@
private static class StatsCompatLogger implements StatsLogger {
private static final ItemInfo DEFAULT_ITEM_INFO = new ItemInfo();
+
+ private Context mContext;
private ItemInfo mItemInfo = DEFAULT_ITEM_INFO;
private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
private OptionalInt mRank = OptionalInt.empty();
@@ -184,6 +151,12 @@
private Optional<FromState> mFromState = Optional.empty();
private Optional<ToState> mToState = Optional.empty();
private Optional<String> mEditText = Optional.empty();
+ private SliceItem mSliceItem;
+ private LauncherAtom.Slice mSlice;
+
+ StatsCompatLogger(Context context) {
+ mContext = context;
+ }
@Override
public StatsLogger withItemInfo(ItemInfo itemInfo) {
@@ -221,10 +194,8 @@
@Override
public StatsLogger withContainerInfo(ContainerInfo containerInfo) {
- if (mItemInfo != DEFAULT_ITEM_INFO) {
- throw new IllegalArgumentException(
- "ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
- }
+ checkState(mItemInfo == DEFAULT_ITEM_INFO,
+ "ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
this.mContainerInfo = Optional.of(containerInfo);
return this;
}
@@ -248,41 +219,96 @@
}
@Override
+ public StatsLogger withSliceItem(@NonNull SliceItem sliceItem) {
+ checkState(mItemInfo == DEFAULT_ITEM_INFO && mSlice == null,
+ "ItemInfo, Slice and SliceItem are mutual exclusive; cannot set more than one"
+ + " of them.");
+ this.mSliceItem = checkNotNull(sliceItem, "expected valid sliceItem but received null");
+ return this;
+ }
+
+ @Override
+ public StatsLogger withSlice(LauncherAtom.Slice slice) {
+ checkState(mItemInfo == DEFAULT_ITEM_INFO && mSliceItem == null,
+ "ItemInfo, Slice and SliceItem are mutual exclusive; cannot set more than one"
+ + " of them.");
+ checkNotNull(slice, "expected valid slice but received null");
+ checkNotNull(slice.getUri(), "expected valid slice uri but received null");
+ this.mSlice = slice;
+ return this;
+ }
+
+ @Override
public void log(EventEnum event) {
if (!Utilities.ATLEAST_R) {
return;
}
+ LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
- if (mItemInfo.container < 0) {
- // Item is not within a folder. Write to StatsLog in same thread.
- write(event, mInstanceId, applyOverwrites(mItemInfo.buildProto()), mSrcState,
- mDstState);
+ if (mSlice == null && mSliceItem != null) {
+ mSlice = LauncherAtom.Slice.newBuilder().setUri(
+ mSliceItem.getSlice().getUri().toString()).build();
+ }
+
+ if (mSlice != null) {
+ Executors.MODEL_EXECUTOR.execute(
+ () -> {
+ LauncherAtom.ItemInfo.Builder itemInfoBuilder =
+ LauncherAtom.ItemInfo.newBuilder().setSlice(mSlice);
+ mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo);
+ write(event, applyOverwrites(itemInfoBuilder.build()));
+ });
+ return;
+ }
+
+ if (mItemInfo.container < 0 || appState == null) {
+ // Write log on the model thread so that logs do not go out of order
+ // (for eg: drop comes after drag)
+ Executors.MODEL_EXECUTOR.execute(
+ () -> write(event, applyOverwrites(mItemInfo.buildProto())));
} else {
// Item is inside the folder, fetch folder info in a BG thread
// and then write to StatsLog.
- LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
+ appState.getModel().enqueueModelUpdateTask(
new BaseModelUpdateTask() {
@Override
public void execute(LauncherAppState app, BgDataModel dataModel,
AllAppsList apps) {
FolderInfo folderInfo = dataModel.folders.get(mItemInfo.container);
- write(event, mInstanceId,
- applyOverwrites(mItemInfo.buildProto(folderInfo)),
- mSrcState, mDstState);
+ write(event, applyOverwrites(mItemInfo.buildProto(folderInfo)));
}
});
}
}
+ @Override
+ public void sendToInteractionJankMonitor(EventEnum event, View view) {
+ if (!(event instanceof LauncherEvent)) {
+ return;
+ }
+ switch ((LauncherEvent) event) {
+ case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN:
+ InteractionJankMonitorWrapper.begin(
+ view,
+ InteractionJankMonitorWrapper.CUJ_ALL_APPS_SCROLL);
+ break;
+ case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END:
+ InteractionJankMonitorWrapper.end(
+ InteractionJankMonitorWrapper.CUJ_ALL_APPS_SCROLL);
+ break;
+ default:
+ break;
+ }
+ }
+
private LauncherAtom.ItemInfo applyOverwrites(LauncherAtom.ItemInfo atomInfo) {
- LauncherAtom.ItemInfo.Builder itemInfoBuilder =
- (LauncherAtom.ItemInfo.Builder) atomInfo.toBuilder();
+ LauncherAtom.ItemInfo.Builder itemInfoBuilder = atomInfo.toBuilder();
mRank.ifPresent(itemInfoBuilder::setRank);
mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo);
if (mFromState.isPresent() || mToState.isPresent() || mEditText.isPresent()) {
- FolderIcon.Builder folderIconBuilder = (FolderIcon.Builder) itemInfoBuilder
+ FolderIcon.Builder folderIconBuilder = itemInfoBuilder
.getFolderIcon()
.toBuilder();
mFromState.ifPresent(folderIconBuilder::setFromLabelState);
@@ -293,8 +319,11 @@
return itemInfoBuilder.build();
}
- private void write(EventEnum event, InstanceId instanceId, LauncherAtom.ItemInfo atomInfo,
- int srcState, int dstState) {
+ @WorkerThread
+ private void write(EventEnum event, LauncherAtom.ItemInfo atomInfo) {
+ InstanceId instanceId = mInstanceId;
+ int srcState = mSrcState;
+ int dstState = mDstState;
if (IS_VERBOSE) {
String name = (event instanceof Enum) ? ((Enum) event).name() :
event.getId() + "";
@@ -307,6 +336,10 @@
atomInfo));
}
+ for (StatsLogConsumer consumer : LOGS_CONSUMER) {
+ consumer.consume(event, atomInfo);
+ }
+
SysUiStatsLog.write(
SysUiStatsLog.LAUNCHER_EVENT,
SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
@@ -315,7 +348,8 @@
null /* launcher extensions, deprecated */,
false /* quickstep_enabled, deprecated */,
event.getId() /* event_id */,
- atomInfo.getItemCase().getNumber() /* target_id */,
+ atomInfo.getAttribute().getNumber() * ATTRIBUTE_MULTIPLIER
+ + atomInfo.getItemCase().getNumber() /* target_id */,
instanceId.getId() /* instance_id TODO */,
0 /* uid TODO */,
getPackageName(atomInfo) /* package_name */,
@@ -332,16 +366,25 @@
atomInfo.getFolderIcon().getFromLabelState().getNumber() /* fromState */,
atomInfo.getFolderIcon().getToLabelState().getNumber() /* toState */,
atomInfo.getFolderIcon().getLabelInfo() /* edittext */,
- getCardinality(atomInfo) /* cardinality */);
+ getCardinality(atomInfo) /* cardinality */,
+ getFeatures(atomInfo) /* features */);
}
}
private static int getCardinality(LauncherAtom.ItemInfo info) {
- switch (info.getContainerInfo().getContainerCase()){
+ switch (info.getContainerInfo().getContainerCase()) {
case PREDICTED_HOTSEAT_CONTAINER:
return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
case SEARCH_RESULT_CONTAINER:
return info.getContainerInfo().getSearchResultContainer().getQueryLength();
+ case EXTENDED_CONTAINERS:
+ ExtendedContainers extendedCont = info.getContainerInfo().getExtendedContainers();
+ if (extendedCont.getContainerCase() == DEVICE_SEARCH_RESULT_CONTAINER) {
+ DeviceSearchResultContainer deviceSearchResultCont = extendedCont
+ .getDeviceSearchResultContainer();
+ return deviceSearchResultCont.hasQueryLength() ? deviceSearchResultCont
+ .getQueryLength() : -1;
+ }
default:
return info.getFolderIcon().getCardinality();
}
@@ -357,6 +400,8 @@
return info.getWidget().getPackageName();
case TASK:
return info.getTask().getPackageName();
+ case SEARCH_ACTION_ITEM:
+ return info.getSearchActionItem().getPackageName();
default:
return null;
}
@@ -372,6 +417,10 @@
return info.getWidget().getComponentName();
case TASK:
return info.getTask().getComponentName();
+ case SEARCH_ACTION_ITEM:
+ return info.getSearchActionItem().getTitle();
+ case SLICE:
+ return info.getSlice().getUri();
default:
return null;
}
@@ -402,9 +451,16 @@
}
private static int getPageId(LauncherAtom.ItemInfo info) {
+ if (info.hasTask()) {
+ return info.getTask().getIndex();
+ }
switch (info.getContainerInfo().getContainerCase()) {
case FOLDER:
return info.getContainerInfo().getFolder().getPageIndex();
+ case HOTSEAT:
+ return info.getContainerInfo().getHotseat().getIndex();
+ case PREDICTED_HOTSEAT_CONTAINER:
+ return info.getContainerInfo().getPredictedHotseatContainer().getIndex();
default:
return info.getContainerInfo().getWorkspace().getPageIndex();
}
@@ -413,6 +469,10 @@
private static int getParentPageId(LauncherAtom.ItemInfo info) {
switch (info.getContainerInfo().getContainerCase()) {
case FOLDER:
+ if (info.getContainerInfo().getFolder().getParentContainerCase()
+ == ParentContainerCase.HOTSEAT) {
+ return info.getContainerInfo().getFolder().getHotseat().getIndex();
+ }
return info.getContainerInfo().getFolder().getWorkspace().getPageIndex();
case SEARCH_RESULT_CONTAINER:
return info.getContainerInfo().getSearchResultContainer().getWorkspace()
@@ -429,6 +489,9 @@
} else if (info.getContainerInfo().getContainerCase() == SEARCH_RESULT_CONTAINER) {
return info.getContainerInfo().getSearchResultContainer().getParentContainerCase()
.getNumber() + SEARCH_RESULT_HIERARCHY_OFFSET;
+ } else if (info.getContainerInfo().getContainerCase() == EXTENDED_CONTAINERS) {
+ return info.getContainerInfo().getExtendedContainers().getContainerCase().getNumber()
+ + EXTENDED_CONTAINERS_HIERARCHY_OFFSET;
} else {
return info.getContainerInfo().getContainerCase().getNumber();
}
@@ -446,7 +509,23 @@
return "ALLAPPS";
default:
return "INVALID";
-
}
}
+
+ private static int getFeatures(LauncherAtom.ItemInfo info) {
+ if (info.getItemCase().equals(LauncherAtom.ItemInfo.ItemCase.WIDGET)) {
+ return info.getWidget().getWidgetFeatures();
+ }
+ return 0;
+ }
+
+
+ /**
+ * Interface to get stats log while it is dispatched to the system
+ */
+ public interface StatsLogConsumer {
+
+ @WorkerThread
+ void consume(EventEnum event, LauncherAtom.ItemInfo atomInfo);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
deleted file mode 100644
index 9ca7f23..0000000
--- a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.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.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 static com.android.systemui.shared.system.LauncherEventUtil.VISIBLE;
-
-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 {
-
- public static final int ALL_APPS_PREDICTION_TIPS = 2;
-
- private static final String TAG = "UserEventDispatcher";
-
- public UserEventDispatcherExtension(Context context) { }
-
- public void logStateChangeAction(int action, int dir, int downX, int downY,
- int srcChildTargetType, int srcParentContainerType,
- int dstContainerType, int pageIndex) {
- new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY,
- dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER);
- super.logStateChangeAction(action, dir, downX, downY, 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/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java
rename to quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
index dfb8c1d..b9879ab 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
@@ -26,7 +26,8 @@
import java.util.function.BiPredicate;
-public class ActivityInitListener<T extends BaseActivity> implements SchedulerCallback<T> {
+public class ActivityInitListener<T extends BaseActivity> implements
+ SchedulerCallback<T> {
private BiPredicate<T, Boolean> mOnInitListener;
private final ActivityTracker<T> mActivityTracker;
@@ -47,6 +48,7 @@
@Override
public final boolean init(T activity, boolean alreadyOnHome) {
if (!mIsRegistered) {
+ // Don't receive any more updates
return false;
}
return handleInit(activity, alreadyOnHome);
@@ -59,18 +61,17 @@
/**
* Registers the activity-created listener. If the activity is already created, then the
* callback provided in the constructor will be called synchronously.
- * @param intent The intent that will be used to initialize the activity, if the activity
- * doesn't already exist. We add the callback as an extra on this intent.
*/
- public void register(Intent intent) {
+ public void register() {
mIsRegistered = true;
- mActivityTracker.runCallbackWhenActivityExists(this, intent);
+ mActivityTracker.registerCallback(this);
}
/**
* After calling this, we won't {@link #init} even when the activity is ready.
*/
public void unregister() {
+ mActivityTracker.unregisterCallback(this);
mIsRegistered = false;
mOnInitListener = null;
}
@@ -82,9 +83,9 @@
*/
public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
Context context, Handler handler, long duration) {
- mIsRegistered = true;
+ register();
Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
- context.startActivity(addToIntent(new Intent(intent)), options);
+ context.startActivity(new Intent(intent), options);
}
}
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
new file mode 100644
index 0000000..7f94839
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
+
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Controls an animation that can go beyond progress = 1, at which point resistance should be
+ * applied. Internally, this is a wrapper around 2 {@link AnimatorPlaybackController}s, one that
+ * runs from progress 0 to 1 like normal, then one that seamlessly continues that animation but
+ * starts applying resistance as well.
+ */
+public class AnimatorControllerWithResistance {
+
+ private enum RecentsResistanceParams {
+ FROM_APP(0.75f, 0.5f, 1f),
+ FROM_OVERVIEW(1f, 0.75f, 0.5f);
+
+ RecentsResistanceParams(float scaleStartResist, float scaleMaxResist,
+ float translationFactor) {
+ this.scaleStartResist = scaleStartResist;
+ this.scaleMaxResist = scaleMaxResist;
+ this.translationFactor = translationFactor;
+ }
+
+ /**
+ * Start slowing down the rate of scaling down when recents view is smaller than this scale.
+ */
+ public final float scaleStartResist;
+
+ /**
+ * Recents view will reach this scale at the very end of the drag.
+ */
+ public final float scaleMaxResist;
+
+ /**
+ * How much translation to apply to RecentsView when the drag reaches the top of the screen,
+ * where 0 will keep it centered and 1 will have it barely touch the top of the screen.
+ */
+ public final float translationFactor;
+ }
+
+ private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DEACCEL;
+ private static final TimeInterpolator RECENTS_TRANSLATE_RESIST_INTERPOLATOR = LINEAR;
+
+ private final AnimatorPlaybackController mNormalController;
+ private final AnimatorPlaybackController mResistanceController;
+
+ // Initialize to -1 so the first 0 gets applied.
+ private float mLastNormalProgress = -1;
+ private float mLastResistProgress;
+
+ public AnimatorControllerWithResistance(AnimatorPlaybackController normalController,
+ AnimatorPlaybackController resistanceController) {
+ mNormalController = normalController;
+ mResistanceController = resistanceController;
+ }
+
+ public AnimatorPlaybackController getNormalController() {
+ return mNormalController;
+ }
+
+ /**
+ * Applies the current progress of the animation.
+ * @param progress From 0 to maxProgress, where 1 is the target we are animating towards.
+ * @param maxProgress > 1, this is where the resistance will be applied.
+ */
+ public void setProgress(float progress, float maxProgress) {
+ float normalProgress = Utilities.boundToRange(progress, 0, 1);
+ if (normalProgress != mLastNormalProgress) {
+ mLastNormalProgress = normalProgress;
+ mNormalController.setPlayFraction(normalProgress);
+ }
+ if (maxProgress <= 1) {
+ return;
+ }
+ float resistProgress = progress <= 1 ? 0 : Utilities.getProgress(progress, 1, maxProgress);
+ if (resistProgress != mLastResistProgress) {
+ mLastResistProgress = resistProgress;
+ mResistanceController.setPlayFraction(resistProgress);
+ }
+ }
+
+ /**
+ * Applies resistance to recents when swiping up past its target position.
+ * @param normalController The controller to run from 0 to 1 before this resistance applies.
+ * @param context Used to compute start and end values.
+ * @param recentsOrientedState Used to compute start and end values.
+ * @param dp Used to compute start and end values.
+ * @param scaleTarget The target for the scaleProperty.
+ * @param scaleProperty Animate the value to change the scale of the window/recents view.
+ * @param translationTarget The target for the translationProperty.
+ * @param translationProperty Animate the value to change the translation of the recents view.
+ */
+ public static <SCALE, TRANSLATION> AnimatorControllerWithResistance createForRecents(
+ AnimatorPlaybackController normalController, Context context,
+ RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
+ FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget,
+ FloatProperty<TRANSLATION> translationProperty) {
+
+ RecentsParams params = new RecentsParams(context, recentsOrientedState, dp, scaleTarget,
+ scaleProperty, translationTarget, translationProperty);
+ PendingAnimation resistAnim = createRecentsResistanceAnim(params);
+
+ AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
+ return new AnimatorControllerWithResistance(normalController, resistanceController);
+ }
+
+ /**
+ * Creates the resistance animation for {@link #createForRecents}, or can be used separately
+ * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}.
+ */
+ public static <SCALE, TRANSLATION> PendingAnimation createRecentsResistanceAnim(
+ RecentsParams<SCALE, TRANSLATION> params) {
+ Rect startRect = new Rect();
+ PagedOrientationHandler orientationHandler = params.recentsOrientedState
+ .getOrientationHandler();
+ LauncherActivityInterface.INSTANCE.calculateTaskSize(params.context, params.dp, startRect,
+ orientationHandler);
+ long distanceToCover = startRect.bottom;
+ PendingAnimation resistAnim = params.resistAnim != null
+ ? params.resistAnim
+ : new PendingAnimation(distanceToCover * 2);
+
+ PointF pivot = new PointF();
+ float fullscreenScale = params.recentsOrientedState.getFullScreenScaleAndPivot(
+ startRect, params.dp, pivot);
+ float prevScaleRate = (fullscreenScale - params.startScale)
+ / (params.dp.heightPx - startRect.bottom);
+ // This is what the scale would be at the end of the drag if we didn't apply resistance.
+ float endScale = params.startScale - prevScaleRate * distanceToCover;
+ // Create an interpolator that resists the scale so the scale doesn't get smaller than
+ // RECENTS_SCALE_MAX_RESIST.
+ float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist,
+ params.startScale, endScale);
+ float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist,
+ params.startScale, endScale);
+ final TimeInterpolator scaleInterpolator = t -> {
+ if (t < startResist) {
+ return t;
+ }
+ float resistProgress = Utilities.getProgress(t, startResist, 1);
+ resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
+ return startResist + resistProgress * (maxResist - startResist);
+ };
+ resistAnim.addFloat(params.scaleTarget, params.scaleProperty, params.startScale, endScale,
+ scaleInterpolator);
+
+ // Compute where the task view would be based on the end scale.
+ RectF endRectF = new RectF(startRect);
+ Matrix temp = new Matrix();
+ temp.setScale(params.resistanceParams.scaleMaxResist,
+ params.resistanceParams.scaleMaxResist, pivot.x, pivot.y);
+ temp.mapRect(endRectF);
+ // Translate such that the task view touches the top of the screen when drag does.
+ float endTranslation = endRectF.top
+ * orientationHandler.getSecondaryTranslationDirectionFactor()
+ * params.resistanceParams.translationFactor;
+ resistAnim.addFloat(params.translationTarget, params.translationProperty,
+ params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
+
+ return resistAnim;
+ }
+
+ /**
+ * Helper method to update or create a PendingAnimation suitable for animating
+ * a RecentsView interaction that started from the overview state.
+ */
+ public static PendingAnimation createRecentsResistanceFromOverviewAnim(
+ Launcher launcher, @Nullable PendingAnimation resistanceAnim) {
+ RecentsView recentsView = launcher.getOverviewPanel();
+ RecentsParams params = new RecentsParams(launcher, recentsView.getPagedViewOrientedState(),
+ launcher.getDeviceProfile(), recentsView, RECENTS_SCALE_PROPERTY, recentsView,
+ TASK_SECONDARY_TRANSLATION)
+ .setResistAnim(resistanceAnim)
+ .setResistanceParams(RecentsResistanceParams.FROM_OVERVIEW)
+ .setStartScale(recentsView.getScaleX());
+ return createRecentsResistanceAnim(params);
+ }
+
+ /**
+ * Params to compute resistance when scaling/translating recents.
+ */
+ private static class RecentsParams<SCALE, TRANSLATION> {
+ // These are all required and can't have default values, hence are final.
+ public final Context context;
+ public final RecentsOrientedState recentsOrientedState;
+ public final DeviceProfile dp;
+ public final SCALE scaleTarget;
+ public final FloatProperty<SCALE> scaleProperty;
+ public final TRANSLATION translationTarget;
+ public final FloatProperty<TRANSLATION> translationProperty;
+
+ // These are not required, or can have a default value that is generally correct.
+ @Nullable public PendingAnimation resistAnim = null;
+ public RecentsResistanceParams resistanceParams = RecentsResistanceParams.FROM_APP;
+ public float startScale = 1f;
+ public float startTranslation = 0f;
+
+ private RecentsParams(Context context, RecentsOrientedState recentsOrientedState,
+ DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty,
+ TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty) {
+ this.context = context;
+ this.recentsOrientedState = recentsOrientedState;
+ this.dp = dp;
+ this.scaleTarget = scaleTarget;
+ this.scaleProperty = scaleProperty;
+ this.translationTarget = translationTarget;
+ this.translationProperty = translationProperty;
+ }
+
+ private RecentsParams setResistAnim(PendingAnimation resistAnim) {
+ this.resistAnim = resistAnim;
+ return this;
+ }
+
+ private RecentsParams setResistanceParams(RecentsResistanceParams resistanceParams) {
+ this.resistanceParams = resistanceParams;
+ return this;
+ }
+
+ private RecentsParams setStartScale(float startScale) {
+ this.startScale = startScale;
+ return this;
+ }
+
+ private RecentsParams setStartTranslation(float startTranslation) {
+ this.startTranslation = startTranslation;
+ return this;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AppCloseConfig.java b/quickstep/src/com/android/quickstep/util/AppCloseConfig.java
new file mode 100644
index 0000000..bec3379
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AppCloseConfig.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+
+/*
+ * Adds getter methods to {@link MultiValueUpdateListener} specific to app close animation,
+ * so that the entire animation can be defined in one place.
+ */
+public abstract class AppCloseConfig extends MultiValueUpdateListener {
+
+ /**
+ * Returns the translation y of the workspace contents.
+ */
+ public abstract float getWorkspaceTransY();
+
+ /*
+ * Returns the scale of the workspace contents.
+ */
+ public abstract float getWorkspaceScale();
+
+ /*
+ * Returns the alpha of the window.
+ */
+ public abstract @FloatRange(from = 0, to = 1) float getWindowAlpha();
+
+ /*
+ * Returns the alpha of the foreground layer of an adaptive icon.
+ */
+ public abstract @IntRange(from = 0, to = 255) int getFgAlpha();
+
+ /*
+ * Returns the corner radius of the window and icon.
+ */
+ public abstract float getCornerRadius();
+
+ /*
+ * Returns the interpolated progress of the animation.
+ */
+ public abstract float getInterpolatedProgress();
+
+}
diff --git a/quickstep/src/com/android/quickstep/util/AssistContentRequester.java b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
new file mode 100644
index 0000000..b1e38eb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
+import android.app.IAssistDataReceiver;
+import android.app.assist.AssistContent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.launcher3.util.Executors;
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Can be used to request the AssistContent from a provided task id, useful for getting the web uri
+ * if provided from the task.
+ */
+public class AssistContentRequester {
+ private static final String TAG = "AssistContentRequester";
+ private static final String ASSIST_KEY_CONTENT = "content";
+
+ /** For receiving content, called on the main thread. */
+ public interface Callback {
+ /**
+ * Called when the {@link android.app.assist.AssistContent} of the requested task is
+ * available.
+ **/
+ void onAssistContentAvailable(AssistContent assistContent);
+ }
+
+ private final IActivityTaskManager mActivityTaskManager;
+ private final String mPackageName;
+ private final Executor mCallbackExecutor;
+ private final Executor mSystemInteractionExecutor;
+
+ // If system loses the callback, our internal cache of original callback will also get cleared.
+ private final Map<Object, Callback> mPendingCallbacks =
+ Collections.synchronizedMap(new WeakHashMap<>());
+
+ public AssistContentRequester(Context context) {
+ mActivityTaskManager = ActivityTaskManager.getService();
+ mPackageName = context.getApplicationContext().getPackageName();
+ mCallbackExecutor = Executors.MAIN_EXECUTOR;
+ mSystemInteractionExecutor = Executors.UI_HELPER_EXECUTOR;
+ }
+
+ /**
+ * Request the {@link AssistContent} from the task with the provided id.
+ *
+ * @param taskId to query for the content.
+ * @param callback to call when the content is available, called on the main thread.
+ */
+ public void requestAssistContent(final int taskId, final Callback callback) {
+ // ActivityTaskManager interaction here is synchronous, so call off the main thread.
+ mSystemInteractionExecutor.execute(() -> {
+ try {
+ mActivityTaskManager.requestAssistDataForTask(
+ new AssistDataReceiver(callback, this), taskId, mPackageName);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Requesting assist content failed for task: " + taskId, e);
+ }
+ });
+ }
+
+ private void executeOnMainExecutor(Runnable callback) {
+ mCallbackExecutor.execute(callback);
+ }
+
+ private static final class AssistDataReceiver extends IAssistDataReceiver.Stub {
+
+ // The AssistDataReceiver binder callback object is passed to a system server, that may
+ // keep hold of it for longer than the lifetime of the AssistContentRequester object,
+ // potentially causing a memory leak. In the callback passed to the system server, only
+ // keep a weak reference to the parent object and lookup its callback if it still exists.
+ private final WeakReference<AssistContentRequester> mParentRef;
+ private final Object mCallbackKey = new Object();
+
+ AssistDataReceiver(Callback callback, AssistContentRequester parent) {
+ parent.mPendingCallbacks.put(mCallbackKey, callback);
+ mParentRef = new WeakReference<>(parent);
+ }
+
+ @Override
+ public void onHandleAssistData(Bundle data) {
+ if (data == null) {
+ return;
+ }
+
+ final AssistContent content = data.getParcelable(ASSIST_KEY_CONTENT);
+ if (content == null) {
+ Log.e(TAG, "Received AssistData, but no AssistContent found");
+ return;
+ }
+
+ AssistContentRequester requester = mParentRef.get();
+ if (requester != null) {
+ Callback callback = requester.mPendingCallbacks.get(mCallbackKey);
+ if (callback != null) {
+ requester.executeOnMainExecutor(
+ () -> callback.onAssistContentAvailable(content));
+ } else {
+ Log.d(TAG, "Callback received after calling UI was disposed of");
+ }
+ } else {
+ Log.d(TAG, "Callback received after Requester was collected");
+ }
+ }
+
+ @Override
+ public void onHandleAssistScreenshot(Bitmap screenshot) {}
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AssistantUtilities.java b/quickstep/src/com/android/quickstep/util/AssistantUtilities.java
new file mode 100644
index 0000000..336f7d1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistantUtilities.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
+
+import android.annotation.TargetApi;
+import android.app.TaskInfo;
+import android.content.Intent;
+import android.os.Build;
+
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+/**
+ * Utility class for interacting with the Assistant.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public final class AssistantUtilities {
+
+ /** Returns true if an Assistant activity that is excluded from recents is running. */
+ public static boolean isExcludedAssistantRunning() {
+ return isExcludedAssistant(ActivityManagerWrapper.getInstance().getRunningTask());
+ }
+
+ /** Returns true if the given task holds an Assistant activity that is excluded from recents. */
+ public static boolean isExcludedAssistant(TaskInfo info) {
+ return info != null
+ && info.configuration.windowConfiguration.getActivityType() == ACTIVITY_TYPE_ASSISTANT
+ && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+ }
+
+ private AssistantUtilities() {}
+}
diff --git a/quickstep/src/com/android/quickstep/util/CancellableTask.java b/quickstep/src/com/android/quickstep/util/CancellableTask.java
new file mode 100644
index 0000000..a6e2e81
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/CancellableTask.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+/**
+ * Utility class to executore a task on background and post the result on UI thread
+ */
+public abstract class CancellableTask<T> implements Runnable {
+
+ private boolean mCancelled = false;
+
+ @Override
+ public final void run() {
+ if (mCancelled) {
+ return;
+ }
+ T result = getResultOnBg();
+ if (mCancelled) {
+ return;
+ }
+ MAIN_EXECUTOR.execute(() -> {
+ if (mCancelled) {
+ return;
+ }
+ handleResult(result);
+ });
+ }
+
+ /**
+ * Called on the worker thread to process the request. The return object is passed to
+ * {@link #handleResult(Object)}
+ */
+ @WorkerThread
+ public abstract T getResultOnBg();
+
+ /**
+ * Called on the UI thread to handle the final result.
+ * @param result
+ */
+ @UiThread
+ public abstract void handleResult(T result);
+
+ /**
+ * Cancels the request. If it is called before {@link #handleResult(Object)}, that method
+ * will not be called
+ */
+ public void cancel() {
+ mCancelled = true;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
index e998e9a..de7dbd6 100644
--- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
+++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
@@ -19,25 +19,35 @@
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.prediction.AppTarget;
+import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipDescription;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Picture;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.net.Uri;
import android.util.Log;
+import android.view.View;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import androidx.core.content.FileProvider;
+import com.android.internal.app.ChooserActivity;
import com.android.launcher3.BuildConfig;
import com.android.quickstep.SystemUiProxy;
import com.android.systemui.shared.recents.model.Task;
@@ -59,6 +69,7 @@
private static final long FILE_LIFE = 1000L /*ms*/ * 60L /*s*/ * 60L /*m*/ * 24L /*h*/;
private static final String SUB_FOLDER = "Overview";
private static final String BASE_NAME = "overview_image_";
+ private static final String TAG = "ImageActionUtils";
/**
* Saves screenshot to location determine by SystemUiProxy
@@ -71,6 +82,41 @@
}
/**
+ * Launch the activity to share image for overview sharing. This is to share cropped bitmap
+ * with specific share targets (with shortcutInfo and appTarget) rendered in overview.
+ */
+ @UiThread
+ public static void shareImage(Context context, Supplier<Bitmap> bitmapSupplier, RectF rectF,
+ ShortcutInfo shortcutInfo, AppTarget appTarget, String tag) {
+ if (bitmapSupplier.get() == null) {
+ return;
+ }
+ Rect crop = new Rect();
+ rectF.round(crop);
+ Intent intent = new Intent();
+ Uri uri = getImageUri(bitmapSupplier.get(), crop, context, tag);
+ ClipData clipdata = new ClipData(new ClipDescription("content",
+ new String[]{"image/png"}),
+ new ClipData.Item(uri));
+ intent.setAction(Intent.ACTION_SEND)
+ .setComponent(new ComponentName(appTarget.getPackageName(), appTarget.getClassName()))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
+ .setType("image/png")
+ .putExtra(Intent.EXTRA_STREAM, uri)
+ .putExtra(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId())
+ .setClipData(clipdata);
+
+ if (context.getUserId() != appTarget.getUser().getIdentifier()) {
+ intent.prepareToLeaveUser(context.getUserId());
+ intent.fixUris(context.getUserId());
+ context.startActivityAsUser(intent, appTarget.getUser());
+ } else {
+ context.startActivity(intent);
+ }
+ }
+
+ /**
* Launch the activity to share image.
*/
@UiThread
@@ -87,6 +133,22 @@
}
/**
+ * Launch the activity to share image with shared element transition.
+ */
+ @UiThread
+ public static void startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier,
+ Rect crop, Intent intent, String tag, View sharedElement) {
+ if (bitmapSupplier.get() == null) {
+ Log.e(tag, "No snapshot available, not starting share.");
+ return;
+ }
+
+ UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(context,
+ bitmapSupplier.get(), crop, intent, ImageActionUtils::getShareIntentForImageUri,
+ tag, sharedElement));
+ }
+
+ /**
* Starts activity based on given intent created from image uri.
*/
@WorkerThread
@@ -94,15 +156,43 @@
Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag) {
Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent);
- // Work around b/159412574
- if (intents.length == 1) {
- context.startActivity(intents[0]);
- } else {
- context.startActivities(intents);
+ try {
+ // Work around b/159412574
+ if (intents.length == 1) {
+ context.startActivity(intents[0]);
+ } else {
+ context.startActivities(intents);
+ }
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found to receive image intent");
}
}
/**
+ * Starts activity based on given intent created from image uri with shared element transition.
+ */
+ @WorkerThread
+ public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop,
+ Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag,
+ View scaledImage) {
+ Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent);
+
+ // Work around b/159412574
+ if (intents.length == 1) {
+ MAIN_EXECUTOR.execute(() -> context.startActivity(intents[0],
+ ActivityOptions.makeSceneTransitionAnimation((Activity) context, scaledImage,
+ ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle()));
+
+ } else {
+ MAIN_EXECUTOR.execute(() -> context.startActivities(intents,
+ ActivityOptions.makeSceneTransitionAnimation((Activity) context, scaledImage,
+ ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle()));
+ }
+ }
+
+
+
+ /**
* Converts image bitmap to Uri by temporarily saving bitmap to cache, and creating Uri pointing
* to that location. Used to be able to share an image with another app.
*
diff --git a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
new file mode 100644
index 0000000..2e5b33a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import android.util.Log;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputConsumerController;
+
+import java.util.function.Supplier;
+
+/**
+ * Utility class which manages proxying input events from {@link InputConsumerController}
+ * to an {@link InputConsumer}
+ */
+public class InputConsumerProxy {
+
+ private static final String TAG = "InputConsumerProxy";
+
+ private final InputConsumerController mInputConsumerController;
+ private Runnable mCallback;
+ private Supplier<InputConsumer> mConsumerSupplier;
+
+ // The consumer is created lazily on demand.
+ private InputConsumer mInputConsumer;
+
+ private boolean mDestroyed = false;
+ private boolean mTouchInProgress = false;
+ private boolean mDestroyPending = false;
+
+ public InputConsumerProxy(InputConsumerController inputConsumerController,
+ Runnable callback, Supplier<InputConsumer> consumerSupplier) {
+ mInputConsumerController = inputConsumerController;
+ mCallback = callback;
+ mConsumerSupplier = consumerSupplier;
+ }
+
+ public void enable() {
+ if (mDestroyed) {
+ return;
+ }
+ mInputConsumerController.setInputListener(this::onInputConsumerEvent);
+ }
+
+ private boolean onInputConsumerEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ onInputConsumerMotionEvent((MotionEvent) ev);
+ } else if (ev instanceof KeyEvent) {
+ initInputConsumerIfNeeded();
+ mInputConsumer.onKeyEvent((KeyEvent) ev);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean onInputConsumerMotionEvent(MotionEvent ev) {
+ int action = ev.getAction();
+
+ // Just to be safe, verify that ACTION_DOWN comes before any other action,
+ // and ignore any ACTION_DOWN after the first one (though that should not happen).
+ if (!mTouchInProgress && action != ACTION_DOWN) {
+ Log.w(TAG, "Received non-down motion before down motion: " + action);
+ return false;
+ }
+ if (mTouchInProgress && action == ACTION_DOWN) {
+ Log.w(TAG, "Received down motion while touch was already in progress");
+ return false;
+ }
+
+ if (action == ACTION_DOWN) {
+ mTouchInProgress = true;
+ initInputConsumerIfNeeded();
+ } else if (action == ACTION_CANCEL || action == ACTION_UP) {
+ // Finish any pending actions
+ mTouchInProgress = false;
+ if (mDestroyPending) {
+ destroy();
+ }
+ }
+ if (mInputConsumer != null) {
+ mInputConsumer.onMotionEvent(ev);
+ }
+
+ return true;
+ }
+
+ public void destroy() {
+ if (mTouchInProgress) {
+ mDestroyPending = true;
+ return;
+ }
+ mDestroyPending = false;
+ mDestroyed = true;
+ mInputConsumerController.setInputListener(null);
+ }
+
+ public void unregisterCallback() {
+ mCallback = null;
+ }
+
+ private void initInputConsumerIfNeeded() {
+ if (mInputConsumer == null) {
+ if (mCallback != null) {
+ mCallback.run();
+ }
+ mInputConsumer = mConsumerSupplier.get();
+ mConsumerSupplier = null;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/InputProxyHandlerFactory.java b/quickstep/src/com/android/quickstep/util/InputProxyHandlerFactory.java
new file mode 100644
index 0000000..8209c09
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/InputProxyHandlerFactory.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.inputconsumers.OverviewInputConsumer;
+
+import java.util.function.Supplier;
+
+/**
+ * A factory that creates a input consumer for
+ * {@link com.android.quickstep.util.InputConsumerProxy}.
+ */
+public class InputProxyHandlerFactory implements Supplier<InputConsumer> {
+
+ private final BaseActivityInterface mActivityInterface;
+ private final GestureState mGestureState;
+
+ @UiThread
+ public InputProxyHandlerFactory(BaseActivityInterface activityInterface,
+ GestureState gestureState) {
+ mActivityInterface = activityInterface;
+ mGestureState = gestureState;
+ }
+
+ /**
+ * Called to create a input proxy for the running task
+ */
+ @Override
+ public InputConsumer get() {
+ StatefulActivity activity = mActivityInterface.getCreatedActivity();
+ return activity == null ? InputConsumer.NO_OP
+ : new OverviewInputConsumer(mGestureState, activity, null, true);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index ae19d73..8834dc2 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -15,16 +15,12 @@
*/
package com.android.quickstep.util;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-
import android.content.Context;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.SysUINavigationMode;
@@ -45,16 +41,10 @@
public static int getShelfTrackingDistance(Context context, DeviceProfile dp,
PagedOrientationHandler orientationHandler) {
// Track the bottom of the window.
- if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
- Rect taskSize = new Rect();
- LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize,
- orientationHandler);
- return (dp.heightPx - taskSize.height()) / 2;
- }
- int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
- int spaceBetweenShelfAndRecents = (int) context.getResources().getDimension(
- R.dimen.task_card_vert_space);
- return shelfHeight + spaceBetweenShelfAndRecents;
+ Rect taskSize = new Rect();
+ LauncherActivityInterface.INSTANCE.calculateTaskSize(
+ context, dp, taskSize, orientationHandler);
+ return orientationHandler.getDistanceToBottomOfRect(dp, taskSize);
}
/**
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index a5d4568..1ed2da3 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -15,17 +15,14 @@
*/
package com.android.quickstep.util;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_LSQ_VELOCITY_PROVIDER;
-
import android.content.Context;
import android.content.res.Resources;
-import android.util.Log;
import android.view.MotionEvent;
+import android.view.VelocityTracker;
import com.android.launcher3.Alarm;
import com.android.launcher3.R;
import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
/**
* Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is
@@ -52,7 +49,7 @@
private final Alarm mForcePauseTimeout;
private final boolean mMakePauseHarderToTrigger;
private final Context mContext;
- private final VelocityProvider mVelocityProvider;
+ private final SystemVelocityProvider mVelocityProvider;
private Float mPreviousVelocity = null;
@@ -86,14 +83,10 @@
mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "creating alarm");
- }
mForcePauseTimeout = new Alarm();
mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
mMakePauseHarderToTrigger = makePauseHarderToTrigger;
- mVelocityProvider = ENABLE_LSQ_VELOCITY_PROVIDER.get()
- ? new LSqVelocityProvider(axis) : new LinearVelocityProvider(axis);
+ mVelocityProvider = new SystemVelocityProvider(axis);
}
/**
@@ -125,14 +118,11 @@
* @param pointerIndex Index for the pointer being tracked in the motion event
*/
public void addPosition(MotionEvent ev, int pointerIndex) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "setting alarm");
- }
mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger
? HARDER_TRIGGER_TIMEOUT
: FORCE_PAUSE_TIMEOUT);
- Float newVelocity = mVelocityProvider.addMotionEvent(ev, pointerIndex);
- if (newVelocity != null && mPreviousVelocity != null) {
+ float newVelocity = mVelocityProvider.addMotionEvent(ev, ev.getPointerId(pointerIndex));
+ if (mPreviousVelocity != null) {
checkMotionPaused(newVelocity, mPreviousVelocity, ev.getEventTime());
}
mPreviousVelocity = newVelocity;
@@ -175,20 +165,24 @@
}
private void updatePaused(boolean isPaused) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "updatePaused: " + isPaused);
- }
if (mDisallowPause) {
isPaused = false;
}
if (mIsPaused != isPaused) {
mIsPaused = isPaused;
+ boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused;
if (mIsPaused) {
AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext);
mHasEverBeenPaused = true;
}
if (mOnMotionPauseListener != null) {
- mOnMotionPauseListener.onMotionPauseChanged(mIsPaused);
+ if (isFirstDetectedPause) {
+ mOnMotionPauseListener.onMotionPauseDetected();
+ }
+ // Null check again as onMotionPauseDetected() maybe have called clear().
+ if (mOnMotionPauseListener != null) {
+ mOnMotionPauseListener.onMotionPauseChanged(mIsPaused);
+ }
}
}
}
@@ -199,9 +193,6 @@
setOnMotionPauseListener(null);
mIsPaused = mHasEverBeenPaused = false;
mSlowStartTime = 0;
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "canceling alarm");
- }
mForcePauseTimeout.cancelAlarm();
}
@@ -210,190 +201,39 @@
}
public interface OnMotionPauseListener {
- void onMotionPauseChanged(boolean isPaused);
+ /** Called only the first time motion pause is detected. */
+ void onMotionPauseDetected();
+ /** Called every time motion changes from paused to not paused and vice versa. */
+ default void onMotionPauseChanged(boolean isPaused) { }
}
- /**
- * Interface to abstract out velocity calculations
- */
- protected interface VelocityProvider {
+ private static class SystemVelocityProvider {
+
+ private final VelocityTracker mVelocityTracker;
+ private final int mAxis;
+
+ SystemVelocityProvider(int axis) {
+ mVelocityTracker = VelocityTracker.obtain();
+ mAxis = axis;
+ }
/**
* Adds a new motion events, and returns the velocity at this point, or null if
* the velocity is not available
*/
- Float addMotionEvent(MotionEvent ev, int pointer);
+ public float addMotionEvent(MotionEvent ev, int pointer) {
+ mVelocityTracker.addMovement(ev);
+ mVelocityTracker.computeCurrentVelocity(1); // px / ms
+ return mAxis == MotionEvent.AXIS_X
+ ? mVelocityTracker.getXVelocity(pointer)
+ : mVelocityTracker.getYVelocity(pointer);
+ }
/**
* Clears all stored motion event records
*/
- void clear();
- }
-
- private static class LinearVelocityProvider implements VelocityProvider {
-
- private Long mPreviousTime = null;
- private Float mPreviousPosition = null;
-
- private final int mAxis;
-
- LinearVelocityProvider(int axis) {
- mAxis = axis;
- }
-
- @Override
- public Float addMotionEvent(MotionEvent ev, int pointer) {
- long time = ev.getEventTime();
- float position = ev.getAxisValue(mAxis, pointer);
- Float velocity = null;
-
- if (mPreviousTime != null && mPreviousPosition != null) {
- long changeInTime = Math.max(1, time - mPreviousTime);
- float changeInPosition = position - mPreviousPosition;
- velocity = changeInPosition / changeInTime;
- }
- mPreviousTime = time;
- mPreviousPosition = position;
- return velocity;
- }
-
- @Override
public void clear() {
- mPreviousTime = null;
- mPreviousPosition = null;
- }
- }
-
- /**
- * Java implementation of {@link android.view.VelocityTracker} using the Least Square (deg 2)
- * algorithm.
- */
- private static class LSqVelocityProvider implements VelocityProvider {
-
- // Maximum age of a motion event to be considered when calculating the velocity.
- private static final long HORIZON_MS = 100;
- // Number of samples to keep.
- private static final int HISTORY_SIZE = 20;
-
- // Position history are stored in a circular array
- private final float[] mHistoricTimes = new float[HISTORY_SIZE];
- private final float[] mHistoricPos = new float[HISTORY_SIZE];
- private int mHistoryCount = 0;
- private int mHistoryStart = 0;
-
- private final int mAxis;
-
- LSqVelocityProvider(int axis) {
- mAxis = axis;
- }
-
- @Override
- public void clear() {
- mHistoryCount = mHistoryStart = 0;
- }
-
- private void addPositionAndTime(float eventTime, float eventPosition) {
- mHistoricTimes[mHistoryStart] = eventTime;
- mHistoricPos[mHistoryStart] = eventPosition;
- mHistoryStart++;
- if (mHistoryStart >= HISTORY_SIZE) {
- mHistoryStart = 0;
- }
- mHistoryCount = Math.min(HISTORY_SIZE, mHistoryCount + 1);
- }
-
- @Override
- public Float addMotionEvent(MotionEvent ev, int pointer) {
- // Add all historic points
- int historyCount = ev.getHistorySize();
- for (int i = 0; i < historyCount; i++) {
- addPositionAndTime(
- ev.getHistoricalEventTime(i), ev.getHistoricalAxisValue(mAxis, pointer, i));
- }
-
- // Start index for the last position (about to be added)
- int eventStartIndex = mHistoryStart;
- addPositionAndTime(ev.getEventTime(), ev.getAxisValue(mAxis, pointer));
- return solveUnweightedLeastSquaresDeg2(eventStartIndex);
- }
-
- /**
- * Solves the instantaneous velocity.
- * Based on solveUnweightedLeastSquaresDeg2 in VelocityTracker.cpp
- */
- private Float solveUnweightedLeastSquaresDeg2(final int pointPos) {
- final float eventTime = mHistoricTimes[pointPos];
-
- float sxi = 0, sxiyi = 0, syi = 0, sxi2 = 0, sxi3 = 0, sxi2yi = 0, sxi4 = 0;
- int count = 0;
- for (int i = 0; i < mHistoryCount; i++) {
- int index = pointPos - i;
- if (index < 0) {
- index += HISTORY_SIZE;
- }
-
- float time = mHistoricTimes[index];
- float age = eventTime - time;
- if (age > HORIZON_MS) {
- break;
- }
- count++;
- float xi = -age;
-
- float yi = mHistoricPos[index];
- float xi2 = xi * xi;
- float xi3 = xi2 * xi;
- float xi4 = xi3 * xi;
- float xiyi = xi * yi;
- float xi2yi = xi2 * yi;
-
- sxi += xi;
- sxi2 += xi2;
- sxiyi += xiyi;
- sxi2yi += xi2yi;
- syi += yi;
- sxi3 += xi3;
- sxi4 += xi4;
- }
-
- if (count < 3) {
- // Too few samples
- if (count == 2) {
- int endPos = pointPos - 1;
- if (endPos < 0) {
- endPos += HISTORY_SIZE;
- }
- float denominator = eventTime - mHistoricTimes[endPos];
- if (denominator != 0) {
- return (eventTime - mHistoricPos[endPos]) / denominator;
-
- }
- }
- return null;
- }
-
- float Sxx = sxi2 - sxi * sxi / count;
- float Sxy = sxiyi - sxi * syi / count;
- float Sxx2 = sxi3 - sxi * sxi2 / count;
- float Sx2y = sxi2yi - sxi2 * syi / count;
- float Sx2x2 = sxi4 - sxi2 * sxi2 / count;
-
- float denominator = Sxx * Sx2x2 - Sxx2 * Sxx2;
- if (denominator == 0) {
- // division by 0 when computing velocity
- return null;
- }
- // Compute a
- // float numerator = Sx2y*Sxx - Sxy*Sxx2;
-
- // Compute b
- float numerator = Sxy * Sx2x2 - Sx2y * Sxx2;
- float b = numerator / denominator;
-
- // Compute c
- // float c = syi/count - b * sxi/count - a * sxi2/count;
-
- return b;
+ mVelocityTracker.clear();
}
}
}
diff --git a/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
index e798d5c..1c3c9c2 100644
--- a/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
+++ b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
@@ -40,10 +40,14 @@
newPercent = prop.mInterpolator.getInterpolation(newPercent);
prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent);
}
- onUpdate(percent);
+ onUpdate(percent, false /* initOnly */);
}
- public abstract void onUpdate(float percent);
+ /**
+ * @param percent The total animation progress.
+ * @param initOnly When true, only does enough work to initialize the animation.
+ */
+ public abstract void onUpdate(float percent, boolean initOnly);
public final class FloatProp {
@@ -64,5 +68,12 @@
mAllProperties.add(this);
}
+
+ /**
+ * Gets the start value.
+ */
+ public float getStartValue() {
+ return mStart;
+ }
}
}
diff --git a/quickstep/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
index 0a98e1b..449dba8 100644
--- a/quickstep/src/com/android/quickstep/util/NavBarPosition.java
+++ b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
@@ -19,7 +19,7 @@
import android.view.Surface;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController.Info;
import com.android.quickstep.SysUINavigationMode;
/**
@@ -30,7 +30,7 @@
private final SysUINavigationMode.Mode mMode;
private final int mDisplayRotation;
- public NavBarPosition(SysUINavigationMode.Mode mode, DefaultDisplay.Info info) {
+ public NavBarPosition(SysUINavigationMode.Mode mode, Info info) {
mMode = mode;
mDisplayRotation = info.rotation;
}
diff --git a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
new file mode 100644
index 0000000..5cf4f0b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.util.Log;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
+
+/**
+ * Runs an animation from overview to home. Currently, this animation is just a wrapper around the
+ * normal state transition and may play a {@link WorkspaceRevealAnim} if we're starting from an
+ * upward fling.
+ */
+public class OverviewToHomeAnim {
+
+ private static final String TAG = "OverviewToHomeAnim";
+
+ private final Launcher mLauncher;
+ private final Runnable mOnReachedHome;
+
+ // Only run mOnReachedHome when both of these are true.
+ private boolean mIsHomeStaggeredAnimFinished;
+ private boolean mIsOverviewHidden;
+
+ public OverviewToHomeAnim(Launcher launcher, Runnable onReachedHome) {
+ mLauncher = launcher;
+ mOnReachedHome = onReachedHome;
+ }
+
+ /**
+ * Starts the animation. If velocity < 0 (i.e. upwards), also plays a
+ * {@link WorkspaceRevealAnim}.
+ */
+ public void animateWithVelocity(float velocity) {
+ StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+ LauncherState startState = stateManager.getState();
+ if (startState != OVERVIEW) {
+ Log.e(TAG, "animateFromOverviewToHome: unexpected start state " + startState);
+ }
+ AnimatorSet anim = new AnimatorSet();
+
+ boolean playWorkspaceRevealAnim = velocity < 0;
+ if (playWorkspaceRevealAnim) {
+ WorkspaceRevealAnim workspaceRevealAnim = new WorkspaceRevealAnim(mLauncher,
+ false /* animateOverviewScrim */);
+ workspaceRevealAnim.addAnimatorListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ mIsHomeStaggeredAnimFinished = true;
+ maybeOverviewToHomeAnimComplete();
+ }
+ });
+ anim.play(workspaceRevealAnim.getAnimators());
+ } else {
+ mIsHomeStaggeredAnimFinished = true;
+ }
+
+ StateAnimationConfig config = new StateAnimationConfig();
+ if (playWorkspaceRevealAnim) {
+ // WorkspaceRevealAnim handles the depth, so don't interfere.
+ config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+ }
+ config.duration = startState.getTransitionDuration(mLauncher);
+ AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+ startState, NORMAL, config);
+ stateAnim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ mIsOverviewHidden = true;
+ maybeOverviewToHomeAnimComplete();
+ }
+ });
+ anim.play(stateAnim);
+ stateManager.setCurrentAnimation(anim, NORMAL);
+ anim.start();
+ }
+
+ private void maybeOverviewToHomeAnimComplete() {
+ if (mIsHomeStaggeredAnimFinished && mIsOverviewHidden) {
+ mOnReachedHome.run();
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ProtoTracer.java b/quickstep/src/com/android/quickstep/util/ProtoTracer.java
new file mode 100644
index 0000000..ef9586d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ProtoTracer.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static com.android.launcher3.tracing.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_H_VALUE;
+import static com.android.launcher3.tracing.LauncherTraceFileProto.MagicNumber.MAGIC_NUMBER_L_VALUE;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import android.os.Trace;
+import com.android.launcher3.tracing.LauncherTraceProto;
+import com.android.launcher3.tracing.LauncherTraceEntryProto;
+import com.android.launcher3.tracing.LauncherTraceFileProto;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.systemui.shared.tracing.FrameProtoTracer;
+import com.android.systemui.shared.tracing.FrameProtoTracer.ProtoTraceParams;
+import com.android.systemui.shared.tracing.ProtoTraceable;
+import com.google.protobuf.MessageLite;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Queue;
+
+
+/**
+ * Controller for coordinating winscope proto tracing.
+ */
+public class ProtoTracer implements ProtoTraceParams<MessageLite.Builder,
+ LauncherTraceFileProto.Builder, LauncherTraceEntryProto.Builder,
+ LauncherTraceProto.Builder> {
+
+ public static final MainThreadInitializedObject<ProtoTracer> INSTANCE =
+ new MainThreadInitializedObject<>(ProtoTracer::new);
+
+ private static final String TAG = "ProtoTracer";
+ private static final long MAGIC_NUMBER_VALUE =
+ ((long) MAGIC_NUMBER_H_VALUE << 32) | MAGIC_NUMBER_L_VALUE;
+
+ private final Context mContext;
+ private final FrameProtoTracer<MessageLite.Builder, LauncherTraceFileProto.Builder,
+ LauncherTraceEntryProto.Builder, LauncherTraceProto.Builder> mProtoTracer;
+
+ public ProtoTracer(Context context) {
+ mContext = context;
+ mProtoTracer = new FrameProtoTracer<>(this);
+ }
+
+ @Override
+ public File getTraceFile() {
+ return new File(mContext.getFilesDir(), "launcher_trace.pb");
+ }
+
+ @Override
+ public LauncherTraceFileProto.Builder getEncapsulatingTraceProto() {
+ return LauncherTraceFileProto.newBuilder();
+ }
+
+ @Override
+ public LauncherTraceEntryProto.Builder updateBufferProto(
+ LauncherTraceEntryProto.Builder reuseObj,
+ ArrayList<ProtoTraceable<LauncherTraceProto.Builder>> traceables) {
+ Trace.beginSection("ProtoTracer.updateBufferProto");
+ LauncherTraceEntryProto.Builder proto = LauncherTraceEntryProto.newBuilder();
+ proto.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
+ LauncherTraceProto.Builder launcherProto = LauncherTraceProto.newBuilder();
+ for (ProtoTraceable t : traceables) {
+ t.writeToProto(launcherProto);
+ }
+ proto.setLauncher(launcherProto);
+ Trace.endSection();
+ return proto;
+ }
+
+ @Override
+ public byte[] serializeEncapsulatingProto(LauncherTraceFileProto.Builder encapsulatingProto,
+ Queue<LauncherTraceEntryProto.Builder> buffer) {
+ Trace.beginSection("ProtoTracer.serializeEncapsulatingProto");
+ encapsulatingProto.setMagicNumber(MAGIC_NUMBER_VALUE);
+ for (LauncherTraceEntryProto.Builder entry : buffer) {
+ encapsulatingProto.addEntry(entry);
+ }
+ byte[] bytes = encapsulatingProto.build().toByteArray();
+ Trace.endSection();
+ return bytes;
+ }
+
+ @Override
+ public byte[] getProtoBytes(MessageLite.Builder proto) {
+ return proto.build().toByteArray();
+ }
+
+ @Override
+ public int getProtoSize(MessageLite.Builder proto) {
+ return proto.build().getSerializedSize();
+ }
+
+ public void start() {
+ mProtoTracer.start();
+ }
+
+ public void stop() {
+ mProtoTracer.stop();
+ }
+
+ public void add(ProtoTraceable<LauncherTraceProto.Builder> traceable) {
+ mProtoTracer.add(traceable);
+ }
+
+ public void remove(ProtoTraceable<LauncherTraceProto.Builder> traceable) {
+ mProtoTracer.remove(traceable);
+ }
+
+ public void scheduleFrameUpdate() {
+ mProtoTracer.scheduleFrameUpdate();
+ }
+
+ public void update() {
+ mProtoTracer.update();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index 9ed2bbe..6d6e802 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -21,19 +21,18 @@
import static com.android.launcher3.LauncherState.HINT_STATE;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
import android.content.SharedPreferences;
-import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.hybridhotseat.HotseatPredictionController;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.OnboardingPrefs;
import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.views.AllAppsEduView;
@@ -41,9 +40,9 @@
/**
* Extends {@link OnboardingPrefs} for quickstep-specific onboarding data.
*/
-public class QuickstepOnboardingPrefs extends OnboardingPrefs<BaseQuickstepLauncher> {
+public class QuickstepOnboardingPrefs extends OnboardingPrefs<QuickstepLauncher> {
- public QuickstepOnboardingPrefs(BaseQuickstepLauncher launcher, SharedPreferences sharedPrefs) {
+ public QuickstepOnboardingPrefs(QuickstepLauncher launcher, SharedPreferences sharedPrefs) {
super(launcher, sharedPrefs);
StateManager<LauncherState> stateManager = launcher.getStateManager();
@@ -65,44 +64,8 @@
});
}
- boolean shelfBounceSeen = getBoolean(SHELF_BOUNCE_SEEN);
- if (!shelfBounceSeen && ENABLE_OVERVIEW_ACTIONS.get()
- && removeShelfFromOverview(launcher)) {
- // There's no shelf in overview, so don't bounce it (can't get to all apps anyway).
- shelfBounceSeen = true;
- mSharedPrefs.edit().putBoolean(SHELF_BOUNCE_SEEN, shelfBounceSeen).apply();
- }
- if (!shelfBounceSeen) {
- stateManager.addStateListener(new StateListener<LauncherState>() {
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- LauncherState prevState = stateManager.getLastState();
-
- if ((finalState == ALL_APPS && prevState == OVERVIEW) ||
- hasReachedMaxCount(SHELF_BOUNCE_COUNT)) {
- mSharedPrefs.edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
- stateManager.removeStateListener(this);
- }
- }
- });
- }
-
- if (!hasReachedMaxCount(ALL_APPS_COUNT)) {
- stateManager.addStateListener(new StateListener<LauncherState>() {
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- if (finalState == ALL_APPS) {
- if (incrementEventCount(ALL_APPS_COUNT)) {
- stateManager.removeStateListener(this);
- mLauncher.getScrimView().updateDragHandleVisibility();
- }
- }
- }
- });
- }
-
- if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() && !hasReachedMaxCount(
- HOTSEAT_DISCOVERY_TIP_COUNT)) {
+ if (!Utilities.IS_RUNNING_IN_TEST_HARNESS
+ && !hasReachedMaxCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
stateManager.addStateListener(new StateListener<LauncherState>() {
boolean mFromAllApps = false;
diff --git a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
new file mode 100644
index 0000000..5c72c8f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.quickstep.views.RecentsView;
+
+public class RecentsAtomicAnimationFactory<ACTIVITY_TYPE extends StatefulActivity, STATE_TYPE>
+ extends AtomicAnimationFactory<STATE_TYPE> {
+
+ public static final int INDEX_RECENTS_FADE_ANIM = AtomicAnimationFactory.NEXT_INDEX + 0;
+ public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = AtomicAnimationFactory.NEXT_INDEX + 1;
+
+ private static final int MY_ANIM_COUNT = 2;
+
+ protected final ACTIVITY_TYPE mActivity;
+
+ public RecentsAtomicAnimationFactory(ACTIVITY_TYPE activity) {
+ super(MY_ANIM_COUNT);
+ mActivity = activity;
+ }
+
+ @Override
+ public Animator createStateElementAnimation(int index, float... values) {
+ switch (index) {
+ case INDEX_RECENTS_FADE_ANIM:
+ return ObjectAnimator.ofFloat(mActivity.getOverviewPanel(),
+ RecentsView.CONTENT_ALPHA, values);
+ case INDEX_RECENTS_TRANSLATE_X_ANIM: {
+ RecentsView rv = mActivity.getOverviewPanel();
+ return new SpringAnimationBuilder(mActivity)
+ .setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE)
+ .setDampingRatio(0.8f)
+ .setStiffness(250)
+ .setValues(values)
+ .build(rv, ADJACENT_PAGE_HORIZONTAL_OFFSET);
+ }
+ default:
+ return super.createStateElementAnimation(index, values);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index d822b6c..7cfd151 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -16,31 +16,25 @@
package com.android.quickstep.util;
-import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
-import static com.android.launcher3.logging.LoggerUtils.extractObjectNameAndAddress;
import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
+import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI;
+import static com.android.quickstep.BaseActivityInterface.getTaskDimension;
import static java.lang.annotation.RetentionPolicy.SOURCE;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.database.ContentObserver;
import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.os.Handler;
-import android.provider.Settings;
import android.util.Log;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
@@ -48,17 +42,17 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.util.WindowBounds;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.SettingsCache;
import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.systemui.shared.system.ConfigurationCompat;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.views.TaskView;
import java.lang.annotation.Retention;
import java.util.function.IntConsumer;
@@ -71,17 +65,12 @@
* This class has initial default state assuming the device and foreground app have
* no ({@link Surface#ROTATION_0} rotation.
*/
-public final class RecentsOrientedState implements SharedPreferences.OnSharedPreferenceChangeListener {
+public class RecentsOrientedState implements
+ SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "RecentsOrientedState";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
- private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- updateAutoRotateSetting();
- }
- };
@Retention(SOURCE)
@IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
public @interface SurfaceRotation {}
@@ -91,6 +80,7 @@
private @SurfaceRotation int mTouchRotation = ROTATION_0;
private @SurfaceRotation int mDisplayRotation = ROTATION_0;
private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0;
+ private @SurfaceRotation int mRecentsRotation = ROTATION_0 - 1;
// Launcher activity supports multiple orientation, but fallback activity does not
private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
@@ -124,16 +114,20 @@
| FLAG_SWIPE_UP_NOT_RUNNING;
private final Context mContext;
- private final ContentResolver mContentResolver;
private final SharedPreferences mSharedPrefs;
private final OrientationEventListener mOrientationListener;
+ private final SettingsCache mSettingsCache;
+ private final SettingsCache.OnChangeListener mRotationChangeListener =
+ isEnabled -> updateAutoRotateSetting();
private final Matrix mTmpMatrix = new Matrix();
private int mFlags;
private int mPreviousRotation = ROTATION_0;
+ private boolean mListenersInitialized = false;
- @Nullable private Configuration mActivityConfiguration;
+ // Combined int which encodes the full state.
+ private int mStateId = 0;
/**
* @param rotationChangeListener Callback for receiving rotation events when rotation watcher
@@ -143,7 +137,6 @@
public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy,
IntConsumer rotationChangeListener) {
mContext = context;
- mContentResolver = context.getContentResolver();
mSharedPrefs = Utilities.getPrefs(context);
mOrientationListener = new OrientationEventListener(context) {
@Override
@@ -159,23 +152,38 @@
mFlags = sizeStrategy.rotationSupportedByActivity
? FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY : 0;
- Resources res = context.getResources();
- int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
- * res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE;
- if (originalSmallestWidth < 600) {
- mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
- }
mFlags |= FLAG_SWIPE_UP_NOT_RUNNING;
+ mSettingsCache = SettingsCache.INSTANCE.get(mContext);
initFlags();
}
/**
- * Sets the configuration for the recents activity, which could affect the activity's rotation
+ * Sets the device profile for the current state.
+ */
+ public void setDeviceProfile(DeviceProfile deviceProfile) {
+ boolean oldMultipleOrientationsSupported = isMultipleOrientationSupportedByDevice();
+ setFlag(FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY, !deviceProfile.allowRotation);
+ if (mListenersInitialized) {
+ boolean newMultipleOrientationsSupported = isMultipleOrientationSupportedByDevice();
+ // If isMultipleOrientationSupportedByDevice is changed, init or destroy listeners
+ // accordingly.
+ if (newMultipleOrientationsSupported != oldMultipleOrientationsSupported) {
+ if (newMultipleOrientationsSupported) {
+ initMultipleOrientationListeners();
+ } else {
+ destroyMultipleOrientationListeners();
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the rotation for the recents activity, which could affect the appearance of task view.
* @see #update(int, int)
*/
- public boolean setActivityConfiguration(Configuration activityConfiguration) {
- mActivityConfiguration = activityConfiguration;
- return update(mTouchRotation, mDisplayRotation);
+ public boolean setRecentsRotation(@SurfaceRotation int recentsRotation) {
+ mRecentsRotation = recentsRotation;
+ return updateHandler();
}
/**
@@ -189,8 +197,7 @@
* Sets if the swipe up gesture is currently running or not
*/
public boolean setGestureActive(boolean isGestureActive) {
- setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive);
- return update(mTouchRotation, mDisplayRotation);
+ return setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive);
}
/**
@@ -203,18 +210,17 @@
*/
public boolean update(
@SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation) {
- mRecentsActivityRotation = inferRecentsActivityRotation(displayRotation);
mDisplayRotation = displayRotation;
mTouchRotation = touchRotation;
mPreviousRotation = touchRotation;
+ return updateHandler();
+ }
- PagedOrientationHandler oldHandler = mOrientationHandler;
+ private boolean updateHandler() {
+ mRecentsActivityRotation = inferRecentsActivityRotation(mDisplayRotation);
if (mRecentsActivityRotation == mTouchRotation
|| (canRecentsActivityRotate() && (mFlags & FLAG_SWIPE_UP_NOT_RUNNING) != 0)) {
mOrientationHandler = PagedOrientationHandler.PORTRAIT;
- if (DEBUG) {
- Log.d(TAG, "current RecentsOrientedState: " + this);
- }
} else if (mTouchRotation == ROTATION_90) {
mOrientationHandler = PagedOrientationHandler.LANDSCAPE;
} else if (mTouchRotation == ROTATION_270) {
@@ -225,21 +231,26 @@
if (DEBUG) {
Log.d(TAG, "current RecentsOrientedState: " + this);
}
- return oldHandler != mOrientationHandler;
+
+ int oldStateId = mStateId;
+ // Each SurfaceRotation value takes two bits
+ mStateId = (((((mFlags << 2)
+ | mDisplayRotation) << 2)
+ | mTouchRotation) << 3)
+ | (mRecentsRotation < 0 ? 7 : mRecentsRotation);
+ return mStateId != oldStateId;
}
@SurfaceRotation
private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
if (isRecentsActivityRotationAllowed()) {
- return mActivityConfiguration == null
- ? displayRotation
- : ConfigurationCompat.getWindowConfigurationRotation(mActivityConfiguration);
+ return mRecentsRotation < 0 ? displayRotation : mRecentsRotation;
} else {
return ROTATION_0;
}
}
- private void setFlag(int mask, boolean enabled) {
+ private boolean setFlag(int mask, boolean enabled) {
boolean wasRotationEnabled = !TestProtocol.sDisableSensorRotation
&& (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED
&& !canRecentsActivityRotate();
@@ -261,6 +272,7 @@
}
});
}
+ return updateHandler();
}
@Override
@@ -271,36 +283,42 @@
}
private void updateAutoRotateSetting() {
- setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver,
- Settings.System.ACCELEROMETER_ROTATION, 1) == 1);
+ setFlag(FLAG_SYSTEM_ROTATION_ALLOWED,
+ mSettingsCache.getValue(ROTATION_SETTING_URI, 1));
}
private void updateHomeRotationSetting() {
- setFlag(FLAG_HOME_ROTATION_ALLOWED_IN_PREFS,
- mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false));
+ boolean homeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false);
+ setFlag(FLAG_HOME_ROTATION_ALLOWED_IN_PREFS, homeRotationEnabled);
+ SystemUiProxy.INSTANCE.get(mContext).setHomeRotationEnabled(homeRotationEnabled);
}
private void initFlags() {
- SysUINavigationMode.Mode currentMode = SysUINavigationMode.getMode(mContext);
- boolean rotationWatcherSupported = mOrientationListener.canDetectOrientation() &&
- currentMode != TWO_BUTTONS;
- setFlag(FLAG_ROTATION_WATCHER_SUPPORTED, rotationWatcherSupported);
+ setFlag(FLAG_ROTATION_WATCHER_SUPPORTED, mOrientationListener.canDetectOrientation());
// initialize external flags
updateAutoRotateSetting();
updateHomeRotationSetting();
}
+ private void initMultipleOrientationListeners() {
+ mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
+ mSettingsCache.register(ROTATION_SETTING_URI, mRotationChangeListener);
+ }
+
+ private void destroyMultipleOrientationListeners() {
+ mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
+ mSettingsCache.unregister(ROTATION_SETTING_URI, mRotationChangeListener);
+ }
+
/**
* Initializes any system values and registers corresponding change listeners. It must be
* paired with {@link #destroyListeners()} call
*/
public void initListeners() {
+ mListenersInitialized = true;
if (isMultipleOrientationSupportedByDevice()) {
- mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
- mContentResolver.registerContentObserver(
- Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
- false, mSystemAutoRotateObserver);
+ initMultipleOrientationListeners();
}
initFlags();
}
@@ -309,9 +327,9 @@
* Unregisters any previously registered listeners.
*/
public void destroyListeners() {
+ mListenersInitialized = false;
if (isMultipleOrientationSupportedByDevice()) {
- mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
- mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
+ destroyMultipleOrientationListeners();
}
setRotationWatcherEnabled(false);
}
@@ -335,6 +353,13 @@
return mRecentsActivityRotation;
}
+ /**
+ * Returns an id that can be used to tracking internal changes
+ */
+ public int getStateId() {
+ return mStateId;
+ }
+
public boolean isMultipleOrientationSupportedByDevice() {
return (mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
== MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE;
@@ -369,15 +394,14 @@
*/
public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) {
Rect insets = dp.getInsets();
- float fullWidth = dp.widthPx - insets.left - insets.right;
- float fullHeight = dp.heightPx - insets.top - insets.bottom;
-
- if (dp.isMultiWindowMode) {
- WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(mContext);
- outPivot.set(bounds.availableSize.x, bounds.availableSize.y);
- } else {
- outPivot.set(fullWidth, fullHeight);
+ float fullWidth = dp.widthPx;
+ float fullHeight = dp.heightPx;
+ if (TaskView.CLIP_STATUS_AND_NAV_BARS) {
+ fullWidth -= insets.left + insets.right;
+ fullHeight -= insets.top + insets.bottom;
}
+
+ getTaskDimension(mContext, dp, outPivot);
float scale = Math.min(outPivot.x / taskView.width(), outPivot.y / taskView.height());
// We also scale the preview as part of fullScreenParams, so account for that as well.
if (fullWidth > 0) {
@@ -512,19 +536,43 @@
}
}
+ /**
+ * Contrary to {@link #postDisplayRotation}.
+ */
+ public static void preDisplayRotation(@SurfaceRotation int displayRotation,
+ float screenWidth, float screenHeight, Matrix out) {
+ switch (displayRotation) {
+ case ROTATION_0:
+ return;
+ case ROTATION_90:
+ out.postRotate(90);
+ out.postTranslate(screenWidth, 0);
+ break;
+ case ROTATION_180:
+ out.postRotate(180);
+ out.postTranslate(screenHeight, screenWidth);
+ break;
+ case ROTATION_270:
+ out.postRotate(270);
+ out.postTranslate(0, screenHeight);
+ break;
+ }
+ }
+
@NonNull
@Override
public String toString() {
boolean systemRotationOn = (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0;
return "["
- + "this=" + extractObjectNameAndAddress(super.toString())
- + " mOrientationHandler=" +
- extractObjectNameAndAddress(mOrientationHandler.toString())
+ + "this=" + nameAndAddress(this)
+ + " mOrientationHandler=" + nameAndAddress(mOrientationHandler)
+ " mDisplayRotation=" + mDisplayRotation
+ " mTouchRotation=" + mTouchRotation
+ " mRecentsActivityRotation=" + mRecentsActivityRotation
+ + " mRecentsRotation=" + mRecentsRotation
+ " isRecentsActivityRotationAllowed=" + isRecentsActivityRotationAllowed()
+ " mSystemRotation=" + systemRotationOn
+ + " mStateId=" + mStateId
+ " mFlags=" + mFlags
+ "]";
}
@@ -534,10 +582,30 @@
*/
public DeviceProfile getLauncherDeviceProfile() {
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
- // TODO also check the natural orientation is landscape or portrait
- return (mRecentsActivityRotation == ROTATION_90
- || mRecentsActivityRotation == ROTATION_270)
- ? idp.landscapeProfile
- : idp.portraitProfile;
+ Point currentSize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
+
+ int width, height;
+ if ((mRecentsActivityRotation == ROTATION_90 || mRecentsActivityRotation == ROTATION_270)) {
+ width = Math.max(currentSize.x, currentSize.y);
+ height = Math.min(currentSize.x, currentSize.y);
+ } else {
+ width = Math.min(currentSize.x, currentSize.y);
+ height = Math.max(currentSize.x, currentSize.y);
+ }
+
+ DeviceProfile bestMatch = idp.supportedProfiles.get(0);
+ float minDiff = Float.MAX_VALUE;
+ for (DeviceProfile profile : idp.supportedProfiles) {
+ float diff = Math.abs(profile.widthPx - width) + Math.abs(profile.heightPx - height);
+ if (diff < minDiff) {
+ minDiff = diff;
+ bestMatch = profile;
+ }
+ }
+ return bestMatch;
+ }
+
+ private static String nameAndAddress(Object obj) {
+ return obj.getClass().getSimpleName() + "@" + obj.hashCode();
}
}
diff --git a/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
new file mode 100644
index 0000000..02ec68a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.FlingSpringAnim;
+import com.android.launcher3.util.DynamicResource;
+import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
+import com.android.systemui.plugins.ResourceProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Applies spring forces to animate from a starting rect to a target rect,
+ * while providing update callbacks to the caller.
+ */
+public class RectFSpringAnim extends ReleaseCheck {
+
+ private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_X =
+ new FloatPropertyCompat<RectFSpringAnim>("rectCenterXSpring") {
+ @Override
+ public float getValue(RectFSpringAnim anim) {
+ return anim.mCurrentCenterX;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim anim, float currentCenterX) {
+ anim.mCurrentCenterX = currentCenterX;
+ anim.onUpdate();
+ }
+ };
+
+ private static final FloatPropertyCompat<RectFSpringAnim> RECT_Y =
+ new FloatPropertyCompat<RectFSpringAnim>("rectYSpring") {
+ @Override
+ public float getValue(RectFSpringAnim anim) {
+ return anim.mCurrentY;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim anim, float y) {
+ anim.mCurrentY = y;
+ anim.onUpdate();
+ }
+ };
+
+ private static final FloatPropertyCompat<RectFSpringAnim> RECT_SCALE_PROGRESS =
+ new FloatPropertyCompat<RectFSpringAnim>("rectScaleProgress") {
+ @Override
+ public float getValue(RectFSpringAnim object) {
+ return object.mCurrentScaleProgress;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim object, float value) {
+ object.mCurrentScaleProgress = value;
+ object.onUpdate();
+ }
+ };
+
+ private final RectF mStartRect;
+ private final RectF mTargetRect;
+ private final RectF mCurrentRect = new RectF();
+ private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>();
+ private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>();
+
+ private float mCurrentCenterX;
+ private float mCurrentY;
+ // If true, tracking the bottom of the rects, else tracking the top.
+ private boolean mTrackingBottomY;
+ private float mCurrentScaleProgress;
+ private FlingSpringAnim mRectXAnim;
+ private FlingSpringAnim mRectYAnim;
+ private SpringAnimation mRectScaleAnim;
+ private boolean mAnimsStarted;
+ private boolean mRectXAnimEnded;
+ private boolean mRectYAnimEnded;
+ private boolean mRectScaleAnimEnded;
+
+ private float mMinVisChange;
+ private float mYOvershoot;
+
+ public RectFSpringAnim(RectF startRect, RectF targetRect, Context context) {
+ mStartRect = startRect;
+ mTargetRect = targetRect;
+ mCurrentCenterX = mStartRect.centerX();
+
+ mTrackingBottomY = startRect.bottom < targetRect.bottom;
+ mCurrentY = mTrackingBottomY ? mStartRect.bottom : mStartRect.top;
+
+ ResourceProvider rp = DynamicResource.provider(context);
+ mMinVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change);
+ mYOvershoot = rp.getDimension(R.dimen.swipe_up_y_overshoot);
+ setCanRelease(true);
+ }
+
+ public void onTargetPositionChanged() {
+ if (mRectXAnim != null && mRectXAnim.getTargetPosition() != mTargetRect.centerX()) {
+ mRectXAnim.updatePosition(mCurrentCenterX, mTargetRect.centerX());
+ }
+
+ if (mRectYAnim != null) {
+ if (mTrackingBottomY && mRectYAnim.getTargetPosition() != mTargetRect.bottom) {
+ mRectYAnim.updatePosition(mCurrentY, mTargetRect.bottom);
+ } else if (!mTrackingBottomY && mRectYAnim.getTargetPosition() != mTargetRect.top) {
+ mRectYAnim.updatePosition(mCurrentY, mTargetRect.top);
+ }
+ }
+ }
+
+ public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
+ mOnUpdateListeners.add(onUpdateListener);
+ }
+
+ public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
+ mAnimatorListeners.add(animatorListener);
+ }
+
+ /**
+ * Starts the fling/spring animation.
+ * @param context The activity context.
+ * @param velocityPxPerMs Velocity of swipe in px/ms.
+ */
+ public void start(Context context, PointF velocityPxPerMs) {
+ // Only tell caller that we ended if both x and y animations have ended.
+ OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
+ mRectXAnimEnded = true;
+ maybeOnEnd();
+ });
+ OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> {
+ mRectYAnimEnded = true;
+ maybeOnEnd();
+ });
+
+ float startX = mCurrentCenterX;
+ float endX = mTargetRect.centerX();
+ float minXValue = Math.min(startX, endX);
+ float maxXValue = Math.max(startX, endX);
+ mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX,
+ velocityPxPerMs.x * 1000, mMinVisChange, minXValue, maxXValue, 1f, onXEndListener);
+
+ float startVelocityY = velocityPxPerMs.y * 1000;
+ // Scale the Y velocity based on the initial velocity to tune the curves.
+ float springVelocityFactor = 0.1f + 0.9f * Math.abs(startVelocityY) / 20000.0f;
+ float startY = mCurrentY;
+ float endY = mTrackingBottomY ? mTargetRect.bottom : mTargetRect.top;
+ float minYValue = Math.min(startY, endY - mYOvershoot);
+ float maxYValue = Math.max(startY, endY);
+ mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, startVelocityY,
+ mMinVisChange, minYValue, maxYValue, springVelocityFactor, onYEndListener);
+
+ float minVisibleChange = Math.abs(1f / mStartRect.height());
+ ResourceProvider rp = DynamicResource.provider(context);
+ float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio);
+ float stiffness = rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness);
+
+ mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS)
+ .setSpring(new SpringForce(1f)
+ .setDampingRatio(damping)
+ .setStiffness(stiffness))
+ .setStartVelocity(velocityPxPerMs.y * minVisibleChange)
+ .setMaxValue(1f)
+ .setMinimumVisibleChange(minVisibleChange)
+ .addEndListener((animation, canceled, value, velocity) -> {
+ mRectScaleAnimEnded = true;
+ maybeOnEnd();
+ });
+
+ setCanRelease(false);
+ mAnimsStarted = true;
+
+ mRectXAnim.start();
+ mRectYAnim.start();
+ mRectScaleAnim.start();
+ for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+ animatorListener.onAnimationStart(null);
+ }
+ }
+
+ public void end() {
+ if (mAnimsStarted) {
+ mRectXAnim.end();
+ mRectYAnim.end();
+ if (mRectScaleAnim.canSkipToEnd()) {
+ mRectScaleAnim.skipToEnd();
+ }
+ }
+ mRectXAnimEnded = true;
+ mRectYAnimEnded = true;
+ mRectScaleAnimEnded = true;
+ maybeOnEnd();
+ }
+
+ private boolean isEnded() {
+ return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
+ }
+
+ private void onUpdate() {
+ if (isEnded()) {
+ // Prevent further updates from being called. This can happen between callbacks for
+ // ending the x/y/scale animations.
+ return;
+ }
+
+ if (!mOnUpdateListeners.isEmpty()) {
+ float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
+ mTargetRect.width());
+ float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(),
+ mTargetRect.height());
+ if (mTrackingBottomY) {
+ mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentY - currentHeight,
+ mCurrentCenterX + currentWidth / 2, mCurrentY);
+ } else {
+ mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentY,
+ mCurrentCenterX + currentWidth / 2, mCurrentY + currentHeight);
+ }
+ for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
+ onUpdateListener.onUpdate(null, mCurrentRect, mCurrentScaleProgress);
+ }
+ }
+ }
+
+ private void maybeOnEnd() {
+ if (mAnimsStarted && isEnded()) {
+ mAnimsStarted = false;
+ setCanRelease(true);
+ for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+ animatorListener.onAnimationEnd(null);
+ }
+ }
+ }
+
+ public void cancel() {
+ if (mAnimsStarted) {
+ for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
+ onUpdateListener.onCancel();
+ }
+ }
+ end();
+ }
+
+ public interface OnUpdateListener {
+ void onUpdate(@Nullable AppCloseConfig values, RectF currentRect, float progress);
+
+ default void onCancel() { }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RectFSpringAnim2.java b/quickstep/src/com/android/quickstep/util/RectFSpringAnim2.java
new file mode 100644
index 0000000..c331a13
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/RectFSpringAnim2.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.Utilities.dpToPx;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.PathParser;
+import android.util.Property;
+import android.view.animation.Interpolator;
+
+import androidx.core.view.animation.PathInterpolatorCompat;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DynamicResource;
+import com.android.systemui.plugins.ResourceProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Applies spring forces to animate from a starting rect to a target rect,
+ * while providing update callbacks to the caller.
+ */
+public class RectFSpringAnim2 extends RectFSpringAnim {
+
+ private static final FloatPropertyCompat<RectFSpringAnim2> RECT_CENTER_X =
+ new FloatPropertyCompat<RectFSpringAnim2>("rectCenterXSpring") {
+ @Override
+ public float getValue(RectFSpringAnim2 anim) {
+ return anim.mCurrentCenterX;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim2 anim, float currentCenterX) {
+ anim.mCurrentCenterX = currentCenterX;
+ anim.onUpdate();
+ }
+ };
+
+ private static final FloatPropertyCompat<RectFSpringAnim2> RECT_Y =
+ new FloatPropertyCompat<RectFSpringAnim2>("rectYSpring") {
+ @Override
+ public float getValue(RectFSpringAnim2 anim) {
+ return anim.mCurrentCenterY;
+ }
+
+ @Override
+ public void setValue(RectFSpringAnim2 anim, float y) {
+ anim.mCurrentCenterY = y;
+ anim.onUpdate();
+ }
+ };
+
+ private static final Property<RectFSpringAnim2, Float> PROGRESS =
+ new Property<RectFSpringAnim2, Float>(Float.class, "rectFProgress") {
+ @Override
+ public Float get(RectFSpringAnim2 rectFSpringAnim) {
+ return rectFSpringAnim.mProgress;
+ }
+
+ @Override
+ public void set(RectFSpringAnim2 rectFSpringAnim, Float progress) {
+ rectFSpringAnim.mProgress = progress;
+ rectFSpringAnim.onUpdate();
+ }
+ };
+
+ private final RectF mStartRect;
+ private final RectF mTargetRect;
+ private final RectF mCurrentRect = new RectF();
+ private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>();
+ private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>();
+
+ private float mCurrentCenterX;
+ private float mCurrentCenterY;
+
+ private float mTargetX;
+ private float mTargetY;
+
+ // If true, tracking the bottom of the rects, else tracking the top.
+ private float mProgress;
+ private SpringAnimation mRectXAnim;
+ private SpringAnimation mRectYAnim;
+ private ValueAnimator mRectScaleAnim;
+ private boolean mAnimsStarted;
+ private boolean mRectXAnimEnded;
+ private boolean mRectYAnimEnded;
+ private boolean mRectScaleAnimEnded;
+
+ private final float mXDamping;
+ private final float mXStiffness;
+
+ private final float mYDamping;
+ private float mYStiffness;
+
+ private long mDuration;
+
+ private final Interpolator mCloseInterpolator;
+
+ private AppCloseConfig mValues;
+ final float mStartRadius;
+ final float mEndRadius;
+
+ final float mHomeTransYEnd;
+ final float mScaleStart;
+
+ public RectFSpringAnim2(RectF startRect, RectF targetRect, Context context, float startRadius,
+ float endRadius) {
+ super(startRect, targetRect, context);
+ mStartRect = startRect;
+ mTargetRect = targetRect;
+
+ mCurrentCenterY = mStartRect.centerY();
+ mCurrentCenterX = mStartRect.centerX();
+
+ mTargetY = mTargetRect.centerY();
+ mTargetX = mTargetRect.centerX();
+
+ ResourceProvider rp = DynamicResource.provider(context);
+ mXDamping = rp.getFloat(R.dimen.swipe_up_rect_2_x_damping_ratio);
+ mXStiffness = rp.getFloat(R.dimen.swipe_up_rect_2_x_stiffness);
+
+ mYDamping = rp.getFloat(R.dimen.swipe_up_rect_2_y_damping_ratio);
+ mYStiffness = rp.getFloat(R.dimen.swipe_up_rect_2_y_stiffness);
+ mDuration = Math.round(rp.getFloat(R.dimen.swipe_up_duration));
+
+ mHomeTransYEnd = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp));
+ mScaleStart = rp.getFloat(R.dimen.swipe_up_scale_start);
+
+ mCloseInterpolator = getAppCloseInterpolator(context);
+
+ // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
+ // rounding at the end of the animation.
+ mStartRadius = startRadius;
+ mEndRadius = endRadius;
+
+ setCanRelease(true);
+ }
+
+ public void onTargetPositionChanged() {
+ if (mRectXAnim != null && mTargetX != mTargetRect.centerX()) {
+ mTargetX = mTargetRect.centerX();
+ mRectXAnim.animateToFinalPosition(mTargetX);
+ }
+
+ if (mRectYAnim != null) {
+ if (mTargetY != mTargetRect.centerY()) {
+ mTargetY = mTargetRect.centerY();
+ mRectYAnim.animateToFinalPosition(mTargetY);
+ }
+ }
+ }
+
+ public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
+ mOnUpdateListeners.add(onUpdateListener);
+ }
+
+ public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
+ mAnimatorListeners.add(animatorListener);
+ }
+
+ /**
+ * Starts the fling/spring animation.
+ * @param context The activity context.
+ * @param velocityPxPerMs Velocity of swipe in px/ms.
+ */
+ public void start(Context context, PointF velocityPxPerMs) {
+ mRectXAnim = new SpringAnimation(this, RECT_CENTER_X)
+ .setStartValue(mCurrentCenterX)
+ .setStartVelocity(velocityPxPerMs.x * 1000)
+ .setSpring(new SpringForce(mTargetX)
+ .setStiffness(mXStiffness)
+ .setDampingRatio(mXDamping));
+ mRectXAnim.addEndListener(((animation, canceled, centerX, velocityX) -> {
+ mRectXAnimEnded = true;
+ maybeOnEnd();
+ }));
+
+ mRectYAnim = new SpringAnimation(this, RECT_Y)
+ .setStartValue(mCurrentCenterY)
+ .setStartVelocity(velocityPxPerMs.y * 1000)
+ .setSpring(new SpringForce(mTargetY)
+ .setStiffness(mYStiffness)
+ .setDampingRatio(mYDamping));
+ mRectYAnim.addEndListener(((animation, canceled, centerY, velocityY) -> {
+ mRectYAnimEnded = true;
+ maybeOnEnd();
+ }));
+
+ mRectScaleAnim = ObjectAnimator.ofFloat(this, PROGRESS, 0, 1f)
+ .setDuration(mDuration);
+ mRectScaleAnim.setInterpolator(mCloseInterpolator);
+ mRectScaleAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mRectScaleAnimEnded = true;
+ maybeOnEnd();
+ }
+ });
+
+ mValues = buildConfig();
+ mRectScaleAnim.addUpdateListener(mValues);
+
+ setCanRelease(false);
+ mAnimsStarted = true;
+
+ mRectXAnim.start();
+ mRectYAnim.start();
+ mRectScaleAnim.start();
+ for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+ animatorListener.onAnimationStart(null);
+ }
+ }
+
+ private AppCloseConfig buildConfig() {
+ return new AppCloseConfig() {
+ FloatProp mHomeTransY = new FloatProp(0, mHomeTransYEnd, 0, mDuration, LINEAR);
+ FloatProp mHomeScale = new FloatProp(mScaleStart, 1f, 0, mDuration, LINEAR);
+ FloatProp mWindowFadeOut = new FloatProp(1f, 0f, 0, 116, LINEAR);
+ // There should be a slight overlap b/w window fading out and fg fading in.
+ // (fg startDelay < window fade out duration)
+ FloatProp mFgFadeIn = new FloatProp(0, 255f, 100, mDuration - 100, LINEAR);
+ FloatProp mRadius = new FloatProp(mStartRadius, mEndRadius, 0, mDuration, LINEAR);
+ FloatProp mThreePointInterpolation = new FloatProp(0, 1, 0, mDuration, LINEAR);
+
+ @Override
+ public float getWorkspaceTransY() {
+ return mHomeTransY.value;
+ }
+
+ @Override
+ public float getWorkspaceScale() {
+ return mHomeScale.value;
+ }
+
+ @Override
+ public float getWindowAlpha() {
+ return mWindowFadeOut.value;
+ }
+
+ @Override
+ public int getFgAlpha() {
+ return (int) mFgFadeIn.value;
+ }
+
+ @Override
+ public float getCornerRadius() {
+ return mRadius.value;
+ }
+
+ @Override
+ public float getInterpolatedProgress() {
+ return mThreePointInterpolation.value;
+ }
+
+ @Override
+ public void onUpdate(float percent, boolean initOnly) {}
+ };
+ }
+
+ public void end() {
+ if (mAnimsStarted) {
+ if (mRectXAnim.canSkipToEnd()) {
+ mRectXAnim.skipToEnd();
+ }
+ if (mRectYAnim.canSkipToEnd()) {
+ mRectYAnim.skipToEnd();
+ }
+ mRectScaleAnim.end();
+ }
+ mRectXAnimEnded = true;
+ mRectYAnimEnded = true;
+ mRectScaleAnimEnded = true;
+ maybeOnEnd();
+ }
+
+ private boolean isEnded() {
+ return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
+ }
+
+ private void onUpdate() {
+ if (isEnded()) {
+ // Prevent further updates from being called. This can happen between callbacks for
+ // ending the x/y/scale animations.
+ return;
+ }
+
+ if (!mOnUpdateListeners.isEmpty()) {
+ float rectProgress = mProgress;
+ float currentWidth = Utilities.mapRange(rectProgress, mStartRect.width(),
+ mTargetRect.width());
+ float currentHeight = Utilities.mapRange(rectProgress, mStartRect.height(),
+ mTargetRect.height());
+
+ mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
+ mCurrentCenterY - currentHeight / 2,
+ mCurrentCenterX + currentWidth / 2,
+ mCurrentCenterY + currentHeight / 2);
+
+ float currentPlayTime = mRectScaleAnimEnded ? mRectScaleAnim.getDuration()
+ : mRectScaleAnim.getCurrentPlayTime();
+ float linearProgress = Math.min(1f, currentPlayTime / mRectScaleAnim.getDuration());
+ for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
+ onUpdateListener.onUpdate(mValues, mCurrentRect, linearProgress);
+ }
+ }
+ }
+
+ private void maybeOnEnd() {
+ if (mAnimsStarted && isEnded()) {
+ mAnimsStarted = false;
+ setCanRelease(true);
+ for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+ animatorListener.onAnimationEnd(null);
+ }
+ }
+ }
+
+ public void cancel() {
+ if (mAnimsStarted) {
+ for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
+ onUpdateListener.onCancel();
+ }
+ }
+ end();
+ }
+
+ private Interpolator getAppCloseInterpolator(Context context) {
+ ResourceProvider rp = DynamicResource.provider(context);
+ String path = String.format(Locale.ENGLISH,
+ "M 0,0 C %f, %f, %f, %f, %f, %f C %f, %f, %f, %f, 1, 1",
+ rp.getFloat(R.dimen.c1_a),
+ rp.getFloat(R.dimen.c1_b),
+ rp.getFloat(R.dimen.c1_c),
+ rp.getFloat(R.dimen.c1_d),
+ rp.getFloat(R.dimen.mp_x),
+ rp.getFloat(R.dimen.mp_y),
+ rp.getFloat(R.dimen.c2_a),
+ rp.getFloat(R.dimen.c2_b),
+ rp.getFloat(R.dimen.c2_c),
+ rp.getFloat(R.dimen.c2_d));
+ return PathInterpolatorCompat.create(PathParser.createPathFromPathData(path));
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index 04308c8..98dbd47 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -21,31 +21,23 @@
import android.os.Handler;
import com.android.launcher3.LauncherAnimationRunner;
-import com.android.launcher3.WrappedLauncherAnimationRunner;
+import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
public abstract class RemoteAnimationProvider {
- LauncherAnimationRunner mAnimationRunner;
+ RemoteAnimationFactory mAnimationRunner;
public abstract AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
RemoteAnimationTargetCompat[] wallpaperTargets);
ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
- mAnimationRunner = new LauncherAnimationRunner(handler,
- false /* startAtFrontOfQueue */) {
-
- @Override
- public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+ mAnimationRunner = (transit, appTargets, wallpaperTargets, nonApps, result) ->
result.setAnimation(createWindowAnimation(appTargets, wallpaperTargets), context);
- }
- };
- final LauncherAnimationRunner wrapper = new WrappedLauncherAnimationRunner(
- mAnimationRunner, false /* startAtFrontOfQueue */);
-
+ final LauncherAnimationRunner wrapper = new LauncherAnimationRunner(
+ handler, mAnimationRunner, false /* startAtFrontOfQueue */);
return ActivityOptionsCompat.makeRemoteAnimation(
new RemoteAnimationAdapterCompat(wrapper, duration, 0));
}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
index 958ee24..81c124f 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
@@ -34,7 +34,8 @@
public RemoteFadeOutAnimationListener(RemoteAnimationTargetCompat[] appTargets,
RemoteAnimationTargetCompat[] wallpaperTargets) {
- mTarget = new RemoteAnimationTargets(appTargets, wallpaperTargets, MODE_CLOSING);
+ mTarget = new RemoteAnimationTargets(appTargets, wallpaperTargets,
+ new RemoteAnimationTargetCompat[0], MODE_CLOSING);
}
@Override
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
index a770e8e..483a1c6 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
@@ -20,10 +20,7 @@
import android.annotation.TargetApi;
import android.content.Context;
-import android.graphics.Insets;
-import android.graphics.Rect;
import android.os.Build;
-import android.view.WindowInsets.Type;
import android.view.WindowManager;
import android.view.WindowMetrics;
@@ -32,7 +29,7 @@
import androidx.annotation.UiThread;
import com.android.launcher3.R;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.WindowBounds;
import java.util.ArrayList;
@@ -73,11 +70,9 @@
*/
private static WindowBounds createDefaultWindowBounds(Context context) {
WindowMetrics wm = context.getSystemService(WindowManager.class).getMaximumWindowMetrics();
- Insets insets = wm.getWindowInsets().getInsets(Type.systemBars());
+ WindowBounds bounds = WindowBounds.fromWindowMetrics(wm);
- WindowBounds bounds = new WindowBounds(wm.getBounds(),
- new Rect(insets.left, insets.top, insets.right, insets.bottom));
- int rotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
+ int rotation = DisplayController.INSTANCE.get(context).getInfo().rotation;
int halfDividerSize = context.getResources()
.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
new file mode 100644
index 0000000..a147b68
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+
+import android.animation.AnimatorSet;
+import android.app.ActivityOptions;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InsettableFrameLayout;
+import com.android.launcher3.LauncherAnimationRunner;
+import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
+import com.android.launcher3.R;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.TaskViewUtils;
+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 com.android.systemui.shared.system.RemoteTransitionCompat;
+import com.android.systemui.shared.system.RemoteTransitionRunner;
+
+/**
+ * Represent data needed for the transient state when user has selected one app for split screen
+ * and is in the process of either a) selecting a second app or b) exiting intention to invoke split
+ */
+public class SplitSelectStateController {
+
+ private final SystemUiProxy mSystemUiProxy;
+ private TaskView mInitialTaskView;
+ private SplitPositionOption mInitialPosition;
+ private Rect mInitialBounds;
+ private final Handler mHandler;
+
+ public SplitSelectStateController(Handler handler, SystemUiProxy systemUiProxy) {
+ mSystemUiProxy = systemUiProxy;
+ mHandler = handler;
+ }
+
+ /**
+ * To be called after first task selected
+ */
+ public void setInitialTaskSelect(TaskView taskView, SplitPositionOption positionOption,
+ Rect initialBounds) {
+ mInitialTaskView = taskView;
+ mInitialPosition = positionOption;
+ mInitialBounds = initialBounds;
+ }
+
+ /**
+ * To be called after second task selected
+ */
+ public void setSecondTaskId(TaskView taskView) {
+ if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
+ // Assume initial task is for top/left part of screen
+ final int[] taskIds = mInitialPosition.mStagePosition == STAGE_POSITION_TOP_OR_LEFT
+ ? new int[]{mInitialTaskView.getTask().key.id, taskView.getTask().key.id}
+ : new int[]{taskView.getTask().key.id, mInitialTaskView.getTask().key.id};
+
+ RemoteSplitLaunchAnimationRunner animationRunner =
+ new RemoteSplitLaunchAnimationRunner(mInitialTaskView, taskView);
+ mSystemUiProxy.startTasks(taskIds[0], null /* mainOptions */, taskIds[1],
+ null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT,
+ new RemoteTransitionCompat(animationRunner, MAIN_EXECUTOR));
+ return;
+ }
+ // Assume initial mInitialTaskId is for top/left part of screen
+ RemoteAnimationFactory initialSplitRunnerWrapped = new SplitLaunchAnimationRunner(
+ mInitialTaskView, 0);
+ RemoteAnimationFactory secondarySplitRunnerWrapped = new SplitLaunchAnimationRunner(
+ taskView, 1);
+ RemoteAnimationRunnerCompat initialSplitRunner = new LauncherAnimationRunner(
+ new Handler(Looper.getMainLooper()), initialSplitRunnerWrapped,
+ true /* startAtFrontOfQueue */);
+ RemoteAnimationRunnerCompat secondarySplitRunner = new LauncherAnimationRunner(
+ new Handler(Looper.getMainLooper()), secondarySplitRunnerWrapped,
+ true /* startAtFrontOfQueue */);
+ ActivityOptions initialOptions = ActivityOptionsCompat.makeRemoteAnimation(
+ new RemoteAnimationAdapterCompat(initialSplitRunner, 300, 150));
+ ActivityOptions secondaryOptions = ActivityOptionsCompat.makeRemoteAnimation(
+ new RemoteAnimationAdapterCompat(secondarySplitRunner, 300, 150));
+ mSystemUiProxy.startTask(mInitialTaskView.getTask().key.id, mInitialPosition.mStageType,
+ mInitialPosition.mStagePosition,
+ /*null*/ initialOptions.toBundle());
+ Pair<Integer, Integer> compliment = getComplimentaryStageAndPosition(mInitialPosition);
+ mSystemUiProxy.startTask(taskView.getTask().key.id, compliment.first,
+ compliment.second,
+ /*null*/ secondaryOptions.toBundle());
+ // After successful launch, call resetState
+ resetState();
+ }
+
+ /**
+ * @return {@link InsettableFrameLayout.LayoutParams} to correctly position the
+ * split placeholder view
+ */
+ public InsettableFrameLayout.LayoutParams getLayoutParamsForActivePosition(Resources resources,
+ DeviceProfile deviceProfile) {
+ InsettableFrameLayout.LayoutParams params =
+ new InsettableFrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
+ boolean topLeftPosition = mInitialPosition.mStagePosition == STAGE_POSITION_TOP_OR_LEFT;
+ if (deviceProfile.isLandscape) {
+ params.width = (int) resources.getDimension(R.dimen.split_placeholder_size);
+ params.gravity = topLeftPosition ? Gravity.START : Gravity.END;
+ } else {
+ params.height = (int) resources.getDimension(R.dimen.split_placeholder_size);
+ params.gravity = Gravity.TOP;
+ }
+
+ return params;
+ }
+
+ @Nullable
+ public SplitPositionOption getActiveSplitPositionOption() {
+ return mInitialPosition;
+ }
+
+ /**
+ * Requires Shell Transitions
+ */
+ private class RemoteSplitLaunchAnimationRunner implements RemoteTransitionRunner {
+
+ private final TaskView mInitialTaskView;
+ private final TaskView mTaskView;
+
+ RemoteSplitLaunchAnimationRunner(TaskView initialTaskView, TaskView taskView) {
+ mInitialTaskView = initialTaskView;
+ mTaskView = taskView;
+ }
+
+ @Override
+ public void startAnimation(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction t, Runnable finishCallback) {
+ TaskViewUtils.composeRecentsSplitLaunchAnimator(mInitialTaskView, mTaskView,
+ info, t, finishCallback);
+ // After successful launch, call resetState
+ resetState();
+ }
+ }
+
+ /**
+ * LEGACY
+ * @return the opposite stage and position from the {@param position} provided as first and
+ * second object, respectively
+ * Ex. If position is has stage = Main and position = Top/Left, this will return
+ * Pair(stage=Side, position=Bottom/Left)
+ */
+ private Pair<Integer, Integer> getComplimentaryStageAndPosition(SplitPositionOption position) {
+ // Right now this is as simple as flipping between 0 and 1
+ int complimentStageType = position.mStageType ^ 1;
+ int complimentStagePosition = position.mStagePosition ^ 1;
+ return new Pair<>(complimentStageType, complimentStagePosition);
+ }
+
+ /**
+ * LEGACY
+ * Remote animation runner for animation to launch an app.
+ */
+ private class SplitLaunchAnimationRunner implements RemoteAnimationFactory {
+
+ private final TaskView mV;
+ private final int mTargetState;
+
+ SplitLaunchAnimationRunner(TaskView v, int targetState) {
+ mV = v;
+ mTargetState = targetState;
+ }
+
+ @Override
+ public void onCreateAnimation(int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ LauncherAnimationRunner.AnimationResult result) {
+ AnimatorSet anim = new AnimatorSet();
+ BaseQuickstepLauncher activity = BaseActivity.fromContext(mV.getContext());
+ TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(anim, mV,
+ appTargets, wallpaperTargets, nonAppTargets, true, activity.getStateManager(),
+ activity.getDepthController(), mTargetState);
+ result.setAnimation(anim, activity);
+ }
+ }
+
+
+ /**
+ * To be called if split select was cancelled
+ */
+ public void resetState() {
+ mInitialTaskView = null;
+ mInitialPosition = null;
+ mInitialBounds = null;
+ }
+
+ public boolean isSplitSelectActive() {
+ return mInitialTaskView != null;
+ }
+
+ public Rect getInitialBounds() {
+ return mInitialBounds;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
new file mode 100644
index 0000000..ccc587c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.DynamicResource;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.plugins.ResourceProvider;
+
+/**
+ * Creates an animation where all the workspace items are moved into their final location,
+ * staggered row by row from the bottom up.
+ * This is used in conjunction with the swipe up to home animation.
+ */
+public class StaggeredWorkspaceAnim {
+
+ private static final int APP_CLOSE_ROW_START_DELAY_MS = 10;
+ // How long it takes to fade in each staggered row.
+ private static final int ALPHA_DURATION_MS = 250;
+ // Should be used for animations running alongside this StaggeredWorkspaceAnim.
+ public static final int DURATION_MS = 250;
+
+ private static final float MAX_VELOCITY_PX_PER_S = 22f;
+
+ private final float mVelocity;
+ private final float mSpringTransY;
+
+ private final AnimatorSet mAnimators = new AnimatorSet();
+ private final @Nullable View mIgnoredView;
+
+ public StaggeredWorkspaceAnim(Launcher launcher, float velocity, boolean animateOverviewScrim,
+ @Nullable View ignoredView) {
+ this(launcher, velocity, animateOverviewScrim, ignoredView, true);
+ }
+
+ public StaggeredWorkspaceAnim(Launcher launcher, float velocity, boolean animateOverviewScrim,
+ @Nullable View ignoredView, boolean staggerWorkspace) {
+ prepareToAnimate(launcher, animateOverviewScrim);
+
+ mIgnoredView = ignoredView;
+ mVelocity = velocity;
+
+ // Scale the translationY based on the initial velocity to better sync the workspace items
+ // with the floating view.
+ float transFactor = 0.2f + 0.9f * Math.abs(velocity) / MAX_VELOCITY_PX_PER_S;
+ mSpringTransY = transFactor * launcher.getResources()
+ .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y);
+
+ if (staggerWorkspace) {
+ DeviceProfile grid = launcher.getDeviceProfile();
+ Workspace workspace = launcher.getWorkspace();
+ Hotseat hotseat = launcher.getHotseat();
+
+ // Hotseat and QSB takes up two additional rows.
+ int totalRows = grid.inv.numRows + (grid.isVerticalBarLayout() ? 0 : 2);
+
+ // Add animation for all the visible workspace pages
+ workspace.forEachVisiblePage(page -> addAnimationForPage((CellLayout) page, totalRows));
+
+ boolean workspaceClipChildren = workspace.getClipChildren();
+ boolean workspaceClipToPadding = workspace.getClipToPadding();
+ boolean hotseatClipChildren = hotseat.getClipChildren();
+ boolean hotseatClipToPadding = hotseat.getClipToPadding();
+
+ workspace.setClipChildren(false);
+ workspace.setClipToPadding(false);
+ hotseat.setClipChildren(false);
+ hotseat.setClipToPadding(false);
+
+ // Set up springs for the hotseat and qsb.
+ ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets();
+ if (grid.isVerticalBarLayout()) {
+ for (int i = hotseatIcons.getChildCount() - 1; i >= 0; i--) {
+ View child = hotseatIcons.getChildAt(i);
+ CellLayout.LayoutParams lp =
+ ((CellLayout.LayoutParams) child.getLayoutParams());
+ addStaggeredAnimationForView(child, lp.cellY + 1, totalRows);
+ }
+ } else {
+ final int hotseatRow, qsbRow;
+ if (grid.isTaskbarPresent) {
+ qsbRow = grid.inv.numRows + 1;
+ hotseatRow = grid.inv.numRows + 2;
+ } else {
+ hotseatRow = grid.inv.numRows + 1;
+ qsbRow = grid.inv.numRows + 2;
+ }
+ for (int i = hotseatIcons.getChildCount() - 1; i >= 0; i--) {
+ View child = hotseatIcons.getChildAt(i);
+ addStaggeredAnimationForView(child, hotseatRow, totalRows);
+ }
+
+ addStaggeredAnimationForView(hotseat.getQsb(), qsbRow, totalRows);
+ }
+
+ mAnimators.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ workspace.setClipChildren(workspaceClipChildren);
+ workspace.setClipToPadding(workspaceClipToPadding);
+ hotseat.setClipChildren(hotseatClipChildren);
+ hotseat.setClipToPadding(hotseatClipToPadding);
+ }
+ });
+ }
+
+ if (animateOverviewScrim) {
+ PendingAnimation pendingAnimation = new PendingAnimation(DURATION_MS);
+ launcher.getWorkspace().getStateTransitionAnimation()
+ .setScrim(pendingAnimation, NORMAL, new StateAnimationConfig());
+ mAnimators.play(pendingAnimation.buildAnim());
+ }
+
+ addDepthAnimationForState(launcher, NORMAL, DURATION_MS);
+
+ mAnimators.play(launcher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f)
+ .setDuration(DURATION_MS));
+ }
+
+ private void addAnimationForPage(CellLayout page, int totalRows) {
+ ShortcutAndWidgetContainer itemsContainer = page.getShortcutsAndWidgets();
+
+ boolean pageClipChildren = page.getClipChildren();
+ boolean pageClipToPadding = page.getClipToPadding();
+
+ page.setClipChildren(false);
+ page.setClipToPadding(false);
+
+ // Set up springs on workspace items.
+ for (int i = itemsContainer.getChildCount() - 1; i >= 0; i--) {
+ View child = itemsContainer.getChildAt(i);
+ CellLayout.LayoutParams lp = ((CellLayout.LayoutParams) child.getLayoutParams());
+ addStaggeredAnimationForView(child, lp.cellY + lp.cellVSpan, totalRows);
+ }
+
+ mAnimators.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ page.setClipChildren(pageClipChildren);
+ page.setClipToPadding(pageClipToPadding);
+ }
+ });
+ }
+
+ /**
+ * Setup workspace with 0 duration to prepare for our staggered animation.
+ */
+ private void prepareToAnimate(Launcher launcher, boolean animateOverviewScrim) {
+ StateAnimationConfig config = new StateAnimationConfig();
+ config.animFlags = SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER | SKIP_SCRIM;
+ config.duration = 0;
+ // setRecentsAttachedToAppWindow() will animate recents out.
+ launcher.getStateManager().createAtomicAnimation(BACKGROUND_APP, NORMAL, config).start();
+
+ // Stop scrolling so that it doesn't interfere with the translation offscreen.
+ launcher.<RecentsView>getOverviewPanel().getScroller().forceFinished(true);
+
+ if (animateOverviewScrim) {
+ launcher.getWorkspace().getStateTransitionAnimation()
+ .setScrim(NO_ANIM_PROPERTY_SETTER, BACKGROUND_APP, config);
+ }
+ }
+
+ public AnimatorSet getAnimators() {
+ return mAnimators;
+ }
+
+ public StaggeredWorkspaceAnim addAnimatorListener(Animator.AnimatorListener listener) {
+ mAnimators.addListener(listener);
+ return this;
+ }
+
+ /**
+ * Starts the animation.
+ */
+ public void start() {
+ mAnimators.start();
+ }
+
+ /**
+ * Adds an alpha/trans animator for {@param v}, with a start delay based on the view's row.
+ *
+ * @param v A view on the workspace.
+ * @param row The bottom-most row that contains the view.
+ * @param totalRows Total number of rows.
+ */
+ private void addStaggeredAnimationForView(View v, int row, int totalRows) {
+ if (mIgnoredView != null && mIgnoredView == v) return;
+ // Invert the rows, because we stagger starting from the bottom of the screen.
+ int invertedRow = totalRows - row;
+ // Add 1 to the inverted row so that the bottom most row has a start delay.
+ long startDelay = (long) ((invertedRow + 1) * APP_CLOSE_ROW_START_DELAY_MS);
+
+ v.setTranslationY(mSpringTransY);
+
+ ResourceProvider rp = DynamicResource.provider(v.getContext());
+ float stiffness = rp.getFloat(R.dimen.staggered_stiffness);
+ float damping = rp.getFloat(R.dimen.staggered_damping_ratio);
+ float endTransY = 0;
+ float springVelocity = Math.abs(mVelocity) * Math.signum(endTransY - mSpringTransY);
+ ValueAnimator springTransY = new SpringAnimationBuilder(v.getContext())
+ .setStiffness(stiffness)
+ .setDampingRatio(damping)
+ .setMinimumVisibleChange(1f)
+ .setStartValue(mSpringTransY)
+ .setEndValue(endTransY)
+ .setStartVelocity(springVelocity)
+ .build(v, VIEW_TRANSLATE_Y);
+ springTransY.setStartDelay(startDelay);
+ springTransY.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ v.setTranslationY(0f);
+ }
+ });
+ mAnimators.play(springTransY);
+
+ v.setAlpha(0);
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
+ alpha.setInterpolator(LINEAR);
+ alpha.setDuration(ALPHA_DURATION_MS);
+ alpha.setStartDelay(startDelay);
+ alpha.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ v.setAlpha(1f);
+ }
+ });
+ mAnimators.play(alpha);
+ }
+
+ private void addDepthAnimationForState(Launcher launcher, LauncherState state, long duration) {
+ if (!(launcher instanceof BaseQuickstepLauncher)) {
+ return;
+ }
+ PendingAnimation builder = new PendingAnimation(duration);
+ DepthController depthController = ((BaseQuickstepLauncher) launcher).getDepthController();
+ depthController.setStateWithAnimation(state, new StateAnimationConfig(), builder);
+ mAnimators.play(builder.buildAnim());
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java b/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java
new file mode 100644
index 0000000..3b4fd31
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.View;
+
+import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+import com.android.systemui.shared.system.ViewRootImplCompat;
+
+import java.util.function.Consumer;
+
+
+/**
+ * Helper class to apply surface transactions in sync with RenderThread similar to
+ * android.view.SyncRtSurfaceTransactionApplier
+ * with some Launcher specific utility methods
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class SurfaceTransactionApplier extends ReleaseCheck {
+
+ private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
+
+ private final SurfaceControl mBarrierSurfaceControl;
+ private final ViewRootImplCompat mTargetViewRootImpl;
+ private final Handler mApplyHandler;
+
+ private int mLastSequenceNumber = 0;
+
+ /**
+ * @param targetView The view in the surface that acts as synchronization anchor.
+ */
+ public SurfaceTransactionApplier(View targetView) {
+ mTargetViewRootImpl = new ViewRootImplCompat(targetView);
+ mBarrierSurfaceControl = mTargetViewRootImpl.getRenderSurfaceControl();
+ mApplyHandler = new Handler(this::onApplyMessage);
+ }
+
+ protected boolean onApplyMessage(Message msg) {
+ if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
+ setCanRelease(msg.arg1 == mLastSequenceNumber);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Schedules applying surface parameters on the next frame.
+ *
+ * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
+ * this method to avoid synchronization issues.
+ */
+ public void scheduleApply(final SurfaceParams... params) {
+ View view = mTargetViewRootImpl.getView();
+ if (view == null) {
+ return;
+ }
+
+ mLastSequenceNumber++;
+ final int toApplySeqNo = mLastSequenceNumber;
+ setCanRelease(false);
+ mTargetViewRootImpl.registerRtFrameCallback(frame -> {
+ if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
+ Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
+ .sendToTarget();
+ return;
+ }
+ Transaction t = new Transaction();
+ for (int i = params.length - 1; i >= 0; i--) {
+ SurfaceParams surfaceParams = params[i];
+ if (surfaceParams.surface.isValid()) {
+ surfaceParams.applyTo(t);
+ }
+ }
+ mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
+ Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
+ .sendToTarget();
+ });
+
+ // Make sure a frame gets scheduled.
+ view.invalidate();
+ }
+
+ /**
+ * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
+ * attached if necessary.
+ */
+ public static void create(
+ final View targetView, final Consumer<SurfaceTransactionApplier> callback) {
+ if (targetView == null) {
+ // No target view, no applier
+ callback.accept(null);
+ } else if (new ViewRootImplCompat(targetView).isValid()) {
+ // Already attached, we're good to go
+ callback.accept(new SurfaceTransactionApplier(targetView));
+ } else {
+ // Haven't been attached before we can get the view root
+ targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ targetView.removeOnAttachStateChangeListener(this);
+ callback.accept(new SurfaceTransactionApplier(targetView));
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ // Do nothing
+ }
+ });
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
new file mode 100644
index 0000000..c0f5c14
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_PIP;
+
+import android.animation.Animator;
+import android.animation.RectEvaluator;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.View;
+import android.window.PictureInPictureSurfaceTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.util.Themes;
+import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+
+/**
+ * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window
+ * when swiping up (in gesture navigation mode).
+ */
+public class SwipePipToHomeAnimator extends RectFSpringAnim {
+ private static final String TAG = SwipePipToHomeAnimator.class.getSimpleName();
+
+ private static final float END_PROGRESS = 1.0f;
+
+ private final int mTaskId;
+ private final ComponentName mComponentName;
+ private final SurfaceControl mLeash;
+ private final Rect mAppBounds = new Rect();
+ private final Matrix mHomeToWindowPositionMap = new Matrix();
+ private final Rect mStartBounds = new Rect();
+ private final RectF mCurrentBoundsF = new RectF();
+ private final Rect mCurrentBounds = new Rect();
+ private final Rect mDestinationBounds = new Rect();
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+
+ /** for calculating transform in {@link #onAnimationUpdate(AppCloseConfig, RectF, float)} */
+ private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
+ private final Rect mSourceHintRectInsets;
+ private final Rect mSourceInsets = new Rect();
+
+ /** for rotation calculations */
+ private final @RecentsOrientedState.SurfaceRotation int mFromRotation;
+ private final Rect mDestinationBoundsTransformed = new Rect();
+
+ /**
+ * Flag to avoid the double-end problem since the leash would have been released
+ * after the first end call and any further operations upon it would lead to NPE.
+ */
+ private boolean mHasAnimationEnded;
+
+ /**
+ * An overlay used to mask changes in content when entering PiP for apps that aren't seamless.
+ */
+ @Nullable
+ private SurfaceControl mContentOverlay;
+
+ /**
+ * @param context {@link Context} provides Launcher resources
+ * @param taskId Task id associated with this animator, see also {@link #getTaskId()}
+ * @param componentName Component associated with this animator,
+ * see also {@link #getComponentName()}
+ * @param leash {@link SurfaceControl} this animator operates on
+ * @param sourceRectHint See the definition in {@link android.app.PictureInPictureParams}
+ * @param appBounds Bounds of the application, sourceRectHint is based on this bounds
+ * @param homeToWindowPositionMap {@link Matrix} to map a Rect from home to window space
+ * @param startBounds Bounds of the application when this animator starts. This can be
+ * different from the appBounds if user has swiped a certain distance and
+ * Launcher has performed transform on the leash.
+ * @param destinationBounds Bounds of the destination this animator ends to
+ * @param fromRotation From rotation if different from final rotation, ROTATION_0 otherwise
+ * @param destinationBoundsTransformed Destination bounds in window space
+ * @param cornerRadius Corner radius in pixel value for PiP window
+ * @param view Attached view for logging purpose
+ */
+ private SwipePipToHomeAnimator(@NonNull Context context,
+ int taskId,
+ @NonNull ComponentName componentName,
+ @NonNull SurfaceControl leash,
+ @Nullable Rect sourceRectHint,
+ @NonNull Rect appBounds,
+ @NonNull Matrix homeToWindowPositionMap,
+ @NonNull RectF startBounds,
+ @NonNull Rect destinationBounds,
+ @RecentsOrientedState.SurfaceRotation int fromRotation,
+ @NonNull Rect destinationBoundsTransformed,
+ int cornerRadius,
+ @NonNull View view) {
+ super(startBounds, new RectF(destinationBoundsTransformed), context);
+ mTaskId = taskId;
+ mComponentName = componentName;
+ mLeash = leash;
+ mAppBounds.set(appBounds);
+ mHomeToWindowPositionMap.set(homeToWindowPositionMap);
+ startBounds.round(mStartBounds);
+ mDestinationBounds.set(destinationBounds);
+ mFromRotation = fromRotation;
+ mDestinationBoundsTransformed.set(destinationBoundsTransformed);
+ mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius);
+
+ if (sourceRectHint != null && (sourceRectHint.width() < destinationBounds.width()
+ || sourceRectHint.height() < destinationBounds.height())) {
+ // This is a situation in which the source hint rect on at least one axis is smaller
+ // than the destination bounds, which presents a problem because we would have to scale
+ // up that axis to fit the bounds. So instead, just fallback to the non-source hint
+ // animation in this case.
+ sourceRectHint = null;
+ }
+
+ if (sourceRectHint == null) {
+ mSourceHintRectInsets = null;
+
+ // Create a new overlay layer
+ SurfaceSession session = new SurfaceSession();
+ mContentOverlay = new SurfaceControl.Builder(session)
+ .setCallsite("SwipePipToHomeAnimator")
+ .setName("PipContentOverlay")
+ .setColorLayer()
+ .build();
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ t.show(mContentOverlay);
+ t.setLayer(mContentOverlay, Integer.MAX_VALUE);
+ int color = Themes.getColorBackground(view.getContext());
+ float[] bgColor = new float[] {Color.red(color) / 255f, Color.green(color) / 255f,
+ Color.blue(color) / 255f};
+ t.setColor(mContentOverlay, bgColor);
+ t.setAlpha(mContentOverlay, 0f);
+ t.reparent(mContentOverlay, mLeash);
+ t.apply();
+
+ addOnUpdateListener((values, currentRect, progress) -> {
+ float alpha = progress < 0.5f
+ ? 0
+ : Utilities.mapToRange(Math.min(progress, 1f), 0.5f, 1f,
+ 0f, 1f, Interpolators.FAST_OUT_SLOW_IN);
+ t.setAlpha(mContentOverlay, alpha);
+ t.apply();
+ });
+ } else {
+ mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left,
+ sourceRectHint.top - appBounds.top,
+ appBounds.right - sourceRectHint.right,
+ appBounds.bottom - sourceRectHint.bottom);
+ }
+
+ addAnimatorListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ InteractionJankMonitorWrapper.begin(view, CUJ_APP_CLOSE_TO_PIP);
+ super.onAnimationStart(animation);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ InteractionJankMonitorWrapper.cancel(CUJ_APP_CLOSE_TO_PIP);
+ }
+
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ InteractionJankMonitorWrapper.end(CUJ_APP_CLOSE_TO_PIP);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mHasAnimationEnded) return;
+ super.onAnimationEnd(animation);
+ mHasAnimationEnded = true;
+ }
+ });
+ addOnUpdateListener(this::onAnimationUpdate);
+ }
+
+ private void onAnimationUpdate(@Nullable AppCloseConfig values, RectF currentRect,
+ float progress) {
+ if (mHasAnimationEnded) return;
+ final SurfaceControl.Transaction tx =
+ PipSurfaceTransactionHelper.newSurfaceControlTransaction();
+ mHomeToWindowPositionMap.mapRect(mCurrentBoundsF, currentRect);
+ onAnimationUpdate(tx, mCurrentBoundsF, progress);
+ tx.apply();
+ }
+
+ private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx,
+ RectF currentRect, float progress) {
+ currentRect.round(mCurrentBounds);
+ final PictureInPictureSurfaceTransaction op;
+ if (mSourceHintRectInsets == null) {
+ // no source rect hint been set, directly scale the window down
+ op = onAnimationScale(progress, tx, mCurrentBounds);
+ } else {
+ // scale and crop according to the source rect hint
+ op = onAnimationScaleAndCrop(progress, tx, mCurrentBounds);
+ }
+ return op;
+ }
+
+ /** scale the window directly with no source rect hint being set */
+ private PictureInPictureSurfaceTransaction onAnimationScale(
+ float progress, SurfaceControl.Transaction tx, Rect bounds) {
+ if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+ final RotatedPosition rotatedPosition = getRotatedPosition(progress);
+ return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds,
+ rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
+ } else {
+ return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds);
+ }
+ }
+
+ /** scale and crop the window with source rect hint */
+ private PictureInPictureSurfaceTransaction onAnimationScaleAndCrop(
+ float progress, SurfaceControl.Transaction tx,
+ Rect bounds) {
+ final Rect insets = mInsetsEvaluator.evaluate(progress, mSourceInsets,
+ mSourceHintRectInsets);
+ if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+ final RotatedPosition rotatedPosition = getRotatedPosition(progress);
+ return mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
+ rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
+ } else {
+ return mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mAppBounds, bounds, insets);
+ }
+ }
+
+ public int getTaskId() {
+ return mTaskId;
+ }
+
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ public Rect getDestinationBounds() {
+ return mDestinationBounds;
+ }
+
+ @Nullable
+ public SurfaceControl getContentOverlay() {
+ return mContentOverlay;
+ }
+
+ /** @return {@link PictureInPictureSurfaceTransaction} for the final leash transaction. */
+ public PictureInPictureSurfaceTransaction getFinishTransaction() {
+ // get the final leash operations but do not apply to the leash.
+ final SurfaceControl.Transaction tx =
+ PipSurfaceTransactionHelper.newSurfaceControlTransaction();
+ return onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS);
+ }
+
+ private RotatedPosition getRotatedPosition(float progress) {
+ final float degree, positionX, positionY;
+ if (mFromRotation == Surface.ROTATION_90) {
+ degree = -90 * progress;
+ positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
+ + mStartBounds.left;
+ positionY = progress * (mDestinationBoundsTransformed.bottom - mStartBounds.top)
+ + mStartBounds.top;
+ } else {
+ degree = 90 * progress;
+ positionX = progress * (mDestinationBoundsTransformed.right - mStartBounds.left)
+ + mStartBounds.left;
+ positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
+ + mStartBounds.top;
+ }
+ return new RotatedPosition(degree, positionX, positionY);
+ }
+
+ /** Builder class for {@link SwipePipToHomeAnimator} */
+ public static class Builder {
+ private Context mContext;
+ private int mTaskId;
+ private ComponentName mComponentName;
+ private SurfaceControl mLeash;
+ private Rect mSourceRectHint;
+ private Rect mDisplayCutoutInsets;
+ private Rect mAppBounds;
+ private Matrix mHomeToWindowPositionMap;
+ private RectF mStartBounds;
+ private Rect mDestinationBounds;
+ private int mCornerRadius;
+ private View mAttachedView;
+ private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0;
+ private final Rect mDestinationBoundsTransformed = new Rect();
+
+ public Builder setContext(Context context) {
+ mContext = context;
+ return this;
+ }
+
+ public Builder setTaskId(int taskId) {
+ mTaskId = taskId;
+ return this;
+ }
+
+ public Builder setComponentName(ComponentName componentName) {
+ mComponentName = componentName;
+ return this;
+ }
+
+ public Builder setLeash(SurfaceControl leash) {
+ mLeash = leash;
+ return this;
+ }
+
+ public Builder setSourceRectHint(Rect sourceRectHint) {
+ mSourceRectHint = new Rect(sourceRectHint);
+ return this;
+ }
+
+ public Builder setAppBounds(Rect appBounds) {
+ mAppBounds = new Rect(appBounds);
+ return this;
+ }
+
+ public Builder setHomeToWindowPositionMap(Matrix homeToWindowPositionMap) {
+ mHomeToWindowPositionMap = new Matrix(homeToWindowPositionMap);
+ return this;
+ }
+
+ public Builder setStartBounds(RectF startBounds) {
+ mStartBounds = new RectF(startBounds);
+ return this;
+ }
+
+ public Builder setDestinationBounds(Rect destinationBounds) {
+ mDestinationBounds = new Rect(destinationBounds);
+ return this;
+ }
+
+ public Builder setCornerRadius(int cornerRadius) {
+ mCornerRadius = cornerRadius;
+ return this;
+ }
+
+ public Builder setAttachedView(View attachedView) {
+ mAttachedView = attachedView;
+ return this;
+ }
+
+ public Builder setFromRotation(TaskViewSimulator taskViewSimulator,
+ @RecentsOrientedState.SurfaceRotation int fromRotation,
+ Rect displayCutoutInsets) {
+ if (fromRotation != Surface.ROTATION_90 && fromRotation != Surface.ROTATION_270) {
+ Log.wtf(TAG, "Not a supported rotation, rotation=" + fromRotation);
+ return this;
+ }
+ final Matrix matrix = new Matrix();
+ taskViewSimulator.applyWindowToHomeRotation(matrix);
+
+ // map the destination bounds into window space. mDestinationBounds is always calculated
+ // in the final home space and the animation runs in original window space.
+ final RectF transformed = new RectF(mDestinationBounds);
+ matrix.mapRect(transformed, new RectF(mDestinationBounds));
+ transformed.round(mDestinationBoundsTransformed);
+
+ mFromRotation = fromRotation;
+ if (displayCutoutInsets != null) {
+ mDisplayCutoutInsets = new Rect(displayCutoutInsets);
+ }
+ return this;
+ }
+
+ public SwipePipToHomeAnimator build() {
+ if (mDestinationBoundsTransformed.isEmpty()) {
+ mDestinationBoundsTransformed.set(mDestinationBounds);
+ }
+ // adjust the mSourceRectHint / mAppBounds by display cutout if applicable.
+ if (mSourceRectHint != null && mDisplayCutoutInsets != null) {
+ if (mFromRotation == Surface.ROTATION_90) {
+ mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
+ } else if (mFromRotation == Surface.ROTATION_270) {
+ mAppBounds.inset(mDisplayCutoutInsets);
+ }
+ }
+ return new SwipePipToHomeAnimator(mContext, mTaskId, mComponentName, mLeash,
+ mSourceRectHint, mAppBounds,
+ mHomeToWindowPositionMap, mStartBounds, mDestinationBounds,
+ mFromRotation, mDestinationBoundsTransformed,
+ mCornerRadius, mAttachedView);
+ }
+ }
+
+ private static class RotatedPosition {
+ private final float degree;
+ private final float positionX;
+ private final float positionY;
+
+ private RotatedPosition(float degree, float positionX, float positionY) {
+ this.degree = degree;
+ this.positionX = positionX;
+ this.positionY = positionY;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskCornerRadius.java b/quickstep/src/com/android/quickstep/util/TaskCornerRadius.java
new file mode 100644
index 0000000..6f9c99a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskCornerRadius.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+public class TaskCornerRadius {
+
+ public static float get(Context context) {
+ Resources resources = context.getResources();
+ if (!supportsRoundedCornersOnWindows(resources)) {
+ return resources.getDimension(R.dimen.task_corner_radius_small);
+ }
+
+ float overriddenRadius =
+ resources.getDimension(R.dimen.task_corner_radius_override);
+ return (overriddenRadius > 0) ? overriddenRadius : Themes.getDialogCornerRadius(context);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
new file mode 100644
index 0000000..cceb872
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.states.RotationHelper.deltaRotation;
+import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
+import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
+import com.android.quickstep.views.TaskView.FullscreenDrawParams;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
+
+/**
+ * A utility class which emulates the layout behavior of TaskView and RecentsView
+ */
+public class TaskViewSimulator implements TransformParams.BuilderProxy {
+
+ private final Rect mTmpCropRect = new Rect();
+ private final RectF mTempRectF = new RectF();
+ private final float[] mTempPoint = new float[2];
+
+ private final Context mContext;
+ private final BaseActivityInterface mSizeStrategy;
+ private final boolean mIsForLiveTile;
+
+ @NonNull
+ private RecentsOrientedState mOrientationState;
+ private final boolean mIsRecentsRtl;
+
+ private final Rect mTaskRect = new Rect();
+ private boolean mDrawsBelowRecents;
+ private final PointF mPivot = new PointF();
+ private DeviceProfile mDp;
+
+ private final Matrix mMatrix = new Matrix();
+ private final Matrix mMatrixTmp = new Matrix();
+ private final Point mRunningTargetWindowPosition = new Point();
+
+ // Thumbnail view properties
+ private final Rect mThumbnailPosition = new Rect();
+ private final ThumbnailData mThumbnailData = new ThumbnailData();
+ private final PreviewPositionHelper mPositionHelper = new PreviewPositionHelper();
+ private final Matrix mInversePositionMatrix = new Matrix();
+
+ // TaskView properties
+ private final FullscreenDrawParams mCurrentFullscreenParams;
+ public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
+ public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
+
+ // RecentsView properties
+ public final AnimatedFloat recentsViewScale = new AnimatedFloat();
+ public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
+ public final AnimatedFloat recentsViewSecondaryTranslation = new AnimatedFloat();
+ public final AnimatedFloat recentsViewPrimaryTranslation = new AnimatedFloat();
+ public final AnimatedFloat recentsViewScroll = new AnimatedFloat();
+
+ // Cached calculations
+ private boolean mLayoutValid = false;
+ private int mOrientationStateId;
+
+ public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
+ this(context, sizeStrategy, false);
+ }
+
+ public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy,
+ boolean isForLiveTile) {
+ mContext = context;
+ mSizeStrategy = sizeStrategy;
+ mIsForLiveTile = isForLiveTile;
+
+ // TODO(b/187074722): Don't create this per-TaskViewSimulator
+ mOrientationState = TraceHelper.allowIpcs("",
+ () -> new RecentsOrientedState(context, sizeStrategy, i -> { }));
+ mOrientationState.setGestureActive(true);
+ mCurrentFullscreenParams = new FullscreenDrawParams(context);
+ mOrientationStateId = mOrientationState.getStateId();
+ Resources resources = context.getResources();
+ mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
+ }
+
+ /**
+ * Sets the device profile for the current state
+ */
+ public void setDp(DeviceProfile dp) {
+ mDp = dp;
+ mLayoutValid = false;
+ mOrientationState.setDeviceProfile(dp);
+ }
+
+ /**
+ * Sets the orientation state used for this animation
+ */
+ public void setOrientationState(@NonNull RecentsOrientedState orientationState) {
+ mOrientationState = orientationState;
+ mLayoutValid = false;
+ }
+
+ /**
+ * @see com.android.quickstep.views.RecentsView#FULLSCREEN_PROGRESS
+ */
+ public float getFullScreenScale() {
+ if (mDp == null) {
+ return 1;
+ }
+ mSizeStrategy.calculateTaskSize(mContext, mDp, mTaskRect,
+ mOrientationState.getOrientationHandler());
+ return mOrientationState.getFullScreenScaleAndPivot(mTaskRect, mDp, mPivot);
+ }
+
+ /**
+ * Sets the targets which the simulator will control
+ */
+ public void setPreview(RemoteAnimationTargetCompat runningTarget) {
+ setPreviewBounds(runningTarget.screenSpaceBounds, runningTarget.contentInsets);
+ mRunningTargetWindowPosition.set(runningTarget.screenSpaceBounds.left,
+ runningTarget.screenSpaceBounds.top);
+ }
+
+ /**
+ * Sets the targets which the simulator will control
+ */
+ public void setPreviewBounds(Rect bounds, Rect insets) {
+ mThumbnailData.insets.set(insets);
+ // TODO: What is this?
+ mThumbnailData.windowingMode = WINDOWING_MODE_FULLSCREEN;
+
+ mThumbnailPosition.set(bounds);
+ mLayoutValid = false;
+ }
+
+ /**
+ * Updates the scroll for RecentsView
+ */
+ public void setScroll(float scroll) {
+ recentsViewScroll.value = scroll;
+ }
+
+ public void setDrawsBelowRecents(boolean drawsBelowRecents) {
+ mDrawsBelowRecents = drawsBelowRecents;
+ }
+
+ /**
+ * Adds animation for all the components corresponding to transition from an app to overview.
+ */
+ public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
+ pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
+ pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, interpolator);
+ }
+
+ /**
+ * Adds animation for all the components corresponding to transition from overview to the app.
+ */
+ public void addOverviewToAppAnim(PendingAnimation pa, TimeInterpolator interpolator) {
+ pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 0, 1, interpolator);
+ pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, 1, getFullScreenScale(), interpolator);
+ }
+
+ /**
+ * Returns the current clipped/visible window bounds in the window coordinate space
+ */
+ public RectF getCurrentCropRect() {
+ // Crop rect is the inverse of thumbnail matrix
+ RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
+ mTempRectF.set(-insets.left, -insets.top,
+ mTaskRect.width() + insets.right, mTaskRect.height() + insets.bottom);
+ mInversePositionMatrix.mapRect(mTempRectF);
+ return mTempRectF;
+ }
+
+ /**
+ * Returns the current task bounds in the Launcher coordinate space.
+ */
+ public RectF getCurrentRect() {
+ RectF result = getCurrentCropRect();
+ mMatrixTmp.set(mMatrix);
+ preDisplayRotation(mOrientationState.getDisplayRotation(), mDp.widthPx, mDp.heightPx,
+ mMatrixTmp);
+ mMatrixTmp.mapRect(result);
+ return result;
+ }
+
+ public RecentsOrientedState getOrientationState() {
+ return mOrientationState;
+ }
+
+ /**
+ * Returns the current transform applied to the window
+ */
+ public Matrix getCurrentMatrix() {
+ return mMatrix;
+ }
+
+ /**
+ * Applies the rotation on the matrix to so that it maps from launcher coordinate space to
+ * window coordinate space.
+ */
+ public void applyWindowToHomeRotation(Matrix matrix) {
+ mMatrix.postTranslate(mDp.windowX, mDp.windowY);
+ postDisplayRotation(deltaRotation(
+ mOrientationState.getRecentsActivityRotation(),
+ mOrientationState.getDisplayRotation()),
+ mDp.widthPx, mDp.heightPx, matrix);
+ matrix.postTranslate(-mRunningTargetWindowPosition.x, -mRunningTargetWindowPosition.y);
+ }
+
+ /**
+ * Applies the target to the previously set parameters
+ */
+ public void apply(TransformParams params) {
+ if (mDp == null || mThumbnailPosition.isEmpty()) {
+ return;
+ }
+ if (!mLayoutValid || mOrientationStateId != mOrientationState.getStateId()) {
+ mLayoutValid = true;
+ mOrientationStateId = mOrientationState.getStateId();
+
+ getFullScreenScale();
+ mThumbnailData.rotation = mOrientationState.getDisplayRotation();
+
+ // mIsRecentsRtl is the inverse of TaskView RTL.
+ boolean isRtlEnabled = !mIsRecentsRtl;
+ mPositionHelper.updateThumbnailMatrix(
+ mThumbnailPosition, mThumbnailData,
+ mTaskRect.width(), mTaskRect.height(),
+ mDp, mOrientationState.getRecentsActivityRotation(), isRtlEnabled);
+ mPositionHelper.getMatrix().invert(mInversePositionMatrix);
+ }
+
+ float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
+ mCurrentFullscreenParams.setProgress(
+ fullScreenProgress, recentsViewScale.value, mTaskRect.width(), mDp,
+ mPositionHelper);
+
+ // Apply thumbnail matrix
+ RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
+ float scale = mCurrentFullscreenParams.mScale;
+ float taskWidth = mTaskRect.width();
+ float taskHeight = mTaskRect.height();
+
+ mMatrix.set(mPositionHelper.getMatrix());
+ mMatrix.postTranslate(insets.left, insets.top);
+ mMatrix.postScale(scale, scale);
+
+ // Apply TaskView matrix: translate, scroll
+ mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+ mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
+ taskPrimaryTranslation.value);
+ mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+ taskSecondaryTranslation.value);
+ mOrientationState.getOrientationHandler().set(
+ mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value);
+
+ // Apply RecentsView matrix
+ mMatrix.postScale(recentsViewScale.value, recentsViewScale.value, mPivot.x, mPivot.y);
+ mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+ recentsViewSecondaryTranslation.value);
+ mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
+ recentsViewPrimaryTranslation.value);
+ applyWindowToHomeRotation(mMatrix);
+
+ // Crop rect is the inverse of thumbnail matrix
+ mTempRectF.set(-insets.left, -insets.top,
+ taskWidth + insets.right, taskHeight + insets.bottom);
+ mInversePositionMatrix.mapRect(mTempRectF);
+ mTempRectF.roundOut(mTmpCropRect);
+
+ params.applySurfaceParams(params.createSurfaceParams(this));
+ }
+
+ @Override
+ public void onBuildTargetParams(
+ Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
+ builder.withMatrix(mMatrix)
+ .withWindowCrop(mTmpCropRect)
+ .withCornerRadius(getCurrentCornerRadius());
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mIsForLiveTile
+ && params.getRecentsSurface() != null) {
+ // When relativeLayer = 0, it reverts the surfaces back to the original order.
+ builder.withRelativeLayerTo(params.getRecentsSurface(),
+ mDrawsBelowRecents ? Integer.MIN_VALUE : 0);
+ }
+ }
+
+ /**
+ * Returns the corner radius that should be applied to the target so that it matches the
+ * TaskView
+ */
+ public float getCurrentCornerRadius() {
+ float visibleRadius = mCurrentFullscreenParams.mCurrentDrawnCornerRadius;
+ mTempPoint[0] = visibleRadius;
+ mTempPoint[1] = 0;
+ mInversePositionMatrix.mapVectors(mTempPoint);
+
+ // Ideally we should use square-root. This is an optimization as one of the dimension is 0.
+ return Math.max(Math.abs(mTempPoint[0]), Math.abs(mTempPoint[1]));
+ }
+
+}
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
new file mode 100644
index 0000000..03d7a37
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.util.FloatProperty;
+import android.view.SurfaceControl;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
+import com.android.quickstep.RemoteAnimationTargets;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+import com.android.systemui.shared.system.TransactionCompat;
+
+public class TransformParams {
+
+ public static FloatProperty<TransformParams> PROGRESS =
+ new FloatProperty<TransformParams>("progress") {
+ @Override
+ public void setValue(TransformParams params, float v) {
+ params.setProgress(v);
+ }
+
+ @Override
+ public Float get(TransformParams params) {
+ return params.getProgress();
+ }
+ };
+
+ public static FloatProperty<TransformParams> TARGET_ALPHA =
+ new FloatProperty<TransformParams>("targetAlpha") {
+ @Override
+ public void setValue(TransformParams params, float v) {
+ params.setTargetAlpha(v);
+ }
+
+ @Override
+ public Float get(TransformParams params) {
+ return params.getTargetAlpha();
+ }
+ };
+
+ private float mProgress;
+ private float mTargetAlpha;
+ private float mCornerRadius;
+ private RemoteAnimationTargets mTargetSet;
+ private SurfaceTransactionApplier mSyncTransactionApplier;
+ private SurfaceControl mRecentsSurface;
+
+ private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
+ private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
+
+ public TransformParams() {
+ mProgress = 0;
+ mTargetAlpha = 1;
+ mCornerRadius = -1;
+ }
+
+ /**
+ * Sets the progress of the transformation, where 0 is the source and 1 is the target. We
+ * automatically adjust properties such as currentRect and cornerRadius based on this
+ * progress, unless they are manually overridden by setting them on this TransformParams.
+ */
+ public TransformParams setProgress(float progress) {
+ mProgress = progress;
+ return this;
+ }
+
+ /**
+ * Sets the corner radius of the transformed window, in pixels. If unspecified (-1), we
+ * simply interpolate between the window's corner radius to the task view's corner radius,
+ * based on {@link #mProgress}.
+ */
+ public TransformParams setCornerRadius(float cornerRadius) {
+ mCornerRadius = cornerRadius;
+ return this;
+ }
+
+ /**
+ * Specifies the alpha of the transformed window. Default is 1.
+ */
+ public TransformParams setTargetAlpha(float targetAlpha) {
+ mTargetAlpha = targetAlpha;
+ return this;
+ }
+
+ /**
+ * Specifies the set of RemoteAnimationTargetCompats that are included in the transformation
+ * that these TransformParams help compute. These TransformParams generally only apply to
+ * the targetSet.apps which match the targetSet.targetMode (e.g. the MODE_CLOSING app when
+ * swiping to home).
+ */
+ public TransformParams setTargetSet(RemoteAnimationTargets targetSet) {
+ mTargetSet = targetSet;
+ return this;
+ }
+
+ /**
+ * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that
+ * are computed based on these TransformParams.
+ */
+ public TransformParams setSyncTransactionApplier(
+ SurfaceTransactionApplier applier) {
+ mSyncTransactionApplier = applier;
+ return this;
+ }
+
+ /**
+ * Sets an alternate function to control transform for non-target apps. The default
+ * implementation keeps the targets visible with alpha=1
+ */
+ public TransformParams setBaseBuilderProxy(BuilderProxy proxy) {
+ mBaseBuilderProxy = proxy;
+ return this;
+ }
+
+ /**
+ * Sets an alternate function to control transform for home target. The default
+ * implementation keeps the targets visible with alpha=1
+ */
+ public TransformParams setHomeBuilderProxy(BuilderProxy proxy) {
+ mHomeBuilderProxy = proxy;
+ return this;
+ }
+
+ public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
+ RemoteAnimationTargets targets = mTargetSet;
+ SurfaceParams[] surfaceParams = new SurfaceParams[targets.unfilteredApps.length];
+ mRecentsSurface = getRecentsSurface(targets);
+
+ for (int i = 0; i < targets.unfilteredApps.length; i++) {
+ RemoteAnimationTargetCompat app = targets.unfilteredApps[i];
+ SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash);
+
+ if (app.mode == targets.targetMode) {
+ if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
+ mHomeBuilderProxy.onBuildTargetParams(builder, app, this);
+ } else {
+ // Fade out Assistant overlay.
+ if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
+ && app.isNotInRecents) {
+ float progress = Utilities.boundToRange(getProgress(), 0, 1);
+ builder.withAlpha(1 - Interpolators.DEACCEL_2_5.getInterpolation(progress));
+ } else {
+ builder.withAlpha(getTargetAlpha());
+ }
+
+ proxy.onBuildTargetParams(builder, app, this);
+ }
+ } else {
+ mBaseBuilderProxy.onBuildTargetParams(builder, app, this);
+ }
+ surfaceParams[i] = builder.build();
+ }
+ return surfaceParams;
+ }
+
+ private static SurfaceControl getRecentsSurface(RemoteAnimationTargets targets) {
+ for (int i = 0; i < targets.unfilteredApps.length; i++) {
+ RemoteAnimationTargetCompat app = targets.unfilteredApps[i];
+ if (app.mode == targets.targetMode) {
+ if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS) {
+ return app.leash.getSurfaceControl();
+ }
+ } else {
+ return app.leash.getSurfaceControl();
+ }
+ }
+ return null;
+ }
+
+ // Pubic getters so outside packages can read the values.
+
+ public float getProgress() {
+ return mProgress;
+ }
+
+ public float getTargetAlpha() {
+ return mTargetAlpha;
+ }
+
+ public float getCornerRadius() {
+ return mCornerRadius;
+ }
+
+ public SurfaceControl getRecentsSurface() {
+ return mRecentsSurface;
+ }
+
+ public RemoteAnimationTargets getTargetSet() {
+ return mTargetSet;
+ }
+
+ public void applySurfaceParams(SurfaceParams... params) {
+ if (mSyncTransactionApplier != null) {
+ mSyncTransactionApplier.scheduleApply(params);
+ } else {
+ TransactionCompat t = new TransactionCompat();
+ for (SurfaceParams param : params) {
+ SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
+ }
+ t.apply();
+ }
+ }
+
+ @FunctionalInterface
+ public interface BuilderProxy {
+
+ BuilderProxy NO_OP = (builder, app, params) -> { };
+ BuilderProxy ALWAYS_VISIBLE = (builder, app, params) ->builder.withAlpha(1);
+
+ void onBuildTargetParams(SurfaceParams.Builder builder,
+ RemoteAnimationTargetCompat app, TransformParams params);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java b/quickstep/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
new file mode 100644
index 0000000..7bbde30
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+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_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+
+/**
+ * Tracks motion events to determine whether a gesture on the nav bar is a swipe up.
+ */
+public class TriggerSwipeUpTouchTracker {
+
+ private final PointF mDownPos = new PointF();
+ private final float mSquaredTouchSlop;
+ private final float mMinFlingVelocity;
+ private final boolean mDisableHorizontalSwipe;
+ private final NavBarPosition mNavBarPosition;
+ private final Runnable mOnInterceptTouch;
+ private final OnSwipeUpListener mOnSwipeUp;
+
+ private boolean mInterceptedTouch;
+ private VelocityTracker mVelocityTracker;
+
+ public TriggerSwipeUpTouchTracker(Context context, boolean disableHorizontalSwipe,
+ NavBarPosition navBarPosition, Runnable onInterceptTouch,
+ OnSwipeUpListener onSwipeUp) {
+ mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+ mMinFlingVelocity = context.getResources().getDimension(
+ R.dimen.quickstep_fling_threshold_speed);
+ mNavBarPosition = navBarPosition;
+ mDisableHorizontalSwipe = disableHorizontalSwipe;
+ mOnInterceptTouch = onInterceptTouch;
+ mOnSwipeUp = onSwipeUp;
+
+ init();
+ }
+
+ /**
+ * Reset some initial values to prepare for the next gesture.
+ */
+ public void init() {
+ mInterceptedTouch = false;
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ /**
+ * @return Whether we have passed the touch slop and are still tracking the gesture.
+ */
+ public boolean interceptedTouch() {
+ return mInterceptedTouch;
+ }
+
+ /**
+ * Track motion events to determine whether an atomic swipe up has occurred.
+ */
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ mVelocityTracker.addMovement(ev);
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ mDownPos.set(ev.getX(), ev.getY());
+ break;
+ }
+ case ACTION_MOVE: {
+ if (!mInterceptedTouch) {
+ float displacementX = ev.getX() - mDownPos.x;
+ float displacementY = ev.getY() - mDownPos.y;
+ if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
+ if (mDisableHorizontalSwipe
+ && Math.abs(displacementX) > Math.abs(displacementY)) {
+ // Horizontal gesture is not allowed in this region
+ endTouchTracking();
+ break;
+ }
+
+ mInterceptedTouch = true;
+
+ if (mOnInterceptTouch != null) {
+ mOnInterceptTouch.run();
+ }
+ }
+ }
+ break;
+ }
+
+ case ACTION_CANCEL:
+ endTouchTracking();
+ break;
+
+ case ACTION_UP: {
+ onGestureEnd(ev);
+ endTouchTracking();
+ break;
+ }
+ }
+ }
+
+ private void endTouchTracking() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void onGestureEnd(MotionEvent ev) {
+ mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
+ float velocityX = mVelocityTracker.getXVelocity();
+ float velocityY = mVelocityTracker.getYVelocity();
+ float velocity = mNavBarPosition.isRightEdge()
+ ? -velocityX
+ : mNavBarPosition.isLeftEdge()
+ ? velocityX
+ : -velocityY;
+
+ final boolean wasFling = Math.abs(velocity) >= mMinFlingVelocity;
+ final boolean isSwipeUp;
+ if (wasFling) {
+ isSwipeUp = velocity > 0;
+ } else {
+ float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
+ float displacementY = ev.getY() - mDownPos.y;
+ isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
+ }
+
+ if (mOnSwipeUp != null) {
+ if (isSwipeUp) {
+ mOnSwipeUp.onSwipeUp(wasFling, new PointF(velocityX, velocityY));
+ } else {
+ mOnSwipeUp.onSwipeUpCancelled();
+ }
+ }
+ }
+
+ /**
+ * Callback when the gesture ends and was determined to be a swipe from the nav bar.
+ */
+ public interface OnSwipeUpListener {
+ /**
+ * Called on touch up if a swipe up was detected.
+ * @param wasFling Whether the swipe was a fling, or just passed touch slop at low velocity.
+ * @param finalVelocity The final velocity of the swipe.
+ */
+ void onSwipeUp(boolean wasFling, PointF finalVelocity);
+
+ /** Called on touch up if a swipe up was not detected. */
+ void onSwipeUpCancelled();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
new file mode 100644
index 0000000..df94d0b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.DynamicResource;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.plugins.ResourceProvider;
+
+/**
+ * Creates an animation that reveals the workspace.
+ * This is used in conjunction with the swipe up to home animation.
+ */
+public class WorkspaceRevealAnim {
+
+ // Should be used for animations running alongside this WorkspaceRevealAnim.
+ public static final int DURATION_MS = 350;
+
+ private final float mScaleStart;
+ private final AnimatorSet mAnimators = new AnimatorSet();
+
+ public WorkspaceRevealAnim(Launcher launcher, boolean animateOverviewScrim) {
+ prepareToAnimate(launcher, animateOverviewScrim);
+
+ ResourceProvider rp = DynamicResource.provider(launcher);
+ mScaleStart = rp.getFloat(R.dimen.swipe_up_scale_start);
+
+ Workspace workspace = launcher.getWorkspace();
+ workspace.setPivotToScaleWithSelf(launcher.getHotseat());
+
+ // Add reveal animations.
+ addRevealAnimatorsForView(workspace);
+ addRevealAnimatorsForView(launcher.getHotseat());
+
+ // Add overview scrim animation.
+ if (animateOverviewScrim) {
+ PendingAnimation overviewScrimBuilder = new PendingAnimation(DURATION_MS);
+ launcher.getWorkspace().getStateTransitionAnimation()
+ .setScrim(overviewScrimBuilder, NORMAL, new StateAnimationConfig());
+ mAnimators.play(overviewScrimBuilder.buildAnim());
+ }
+
+ // Add depth controller animation.
+ if (launcher instanceof BaseQuickstepLauncher) {
+ PendingAnimation depthBuilder = new PendingAnimation(DURATION_MS);
+ DepthController depth = ((BaseQuickstepLauncher) launcher).getDepthController();
+ depth.setStateWithAnimation(NORMAL, new StateAnimationConfig(), depthBuilder);
+ mAnimators.play(depthBuilder.buildAnim());
+ }
+
+ // Add sysui scrim animation.
+ mAnimators.play(launcher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f));
+
+ mAnimators.setDuration(DURATION_MS);
+ mAnimators.setInterpolator(Interpolators.DECELERATED_EASE);
+ }
+
+ private void addRevealAnimatorsForView(View v) {
+ ObjectAnimator scale = ObjectAnimator.ofFloat(v, SCALE_PROPERTY, mScaleStart, 1f);
+ scale.setDuration(DURATION_MS);
+ scale.setInterpolator(Interpolators.DECELERATED_EASE);
+ mAnimators.play(scale);
+
+ ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0, 1f);
+ alpha.setDuration(DURATION_MS);
+ alpha.setInterpolator(Interpolators.DECELERATED_EASE);
+ mAnimators.play(alpha);
+
+ mAnimators.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ SCALE_PROPERTY.set(v, 1f);
+ v.setAlpha(1f);
+ }
+ });
+ }
+
+ /**
+ * Setup workspace with 0 duration.
+ */
+ private void prepareToAnimate(Launcher launcher, boolean animateOverviewScrim) {
+ StateAnimationConfig config = new StateAnimationConfig();
+ config.animFlags = SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER | SKIP_SCRIM;
+ config.duration = 0;
+ // setRecentsAttachedToAppWindow() will animate recents out.
+ launcher.getStateManager().createAtomicAnimation(BACKGROUND_APP, NORMAL, config).start();
+
+ // Stop scrolling so that it doesn't interfere with the translation offscreen.
+ launcher.<RecentsView>getOverviewPanel().getScroller().forceFinished(true);
+
+ if (animateOverviewScrim) {
+ launcher.getWorkspace().getStateTransitionAnimation()
+ .setScrim(NO_ANIM_PROPERTY_SETTER, BACKGROUND_APP, config);
+ }
+ }
+
+ public AnimatorSet getAnimators() {
+ return mAnimators;
+ }
+
+ public WorkspaceRevealAnim addAnimatorListener(Animator.AnimatorListener listener) {
+ mAnimators.addListener(listener);
+ return this;
+ }
+
+ /**
+ * Starts the animation.
+ */
+ public void start() {
+ mAnimators.start();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
new file mode 100644
index 0000000..e9d7c3c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_EDU_SHOWN;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.util.MultiValueUpdateListener;
+
+/**
+ * View used to educate the user on how to access All Apps when in No Nav Button navigation mode.
+ * If the user drags on the view, the animation is overridden so the user can swipe to All Apps or
+ * Home.
+ */
+public class AllAppsEduView extends AbstractFloatingView {
+
+ private Launcher mLauncher;
+ private AllAppsEduTouchController mTouchController;
+
+ private AnimatorSet mAnimation;
+
+ private GradientDrawable mCircle;
+ private GradientDrawable mGradient;
+
+ private int mCircleSizePx;
+ private int mPaddingPx;
+ private int mWidthPx;
+ private int mMaxHeightPx;
+
+ private boolean mCanInterceptTouch;
+
+ public AllAppsEduView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle);
+ mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size);
+ mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding);
+ mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width);
+ mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ mGradient.draw(canvas);
+ mCircle.draw(canvas);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mIsOpen = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mIsOpen = false;
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ mLauncher.getDragLayer().removeView(this);
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_ALL_APPS_EDU) != 0;
+ }
+
+ @Override
+ public boolean onBackPressed() {
+ return true;
+ }
+
+ @Override
+ public boolean canInterceptEventsInSystemGestureRegion() {
+ return true;
+ }
+
+
+ private boolean shouldInterceptTouch(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mCanInterceptTouch = (ev.getEdgeFlags() & EDGE_NAV_BAR) == 0;
+ }
+ return mCanInterceptTouch;
+ }
+
+ @Override
+ public boolean onControllerTouchEvent(MotionEvent ev) {
+ if (shouldInterceptTouch(ev)) {
+ mTouchController.onControllerTouchEvent(ev);
+ updateAnimationOnTouchEvent(ev);
+ }
+ return true;
+ }
+
+ private void updateAnimationOnTouchEvent(MotionEvent ev) {
+ if (mAnimation == null) {
+ return;
+ }
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mAnimation.pause();
+ return;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mAnimation.resume();
+ return;
+ }
+
+ if (mTouchController.isDraggingOrSettling()) {
+ mAnimation = null;
+ handleClose(false);
+ }
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (shouldInterceptTouch(ev)) {
+ mTouchController.onControllerInterceptTouchEvent(ev);
+ updateAnimationOnTouchEvent(ev);
+ }
+ return true;
+ }
+
+ private void playAnimation() {
+ if (mAnimation != null) {
+ return;
+ }
+ mAnimation = new AnimatorSet();
+
+ final Rect circleBoundsOg = new Rect(mCircle.getBounds());
+ final Rect gradientBoundsOg = new Rect(mGradient.getBounds());
+ final Rect temp = new Rect();
+ final float transY = mMaxHeightPx - mCircleSizePx - mPaddingPx;
+
+ // 1st: Circle alpha/scale
+ int firstPart = 600;
+ // 2nd: Circle animates upwards, Gradient alpha fades in, Gradient grows, All Apps hint
+ int secondPart = 1200;
+ int introDuration = firstPart + secondPart;
+
+ AnimatorPlaybackController stateAnimationController =
+ mTouchController.initAllAppsAnimation();
+ float maxAllAppsProgress = 0.75f;
+
+ ValueAnimator intro = ValueAnimator.ofFloat(0, 1f);
+ intro.setInterpolator(LINEAR);
+ intro.setDuration(introDuration);
+ intro.addUpdateListener((new MultiValueUpdateListener() {
+ FloatProp mCircleAlpha = new FloatProp(0, 255, 0, firstPart, LINEAR);
+ FloatProp mCircleScale = new FloatProp(2f, 1f, 0, firstPart, OVERSHOOT_1_7);
+ FloatProp mDeltaY = new FloatProp(0, transY, firstPart, secondPart, FAST_OUT_SLOW_IN);
+ FloatProp mGradientAlpha = new FloatProp(0, 255, firstPart, secondPart * 0.3f, LINEAR);
+
+ @Override
+ public void onUpdate(float progress, boolean initOnly) {
+ temp.set(circleBoundsOg);
+ temp.offset(0, (int) -mDeltaY.value);
+ Utilities.scaleRectAboutCenter(temp, mCircleScale.value);
+ mCircle.setBounds(temp);
+ mCircle.setAlpha((int) mCircleAlpha.value);
+ mGradient.setAlpha((int) mGradientAlpha.value);
+
+ temp.set(gradientBoundsOg);
+ temp.top -= mDeltaY.value;
+ mGradient.setBounds(temp);
+ invalidate();
+
+ float stateProgress = Utilities.mapToRange(mDeltaY.value, 0, transY, 0,
+ maxAllAppsProgress, LINEAR);
+ stateAnimationController.setPlayFraction(stateProgress);
+ }
+ }));
+ intro.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCircle.setAlpha(0);
+ mGradient.setAlpha(0);
+ }
+ });
+ mLauncher.getAppsView().setVisibility(View.VISIBLE);
+ mAnimation.play(intro);
+
+ ValueAnimator closeAllApps = ValueAnimator.ofFloat(maxAllAppsProgress, 0f);
+ closeAllApps.addUpdateListener(valueAnimator -> {
+ stateAnimationController.setPlayFraction((float) valueAnimator.getAnimatedValue());
+ });
+ closeAllApps.setInterpolator(FAST_OUT_SLOW_IN);
+ closeAllApps.setStartDelay(introDuration);
+ closeAllApps.setDuration(250);
+ mAnimation.play(closeAllApps);
+
+ mAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimation = null;
+ // Handles cancelling the animation used to hint towards All Apps.
+ mLauncher.getStateManager().goToState(NORMAL, false);
+ handleClose(false);
+ }
+ });
+ mAnimation.start();
+ }
+
+ private void init(Launcher launcher) {
+ mLauncher = launcher;
+ mTouchController = new AllAppsEduTouchController(mLauncher);
+
+ int accentColor = Themes.getColorAccent(launcher);
+ mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
+ Themes.getAttrBoolean(launcher, R.attr.isMainColorDark)
+ ? new int[]{0xB3FFFFFF, 0x00FFFFFF}
+ : new int[]{ColorUtils.setAlphaComponent(accentColor, 127),
+ ColorUtils.setAlphaComponent(accentColor, 0)});
+ float r = mWidthPx / 2f;
+ mGradient.setCornerRadii(new float[]{r, r, r, r, 0, 0, 0, 0});
+
+ int top = mMaxHeightPx - mCircleSizePx + mPaddingPx;
+ mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx);
+ mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx);
+
+ DeviceProfile grid = launcher.getDeviceProfile();
+ DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx);
+ lp.ignoreInsets = true;
+ lp.leftMargin = (grid.widthPx - mWidthPx) / 2;
+ lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx;
+ setLayoutParams(lp);
+ }
+
+ /**
+ * Shows the All Apps education view and plays the animation.
+ */
+ public static void show(Launcher launcher) {
+ final DragLayer dragLayer = launcher.getDragLayer();
+ AllAppsEduView view = (AllAppsEduView) launcher.getLayoutInflater().inflate(
+ R.layout.all_apps_edu_view, dragLayer, false);
+ view.init(launcher);
+ launcher.getDragLayer().addView(view);
+ launcher.getStatsLogManager().logger().log(LAUNCHER_ALL_APPS_EDU_SHOWN);
+
+ view.requestLayout();
+ view.playAnimation();
+ }
+
+ private static class AllAppsEduTouchController extends PortraitStatesTouchController {
+
+ private AllAppsEduTouchController(Launcher l) {
+ super(l);
+ }
+
+ @Override
+ protected boolean canInterceptTouch(MotionEvent ev) {
+ return true;
+ }
+
+ private AnimatorPlaybackController initAllAppsAnimation() {
+ mFromState = NORMAL;
+ mToState = ALL_APPS;
+ mProgressMultiplier = initCurrentAnimation();
+ return mCurrentAnimation;
+ }
+
+ private boolean isDraggingOrSettling() {
+ return mDetector.isDraggingOrSettling();
+ }
+ }
+}
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 0000000..b9a9006
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -0,0 +1,223 @@
+/*
+ * 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.util.AttributeSet;
+import android.util.FloatProperty;
+import android.widget.Button;
+
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.touch.PagedOrientationHandler;
+
+public class ClearAllButton extends Button {
+
+ public static final FloatProperty<ClearAllButton> VISIBILITY_ALPHA =
+ new FloatProperty<ClearAllButton>("visibilityAlpha") {
+ @Override
+ public Float get(ClearAllButton view) {
+ return view.mVisibilityAlpha;
+ }
+
+ @Override
+ public void setValue(ClearAllButton view, float v) {
+ view.setVisibilityAlpha(v);
+ }
+ };
+
+ private final StatefulActivity mActivity;
+ private float mScrollAlpha = 1;
+ private float mContentAlpha = 1;
+ private float mVisibilityAlpha = 1;
+ private float mFullscreenProgress = 1;
+ private float mGridProgress = 1;
+
+ private boolean mIsRtl;
+ private float mNormalTranslationPrimary;
+ private float mFullscreenTranslationPrimary;
+ private float mGridTranslationPrimary;
+ private float mGridScrollOffset;
+ private float mScrollOffsetPrimary;
+
+ private int mSidePadding;
+
+ public ClearAllButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ mActivity = StatefulActivity.fromContext(context);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
+ mSidePadding = orientationHandler.getClearAllSidePadding(getRecentsView(), mIsRtl);
+ }
+
+ private RecentsView getRecentsView() {
+ return (RecentsView) getParent();
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+ mIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public void setContentAlpha(float alpha) {
+ if (mContentAlpha != alpha) {
+ mContentAlpha = alpha;
+ updateAlpha();
+ }
+ }
+
+ public void setVisibilityAlpha(float alpha) {
+ if (mVisibilityAlpha != alpha) {
+ mVisibilityAlpha = alpha;
+ updateAlpha();
+ }
+ }
+
+ public void onRecentsViewScroll(int scroll, boolean gridEnabled) {
+ RecentsView recentsView = getRecentsView();
+ if (recentsView == null) {
+ return;
+ }
+
+ PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+ float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
+ if (orientationSize == 0) {
+ return;
+ }
+
+ int clearAllScroll = recentsView.getClearAllScroll();
+ int adjustedScrollFromEdge = Math.abs(scroll - clearAllScroll);
+ float shift = Math.min(adjustedScrollFromEdge, orientationSize);
+ mNormalTranslationPrimary = mIsRtl ? -shift : shift;
+ if (!gridEnabled) {
+ mNormalTranslationPrimary += mSidePadding;
+ }
+ applyPrimaryTranslation();
+ applySecondaryTranslation();
+ mScrollAlpha = 1 - shift / orientationSize;
+ updateAlpha();
+ }
+
+ private void updateAlpha() {
+ final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
+ setAlpha(alpha);
+ setClickable(Math.min(alpha, 1) == 1);
+ }
+
+ public void setFullscreenTranslationPrimary(float fullscreenTranslationPrimary) {
+ mFullscreenTranslationPrimary = fullscreenTranslationPrimary;
+ applyPrimaryTranslation();
+ }
+
+ public void setGridTranslationPrimary(float gridTranslationPrimary) {
+ mGridTranslationPrimary = gridTranslationPrimary;
+ applyPrimaryTranslation();
+ }
+
+ public void setGridScrollOffset(float gridScrollOffset) {
+ mGridScrollOffset = gridScrollOffset;
+ }
+
+ public void setScrollOffsetPrimary(float scrollOffsetPrimary) {
+ mScrollOffsetPrimary = scrollOffsetPrimary;
+ }
+
+ public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
+ float scrollAdjustment = 0;
+ if (fullscreenEnabled) {
+ scrollAdjustment += mFullscreenTranslationPrimary;
+ }
+ if (gridEnabled) {
+ scrollAdjustment += mGridTranslationPrimary + mGridScrollOffset;
+ }
+ scrollAdjustment += mScrollOffsetPrimary;
+ return scrollAdjustment;
+ }
+
+ public float getOffsetAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
+ return getScrollAdjustment(fullscreenEnabled, gridEnabled);
+ }
+
+ /**
+ * Adjust translation when this TaskView is about to be shown fullscreen.
+ *
+ * @param progress: 0 = no translation; 1 = translate according to TaskVIew translations.
+ */
+ public void setFullscreenProgress(float progress) {
+ mFullscreenProgress = progress;
+ applyPrimaryTranslation();
+ }
+
+ /**
+ * Moves ClearAllButton between carousel and 2 row grid.
+ *
+ * @param gridProgress 0 = carousel; 1 = 2 row grid.
+ */
+ public void setGridProgress(float gridProgress) {
+ mGridProgress = gridProgress;
+ applyPrimaryTranslation();
+ }
+
+ private void applyPrimaryTranslation() {
+ RecentsView recentsView = getRecentsView();
+ if (recentsView == null) {
+ return;
+ }
+
+ PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+ orientationHandler.getPrimaryViewTranslate().set(this,
+ orientationHandler.getPrimaryValue(0f, getOriginalTranslationY())
+ + mNormalTranslationPrimary + getFullscreenTrans(
+ mFullscreenTranslationPrimary) + getGridTrans(mGridTranslationPrimary));
+ }
+
+ private void applySecondaryTranslation() {
+ RecentsView recentsView = getRecentsView();
+ if (recentsView == null) {
+ return;
+ }
+
+ PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+ orientationHandler.getSecondaryViewTranslate().set(this,
+ orientationHandler.getSecondaryValue(0f, getOriginalTranslationY()));
+ }
+
+ private float getFullscreenTrans(float endTranslation) {
+ return mFullscreenProgress > 0 ? endTranslation : 0;
+ }
+
+ private float getGridTrans(float endTranslation) {
+ return mGridProgress > 0 ? endTranslation : 0;
+ }
+
+ /**
+ * Get the Y translation that is set in the original layout position, before scrolling.
+ */
+ private float getOriginalTranslationY() {
+ return mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx / 2.0f;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
new file mode 100644
index 0000000..7c8041c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.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.views;
+
+import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+
+import static com.android.launcher3.Utilities.prefixTextWithIcon;
+import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
+
+import android.annotation.TargetApi;
+import android.app.ActivityOptions;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.AppUsageLimit;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.os.Build;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.annotation.StringRes;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.time.Duration;
+import java.util.Locale;
+
+@TargetApi(Build.VERSION_CODES.Q)
+public final class DigitalWellBeingToast {
+ static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
+ static final int MINUTE_MS = 60000;
+
+ private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
+
+ private final BaseDraggingActivity mActivity;
+ private final TaskView mTaskView;
+ private final LauncherApps mLauncherApps;
+
+ private Task mTask;
+ private boolean mHasLimit;
+ private long mAppRemainingTimeMs;
+ private View mBanner;
+ private ViewOutlineProvider mOldBannerOutlineProvider;
+ private float mBannerOffsetPercentage;
+ private float mVerticalOffset = 0f;
+
+ public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
+ mActivity = activity;
+ mTaskView = taskView;
+ mLauncherApps = activity.getSystemService(LauncherApps.class);
+ }
+
+ private void setNoLimit() {
+ mHasLimit = false;
+ mTaskView.setContentDescription(mTask.titleDescription);
+ replaceBanner(null);
+ mAppRemainingTimeMs = 0;
+ }
+
+ private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
+ mAppRemainingTimeMs = appRemainingTimeMs;
+ mHasLimit = true;
+ TextView toast = mActivity.getViewCache().getView(R.layout.digital_wellbeing_toast,
+ mActivity, mTaskView);
+ toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
+ toast.setOnClickListener(this::openAppUsageSettings);
+ replaceBanner(toast);
+
+ mTaskView.setContentDescription(
+ getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
+ }
+
+ public String getText() {
+ return getText(mAppRemainingTimeMs);
+ }
+
+ public boolean hasLimit() {
+ return mHasLimit;
+ }
+
+ public void initialize(Task task) {
+ mTask = task;
+
+ if (task.key.userId != UserHandle.myUserId()) {
+ setNoLimit();
+ return;
+ }
+
+ THREAD_POOL_EXECUTOR.execute(() -> {
+ final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
+ task.getTopComponent().getPackageName(),
+ UserHandle.of(task.key.userId));
+
+ final long appUsageLimitTimeMs =
+ usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
+ final long appRemainingTimeMs =
+ usageLimit != null ? usageLimit.getUsageRemaining() : -1;
+
+ mTaskView.post(() -> {
+ if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+ setNoLimit();
+ } else {
+ setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
+ }
+ });
+ });
+ }
+
+ private String getReadableDuration(
+ Duration duration,
+ FormatWidth formatWidthHourAndMinute,
+ @StringRes int durationLessThanOneMinuteStringId,
+ boolean forceFormatWidth) {
+ int hours = Math.toIntExact(duration.toHours());
+ int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
+
+ // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero.
+ if (hours > 0 && minutes > 0) {
+ return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute)
+ .formatMeasures(
+ new Measure(hours, MeasureUnit.HOUR),
+ new Measure(minutes, MeasureUnit.MINUTE));
+ }
+
+ // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced).
+ if (hours > 0) {
+ return MeasureFormat.getInstance(
+ Locale.getDefault(),
+ forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(hours, MeasureUnit.HOUR));
+ }
+
+ // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced).
+ if (minutes > 0) {
+ return MeasureFormat.getInstance(
+ Locale.getDefault()
+ , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE));
+ }
+
+ // Use a specific string for usage less than one minute but non-zero.
+ if (duration.compareTo(Duration.ZERO) > 0) {
+ return mActivity.getString(durationLessThanOneMinuteStringId);
+ }
+
+ // Otherwise, return 0-minute string.
+ return MeasureFormat.getInstance(
+ Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(0, MeasureUnit.MINUTE));
+ }
+
+ private String getReadableDuration(
+ Duration duration,
+ FormatWidth formatWidthHourAndMinute,
+ @StringRes int durationLessThanOneMinuteStringId) {
+ return getReadableDuration(
+ duration,
+ formatWidthHourAndMinute,
+ durationLessThanOneMinuteStringId,
+ /* forceFormatWidth= */ false);
+ }
+
+ private String getRoundedUpToMinuteReadableDuration(long remainingTime) {
+ final Duration duration = Duration.ofMillis(
+ remainingTime > MINUTE_MS ?
+ (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
+ remainingTime);
+ return getReadableDuration(
+ duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
+ }
+
+ private String getText(long remainingTime) {
+ return mActivity.getString(
+ R.string.time_left_for_app,
+ getRoundedUpToMinuteReadableDuration(remainingTime));
+ }
+
+ public void openAppUsageSettings(View view) {
+ final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
+ .putExtra(Intent.EXTRA_PACKAGE_NAME,
+ mTask.getTopComponent().getPackageName()).addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ try {
+ final BaseActivity activity = BaseActivity.fromContext(view.getContext());
+ final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
+ view, 0, 0,
+ view.getWidth(), view.getHeight());
+ activity.startActivity(intent, options.toBundle());
+
+ // TODO: add WW logging on the app usage settings click.
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Failed to open app usage settings for task "
+ + mTask.getTopComponent().getPackageName(), e);
+ }
+ }
+
+ private String getContentDescriptionForTask(
+ Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
+ return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
+ mActivity.getString(
+ R.string.task_contents_description_with_remaining_time,
+ task.titleDescription,
+ getText(appRemainingTimeMs)) :
+ task.titleDescription;
+ }
+
+ private void replaceBanner(View view) {
+ resetOldBanner();
+ setBanner(view);
+ }
+
+ private void resetOldBanner() {
+ if (mBanner != null) {
+ mBanner.setOutlineProvider(mOldBannerOutlineProvider);
+ mTaskView.removeView(mBanner);
+ mBanner.setOnClickListener(null);
+ mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner);
+ }
+ }
+
+ private void setBanner(View view) {
+ mBanner = view;
+ if (view != null) {
+ setupAndAddBanner();
+ setBannerOutline();
+ }
+ }
+
+ private void setupAndAddBanner() {
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) mBanner.getLayoutParams();
+ layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
+ layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
+ mTaskView.getThumbnail().getLayoutParams()).bottomMargin;
+ mBanner.setTranslationY(mBannerOffsetPercentage * mBanner.getHeight());
+ mTaskView.addView(mBanner);
+ }
+
+ private void setBannerOutline() {
+ mOldBannerOutlineProvider = mBanner.getOutlineProvider();
+ mBanner.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ mOldBannerOutlineProvider.getOutline(view, outline);
+ outline.offset(0, Math.round(-view.getTranslationY() + mVerticalOffset));
+ }
+ });
+ mBanner.setClipToOutline(true);
+ }
+
+ void updateBannerOffset(float offsetPercentage, float verticalOffset) {
+ if (mBanner != null && mBannerOffsetPercentage != offsetPercentage) {
+ mVerticalOffset = verticalOffset;
+ mBannerOffsetPercentage = offsetPercentage;
+ mBanner.setTranslationY(offsetPercentage * mBanner.getHeight() + mVerticalOffset);
+ mBanner.invalidateOutline();
+ }
+ }
+
+ void setBannerColorTint(int color, float amount) {
+ if (mBanner == null) {
+ return;
+ }
+ if (amount == 0) {
+ mBanner.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ Paint layerPaint = new Paint();
+ layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
+ mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint);
+ mBanner.setLayerPaint(layerPaint);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
new file mode 100644
index 0000000..1548268
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.RemoteViews.RemoteViewOutlineProvider;
+
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+
+import java.util.stream.IntStream;
+
+/**
+ * Mimics the appearance of the background view of a {@link LauncherAppWidgetHostView} through a
+ * an App Widget activity launch animation.
+ */
+@TargetApi(Build.VERSION_CODES.S)
+final class FloatingWidgetBackgroundView extends View {
+ private final ColorDrawable mFallbackDrawable = new ColorDrawable();
+ private final DrawableProperties mForegroundProperties = new DrawableProperties();
+ private final DrawableProperties mBackgroundProperties = new DrawableProperties();
+
+ private Drawable mOriginalForeground;
+ private Drawable mOriginalBackground;
+ private float mFinalRadius;
+ private float mInitialOutlineRadius;
+ private float mOutlineRadius;
+ private boolean mIsUsingFallback;
+ private View mSourceView;
+
+ FloatingWidgetBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
+ }
+ });
+ setClipToOutline(true);
+ }
+
+ void init(LauncherAppWidgetHostView hostView, View backgroundView, float finalRadius,
+ int fallbackBackgroundColor) {
+ mFinalRadius = finalRadius;
+ mSourceView = backgroundView;
+ mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView);
+ mIsUsingFallback = false;
+ if (isSupportedDrawable(backgroundView.getForeground())) {
+ mOriginalForeground = backgroundView.getForeground();
+ mForegroundProperties.init(
+ mOriginalForeground.getConstantState().newDrawable().mutate());
+ setForeground(mForegroundProperties.mDrawable);
+ Drawable clipPlaceholder =
+ mOriginalForeground.getConstantState().newDrawable().mutate();
+ clipPlaceholder.setAlpha(0);
+ mSourceView.setForeground(clipPlaceholder);
+ }
+ if (isSupportedDrawable(backgroundView.getBackground())) {
+ mOriginalBackground = backgroundView.getBackground();
+ mBackgroundProperties.init(
+ mOriginalBackground.getConstantState().newDrawable().mutate());
+ setBackground(mBackgroundProperties.mDrawable);
+ Drawable clipPlaceholder =
+ mOriginalBackground.getConstantState().newDrawable().mutate();
+ clipPlaceholder.setAlpha(0);
+ mSourceView.setBackground(clipPlaceholder);
+ } else if (mOriginalForeground == null) {
+ mFallbackDrawable.setColor(fallbackBackgroundColor);
+ setBackground(mFallbackDrawable);
+ mIsUsingFallback = true;
+ }
+ }
+
+ /** Update the animated properties of the drawables. */
+ void update(float cornerRadiusProgress, float fallbackAlpha) {
+ if (isUninitialized()) return;
+ mOutlineRadius = mInitialOutlineRadius + (mFinalRadius - mInitialOutlineRadius)
+ * cornerRadiusProgress;
+ mForegroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+ mBackgroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+ setAlpha(mIsUsingFallback ? fallbackAlpha : 1f);
+ }
+
+ /** Restores the drawables to the source view. */
+ void finish() {
+ if (isUninitialized()) return;
+ if (mOriginalForeground != null) mSourceView.setForeground(mOriginalForeground);
+ if (mOriginalBackground != null) mSourceView.setBackground(mOriginalBackground);
+ }
+
+ void recycle() {
+ mSourceView = null;
+ mOriginalForeground = null;
+ mOriginalBackground = null;
+ mOutlineRadius = 0;
+ mFinalRadius = 0;
+ setForeground(null);
+ setBackground(null);
+ }
+
+ /** Get the largest of drawable corner radii or background view outline radius. */
+ float getMaximumRadius() {
+ if (isUninitialized()) return 0;
+ return Math.max(mInitialOutlineRadius, Math.max(getMaxRadius(mOriginalForeground),
+ getMaxRadius(mOriginalBackground)));
+ }
+
+ private boolean isUninitialized() {
+ return mSourceView == null;
+ }
+
+ /** Returns the maximum corner radius of {@param drawable}. */
+ private static float getMaxRadius(Drawable drawable) {
+ if (!(drawable instanceof GradientDrawable)) return 0;
+ float[] cornerRadii = ((GradientDrawable) drawable).getCornerRadii();
+ float cornerRadius = ((GradientDrawable) drawable).getCornerRadius();
+ double radiiMax = cornerRadii == null ? 0 : IntStream.range(0, cornerRadii.length)
+ .mapToDouble(i -> cornerRadii[i]).max().orElse(0);
+ return Math.max(cornerRadius, (float) radiiMax);
+ }
+
+ /** Returns whether the given drawable type is supported. */
+ private static boolean isSupportedDrawable(Drawable drawable) {
+ return drawable instanceof ColorDrawable || (drawable instanceof GradientDrawable
+ && ((GradientDrawable) drawable).getShape() == GradientDrawable.RECTANGLE);
+ }
+
+ /** Corner radius from source view's outline, or enforced view. */
+ private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) {
+ if (RoundedCornerEnforcement.isRoundedCornerEnabled()
+ && hostView.hasEnforcedCornerRadius()) {
+ return hostView.getEnforcedCornerRadius();
+ } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider
+ && v.getClipToOutline()) {
+ return ((RemoteViewOutlineProvider) v.getOutlineProvider()).getRadius();
+ }
+ return 0;
+ }
+
+ /** Stores and modifies a drawable's properties through an animation. */
+ private static class DrawableProperties {
+ private Drawable mDrawable;
+ private float mOriginalRadius;
+ private float[] mOriginalRadii;
+ private final float[] mTmpRadii = new float[8];
+
+ /** Store a drawable's animated properties. */
+ void init(Drawable drawable) {
+ mDrawable = drawable;
+ if (!(drawable instanceof GradientDrawable)) return;
+ mOriginalRadius = ((GradientDrawable) drawable).getCornerRadius();
+ mOriginalRadii = ((GradientDrawable) drawable).getCornerRadii();
+ }
+
+ /**
+ * Update the drawable for the given animation state.
+ *
+ * @param finalRadius the radius of each corner when {@param progress} is 1
+ * @param progress the linear progress of the corner radius from its original value to
+ * {@param finalRadius}
+ */
+ void updateDrawable(float finalRadius, float progress) {
+ if (!(mDrawable instanceof GradientDrawable)) return;
+ GradientDrawable d = (GradientDrawable) mDrawable;
+ if (mOriginalRadii != null) {
+ for (int i = 0; i < mOriginalRadii.length; i++) {
+ mTmpRadii[i] = mOriginalRadii[i] + (finalRadius - mOriginalRadii[i]) * progress;
+ }
+ d.setCornerRadii(mTmpRadii);
+ } else {
+ d.setCornerRadius(mOriginalRadius + (finalRadius - mOriginalRadius) * progress);
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
new file mode 100644
index 0000000..88b11a0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Size;
+import android.view.GhostView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.FloatingView;
+import com.android.launcher3.views.ListenerView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+/** A view that mimics an App Widget through a launch animation. */
+@TargetApi(Build.VERSION_CODES.S)
+public class FloatingWidgetView extends FrameLayout implements AnimatorListener,
+ OnGlobalLayoutListener, FloatingView {
+ private static final Matrix sTmpMatrix = new Matrix();
+
+ private final Launcher mLauncher;
+ private final ListenerView mListenerView;
+ private final FloatingWidgetBackgroundView mBackgroundView;
+ private final RectF mBackgroundOffset = new RectF();
+
+ private LauncherAppWidgetHostView mAppWidgetView;
+ private View mAppWidgetBackgroundView;
+ private RectF mBackgroundPosition;
+ private GhostView mForegroundOverlayView;
+
+ private Runnable mEndRunnable;
+ private Runnable mFastFinishRunnable;
+ private Runnable mOnTargetChangeRunnable;
+ private boolean mAppTargetIsTranslucent;
+
+ private float mIconOffsetY;
+
+ public FloatingWidgetView(Context context) {
+ this(context, null);
+ }
+
+ public FloatingWidgetView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FloatingWidgetView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+ mListenerView = new ListenerView(context, attrs);
+ mBackgroundView = new FloatingWidgetBackgroundView(context, attrs, defStyleAttr);
+ addView(mBackgroundView);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ Runnable endRunnable = mEndRunnable;
+ mEndRunnable = null;
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnGlobalLayoutListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public void onGlobalLayout() {
+ if (isUninitialized()) return;
+ positionViews();
+ if (mOnTargetChangeRunnable != null) {
+ mOnTargetChangeRunnable.run();
+ }
+ }
+
+ /** Sets a runnable that is called on global layout change. */
+ public void setOnTargetChangeListener(Runnable onTargetChangeListener) {
+ mOnTargetChangeRunnable = onTargetChangeListener;
+ }
+
+ /** Sets a runnable that is called after a call to {@link #fastFinish()}. */
+ public void setFastFinishRunnable(Runnable runnable) {
+ mFastFinishRunnable = runnable;
+ }
+
+ /** Callback at the end or early exit of the animation. */
+ @Override
+ public void fastFinish() {
+ if (isUninitialized()) return;
+ Runnable fastFinishRunnable = mFastFinishRunnable;
+ if (fastFinishRunnable != null) {
+ fastFinishRunnable.run();
+ }
+ Runnable endRunnable = mEndRunnable;
+ mEndRunnable = null;
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ }
+
+ private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView,
+ RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius,
+ boolean appTargetIsTranslucent, int fallbackBackgroundColor) {
+ mAppWidgetView = originalView;
+ // Deferrals must begin before GhostView is created. See b/190818220
+ mAppWidgetView.beginDeferringUpdates();
+ mBackgroundPosition = widgetBackgroundPosition;
+ mAppTargetIsTranslucent = appTargetIsTranslucent;
+ mEndRunnable = () -> finish(dragLayer);
+
+ mAppWidgetBackgroundView = RoundedCornerEnforcement.findBackground(mAppWidgetView);
+ if (mAppWidgetBackgroundView == null) {
+ mAppWidgetBackgroundView = mAppWidgetView;
+ }
+
+ getRelativePosition(mAppWidgetBackgroundView, dragLayer, mBackgroundPosition);
+ getRelativePosition(mAppWidgetBackgroundView, mAppWidgetView, mBackgroundOffset);
+ if (!mAppTargetIsTranslucent) {
+ mBackgroundView.init(mAppWidgetView, mAppWidgetBackgroundView, windowCornerRadius,
+ fallbackBackgroundColor);
+ // Layout call before GhostView creation so that the overlaid view isn't clipped
+ layout(0, 0, windowSize.getWidth(), windowSize.getHeight());
+ mForegroundOverlayView = GhostView.addGhost(mAppWidgetView, this);
+ positionViews();
+ }
+
+ mListenerView.setListener(this::fastFinish);
+ dragLayer.addView(mListenerView);
+ }
+
+ /**
+ * Updates the position and opacity of the floating widget's components.
+ *
+ * @param backgroundPosition the new position of the widget's background relative to the
+ * {@link FloatingWidgetView}'s parent
+ * @param floatingWidgetAlpha the overall opacity of the {@link FloatingWidgetView}
+ * @param foregroundAlpha the opacity of the foreground layer
+ * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App
+ * Widget doesn't have a background
+ * @param cornerRadiusProgress progress of the corner radius animation, where 0 is the
+ * original radius and 1 is the window radius
+ */
+ public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha,
+ float fallbackBackgroundAlpha, float cornerRadiusProgress) {
+ if (isUninitialized() || mAppTargetIsTranslucent) return;
+ setAlpha(floatingWidgetAlpha);
+ mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha);
+ mAppWidgetView.setAlpha(foregroundAlpha);
+ mBackgroundPosition = backgroundPosition;
+ positionViews();
+ }
+
+ @Override
+ public void setPositionOffsetY(float y) {
+ mIconOffsetY = y;
+ onGlobalLayout();
+ }
+
+ /** Sets the layout parameters of the floating view and its background view child. */
+ private void positionViews() {
+ LayoutParams layoutParams = (LayoutParams) getLayoutParams();
+ layoutParams.setMargins(0, 0, 0, 0);
+ setLayoutParams(layoutParams);
+
+ // FloatingWidgetView layout is forced LTR
+ mBackgroundView.setTranslationX(mBackgroundPosition.left);
+ mBackgroundView.setTranslationY(mBackgroundPosition.top + mIconOffsetY);
+ LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams();
+ backgroundParams.leftMargin = 0;
+ backgroundParams.topMargin = 0;
+ backgroundParams.width = (int) mBackgroundPosition.width();
+ backgroundParams.height = (int) mBackgroundPosition.height();
+ mBackgroundView.setLayoutParams(backgroundParams);
+
+ if (mForegroundOverlayView != null) {
+ sTmpMatrix.reset();
+ float foregroundScale =
+ mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth();
+ sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(),
+ -mBackgroundOffset.top - mAppWidgetView.getTop());
+ sTmpMatrix.postScale(foregroundScale, foregroundScale);
+ sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top
+ + mIconOffsetY);
+ mForegroundOverlayView.setMatrix(sTmpMatrix);
+ }
+ }
+
+ private void finish(DragLayer dragLayer) {
+ mAppWidgetView.setAlpha(1f);
+ GhostView.removeGhost(mAppWidgetView);
+ ((ViewGroup) dragLayer.getParent()).removeView(this);
+ dragLayer.removeView(mListenerView);
+ mBackgroundView.finish();
+ // Removing GhostView must occur before ending deferrals. See b/190818220
+ mAppWidgetView.endDeferringUpdates();
+ recycle();
+ mLauncher.getViewCache().recycleView(R.layout.floating_widget_view, this);
+ }
+
+ public float getInitialCornerRadius() {
+ return mBackgroundView.getMaximumRadius();
+ }
+
+ private boolean isUninitialized() {
+ return mForegroundOverlayView == null;
+ }
+
+ private void recycle() {
+ mIconOffsetY = 0;
+ mEndRunnable = null;
+ mFastFinishRunnable = null;
+ mOnTargetChangeRunnable = null;
+ mBackgroundPosition = null;
+ mListenerView.setListener(null);
+ mAppWidgetView = null;
+ mForegroundOverlayView = null;
+ mAppWidgetBackgroundView = null;
+ mBackgroundView.recycle();
+ }
+
+ /**
+ * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of
+ * {@param originalView}.
+ *
+ * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's
+ * background bounds
+ * @param windowSize the size of the window when launched
+ * @param windowCornerRadius the corner radius of the window
+ */
+ public static FloatingWidgetView getFloatingWidgetView(Launcher launcher,
+ LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition,
+ Size windowSize, float windowCornerRadius, boolean appTargetsAreTranslucent,
+ int fallbackBackgroundColor) {
+ final DragLayer dragLayer = launcher.getDragLayer();
+ ViewGroup parent = (ViewGroup) dragLayer.getParent();
+ FloatingWidgetView floatingView =
+ launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent);
+ floatingView.recycle();
+
+ floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowSize,
+ windowCornerRadius, appTargetsAreTranslucent, fallbackBackgroundColor);
+ parent.addView(floatingView);
+ return floatingView;
+ }
+
+ /**
+ * Extract a background color from a target's task description, or fall back to the given
+ * context's theme background color.
+ */
+ public static int getDefaultBackgroundColor(
+ Context context, RemoteAnimationTargetCompat target) {
+ return (target != null && target.taskInfo.taskDescription != null)
+ ? target.taskInfo.taskDescription.getBackgroundColor()
+ : Themes.getColorBackground(context);
+ }
+
+ private static void getRelativePosition(View descendant, View ancestor, RectF position) {
+ float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()};
+ Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points,
+ false /* includeRootScroll */, true /* ignoreTransform */);
+ position.set(
+ Math.min(points[0], points[2]),
+ Math.min(points[1], points[3]),
+ Math.max(points[0], points[2]),
+ Math.max(points[1], points[3]));
+ }
+}
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 0000000..5b0ade0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * 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();
+ }
+
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+
+ @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;
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ super.setAlpha(alpha);
+ if (alpha > 0) {
+ setVisibility(VISIBLE);
+ } else {
+ setVisibility(INVISIBLE);
+ }
+ }
+
+ /**
+ * Set the tint color of the icon, useful for scrimming or dimming.
+ *
+ * @param color to blend in.
+ * @param amount [0,1] 0 no tint, 1 full tint
+ */
+ public void setIconColorTint(int color, float amount) {
+ if (mDrawable != null) {
+ mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
+ }
+ }
+}
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 0000000..65956d5
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -0,0 +1,260 @@
+/*
+ * 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.LauncherState.CLEAR_ALL_BUTTON;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
+import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
+import static com.android.launcher3.LauncherState.SPRING_LOADED;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.quickstep.LauncherActivityInterface;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.RecentsExtraCard;
+
+/**
+ * {@link RecentsView} used in Launcher activity
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class LauncherRecentsView extends RecentsView<BaseQuickstepLauncher, LauncherState>
+ implements StateListener<LauncherState> {
+
+ private RecentsExtraCard mRecentsExtraCardPlugin;
+ private RecentsExtraViewContainer mRecentsExtraViewContainer;
+ private PluginListener<RecentsExtraCard> mRecentsExtraCardPluginListener =
+ new PluginListener<RecentsExtraCard>() {
+ @Override
+ public void onPluginConnected(RecentsExtraCard recentsExtraCard, Context context) {
+ createRecentsExtraCard();
+ mRecentsExtraCardPlugin = recentsExtraCard;
+ mRecentsExtraCardPlugin.setupView(context, mRecentsExtraViewContainer, mActivity);
+ }
+
+ @Override
+ public void onPluginDisconnected(RecentsExtraCard plugin) {
+ removeView(mRecentsExtraViewContainer);
+ mRecentsExtraCardPlugin = null;
+ mRecentsExtraViewContainer = null;
+ }
+ };
+
+ 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, LauncherActivityInterface.INSTANCE);
+ mActivity.getStateManager().addStateListener(this);
+ }
+
+ @Override
+ public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
+ super.init(actionsView, splitPlaceholderView);
+ setContentAlpha(0);
+ }
+
+ @Override
+ public void startHome() {
+ mActivity.getStateManager().goToState(NORMAL);
+ }
+
+ @Override
+ protected void onTaskLaunchAnimationEnd(boolean success) {
+ if (success) {
+ mActivity.getStateManager().moveToRestState();
+ } else {
+ LauncherState state = mActivity.getStateManager().getState();
+ mActivity.getAllAppsController().setState(state);
+ }
+ super.onTaskLaunchAnimationEnd(success);
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+
+ setLayoutRotation(Surface.ROTATION_0, Surface.ROTATION_0);
+ }
+
+ @Override
+ public void onStateTransitionStart(LauncherState toState) {
+ setOverviewStateEnabled(toState.overviewUi);
+ setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
+ setOverviewFullscreenEnabled(toState.getOverviewFullscreenProgress() == 1);
+ setFreezeViewVisibility(true);
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == NORMAL || finalState == SPRING_LOADED) {
+ // Clean-up logic that occurs when recents is no longer in use/visible.
+ reset();
+ }
+ setOverlayEnabled(finalState == OVERVIEW || finalState == OVERVIEW_MODAL_TASK);
+ setFreezeViewVisibility(false);
+ }
+
+ @Override
+ public void setOverviewStateEnabled(boolean enabled) {
+ super.setOverviewStateEnabled(enabled);
+ if (enabled) {
+ LauncherState state = mActivity.getStateManager().getState();
+ boolean hasClearAllButton = (state.getVisibleElements(mActivity)
+ & CLEAR_ALL_BUTTON) != 0;
+ setDisallowScrollToClearAll(!hasClearAllButton);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ boolean result = super.onTouchEvent(ev);
+ // Do not let touch escape to siblings below this view.
+ return result || mActivity.getStateManager().getState().overviewUi;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(
+ mRecentsExtraCardPluginListener, RecentsExtraCard.class);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(
+ mRecentsExtraCardPluginListener);
+ }
+
+ @Override
+ protected int computeMinScroll() {
+ if (canComputeScrollX() && !mIsRtl) {
+ return computeScrollX();
+ }
+ return super.computeMinScroll();
+ }
+
+ @Override
+ protected int computeMaxScroll() {
+ if (canComputeScrollX() && mIsRtl) {
+ return computeScrollX();
+ }
+ return super.computeMaxScroll();
+ }
+
+ private boolean canComputeScrollX() {
+ return mRecentsExtraCardPlugin != null && getTaskViewCount() > 0
+ && !mDisallowScrollToClearAll;
+ }
+
+ private int computeScrollX() {
+ int scrollIndex = getTaskViewStartIndex() - 1;
+ while (scrollIndex >= 0 && getChildAt(scrollIndex) instanceof RecentsExtraViewContainer
+ && ((RecentsExtraViewContainer) getChildAt(scrollIndex)).isScrollable()) {
+ scrollIndex--;
+ }
+ return getScrollForPage(scrollIndex + 1);
+ }
+
+ private void createRecentsExtraCard() {
+ mRecentsExtraViewContainer = new RecentsExtraViewContainer(getContext());
+ FrameLayout.LayoutParams helpCardParams =
+ new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT);
+ mRecentsExtraViewContainer.setLayoutParams(helpCardParams);
+ mRecentsExtraViewContainer.setScrollable(true);
+ addView(mRecentsExtraViewContainer, 0);
+ }
+
+ @Override
+ public boolean hasRecentsExtraCard() {
+ return mRecentsExtraViewContainer != null;
+ }
+
+ @Override
+ public void setContentAlpha(float alpha) {
+ super.setContentAlpha(alpha);
+ if (mRecentsExtraViewContainer != null) {
+ mRecentsExtraViewContainer.setAlpha(alpha);
+ }
+ }
+
+ @Override
+ protected DepthController getDepthController() {
+ return mActivity.getDepthController();
+ }
+
+ @Override
+ public void setModalStateEnabled(boolean isModalState) {
+ super.setModalStateEnabled(isModalState);
+ if (isModalState) {
+ mActivity.getStateManager().goToState(LauncherState.OVERVIEW_MODAL_TASK);
+ } else {
+ if (mActivity.isInState(LauncherState.OVERVIEW_MODAL_TASK)) {
+ mActivity.getStateManager().goToState(LauncherState.OVERVIEW);
+ }
+ }
+ }
+
+ @Override
+ protected void onDismissAnimationEnds() {
+ super.onDismissAnimationEnds();
+ if (mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
+ // We want to keep the tasks translations in this temporary state
+ // after resetting the rest above
+ setTaskViewsPrimarySplitTranslation(mTaskViewsPrimarySplitTranslation);
+ setTaskViewsSecondarySplitTranslation(mTaskViewsSecondarySplitTranslation);
+ }
+ }
+
+ @Override
+ public void initiateSplitSelect(TaskView taskView,
+ SplitConfigurationOptions.SplitPositionOption splitPositionOption) {
+ super.initiateSplitSelect(taskView, splitPositionOption);
+ mActivity.getStateManager().goToState(LauncherState.OVERVIEW_SPLIT_SELECT);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ // If overview is in modal state when rotate, reset it to overview state without running
+ // animation.
+ if (mActivity.isInState(OVERVIEW_MODAL_TASK)) {
+ mActivity.getStateManager().goToState(LauncherState.OVERVIEW, false);
+ resetModalVisuals();
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
new file mode 100644
index 0000000..8c115e5
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views;
+
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SHARE;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
+import com.android.quickstep.util.LayoutUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * View for showing action buttons in Overview
+ */
+public class OverviewActionsView<T extends OverlayUICallbacks> extends FrameLayout
+ implements OnClickListener, Insettable {
+
+ private final Rect mInsets = new Rect();
+
+ @IntDef(flag = true, value = {
+ HIDDEN_NON_ZERO_ROTATION,
+ HIDDEN_NO_TASKS,
+ HIDDEN_NO_RECENTS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActionsHiddenFlags { }
+
+ public static final int HIDDEN_NON_ZERO_ROTATION = 1 << 0;
+ public static final int HIDDEN_NO_TASKS = 1 << 1;
+ public static final int HIDDEN_NO_RECENTS = 1 << 2;
+
+ @IntDef(flag = true, value = {
+ DISABLED_SCROLLING,
+ DISABLED_ROTATED,
+ DISABLED_NO_THUMBNAIL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActionsDisabledFlags { }
+
+ public static final int DISABLED_SCROLLING = 1 << 0;
+ public static final int DISABLED_ROTATED = 1 << 1;
+ public static final int DISABLED_NO_THUMBNAIL = 1 << 2;
+
+ private static final int INDEX_CONTENT_ALPHA = 0;
+ private static final int INDEX_VISIBILITY_ALPHA = 1;
+ private static final int INDEX_FULLSCREEN_ALPHA = 2;
+ private static final int INDEX_HIDDEN_FLAGS_ALPHA = 3;
+ private static final int INDEX_SCROLL_ALPHA = 4;
+
+ private final MultiValueAlpha mMultiValueAlpha;
+
+ @ActionsHiddenFlags
+ private int mHiddenFlags;
+
+ @ActionsDisabledFlags
+ protected int mDisabledFlags;
+
+ protected T mCallbacks;
+
+ private float mModalness;
+ private float mModalTransformY;
+
+ protected DeviceProfile mDp;
+
+ public OverviewActionsView(Context context) {
+ this(context, null);
+ }
+
+ public OverviewActionsView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr, 0);
+ mMultiValueAlpha = new MultiValueAlpha(this, 5);
+ mMultiValueAlpha.setUpdateVisibility(true);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ View share = findViewById(R.id.action_share);
+ share.setOnClickListener(this);
+ findViewById(R.id.action_screenshot).setOnClickListener(this);
+ if (ENABLE_OVERVIEW_SHARE.get()) {
+ share.setVisibility(VISIBLE);
+ findViewById(R.id.oav_three_button_space).setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * Set listener for callbacks on action button taps.
+ *
+ * @param callbacks for callbacks, or {@code null} to clear the listener.
+ */
+ public void setCallbacks(T callbacks) {
+ mCallbacks = callbacks;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mCallbacks == null) {
+ return;
+ }
+ int id = view.getId();
+ if (id == R.id.action_share) {
+ mCallbacks.onShare();
+ } else if (id == R.id.action_screenshot) {
+ mCallbacks.onScreenshot();
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ mInsets.set(insets);
+ updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
+ updateHorizontalPadding();
+ }
+
+ public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean enable) {
+ if (enable) {
+ mHiddenFlags |= visibilityFlags;
+ } else {
+ mHiddenFlags &= ~visibilityFlags;
+ }
+ boolean isHidden = mHiddenFlags != 0;
+ mMultiValueAlpha.getProperty(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
+ }
+
+ /**
+ * Updates the proper disabled flag to indicate whether OverviewActionsView should be enabled.
+ * Ignores DISABLED_ROTATED flag for determining enabled. Flag is used to enable/disable
+ * buttons individually, currently done for select button in subclass.
+ *
+ * @param disabledFlags The flag to update.
+ * @param enable Whether to enable the disable flag: True will cause view to be disabled.
+ */
+ public void updateDisabledFlags(@ActionsDisabledFlags int disabledFlags, boolean enable) {
+ if (enable) {
+ mDisabledFlags |= disabledFlags;
+ } else {
+ mDisabledFlags &= ~disabledFlags;
+ }
+ //
+ boolean isEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
+ LayoutUtils.setViewEnabled(this, isEnabled);
+ }
+
+ public AlphaProperty getContentAlpha() {
+ return mMultiValueAlpha.getProperty(INDEX_CONTENT_ALPHA);
+ }
+
+ public AlphaProperty getVisibilityAlpha() {
+ return mMultiValueAlpha.getProperty(INDEX_VISIBILITY_ALPHA);
+ }
+
+ public AlphaProperty getFullscreenAlpha() {
+ return mMultiValueAlpha.getProperty(INDEX_FULLSCREEN_ALPHA);
+ }
+
+ public AlphaProperty getScrollAlpha() {
+ return mMultiValueAlpha.getProperty(INDEX_SCROLL_ALPHA);
+ }
+
+ private void updateHorizontalPadding() {
+ setPadding(mInsets.left, 0, mInsets.right, 0);
+ }
+
+ /** Updates vertical margins for different navigation mode or configuration changes. */
+ public void updateVerticalMargin(Mode mode) {
+ if (mDp == null) {
+ return;
+ }
+ LayoutParams actionParams = (LayoutParams) findViewById(
+ R.id.action_buttons).getLayoutParams();
+ actionParams.setMargins(
+ actionParams.leftMargin, getOverviewActionsTopMarginPx(mode, mDp),
+ actionParams.rightMargin, getOverviewActionsBottomMarginPx(mode, mDp));
+ }
+
+ /**
+ * Set the device profile for this view to draw with.
+ */
+ public void setDp(DeviceProfile dp) {
+ mDp = dp;
+ updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
+ requestLayout();
+ }
+
+ /**
+ * The current task is fully modal (modalness = 1) when it is shown on its own in a modal
+ * way. Modalness 0 means the task is shown in context with all the other tasks.
+ */
+ public void setTaskModalness(float modalness) {
+ mModalness = modalness;
+ applyTranslationY();
+ }
+
+ public void setModalTransformY(float modalTransformY) {
+ mModalTransformY = modalTransformY;
+ applyTranslationY();
+ }
+
+ private void applyTranslationY() {
+ setTranslationY(getModalTrans(mModalTransformY));
+ }
+
+ private float getModalTrans(float endTranslation) {
+ float progress = ACCEL_DEACCEL.getInterpolation(mModalness);
+ return Utilities.mapRange(progress, 0, endTranslation);
+ }
+
+ /** Get the top margin associated with the action buttons in Overview. */
+ public static int getOverviewActionsTopMarginPx(
+ SysUINavigationMode.Mode mode, DeviceProfile dp) {
+ // In vertical bar, use the smaller task margin for the top regardless of mode
+ if (dp.isVerticalBarLayout()) {
+ return dp.overviewTaskMarginPx;
+ }
+
+ if (mode == SysUINavigationMode.Mode.THREE_BUTTONS) {
+ return dp.overviewActionsMarginThreeButtonPx;
+ }
+
+ return dp.overviewActionsMarginGesturePx;
+ }
+
+ /** Get the bottom margin associated with the action buttons in Overview. */
+ public static int getOverviewActionsBottomMarginPx(
+ SysUINavigationMode.Mode mode, DeviceProfile dp) {
+ int inset = dp.getInsets().bottom;
+
+ if (dp.isVerticalBarLayout()) {
+ return inset;
+ }
+
+ if (mode == SysUINavigationMode.Mode.THREE_BUTTONS) {
+ return dp.overviewActionsMarginThreeButtonPx + inset;
+ }
+
+ return dp.overviewActionsMarginGesturePx + inset;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsExtraViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsExtraViewContainer.java
new file mode 100644
index 0000000..16bc3bc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsExtraViewContainer.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Empty view to house recents overview extra card
+ */
+public class RecentsExtraViewContainer extends FrameLayout {
+
+ private boolean mScrollable = false;
+
+ public RecentsExtraViewContainer(Context context) {
+ super(context);
+ }
+
+ public RecentsExtraViewContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public RecentsExtraViewContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Determine whether the view should be scrolled to in the recents overview, similar to the
+ * taskviews.
+ * @return true if viewed should be scrolled to, false if not
+ */
+ public boolean isScrollable() {
+ return mScrollable;
+ }
+
+ public void setScrollable(boolean scrollable) {
+ this.mScrollable = scrollable;
+ }
+}
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 0000000..3887060
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -0,0 +1,3946 @@
+/*
+ * 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.view.Surface.ROTATION_0;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
+import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
+import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.Utilities.mapToRange;
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_0_5;
+import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
+import static com.android.launcher3.statehandlers.DepthController.DEPTH;
+import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
+import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.LayoutTransition;
+import android.animation.LayoutTransition.TransitionListener;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.LocusId;
+import android.content.res.Configuration;
+import android.graphics.BlendMode;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.SparseBooleanArray;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+import android.widget.OverScroller;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringProperty;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.cache.HandlerRunnable;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.ResourceBasedOverride.Overrides;
+import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.TranslateEdgeEffect;
+import com.android.launcher3.util.ViewPool;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
+import com.android.quickstep.RemoteAnimationTargets;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskOverlayFactory;
+import com.android.quickstep.TaskThumbnailCache;
+import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.ViewUtils;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SplitScreenBounds;
+import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
+import com.android.systemui.plugins.ResourceProvider;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.wm.shell.pip.IPipAnimationListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A list of recent tasks.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>,
+ STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
+ TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+ TaskVisualsChangeListener, SplitScreenBounds.OnChangeListener {
+
+ // TODO(b/184899234): We use this timeout to wait a fixed period after switching to the
+ // screenshot when dismissing the current live task to ensure the app can try and get stopped.
+ private static final int REMOVE_TASK_WAIT_FOR_APP_STOP_MS = 100;
+
+ public static final FloatProperty<RecentsView> CONTENT_ALPHA =
+ new FloatProperty<RecentsView>("contentAlpha") {
+ @Override
+ public void setValue(RecentsView view, float v) {
+ view.setContentAlpha(v);
+ }
+
+ @Override
+ public Float get(RecentsView view) {
+ return view.getContentAlpha();
+ }
+ };
+
+ public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS =
+ new FloatProperty<RecentsView>("fullscreenProgress") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setFullscreenProgress(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mFullscreenProgress;
+ }
+ };
+
+ public static final FloatProperty<RecentsView> TASK_MODALNESS =
+ new FloatProperty<RecentsView>("taskModalness") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setTaskModalness(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mTaskModalness;
+ }
+ };
+
+ public static final FloatProperty<RecentsView> ADJACENT_PAGE_HORIZONTAL_OFFSET =
+ new FloatProperty<RecentsView>("adjacentPageHorizontalOffset") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ if (recentsView.mAdjacentPageHorizontalOffset != v) {
+ recentsView.mAdjacentPageHorizontalOffset = v;
+ recentsView.updatePageOffsets();
+ }
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mAdjacentPageHorizontalOffset;
+ }
+ };
+
+ /**
+ * Can be used to tint the color of the RecentsView to simulate a scrim that can views
+ * excluded from. Really should be a proper scrim.
+ * TODO(b/187528071): Remove this and replace with a real scrim.
+ */
+ private static final FloatProperty<RecentsView> COLOR_TINT =
+ new FloatProperty<RecentsView>("colorTint") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setColorTint(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.getColorTint();
+ }
+ };
+
+ /**
+ * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
+ * are currently both used to apply secondary translation. Should their use cases change to be
+ * more specific, we'd want to create a similar FloatProperty just for a TaskView's
+ * offsetX/Y property
+ */
+ public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION =
+ new FloatProperty<RecentsView>("taskSecondaryTranslation") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setTaskViewsResistanceTranslation(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mTaskViewsSecondaryTranslation;
+ }
+ };
+
+ /**
+ * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they
+ * are currently both used to apply secondary translation. Should their use cases change to be
+ * more specific, we'd want to create a similar FloatProperty just for a TaskView's
+ * offsetX/Y property
+ */
+ public static final FloatProperty<RecentsView> TASK_PRIMARY_SPLIT_TRANSLATION =
+ new FloatProperty<RecentsView>("taskPrimarySplitTranslation") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setTaskViewsPrimarySplitTranslation(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mTaskViewsPrimarySplitTranslation;
+ }
+ };
+
+ public static final FloatProperty<RecentsView> TASK_SECONDARY_SPLIT_TRANSLATION =
+ new FloatProperty<RecentsView>("taskSecondarySplitTranslation") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setTaskViewsSecondarySplitTranslation(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mTaskViewsSecondarySplitTranslation;
+ }
+ };
+
+ /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
+ public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
+ new FloatProperty<RecentsView>("recentsScale") {
+ @Override
+ public void setValue(RecentsView view, float scale) {
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ view.mLastComputedTaskStartPushOutDistance = null;
+ view.mLastComputedTaskEndPushOutDistance = null;
+ view.mLiveTileTaskViewSimulator.recentsViewScale.value = scale;
+ view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation);
+ view.updatePageOffsets();
+ }
+
+ @Override
+ public Float get(RecentsView view) {
+ return view.getScaleX();
+ }
+ };
+
+ public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS =
+ new FloatProperty<RecentsView>("recentsGrid") {
+ @Override
+ public void setValue(RecentsView view, float gridProgress) {
+ view.setGridProgress(gridProgress);
+ }
+
+ @Override
+ public Float get(RecentsView view) {
+ return view.mGridProgress;
+ }
+ };
+
+ // OverScroll constants
+ private static final int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270;
+
+ private static final int DISMISS_TASK_DURATION = 300;
+ private static final int ADDITION_TASK_DURATION = 200;
+ private static final float INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.55f;
+ private static final float ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.05f;
+
+ protected final RecentsOrientedState mOrientationState;
+ protected final BaseActivityInterface<STATE_TYPE, ACTIVITY_TYPE> mSizeStrategy;
+ protected RecentsAnimationController mRecentsAnimationController;
+ protected SurfaceTransactionApplier mSyncTransactionApplier;
+ protected int mTaskWidth;
+ protected int mTaskHeight;
+ protected final TransformParams mLiveTileParams = new TransformParams();
+ protected final TaskViewSimulator mLiveTileTaskViewSimulator;
+ protected final Rect mLastComputedTaskSize = new Rect();
+ protected final Rect mLastComputedGridSize = new Rect();
+ protected final Rect mLastComputedGridTaskSize = new Rect();
+ // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
+ protected Float mLastComputedTaskStartPushOutDistance = null;
+ protected Float mLastComputedTaskEndPushOutDistance = null;
+ protected boolean mEnableDrawingLiveTile = false;
+ protected final Rect mTempRect = new Rect();
+ protected final RectF mTempRectF = new RectF();
+ private final PointF mTempPointF = new PointF();
+ private final float[] mTempFloat = new float[1];
+ private final List<OnScrollChangedListener> mScrollListeners = new ArrayList<>();
+
+ // The threshold at which we update the SystemUI flags when animating from the task into the app
+ public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f;
+
+ protected final ACTIVITY_TYPE mActivity;
+ private final float mFastFlingVelocity;
+ private final RecentsModel mModel;
+ private final int mRowSpacing;
+ private final int mGridSideMargin;
+ private final ClearAllButton mClearAllButton;
+ private final Rect mClearAllButtonDeadZoneRect = new Rect();
+ private final Rect mTaskViewDeadZoneRect = new Rect();
+ /**
+ * Reflects if Recents is currently in the middle of a gesture
+ */
+ private boolean mGestureActive;
+
+ // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
+ private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
+
+ private final InvariantDeviceProfile mIdp;
+
+ private final ViewPool<TaskView> mTaskViewPool;
+
+ private final TaskOverlayFactory mTaskOverlayFactory;
+
+ protected boolean mDisallowScrollToClearAll;
+ private boolean mOverlayEnabled;
+ protected boolean mFreezeViewVisibility;
+ private boolean mOverviewGridEnabled;
+ private boolean mOverviewFullscreenEnabled;
+
+ private float mAdjacentPageHorizontalOffset = 0;
+ protected float mTaskViewsSecondaryTranslation = 0;
+ protected float mTaskViewsPrimarySplitTranslation = 0;
+ protected float mTaskViewsSecondarySplitTranslation = 0;
+ // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
+ private float mGridProgress = 0;
+ private final IntSet mTopRowIdSet = new IntSet();
+
+ // The GestureEndTarget that is still in progress.
+ protected GestureState.GestureEndTarget mCurrentGestureEndTarget;
+
+ // TODO(b/187528071): Remove these and replace with a real scrim.
+ private float mColorTint;
+ private final int mTintingColor;
+ private ObjectAnimator mTintingAnimator;
+
+ private int mOverScrollShift = 0;
+
+ /**
+ * TODO: Call reloadIdNeeded in onTaskStackChanged.
+ */
+ private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+ @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;
+ }
+
+ reloadIfNeeded();
+ enableLayoutTransitions();
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ if (!mHandleTaskStackChanges) {
+ return;
+ }
+
+ TaskView taskView = getTaskView(taskId);
+ if (taskView == null) {
+ return;
+ }
+ Task.TaskKey taskKey = taskView.getTask().key;
+ UI_HELPER_EXECUTOR.execute(new HandlerRunnable<>(
+ UI_HELPER_EXECUTOR.getHandler(),
+ () -> PackageManagerWrapper.getInstance()
+ .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
+ MAIN_EXECUTOR,
+ apkRemoved -> {
+ if (apkRemoved) {
+ dismissTask(taskId);
+ } else {
+ mModel.isTaskRemoved(taskKey.id, taskRemoved -> {
+ if (taskRemoved) {
+ dismissTask(taskId);
+ }
+ });
+ }
+ }));
+ }
+ };
+
+ private final PinnedStackAnimationListener mIPipAnimationListener =
+ new PinnedStackAnimationListener();
+ private int mPipCornerRadius;
+
+ // Used to keep track of the last requested task list id, so that we do not request to load the
+ // tasks again if we have already requested it and the task list has not changed
+ private int mTaskListChangeId = -1;
+
+ // Only valid until the launcher state changes to NORMAL
+ protected int mRunningTaskId = -1;
+ protected boolean mRunningTaskTileHidden;
+ private Task mTmpRunningTask;
+ protected int mFocusedTaskId = -1;
+ private float mFocusedTaskRatio;
+
+ private boolean mRunningTaskIconScaledDown = false;
+ private boolean mRunningTaskShowScreenshot = false;
+
+ private boolean mOverviewStateEnabled;
+ private boolean mHandleTaskStackChanges;
+ private boolean mSwipeDownShouldLaunchApp;
+ private boolean mTouchDownToStartHome;
+ private final float mSquaredTouchSlop;
+ private int mDownX;
+ private int mDownY;
+
+ private PendingAnimation mPendingAnimation;
+ private LayoutTransition mLayoutTransition;
+
+ @ViewDebug.ExportedProperty(category = "launcher")
+ protected float mContentAlpha = 1;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ protected float mFullscreenProgress = 0;
+ /**
+ * How modal is the current task to be displayed, 1 means the task is fully modal and no other
+ * tasks are show. 0 means the task is displays in context in the list with other tasks.
+ */
+ @ViewDebug.ExportedProperty(category = "launcher")
+ protected float mTaskModalness = 0;
+
+ // Keeps track of task id whose visual state should not be reset
+ private int mIgnoreResetTaskId = -1;
+
+ // 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 OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
+ private Layout mEmptyTextLayout;
+
+ /**
+ * Placeholder view indicating where the first split screen selected app will be placed
+ */
+ private SplitPlaceholderView mSplitPlaceholderView;
+ /**
+ * The first task that split screen selection was initiated with. When split select state is
+ * initialized, we create a
+ * {@link #createTaskDismissAnimation(TaskView, boolean, boolean, long)} for this TaskView but
+ * don't actually remove the task since the user might back out. As such, we also ensure this
+ * View doesn't go back into the {@link #mTaskViewPool}, see {@link #onViewRemoved(View)}
+ */
+ private TaskView mSplitHiddenTaskView;
+ /**
+ * Keeps track of the index of the TaskView that split screen was initialized with so we know
+ * where to insert it back into list of taskViews in case user backs out of entering split
+ * screen.
+ * NOTE: This index is the index while {@link #mSplitHiddenTaskView} was a child of recentsView,
+ * this doesn't get adjusted to reflect the new child count after the taskView is dismissed/
+ * removed from recentsView
+ */
+ private int mSplitHiddenTaskViewIndex;
+
+ // Keeps track of the index where the first TaskView should be
+ private int mTaskViewStartIndex = 0;
+ private OverviewActionsView mActionsView;
+
+ private MultiWindowModeChangedListener mMultiWindowModeChangedListener =
+ new MultiWindowModeChangedListener() {
+ @Override
+ public void onMultiWindowModeChanged(boolean inMultiWindowMode) {
+ mOrientationState.setMultiWindowMode(inMultiWindowMode);
+ setLayoutRotation(mOrientationState.getTouchRotation(),
+ mOrientationState.getDisplayRotation());
+ updateChildTaskOrientations();
+ if (!inMultiWindowMode && mOverviewStateEnabled) {
+ // TODO: Re-enable layout transitions for addition of the unpinned task
+ reloadIfNeeded();
+ }
+ }
+ };
+
+ private RunnableList mSideTaskLaunchCallback;
+
+ public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
+ BaseActivityInterface sizeStrategy) {
+ super(context, attrs, defStyleAttr);
+ setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
+ setEnableFreeScroll(true);
+ mSizeStrategy = sizeStrategy;
+ mActivity = BaseActivity.fromContext(context);
+ mOrientationState = new RecentsOrientedState(
+ context, mSizeStrategy, this::animateRecentsRotationInPlace);
+ final int rotation = mActivity.getDisplay().getRotation();
+ mOrientationState.setRecentsRotation(rotation);
+
+ mFastFlingVelocity = getResources()
+ .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
+ mModel = RecentsModel.INSTANCE.get(context);
+ mIdp = InvariantDeviceProfile.INSTANCE.get(context);
+
+ mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
+ .inflate(R.layout.overview_clear_all_button, this, false);
+ mClearAllButton.setOnClickListener(this::dismissAllTasks);
+ mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
+ 10 /* initial size */);
+
+ mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
+ setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
+ mRowSpacing = getResources().getDimensionPixelSize(R.dimen.overview_grid_row_spacing);
+ mGridSideMargin = getResources().getDimensionPixelSize(R.dimen.overview_grid_side_margin);
+ mSquaredTouchSlop = squaredTouchSlop(context);
+
+ 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));
+ mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
+ Typeface.NORMAL));
+ mEmptyMessagePaint.setAntiAlias(true);
+ mEmptyMessagePadding = getResources()
+ .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
+ setWillNotDraw(false);
+ updateEmptyMessage();
+ mOrientationHandler = mOrientationState.getOrientationHandler();
+
+ mTaskOverlayFactory = Overrides.getObject(
+ TaskOverlayFactory.class,
+ context.getApplicationContext(),
+ R.string.task_overlay_factory_class);
+
+ // Initialize quickstep specific cache params here, as this is constructed only once
+ mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
+
+ mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy(),
+ true /* isForLiveTile */);
+ mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+ mLiveTileTaskViewSimulator.setOrientationState(mOrientationState);
+ mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
+
+ mTintingColor = getForegroundScrimDimColor(context);
+ }
+
+ public OverScroller getScroller() {
+ return mScroller;
+ }
+
+ public boolean isRtl() {
+ return mIsRtl;
+ }
+
+ @Override
+ protected void initEdgeEffect() {
+ mEdgeGlowLeft = new TranslateEdgeEffect(getContext());
+ mEdgeGlowRight = new TranslateEdgeEffect(getContext());
+ }
+
+ @Override
+ protected void drawEdgeEffect(Canvas canvas) {
+ // Do not draw edge effect
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ // Draw overscroll
+ if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) {
+ final int restoreCount = canvas.save();
+
+ int primarySize = mOrientationHandler.getPrimaryValue(getWidth(), getHeight());
+ int scroll = OverScroll.dampedScroll(getUndampedOverScrollShift(), primarySize);
+ mOrientationHandler.set(canvas, CANVAS_TRANSLATE, scroll);
+
+ if (mOverScrollShift != scroll) {
+ mOverScrollShift = scroll;
+ dispatchScrollChanged();
+ }
+
+ super.dispatchDraw(canvas);
+ canvas.restoreToCount(restoreCount);
+ } else {
+ if (mOverScrollShift != 0) {
+ mOverScrollShift = 0;
+ dispatchScrollChanged();
+ }
+ super.dispatchDraw(canvas);
+ }
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+ && mLiveTileParams.getTargetSet() != null) {
+ redrawLiveTile();
+ }
+ }
+
+ private float getUndampedOverScrollShift() {
+ final int width = getWidth();
+ final int height = getHeight();
+ int primarySize = mOrientationHandler.getPrimaryValue(width, height);
+ int secondarySize = mOrientationHandler.getSecondaryValue(width, height);
+
+ float effectiveShift = 0;
+ if (!mEdgeGlowLeft.isFinished()) {
+ mEdgeGlowLeft.setSize(secondarySize, primarySize);
+ if (((TranslateEdgeEffect) mEdgeGlowLeft).getTranslationShift(mTempFloat)) {
+ effectiveShift = mTempFloat[0];
+ postInvalidateOnAnimation();
+ }
+ }
+ if (!mEdgeGlowRight.isFinished()) {
+ mEdgeGlowRight.setSize(secondarySize, primarySize);
+ if (((TranslateEdgeEffect) mEdgeGlowRight).getTranslationShift(mTempFloat)) {
+ effectiveShift -= mTempFloat[0];
+ postInvalidateOnAnimation();
+ }
+ }
+
+ return effectiveShift * primarySize;
+ }
+
+ /**
+ * Returns the view shift due to overscroll
+ */
+ public int getOverScrollShift() {
+ return mOverScrollShift;
+ }
+
+ @Override
+ public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
+ if (mHandleTaskStackChanges) {
+ TaskView taskView = getTaskView(taskId);
+ if (taskView != null) {
+ Task task = taskView.getTask();
+ taskView.getThumbnail().setThumbnail(task, thumbnailData);
+ return task;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onTaskIconChanged(String pkg, UserHandle user) {
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView tv = getTaskViewAt(i);
+ Task task = tv.getTask();
+ if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
+ && task.key.userId == user.getIdentifier()) {
+ task.icon = null;
+ if (tv.getIconView().getDrawable() != null) {
+ tv.onTaskListVisibilityChanged(true /* visible */);
+ }
+ }
+ }
+ }
+
+ /**
+ * Update the thumbnail of the task.
+ * @param refreshNow Refresh immediately if it's true.
+ */
+ public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) {
+ TaskView taskView = getTaskView(taskId);
+ if (taskView != null) {
+ taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow);
+ }
+ return taskView;
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ updateTaskStackListenerState();
+ }
+
+ public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
+ mActionsView = actionsView;
+ mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+ mSplitPlaceholderView = splitPlaceholderView;
+ }
+
+ public SplitPlaceholderView getSplitPlaceholder() {
+ return mSplitPlaceholderView;
+ }
+
+ public boolean isSplitSelectionActive() {
+ return mSplitPlaceholderView.getSplitController().isSplitSelectActive();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ updateTaskStackListenerState();
+ mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
+ mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
+ mSyncTransactionApplier = new SurfaceTransactionApplier(this);
+ mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier);
+ RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
+ mIPipAnimationListener.setActivityAndRecentsView(mActivity, this);
+ SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
+ mIPipAnimationListener);
+ mOrientationState.initListeners();
+ SplitScreenBounds.INSTANCE.addOnChangeListener(this);
+ mTaskOverlayFactory.initListeners();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ updateTaskStackListenerState();
+ mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
+ mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
+ mSyncTransactionApplier = null;
+ mLiveTileParams.setSyncTransactionApplier(null);
+ executeSideTaskLaunchCallback();
+ RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
+ SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
+ SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
+ mIPipAnimationListener.setActivityAndRecentsView(null, null);
+ mOrientationState.destroyListeners();
+ mTaskOverlayFactory.removeListeners();
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+
+ // Clear the task data for the removed child if it was visible unless it's the initial
+ // taskview for entering split screen, we only pretend to dismiss the task
+ if (child instanceof TaskView && child != mSplitHiddenTaskView) {
+ TaskView taskView = (TaskView) child;
+ mHasVisibleTaskData.delete(taskView.getTask().key.id);
+ mTaskViewPool.recycle(taskView);
+ mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+ }
+ updateTaskStartIndex(child);
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ child.setAlpha(mContentAlpha);
+ // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the
+ // child direction back to match system settings.
+ child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL);
+ updateTaskStartIndex(child);
+ mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false);
+ updateEmptyMessage();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ maybeDrawEmptyMessage(canvas);
+ super.draw(canvas);
+ }
+
+ public void addSideTaskLaunchCallback(RunnableList callback) {
+ if (mSideTaskLaunchCallback == null) {
+ mSideTaskLaunchCallback = new RunnableList();
+ }
+ mSideTaskLaunchCallback.add(callback::executeAllAndDestroy);
+ }
+
+ private void executeSideTaskLaunchCallback() {
+ if (mSideTaskLaunchCallback != null) {
+ mSideTaskLaunchCallback.executeAllAndDestroy();
+ mSideTaskLaunchCallback = null;
+ }
+ }
+
+ public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
+ if (mRunningTaskId != -1 && mRunningTaskId == taskId) {
+ RemoteAnimationTargets targets = getLiveTileParams().getTargetSet();
+ if (targets != null && targets.findTask(taskId) != null) {
+ launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers,
+ targets.nonApps);
+ }
+ }
+ }
+
+ public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps,
+ RemoteAnimationTargetCompat[] wallpaper, RemoteAnimationTargetCompat[] nonApps) {
+ AnimatorSet anim = new AnimatorSet();
+ TaskView taskView = getTaskView(taskId);
+ if (taskView == null || !isTaskViewVisible(taskView)) {
+ // TODO: Refine this animation.
+ SurfaceTransactionApplier surfaceApplier =
+ new SurfaceTransactionApplier(mActivity.getDragLayer());
+ ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+ appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
+ appAnimator.setInterpolator(ACCEL_DEACCEL);
+ appAnimator.addUpdateListener(valueAnimator -> {
+ float percent = valueAnimator.getAnimatedFraction();
+ SurfaceParams.Builder builder = new SurfaceParams.Builder(
+ apps[apps.length - 1].leash);
+ Matrix matrix = new Matrix();
+ matrix.postScale(percent, percent);
+ matrix.postTranslate(mActivity.getDeviceProfile().widthPx * (1 - percent) / 2,
+ mActivity.getDeviceProfile().heightPx * (1 - percent) / 2);
+ builder.withAlpha(percent).withMatrix(matrix);
+ surfaceApplier.scheduleApply(builder.build());
+ });
+ anim.play(appAnimator);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finishRecentsAnimation(false /* toRecents */, null);
+ }
+ });
+ } else {
+ TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps,
+ true /* launcherClosing */, mActivity.getStateManager(), this,
+ getDepthController());
+ }
+ anim.start();
+ }
+
+ private void updateTaskStartIndex(View affectingView) {
+ if (!(affectingView instanceof TaskView) && !(affectingView instanceof ClearAllButton)) {
+ int childCount = getChildCount();
+
+ mTaskViewStartIndex = 0;
+ while (mTaskViewStartIndex < childCount
+ && !(getChildAt(mTaskViewStartIndex) instanceof TaskView)) {
+ mTaskViewStartIndex++;
+ }
+ }
+ }
+
+ public boolean isTaskViewVisible(TaskView tv) {
+ if (showAsGrid()) {
+ int screenStart = mOrientationHandler.getPrimaryScroll(this);
+ int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
+ return isTaskViewWithinBounds(tv, screenStart, screenEnd);
+ } else {
+ // For now, just check if it's the active task or an adjacent task
+ return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
+ }
+ }
+
+ private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
+ int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
+ showAsFullscreen(), showAsGrid());
+ int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
+ showAsFullscreen()));
+ int taskEnd = taskStart + taskSize;
+ return (taskStart >= start && taskStart <= end) || (taskEnd >= start
+ && taskEnd <= end);
+ }
+
+ public TaskView getTaskView(int taskId) {
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView taskView = getTaskViewAt(i);
+ if (taskView.hasTaskId(taskId)) {
+ return taskView;
+ }
+ }
+ return null;
+ }
+
+ public void setOverviewStateEnabled(boolean enabled) {
+ mOverviewStateEnabled = enabled;
+ updateTaskStackListenerState();
+ mOrientationState.setRotationWatcherEnabled(enabled);
+ if (!enabled) {
+ // Reset the running task when leaving overview since it can still have a reference to
+ // its thumbnail
+ mTmpRunningTask = null;
+ if (mSplitPlaceholderView.getSplitController().isSplitSelectActive()) {
+ cancelSplitSelect(false);
+ }
+ }
+ updateLocusId();
+ }
+
+ /**
+ * Whether the Clear All button is hidden or fully visible. Used to determine if center
+ * displayed page is a task or the Clear All button.
+ *
+ * @return True = Clear All button not fully visible, center page is a task. False = Clear All
+ * button fully visible, center page is Clear All button.
+ */
+ public boolean isClearAllHidden() {
+ return mClearAllButton.getAlpha() != 1f;
+ }
+
+ @Override
+ protected void onPageBeginTransition() {
+ super.onPageBeginTransition();
+ mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true);
+ }
+
+ @Override
+ protected void onPageEndTransition() {
+ super.onPageEndTransition();
+ if (isClearAllHidden()) {
+ mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
+ }
+ if (getNextPage() > 0) {
+ setSwipeDownShouldLaunchApp(true);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ super.onTouchEvent(ev);
+
+ if (showAsGrid()) {
+ int taskCount = getTaskViewCount();
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
+ // Keep consuming events to pass to delegate
+ return true;
+ }
+ }
+ } else {
+ TaskView taskView = getCurrentPageTaskView();
+ if (taskView != null && taskView.offerTouchToChildren(ev)) {
+ // Keep consuming events to pass to delegate
+ return true;
+ }
+ }
+
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_UP:
+ if (mTouchDownToStartHome) {
+ startHome();
+ }
+ mTouchDownToStartHome = false;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mTouchDownToStartHome = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // Passing the touch slop will not allow dismiss to home
+ if (mTouchDownToStartHome &&
+ (isHandlingTouch() ||
+ squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) {
+ mTouchDownToStartHome = false;
+ }
+ break;
+ case MotionEvent.ACTION_DOWN:
+ // Touch down anywhere but the deadzone around the visible clear all button and
+ // between the task views will start home on touch up
+ if (!isHandlingTouch() && !isModal()) {
+ if (mShowEmptyMessage) {
+ mTouchDownToStartHome = true;
+ } else {
+ updateDeadZoneRects();
+ final boolean clearAllButtonDeadZoneConsumed =
+ mClearAllButton.getAlpha() == 1
+ && mClearAllButtonDeadZoneRect.contains(x, y);
+ final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+ if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
+ && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
+ mTouchDownToStartHome = true;
+ }
+ }
+ }
+ mDownX = x;
+ mDownY = y;
+ break;
+ }
+
+ return isHandlingTouch();
+ }
+
+ @Override
+ protected void onNotSnappingToPageInFreeScroll() {
+ int finalPos = mScroller.getFinalX();
+ if (!showAsGrid() && finalPos > mMinScroll && finalPos < mMaxScroll) {
+ int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
+ int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
+
+ // If scrolling ends in the half of the added space that is closer to
+ // the end, settle to the end. Otherwise snap to the nearest page.
+ // If flinging past one of the ends, don't change the velocity as it
+ // will get stopped at the end anyway.
+ int pageSnapped = finalPos < (firstPageScroll + mMinScroll) / 2
+ ? mMinScroll
+ : finalPos > (lastPageScroll + mMaxScroll) / 2
+ ? mMaxScroll
+ : getScrollForPage(mNextPage);
+
+ mScroller.setFinalX(pageSnapped);
+ // Ensure the scroll/snap doesn't happen too fast;
+ int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
+ - mScroller.getDuration();
+ if (extraScrollDuration > 0) {
+ mScroller.extendDuration(extraScrollDuration);
+ }
+ }
+ }
+
+ @Override
+ protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
+ // Enables swiping to the left or right only if the task overlay is not modal.
+ if (!isModal()) {
+ super.determineScrollingStart(ev, touchSlopScale);
+ }
+ }
+
+ protected void applyLoadPlan(ArrayList<Task> tasks) {
+ if (mPendingAnimation != null) {
+ mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks));
+ return;
+ }
+
+ if (tasks == null || tasks.isEmpty()) {
+ removeTasksViewsAndClearAllButton();
+ onTaskStackUpdated();
+ return;
+ }
+
+ int currentTaskId = -1;
+ TaskView currentTaskView = getTaskViewAtByAbsoluteIndex(mCurrentPage);
+ if (currentTaskView != null) {
+ currentTaskId = currentTaskView.getTask().key.id;
+ }
+
+ // Unload existing visible task data
+ unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+
+ TaskView ignoreResetTaskView =
+ mIgnoreResetTaskId == -1 ? null : getTaskView(mIgnoreResetTaskId);
+
+ final int requiredTaskCount = tasks.size();
+ if (getTaskViewCount() != requiredTaskCount) {
+ if (indexOfChild(mClearAllButton) != -1) {
+ removeView(mClearAllButton);
+ }
+ for (int i = getTaskViewCount(); i < requiredTaskCount; i++) {
+ addView(mTaskViewPool.getView());
+ }
+ while (getTaskViewCount() > requiredTaskCount) {
+ removeView(getChildAt(getChildCount() - 1));
+ }
+ if (requiredTaskCount > 0) {
+ addView(mClearAllButton);
+ }
+ }
+
+ // Rebind and reset all task views
+ for (int i = requiredTaskCount - 1; i >= 0; i--) {
+ final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
+ final Task task = tasks.get(i);
+ final TaskView taskView = (TaskView) getChildAt(pageIndex);
+ taskView.bind(task, mOrientationState);
+ }
+ updateTaskSize();
+
+ int targetPage = -1;
+ if (mNextPage == INVALID_PAGE) {
+ // Set the current page to the running task, but not if settling on new task.
+ TaskView runningTaskView = getRunningTaskView();
+ if (runningTaskView != null) {
+ targetPage = indexOfChild(runningTaskView);
+ } else if (getTaskViewCount() > 0) {
+ targetPage = indexOfChild(getTaskViewAt(0));
+ }
+ } else if (currentTaskId != -1) {
+ currentTaskView = getTaskView(currentTaskId);
+ if (currentTaskView != null) {
+ targetPage = indexOfChild(currentTaskView);
+ }
+ }
+ if (targetPage != -1 && mCurrentPage != targetPage) {
+ setCurrentPage(targetPage);
+ }
+
+ if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreResetTaskView) {
+ // If the taskView mapping is changing, do not preserve the visuals. Since we are
+ // mostly preserving the first task, and new taskViews are added to the end, it should
+ // generally map to the same task.
+ mIgnoreResetTaskId = -1;
+ }
+ resetTaskVisuals();
+ onTaskStackUpdated();
+ updateEnabledOverlays();
+ }
+
+ private boolean isModal() {
+ return mTaskModalness > 0;
+ }
+
+ public boolean isLoadingTasks() {
+ return mModel.isLoadingTasksInBackground();
+ }
+
+ private void removeTasksViewsAndClearAllButton() {
+ for (int i = getTaskViewCount() - 1; i >= 0; i--) {
+ removeView(getTaskViewAt(i));
+ }
+ if (indexOfChild(mClearAllButton) != -1) {
+ removeView(mClearAllButton);
+ }
+ }
+
+ public int getTaskViewCount() {
+ int taskViewCount = getChildCount() - mTaskViewStartIndex;
+ if (indexOfChild(mClearAllButton) != -1) {
+ taskViewCount--;
+ }
+ return taskViewCount;
+ }
+
+ protected void onTaskStackUpdated() {
+ // Lazily update the empty message only when the task stack is reapplied
+ updateEmptyMessage();
+ }
+
+ public void resetTaskVisuals() {
+ for (int i = getTaskViewCount() - 1; i >= 0; i--) {
+ TaskView taskView = getTaskViewAt(i);
+ if (mIgnoreResetTaskId != taskView.getTask().key.id) {
+ taskView.resetViewTransforms();
+ taskView.setStableAlpha(mContentAlpha);
+ taskView.setFullscreenProgress(mFullscreenProgress);
+ taskView.setModalness(mTaskModalness);
+ }
+ }
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ // Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need
+ // to reset the params after it settles in Overview from swipe up so that we don't
+ // render with obsolete param values.
+ mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = 0;
+ mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = 0;
+ mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
+ mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+
+ // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
+ // null.
+ if (!mRunningTaskShowScreenshot) {
+ setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot);
+ }
+ }
+ if (mRunningTaskTileHidden) {
+ setRunningTaskHidden(mRunningTaskTileHidden);
+ }
+
+ // Force apply the scale.
+ if (mIgnoreResetTaskId != mRunningTaskId) {
+ applyRunningTaskIconScale();
+ }
+
+ updateCurveProperties();
+ // Update the set of visible task's data
+ loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+ setTaskModalness(0);
+ setColorTint(0);
+ }
+
+ public void setFullscreenProgress(float fullscreenProgress) {
+ mFullscreenProgress = fullscreenProgress;
+ int taskCount = getTaskViewCount();
+ for (int i = 0; i < taskCount; i++) {
+ getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
+ }
+ mClearAllButton.setFullscreenProgress(fullscreenProgress);
+
+ // Fade out the actions view quickly (0.1 range)
+ mActionsView.getFullscreenAlpha().setValue(
+ mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
+ }
+
+ 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);
+
+ // Update DeviceProfile dependant state.
+ DeviceProfile dp = mActivity.getDeviceProfile();
+ setOverviewGridEnabled(
+ mActivity.getStateManager().getState().displayOverviewTasksAsGrid(dp));
+
+ // Propagate DeviceProfile change event.
+ mLiveTileTaskViewSimulator.setDp(dp);
+ mActionsView.setDp(dp);
+ mOrientationState.setDeviceProfile(dp);
+
+ // Update RecentsView adn TaskView's DeviceProfile dependent layout.
+ updateOrientationHandler();
+ }
+
+ private void updateOrientationHandler() {
+ updateOrientationHandler(true);
+ }
+
+ private void updateOrientationHandler(boolean forceRecreateDragLayerControllers) {
+ // Handle orientation changes.
+ PagedOrientationHandler oldOrientationHandler = mOrientationHandler;
+ mOrientationHandler = mOrientationState.getOrientationHandler();
+
+ mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
+ setLayoutDirection(mIsRtl
+ ? View.LAYOUT_DIRECTION_RTL
+ : View.LAYOUT_DIRECTION_LTR);
+ mClearAllButton.setLayoutDirection(mIsRtl
+ ? View.LAYOUT_DIRECTION_LTR
+ : View.LAYOUT_DIRECTION_RTL);
+ mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
+
+ if (forceRecreateDragLayerControllers
+ || !mOrientationHandler.equals(oldOrientationHandler)) {
+ // Changed orientations, update controllers so they intercept accordingly.
+ mActivity.getDragLayer().recreateControllers();
+ }
+
+ boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0
+ || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
+ mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
+ !mOrientationState.canRecentsActivityRotate() && isInLandscape);
+
+ // Update TaskView's DeviceProfile dependent layout.
+ updateChildTaskOrientations();
+
+ // Recalculate DeviceProfile dependent layout.
+ updateSizeAndPadding();
+
+ requestLayout();
+ // Reapply the current page to update page scrolls.
+ setCurrentPage(mCurrentPage);
+ }
+
+ // Update task size and padding that are dependent on DeviceProfile and insets.
+ private void updateSizeAndPadding() {
+ DeviceProfile dp = mActivity.getDeviceProfile();
+ getTaskSize(mTempRect);
+ mTaskWidth = mTempRect.width();
+ mTaskHeight = mTempRect.height();
+
+ mTempRect.top -= dp.overviewTaskThumbnailTopMarginPx;
+ setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
+ dp.widthPx - mInsets.right - mTempRect.right,
+ dp.heightPx - mInsets.bottom - mTempRect.bottom);
+
+ mSizeStrategy.calculateGridSize(mActivity, mActivity.getDeviceProfile(),
+ mLastComputedGridSize);
+ mSizeStrategy.calculateGridTaskSize(mActivity, mActivity.getDeviceProfile(),
+ mLastComputedGridTaskSize, mOrientationHandler);
+
+ // Force TaskView to update size from thumbnail
+ updateTaskSize();
+
+ // Update ActionsView position
+ if (mActionsView != null) {
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) mActionsView.getLayoutParams();
+ if (dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+ layoutParams.gravity = Gravity.BOTTOM;
+ layoutParams.bottomMargin =
+ dp.heightPx - mInsets.bottom - mLastComputedGridSize.bottom;
+ layoutParams.leftMargin = mLastComputedTaskSize.left;
+ layoutParams.rightMargin = dp.widthPx - mLastComputedTaskSize.right;
+ // When in modal state, remove bottom margin to avoid covering content.
+ mActionsView.setModalTransformY(layoutParams.bottomMargin);
+ } else {
+ layoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+ layoutParams.bottomMargin = 0;
+ layoutParams.leftMargin = 0;
+ layoutParams.rightMargin = 0;
+ mActionsView.setModalTransformY(0);
+ }
+ mActionsView.setLayoutParams(layoutParams);
+ }
+ }
+
+ /**
+ * Updates TaskView scaling and translation required to support variable width.
+ */
+ private void updateTaskSize() {
+ final int taskCount = getTaskViewCount();
+ if (taskCount == 0) {
+ return;
+ }
+
+ float accumulatedTranslationX = 0;
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ taskView.updateTaskSize();
+ taskView.getPrimaryFullscreenTranslationProperty().set(taskView,
+ accumulatedTranslationX);
+ taskView.getSecondaryFullscreenTranslationProperty().set(taskView, 0f);
+ // Compensate space caused by TaskView scaling.
+ float widthDiff =
+ taskView.getLayoutParams().width * (1 - taskView.getFullscreenScale());
+ accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff;
+ }
+
+ mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
+
+ updateGridProperties();
+ }
+
+ public void getTaskSize(Rect outRect) {
+ mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
+ mOrientationHandler);
+ mLastComputedTaskSize.set(outRect);
+ }
+
+ /**
+ * Returns the size of task selected to enter modal state.
+ */
+ public Point getSelectedTaskSize() {
+ mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), mTempRect,
+ mOrientationHandler);
+ int taskWidth = mTempRect.width();
+ int taskHeight = mTempRect.height();
+ if (mFocusedTaskId != -1) {
+ int boxLength = Math.max(taskWidth, taskHeight);
+ if (mFocusedTaskRatio > 1) {
+ taskWidth = boxLength;
+ taskHeight = (int) (boxLength / mFocusedTaskRatio);
+ } else {
+ taskWidth = (int) (boxLength * mFocusedTaskRatio);
+ taskHeight = boxLength;
+ }
+ }
+ return new Point(taskWidth, taskHeight);
+ }
+
+ /** Gets the last computed task size */
+ public Rect getLastComputedTaskSize() {
+ return mLastComputedTaskSize;
+ }
+
+ public Rect getLastComputedGridTaskSize() {
+ return mLastComputedGridTaskSize;
+ }
+
+ /** Gets the task size for modal state. */
+ public void getModalTaskSize(Rect outRect) {
+ mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
+ }
+
+ @Override
+ protected boolean computeScrollHelper() {
+ boolean scrolling = super.computeScrollHelper();
+ boolean isFlingingFast = false;
+ updateCurveProperties();
+ if (scrolling || isHandlingTouch()) {
+ 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(TaskView.FLAG_UPDATE_ALL);
+
+ // After scrolling, update ActionsView's visibility.
+ TaskView focusedTaskView = getFocusedTaskView();
+ if (focusedTaskView != null) {
+ float scrollDiff = Math.abs(getScrollForPage(indexOfChild(focusedTaskView))
+ - mOrientationHandler.getPrimaryScroll(this));
+ float delta = (mGridSideMargin - scrollDiff) / (float) mGridSideMargin;
+ mActionsView.getScrollAlpha().setValue(Utilities.boundToRange(delta, 0, 1));
+ }
+ }
+
+ // Update the high res thumbnail loader state
+ mModel.getThumbnailCache().getHighResLoadingState().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;
+ }
+ int scroll = mOrientationHandler.getPrimaryScroll(this);
+ mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled);
+ }
+
+ @Override
+ protected int getDestinationPage(int scaledScroll) {
+ if (!(mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get())) {
+ return super.getDestinationPage(scaledScroll);
+ }
+
+ final int childCount = getChildCount();
+ if (mPageScrolls == null || childCount != mPageScrolls.length) {
+ return -1;
+ }
+
+ // When in tablet with variable task width, return the page which scroll is closest to
+ // screenStart instead of page nearest to center of screen.
+ int minDistanceFromScreenStart = Integer.MAX_VALUE;
+ int minDistanceFromScreenStartIndex = -1;
+ for (int i = 0; i < childCount; ++i) {
+ int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
+ if (distanceFromScreenStart < minDistanceFromScreenStart) {
+ minDistanceFromScreenStart = distanceFromScreenStart;
+ minDistanceFromScreenStartIndex = i;
+ }
+ }
+ return minDistanceFromScreenStartIndex;
+ }
+
+ /**
+ * Iterates through all the tasks, 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(@TaskView.TaskDataChanges int dataChanges) {
+ if (!mOverviewStateEnabled || mTaskListChangeId == -1) {
+ // Skip loading visible task data if we've already left the overview state, or if the
+ // task list hasn't been loaded yet (the task views will not reflect the task list)
+ return;
+ }
+
+ int lower = 0;
+ int upper = 0;
+ int visibleStart = 0;
+ int visibleEnd = 0;
+ if (showAsGrid()) {
+ int screenStart = mOrientationHandler.getPrimaryScroll(this);
+ int pageOrientedSize = mOrientationHandler.getMeasuredSize(this);
+ int halfScreenSize = pageOrientedSize / 2;
+ // Use +/- 50% screen width as visible area.
+ visibleStart = screenStart - halfScreenSize;
+ visibleEnd = screenStart + pageOrientedSize + halfScreenSize;
+ } else {
+ int centerPageIndex = getPageNearestToCenterOfScreen();
+ int numChildren = getChildCount();
+ lower = Math.max(0, centerPageIndex - 2);
+ upper = Math.min(centerPageIndex + 2, numChildren - 1);
+ }
+
+ // Update the task data for the in/visible children
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView taskView = getTaskViewAt(i);
+ Task task = taskView.getTask();
+ int index = indexOfChild(taskView);
+ boolean visible;
+ if (showAsGrid()) {
+ visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
+ } else {
+ visible = lower <= index && index <= 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)) {
+ // Ignore thumbnail update if it's current running task during the gesture
+ // We snapshot at end of gesture, it will update then
+ int changes = dataChanges;
+ if (taskView == getRunningTaskView() && mGestureActive) {
+ changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL;
+ }
+ taskView.onTaskListVisibilityChanged(true /* visible */, changes);
+ }
+ mHasVisibleTaskData.put(task.key.id, visible);
+ } else {
+ if (mHasVisibleTaskData.get(task.key.id)) {
+ taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
+ }
+ mHasVisibleTaskData.delete(task.key.id);
+ }
+ }
+ }
+
+ /**
+ * Unloads any associated data from the currently visible tasks
+ */
+ private void unloadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
+ for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
+ if (mHasVisibleTaskData.valueAt(i)) {
+ TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
+ if (taskView != null) {
+ taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
+ }
+ }
+ }
+ mHasVisibleTaskData.clear();
+ }
+
+ @Override
+ public void onHighResLoadingStateChanged(boolean enabled) {
+ // Whenever the high res loading state changes, poke each of the visible tasks to see if
+ // they want to updated their thumbnail state
+ for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
+ if (mHasVisibleTaskData.valueAt(i)) {
+ TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
+ if (taskView != null) {
+ // Poke the view again, which will trigger it to load high res if the state
+ // is enabled
+ taskView.onTaskListVisibilityChanged(true /* visible */);
+ }
+ }
+ }
+ }
+
+ public abstract void startHome();
+
+ /** `true` if there is a +1 space available in overview. */
+ public boolean hasRecentsExtraCard() {
+ return false;
+ }
+
+ public void reset() {
+ setCurrentTask(-1);
+ mIgnoreResetTaskId = -1;
+ mTaskListChangeId = -1;
+ mFocusedTaskId = -1;
+
+ if (mRecentsAnimationController != null) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
+ // We are still drawing the live tile, finish it now to clean up.
+ finishRecentsAnimation(true /* toRecents */, null);
+ } else {
+ mRecentsAnimationController = null;
+ }
+ }
+ setEnableDrawingLiveTile(false);
+ mLiveTileParams.setTargetSet(null);
+ mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
+
+ // These are relatively expensive and don't need to be done this frame (RecentsView isn't
+ // visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
+ post(() -> {
+ unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+ setCurrentPage(0);
+ LayoutUtils.setViewEnabled(mActionsView, true);
+ if (mOrientationState.setGestureActive(false)) {
+ updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false);
+ }
+ });
+ }
+
+ public int getRunningTaskId() {
+ return mRunningTaskId;
+ }
+
+ public @Nullable TaskView getRunningTaskView() {
+ return getTaskView(mRunningTaskId);
+ }
+
+ public int getRunningTaskIndex() {
+ return getTaskIndexForId(mRunningTaskId);
+ }
+
+ public @Nullable TaskView getFocusedTaskView() {
+ return getTaskView(mFocusedTaskId);
+ }
+
+ /**
+ * Returns the width to height ratio of the focused {@link TaskView}.
+ */
+ public float getFocusedTaskRatio() {
+ return mFocusedTaskRatio;
+ }
+
+ /**
+ * Get the index of the task view whose id matches {@param taskId}.
+ * @return -1 if there is no task view for the task id, else the index of the task view.
+ */
+ public int getTaskIndexForId(int taskId) {
+ TaskView tv = getTaskView(taskId);
+ return tv == null ? -1 : indexOfChild(tv);
+ }
+
+ public int getTaskViewStartIndex() {
+ return mTaskViewStartIndex;
+ }
+
+ /**
+ * Reloads the view if anything in recents changed.
+ */
+ public void reloadIfNeeded() {
+ if (!mModel.isTaskListValid(mTaskListChangeId)) {
+ mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
+ }
+ }
+
+ /**
+ * Called when a gesture from an app is starting.
+ */
+ public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) {
+ mGestureActive = true;
+ // This needs to be called before the other states are set since it can create the task view
+ if (mOrientationState.setGestureActive(true)) {
+ updateOrientationHandler();
+ }
+
+ showCurrentTask(runningTaskInfo);
+ setEnableFreeScroll(false);
+ setEnableDrawingLiveTile(false);
+ setRunningTaskHidden(true);
+ setRunningTaskIconScaledDown(true);
+ }
+
+ /**
+ * Called only when a swipe-up gesture from an app has completed. Only called after
+ * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
+ */
+ public void onSwipeUpAnimationSuccess() {
+ if (getRunningTaskView() != null) {
+ animateUpRunningTaskIconScale();
+ }
+ setSwipeDownShouldLaunchApp(true);
+ }
+
+ private void animateRecentsRotationInPlace(int newRotation) {
+ if (mOrientationState.canRecentsActivityRotate()) {
+ // Let system take care of the rotation
+ return;
+ }
+ AnimatorSet pa = setRecentsChangedOrientation(true);
+ pa.addListener(AnimatorListeners.forSuccessCallback(() -> {
+ setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
+ mActivity.getDragLayer().recreateControllers();
+ setRecentsChangedOrientation(false).start();
+ }));
+ pa.start();
+ }
+
+ public AnimatorSet setRecentsChangedOrientation(boolean fadeInChildren) {
+ getRunningTaskIndex();
+ int runningIndex = getCurrentPage();
+ AnimatorSet as = new AnimatorSet();
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ if (runningIndex == i) {
+ continue;
+ }
+ View taskView = getTaskViewAt(i);
+ as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1));
+ }
+ return as;
+ }
+
+
+ private void updateChildTaskOrientations() {
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ getTaskViewAt(i).setOrientationState(mOrientationState);
+ }
+ TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU);
+ if (tv != null) {
+ tv.onRotationChanged();
+ }
+ }
+
+ /**
+ * Called when a gesture from an app has finished, and an end target has been determined.
+ */
+ public void onPrepareGestureEndAnimation(
+ @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget) {
+ if (mSizeStrategy.stateFromGestureEndTarget(endTarget)
+ .displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) {
+ if (animatorSet == null) {
+ setGridProgress(1);
+ } else {
+ animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
+ }
+ }
+ mCurrentGestureEndTarget = endTarget;
+ if (endTarget == GestureState.GestureEndTarget.NEW_TASK
+ || endTarget == GestureState.GestureEndTarget.LAST_TASK) {
+ // When switching to tasks in quick switch, ensures the snapped page's scroll maintain
+ // invariant between quick switch and overview, to ensure a smooth animation transition.
+ updateGridProperties();
+ } else if (endTarget == GestureState.GestureEndTarget.RECENTS) {
+ setEnableFreeScroll(true);
+ }
+ }
+
+ /**
+ * Called when a gesture from an app has finished, and the animation to the target has ended.
+ */
+ public void onGestureAnimationEnd() {
+ mGestureActive = false;
+ if (mOrientationState.setGestureActive(false)) {
+ updateOrientationHandler();
+ }
+
+ setEnableFreeScroll(true);
+ setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
+ if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ setRunningTaskViewShowScreenshot(true);
+ }
+ setRunningTaskHidden(false);
+ animateUpRunningTaskIconScale();
+
+ if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS
+ && (!showAsGrid() || getFocusedTaskView() != null)) {
+ animateActionsViewIn();
+ }
+
+ mCurrentGestureEndTarget = null;
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mActivity.getDeviceProfile().isMultiWindowMode) {
+ switchToScreenshot(
+ () -> finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
+ null));
+ }
+ }
+
+ /**
+ * Returns true if we should add a stub taskView for the running task id
+ */
+ protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
+ return runningTaskInfo != null && getTaskView(runningTaskInfo.taskId) == null;
+ }
+
+ /**
+ * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
+ *
+ * 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 showCurrentTask(RunningTaskInfo runningTaskInfo) {
+ if (shouldAddStubTaskView(runningTaskInfo)) {
+ boolean wasEmpty = getChildCount() == 0;
+ // Add an empty view for now until the task plan is loaded and applied
+ final TaskView taskView = mTaskViewPool.getView();
+ addView(taskView, mTaskViewStartIndex);
+ if (wasEmpty) {
+ addView(mClearAllButton);
+ }
+ // 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 = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false);
+ taskView.bind(mTmpRunningTask, mOrientationState);
+
+ // Measure and layout immediately so that the scroll values is updated instantly
+ // as the user might be quick-switching
+ measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
+ makeMeasureSpec(getMeasuredHeight(), EXACTLY));
+ layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ boolean runningTaskTileHidden = mRunningTaskTileHidden;
+ int runningTaskId = runningTaskInfo == null ? -1 : runningTaskInfo.taskId;
+ setCurrentTask(runningTaskId);
+ if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+ setFocusedTask(runningTaskId);
+ }
+ setCurrentPage(getRunningTaskIndex());
+ setRunningTaskViewShowScreenshot(false);
+ setRunningTaskHidden(runningTaskTileHidden);
+ // Update task size after setting current task.
+ updateTaskSize();
+
+ // Reload the task list
+ mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
+ }
+
+ /**
+ * Sets the running task id, cleaning up the old running task if necessary.
+ */
+ public void setCurrentTask(int runningTaskId) {
+ if (mRunningTaskId == runningTaskId) {
+ return;
+ }
+
+ if (mRunningTaskId != -1) {
+ // Reset the state on the old running task view
+ setRunningTaskIconScaledDown(false);
+ setRunningTaskViewShowScreenshot(true);
+ setRunningTaskHidden(false);
+ }
+ mRunningTaskId = runningTaskId;
+ }
+
+ /**
+ * Sets the focused task id and store the width to height ratio of the focused task.
+ */
+ protected void setFocusedTask(int focusedTaskId) {
+ mFocusedTaskId = focusedTaskId;
+ mFocusedTaskRatio =
+ mLastComputedTaskSize.width() / (float) mLastComputedTaskSize.height();
+ }
+
+ /**
+ * Hides the tile associated with {@link #mRunningTaskId}
+ */
+ public void setRunningTaskHidden(boolean isHidden) {
+ mRunningTaskTileHidden = isHidden;
+ TaskView runningTask = getRunningTaskView();
+ if (runningTask != null) {
+ runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha);
+ if (!isHidden) {
+ AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask,
+ AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
+ }
+ }
+ }
+
+ private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mRunningTaskShowScreenshot = showScreenshot;
+ TaskView runningTaskView = getRunningTaskView();
+ if (runningTaskView != null) {
+ runningTaskView.setShowScreenshot(mRunningTaskShowScreenshot);
+ }
+ }
+ }
+
+ public void setRunningTaskIconScaledDown(boolean isScaledDown) {
+ if (mRunningTaskIconScaledDown != isScaledDown) {
+ mRunningTaskIconScaledDown = isScaledDown;
+ applyRunningTaskIconScale();
+ }
+ }
+
+ public boolean isTaskIconScaledDown(TaskView taskView) {
+ return mRunningTaskIconScaledDown && getRunningTaskView() == taskView;
+ }
+
+ private void applyRunningTaskIconScale() {
+ TaskView firstTask = getRunningTaskView();
+ if (firstTask != null) {
+ firstTask.setIconScaleAndDim(mRunningTaskIconScaledDown ? 0 : 1);
+ }
+ }
+
+ private void animateActionsViewIn() {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(
+ mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 0, 1);
+ anim.setDuration(TaskView.SCALE_ICON_DURATION);
+ anim.start();
+ }
+
+ private void animateActionsViewOut() {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(
+ mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 1, 0);
+ anim.setDuration(TaskView.SCALE_ICON_DURATION);
+ anim.start();
+ }
+
+ public void animateUpRunningTaskIconScale() {
+ mRunningTaskIconScaledDown = false;
+ TaskView firstTask = getRunningTaskView();
+ if (firstTask != null) {
+ firstTask.setIconScaleAnimStartProgress(0f);
+ firstTask.animateIconScaleAndDimIntoView();
+ }
+ }
+
+ /** Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
+ * layout.
+ * This method is used when no task dismissal has occurred.
+ */
+ private void updateGridProperties() {
+ updateGridProperties(false);
+ }
+
+ /**
+ * Updates TaskView and ClearAllButton scaling and translation required to turn into grid
+ * layout.
+ * This method only calculates the potential position and depends on {@link #setGridProgress} to
+ * apply the actual scaling and translation.
+ *
+ * @param isTaskDismissal indicates if update was called due to task dismissal
+ */
+ private void updateGridProperties(boolean isTaskDismissal) {
+ int taskCount = getTaskViewCount();
+ if (taskCount == 0) {
+ return;
+ }
+
+ final int boxLength = Math.max(mLastComputedGridTaskSize.width(),
+ mLastComputedGridTaskSize.height());
+ int taskTopMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
+
+ /*
+ * taskGridVerticalDiff is used to position the top of a task in the top row of the grid
+ * heightOffset is the vertical space one grid task takes + space between top and
+ * bottom row
+ * Summed together they provide the top position for bottom row of grid tasks
+ */
+ final float taskGridVerticalDiff =
+ mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
+ final float heightOffset = (boxLength + taskTopMargin) + mRowSpacing;
+
+ int topRowWidth = 0;
+ int bottomRowWidth = 0;
+ float topAccumulatedTranslationX = 0;
+ float bottomAccumulatedTranslationX = 0;
+
+ // Contains whether the child index is in top or bottom of grid (for non-focused task)
+ // Different from mTopRowIdSet, which contains the taskId of what task is in top row
+ IntSet topSet = new IntSet();
+ IntSet bottomSet = new IntSet();
+
+ // Horizontal grid translation for each task
+ float[] gridTranslations = new float[taskCount];
+
+ int focusedTaskIndex = Integer.MAX_VALUE;
+ int focusedTaskShift = 0;
+ int focusedTaskWidthAndSpacing = 0;
+ int snappedTaskRowWidth = 0;
+ int snappedPage = getNextPage();
+ TaskView snappedTaskView = getTaskViewAtByAbsoluteIndex(snappedPage);
+
+ if (!isTaskDismissal) {
+ mTopRowIdSet.clear();
+ }
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing;
+ // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
+ // which case keep tasks in their respective rows. For the running task, don't join
+ // the grid.
+ if (taskView.isFocusedTask()) {
+ topRowWidth += taskWidthAndSpacing;
+ bottomRowWidth += taskWidthAndSpacing;
+
+ focusedTaskIndex = i;
+ focusedTaskWidthAndSpacing = taskWidthAndSpacing;
+ gridTranslations[i] += focusedTaskShift;
+ gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
+
+ // Center view vertically in case it's from different orientation.
+ taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
+ - taskView.getLayoutParams().height) / 2f);
+
+ if (taskView == snappedTaskView) {
+ // If focused task is snapped, the row width is just task width and spacing.
+ snappedTaskRowWidth = taskWidthAndSpacing;
+ }
+ } else {
+ if (i > focusedTaskIndex) {
+ // For tasks after the focused task, shift by focused task's width and spacing.
+ gridTranslations[i] +=
+ mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+ } else {
+ // For task before the focused task, accumulate the width and spacing to
+ // calculate the distance focused task need to shift.
+ focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
+ }
+ int taskId = taskView.getTask().key.id;
+ boolean isTopRow = isTaskDismissal ? mTopRowIdSet.contains(taskId)
+ : topRowWidth <= bottomRowWidth;
+ if (isTopRow) {
+ topRowWidth += taskWidthAndSpacing;
+ topSet.add(i);
+ mTopRowIdSet.add(taskId);
+
+ taskView.setGridTranslationY(taskGridVerticalDiff);
+
+ // Move horizontally into empty space.
+ float widthOffset = 0;
+ for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
+ if (j == focusedTaskIndex) {
+ continue;
+ }
+ widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+ }
+
+ float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
+ gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX;
+ topAccumulatedTranslationX += currentTaskTranslationX;
+ } else {
+ bottomRowWidth += taskWidthAndSpacing;
+ bottomSet.add(i);
+
+ // Move into bottom row.
+ taskView.setGridTranslationY(heightOffset + taskGridVerticalDiff);
+
+ // Move horizontally into empty space.
+ float widthOffset = 0;
+ for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
+ if (j == focusedTaskIndex) {
+ continue;
+ }
+ widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+ }
+
+ float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
+ gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX;
+ bottomAccumulatedTranslationX += currentTaskTranslationX;
+ }
+ if (taskView == snappedTaskView) {
+ snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
+ }
+ }
+ }
+
+ // We need to maintain snapped task's page scroll invariant between quick switch and
+ // overview, so we sure snapped task's grid translation is 0, and add a non-fullscreen
+ // translationX that is the same as snapped task's full scroll adjustment.
+ float snappedTaskFullscreenScrollAdjustment = 0;
+ float snappedTaskGridTranslationX = 0;
+ if (snappedTaskView != null) {
+ snappedTaskFullscreenScrollAdjustment = snappedTaskView.getScrollAdjustment(
+ /*fullscreenEnabled=*/true, /*gridEnabled=*/false);
+ snappedTaskGridTranslationX = gridTranslations[snappedPage - mTaskViewStartIndex];
+ }
+
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX);
+ taskView.getPrimaryNonFullscreenTranslationProperty().set(taskView,
+ snappedTaskFullscreenScrollAdjustment);
+ taskView.getSecondaryNonFullscreenTranslationProperty().set(taskView, 0f);
+ }
+
+ // Use the accumulated translation of the row containing the last task.
+ float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1)
+ ? topAccumulatedTranslationX : bottomAccumulatedTranslationX;
+
+ // If the last task is on the shorter row, ClearAllButton will embed into the shorter row
+ // which is not what we want. Compensate the width difference of the 2 rows in that case.
+ float shorterRowCompensation = 0;
+ if (topRowWidth <= bottomRowWidth) {
+ if (topSet.contains(taskCount - 1)) {
+ shorterRowCompensation = bottomRowWidth - topRowWidth;
+ }
+ } else {
+ if (bottomSet.contains(taskCount - 1)) {
+ shorterRowCompensation = topRowWidth - bottomRowWidth;
+ }
+ }
+ float clearAllShorterRowCompensation =
+ mIsRtl ? -shorterRowCompensation : shorterRowCompensation;
+
+ // If the total width is shorter than one grid's width, move ClearAllButton further away
+ // accordingly. Update longRowWidth if ClearAllButton has been moved.
+ float clearAllShortTotalCompensation = 0;
+ int longRowWidth = Math.max(topRowWidth, bottomRowWidth);
+ if (longRowWidth < mLastComputedGridSize.width()) {
+ float shortTotalCompensation = mLastComputedGridSize.width() - longRowWidth;
+ clearAllShortTotalCompensation =
+ mIsRtl ? -shortTotalCompensation : shortTotalCompensation;
+ longRowWidth = mLastComputedGridSize.width();
+ }
+
+ float clearAllTotalTranslationX =
+ clearAllAccumulatedTranslation + clearAllShorterRowCompensation
+ + clearAllShortTotalCompensation + snappedTaskFullscreenScrollAdjustment;
+ if (focusedTaskIndex < taskCount) {
+ // Shift by focused task's width and spacing if a task is focused.
+ clearAllTotalTranslationX +=
+ mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing;
+ }
+
+ // Make sure there are enough space between snapped page and ClearAllButton, for the case
+ // of swiping up after quick switch.
+ if (snappedTaskView != null) {
+ int distanceFromClearAll = longRowWidth - snappedTaskRowWidth;
+ int minimumDistance =
+ mLastComputedGridSize.width() - snappedTaskView.getLayoutParams().width;
+ if (distanceFromClearAll < minimumDistance) {
+ int distanceDifference = minimumDistance - distanceFromClearAll;
+ clearAllTotalTranslationX += mIsRtl ? -distanceDifference : distanceDifference;
+ }
+ }
+
+ mClearAllButton.setGridTranslationPrimary(
+ clearAllTotalTranslationX - snappedTaskGridTranslationX);
+ mClearAllButton.setGridScrollOffset(
+ mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left
+ : mLastComputedTaskSize.right - mLastComputedGridSize.right);
+
+ setGridProgress(mGridProgress);
+ }
+
+ private boolean isSameGridRow(TaskView taskView1, TaskView taskView2) {
+ if (taskView1 == null || taskView2 == null) {
+ return false;
+ }
+ int taskId1 = taskView1.getTask().key.id;
+ int taskId2 = taskView2.getTask().key.id;
+ if (taskId1 == mFocusedTaskId || taskId2 == mFocusedTaskId) {
+ return false;
+ }
+ return (mTopRowIdSet.contains(taskId1) && mTopRowIdSet.contains(taskId2)) || (
+ !mTopRowIdSet.contains(taskId1) && !mTopRowIdSet.contains(taskId2));
+ }
+
+ /**
+ * Moves TaskView and ClearAllButton between carousel and 2 row grid.
+ *
+ * @param gridProgress 0 = carousel; 1 = 2 row grid.
+ */
+ private void setGridProgress(float gridProgress) {
+ int taskCount = getTaskViewCount();
+ if (taskCount == 0) {
+ return;
+ }
+
+ mGridProgress = gridProgress;
+
+ for (int i = 0; i < taskCount; i++) {
+ getTaskViewAt(i).setGridProgress(gridProgress);
+ }
+ mClearAllButton.setGridProgress(gridProgress);
+ }
+
+ private void enableLayoutTransitions() {
+ if (mLayoutTransition == null) {
+ mLayoutTransition = new LayoutTransition();
+ mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING);
+ mLayoutTransition.setDuration(ADDITION_TASK_DURATION);
+ mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0);
+
+ mLayoutTransition.addTransitionListener(new TransitionListener() {
+ @Override
+ public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
+ View view, int i) {
+ }
+
+ @Override
+ public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
+ View view, int i) {
+ // When the unpinned task is added, snap to first page and disable transitions
+ if (view instanceof TaskView) {
+ snapToPage(0);
+ setLayoutTransition(null);
+ }
+
+ }
+ });
+ }
+ setLayoutTransition(mLayoutTransition);
+ }
+
+ public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) {
+ mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp;
+ }
+
+ public boolean shouldSwipeDownLaunchApp() {
+ return mSwipeDownShouldLaunchApp;
+ }
+
+ public void setIgnoreResetTask(int taskId) {
+ mIgnoreResetTaskId = taskId;
+ }
+
+ public void clearIgnoreResetTask(int taskId) {
+ if (mIgnoreResetTaskId == taskId) {
+ mIgnoreResetTaskId = -1;
+ }
+ }
+
+ private void addDismissedTaskAnimations(TaskView taskView, long duration,
+ PendingAnimation anim) {
+ // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
+ // alpha is set to 0 so that it can be recycled in the view pool properly
+ anim.setFloat(taskView, VIEW_ALPHA, 0, clampToProgress(ACCEL, 0, 0.5f));
+ SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController();
+
+ ResourceProvider rp = DynamicResource.provider(mActivity);
+ SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
+ .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
+ .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness));
+ FloatProperty<TaskView> dismissingTaskViewTranslate =
+ taskView.getSecondaryDissmissTranslationProperty();
+ // TODO(b/186800707) translate entire grid size distance
+ int translateDistance = mOrientationHandler.getSecondaryDimension(taskView);
+ int positiveNegativeFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
+ if (splitController.isSplitSelectActive()) {
+ // Have the task translate towards whatever side was just pinned
+ int dir = mOrientationHandler.getSplitTaskViewDismissDirection(splitController
+ .getActiveSplitPositionOption(), mActivity.getDeviceProfile());
+ switch (dir) {
+ case PagedOrientationHandler.SPLIT_TRANSLATE_SECONDARY_NEGATIVE:
+ dismissingTaskViewTranslate = taskView
+ .getSecondaryDissmissTranslationProperty();
+ positiveNegativeFactor = -1;
+ break;
+
+ case PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_POSITIVE:
+ dismissingTaskViewTranslate = taskView.getPrimaryDismissTranslationProperty();
+ positiveNegativeFactor = 1;
+ break;
+
+ case PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_NEGATIVE:
+ dismissingTaskViewTranslate = taskView.getPrimaryDismissTranslationProperty();
+ positiveNegativeFactor = -1;
+ break;
+ default:
+ throw new IllegalStateException("Invalid split task translation: " + dir);
+ }
+ }
+ // Double translation distance so dismissal drag is the full height, as we only animate
+ // the drag for the first half of the progress.
+ anim.add(ObjectAnimator.ofFloat(taskView, dismissingTaskViewTranslate,
+ positiveNegativeFactor * translateDistance * 2).setDuration(duration), LINEAR, sp);
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+ && taskView.isRunningTask()) {
+ anim.addOnFrameCallback(() -> {
+ mLiveTileTaskViewSimulator.taskSecondaryTranslation.value =
+ mOrientationHandler.getSecondaryValue(
+ taskView.getTranslationX(),
+ taskView.getTranslationY());
+ redrawLiveTile();
+ });
+ }
+ }
+
+ public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
+ boolean shouldRemoveTask, long duration) {
+ if (mPendingAnimation != null) {
+ mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd();
+ }
+ PendingAnimation anim = new PendingAnimation(duration);
+
+ int count = getPageCount();
+ if (count == 0) {
+ return anim;
+ }
+
+ int[] oldScroll = new int[count];
+ int[] newScroll = new int[count];
+ getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
+ getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
+ int taskCount = getTaskViewCount();
+ int scrollDiffPerPage = 0;
+ if (count > 1) {
+ scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
+ }
+ int draggedIndex = indexOfChild(taskView);
+
+ boolean isFocusedTaskDismissed = taskView.getTask().key.id == mFocusedTaskId;
+ if (isFocusedTaskDismissed && showAsGrid()) {
+ anim.setFloat(mActionsView, VIEW_ALPHA, 0, clampToProgress(ACCEL_0_5, 0, 0.5f));
+ }
+ float dismissedTaskWidth = taskView.getLayoutParams().width + mPageSpacing;
+ boolean needsCurveUpdates = false;
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if (child == taskView) {
+ if (animateTaskView) {
+ addDismissedTaskAnimations(taskView, duration, anim);
+ }
+ } else if (!showAsGrid()) {
+ // Compute scroll offsets from task dismissal for animation.
+ // 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 = taskCount - 1;
+ 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) {
+ FloatProperty translationProperty = child instanceof TaskView
+ ? ((TaskView) child).getPrimaryDismissTranslationProperty()
+ : mOrientationHandler.getPrimaryViewTranslate();
+
+ float additionalDismissDuration =
+ ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
+ i - draggedIndex);
+ anim.setFloat(child, translationProperty, scrollDiff, clampToProgress(LINEAR,
+ Utilities.boundToRange(INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ + additionalDismissDuration, 0f, 1f), 1));
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+ && child instanceof TaskView
+ && ((TaskView) child).isRunningTask()) {
+ anim.addOnFrameCallback(() -> {
+ mLiveTileTaskViewSimulator.taskPrimaryTranslation.value =
+ mOrientationHandler.getPrimaryValue(child.getTranslationX(),
+ child.getTranslationY());
+ redrawLiveTile();
+ });
+ }
+ needsCurveUpdates = true;
+ }
+ } else if (child instanceof TaskView) {
+ // Animate task with index >= dismissed index and in the same row as the
+ // dismissed index, or if the dismissed task was the focused task. Offset
+ // successive task dismissal durations for a staggered effect.
+ if (isFocusedTaskDismissed || (i >= draggedIndex && isSameGridRow((TaskView) child,
+ taskView))) {
+ FloatProperty translationProperty =
+ ((TaskView) child).getPrimaryDismissTranslationProperty();
+ float additionalDismissDuration =
+ ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
+ i - draggedIndex);
+ anim.setFloat(child, translationProperty,
+ !mIsRtl ? -dismissedTaskWidth : dismissedTaskWidth,
+ clampToProgress(LINEAR, Utilities.boundToRange(
+ INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ + additionalDismissDuration, 0f, 1f), 1));
+ }
+ }
+ }
+
+ if (needsCurveUpdates) {
+ anim.addOnFrameCallback(this::updateCurveProperties);
+ }
+
+ // Add a tiny bit of translation Z, so that it draws on top of other views
+ if (animateTaskView) {
+ taskView.setTranslationZ(0.1f);
+ }
+
+ mPendingAnimation = anim;
+ mPendingAnimation.addEndListener(new Consumer<Boolean>() {
+ @Override
+ public void accept(Boolean success) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+ && taskView.isRunningTask() && success) {
+ finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
+ () -> onEnd(success));
+ } else {
+ onEnd(success);
+ }
+ }
+
+ @SuppressWarnings("WrongCall")
+ private void onEnd(boolean success) {
+ if (success) {
+ if (shouldRemoveTask) {
+ if (taskView.getTask() != null) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask()) {
+ finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
+ () -> removeTaskInternal(taskView));
+ } else {
+ removeTaskInternal(taskView);
+ }
+ mActivity.getStatsLogManager().logger()
+ .withItemInfo(taskView.getItemInfo())
+ .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
+ }
+ }
+
+ // Reset task translations as they may have updated via animations in
+ // createTaskDismissAnimation
+ resetTaskVisuals();
+
+ int pageToSnapTo = mCurrentPage;
+ // Snap to start if focused task was dismissed, as after quick switch it could
+ // be at any page but the focused task always displays at the start.
+ if (taskView.getTask().key.id == mFocusedTaskId) {
+ pageToSnapTo = mTaskViewStartIndex;
+ } else if (draggedIndex < pageToSnapTo || pageToSnapTo == (getTaskViewCount()
+ - 1)) {
+ pageToSnapTo -= 1;
+ }
+ removeViewInLayout(taskView);
+
+ if (getTaskViewCount() == 0) {
+ removeViewInLayout(mClearAllButton);
+ startHome();
+ } else {
+ snapToPageImmediately(pageToSnapTo);
+ dispatchScrollChanged();
+ // Grid got messed up, reapply.
+ updateGridProperties(true);
+ if (showAsGrid() && getFocusedTaskView() == null
+ && mActionsView.getVisibilityAlpha().getValue() == 1) {
+ animateActionsViewOut();
+ }
+ }
+ // Update the layout synchronously so that the position of next view is
+ // immediately available.
+ onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
+ }
+ onDismissAnimationEnds();
+ mPendingAnimation = null;
+ }
+ });
+ return anim;
+ }
+
+ private void removeTaskInternal(TaskView taskView) {
+ UI_HELPER_EXECUTOR.getHandler().postDelayed(() ->
+ ActivityManagerWrapper.getInstance().removeTask(
+ taskView.getTask().key.id),
+ REMOVE_TASK_WAIT_FOR_APP_STOP_MS);
+ }
+
+ /**
+ * @return {@code true} if one of the task thumbnails would intersect/overlap with the
+ * {@link #mSplitPlaceholderView}
+ */
+ public boolean shouldShiftThumbnailsForSplitSelect(@SplitConfigurationOptions.StagePosition
+ int stagePosition) {
+ if (!mActivity.getDeviceProfile().isTablet) {
+ // Never enough space on phones
+ return true;
+ } else if (!mActivity.getDeviceProfile().isLandscape) {
+ return false;
+ }
+
+ Rect splitBounds = new Rect();
+ float placeholderSize = getResources().getDimension(R.dimen.split_placeholder_size);
+ // This acts as a best approximation on where the splitplaceholder view would be,
+ // doesn't need to be exact necessarily. This also doesn't need to take translations
+ // into account since placeholder view is not translated
+ if (stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ splitBounds.set((int) (getWidth() - placeholderSize), 0, getWidth(), getHeight());
+ } else {
+ splitBounds.set(0, 0, (int) (placeholderSize), getHeight());
+ }
+ Rect taskBounds = new Rect();
+ int taskCount = getTaskViewCount();
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ if (taskView == mSplitHiddenTaskView && taskView != getFocusedTaskView()) {
+ // Case where the hidden task view would have overlapped w/ placeholder,
+ // but because it's going to hide we don't care
+ // TODO (b/187312247) edge case for thumbnails that are off screen but scroll on
+ continue;
+ }
+ taskView.getBoundsOnScreen(taskBounds);
+ if (Rect.intersects(taskBounds, splitBounds)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected void onDismissAnimationEnds() {
+ }
+
+ public PendingAnimation createAllTasksDismissAnimation(long duration) {
+ if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
+ throw new IllegalStateException("Another pending animation is still running");
+ }
+ PendingAnimation anim = new PendingAnimation(duration);
+
+ int count = getTaskViewCount();
+ for (int i = 0; i < count; i++) {
+ addDismissedTaskAnimations(getTaskViewAt(i), duration, anim);
+ }
+
+ mPendingAnimation = anim;
+ mPendingAnimation.addEndListener(isSuccess -> {
+ if (isSuccess) {
+ // Remove all the task views now
+ finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
+ UI_HELPER_EXECUTOR.getHandler().postDelayed(
+ ActivityManagerWrapper.getInstance()::removeAllRecentTasks,
+ REMOVE_TASK_WAIT_FOR_APP_STOP_MS);
+ removeTasksViewsAndClearAllButton();
+ startHome();
+ });
+ }
+ mPendingAnimation = null;
+ });
+ return anim;
+ }
+
+ private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
+ if (pageCount == 0) {
+ return false;
+ }
+ final int newPageUnbound = getNextPage() + delta;
+ if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) {
+ return false;
+ }
+ snapToPage((newPageUnbound + pageCount) % pageCount);
+ getChildAt(getNextPage()).requestFocus();
+ return true;
+ }
+
+ private void runDismissAnimation(PendingAnimation pendingAnim) {
+ AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
+ controller.dispatchOnStart();
+ controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
+ controller.start();
+ }
+
+ @UiThread
+ private void dismissTask(int taskId) {
+ TaskView taskView = getTaskView(taskId);
+ if (taskView == null) {
+ return;
+ }
+ dismissTask(taskView, true /* animate */, false /* removeTask */);
+ }
+
+ public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
+ runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask,
+ DISMISS_TASK_DURATION));
+ }
+
+ @SuppressWarnings("unused")
+ private void dismissAllTasks(View view) {
+ runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
+ mActivity.getStatsLogManager().logger().log(LAUNCHER_TASK_CLEAR_ALL);
+ }
+
+ private void dismissCurrentTask() {
+ TaskView taskView = getNextPageTaskView();
+ if (taskView != null) {
+ dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_TAB:
+ return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1,
+ event.isAltPressed() /* cycle */);
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */);
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */);
+ case KeyEvent.KEYCODE_DEL:
+ case KeyEvent.KEYCODE_FORWARD_DEL:
+ dismissCurrentTask();
+ return true;
+ case KeyEvent.KEYCODE_NUMPAD_DOT:
+ if (event.isAltPressed()) {
+ // Numpad DEL pressed while holding Alt.
+ dismissCurrentTask();
+ 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) {
+ if (alpha == mContentAlpha) {
+ return;
+ }
+ alpha = Utilities.boundToRange(alpha, 0, 1);
+ mContentAlpha = alpha;
+ for (int i = getTaskViewCount() - 1; i >= 0; i--) {
+ TaskView child = getTaskViewAt(i);
+ if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) {
+ child.setStableAlpha(alpha);
+ }
+ }
+ mClearAllButton.setContentAlpha(mContentAlpha);
+ int alphaInt = Math.round(alpha * 255);
+ mEmptyMessagePaint.setAlpha(alphaInt);
+ mEmptyIcon.setAlpha(alphaInt);
+ mActionsView.getContentAlpha().setValue(mContentAlpha);
+
+ if (alpha > 0) {
+ setVisibility(VISIBLE);
+ } else if (!mFreezeViewVisibility) {
+ setVisibility(INVISIBLE);
+ }
+ }
+
+ /**
+ * Freezes the view visibility change. When frozen, the view will not change its visibility
+ * to gone due to alpha changes.
+ */
+ public void setFreezeViewVisibility(boolean freezeViewVisibility) {
+ if (mFreezeViewVisibility != freezeViewVisibility) {
+ mFreezeViewVisibility = freezeViewVisibility;
+ if (!mFreezeViewVisibility) {
+ setVisibility(mContentAlpha > 0 ? VISIBLE : INVISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ if (mActionsView != null) {
+ mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
+ if (visibility != VISIBLE) {
+ mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
+ }
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateRecentsRotation();
+ }
+
+ /**
+ * Updates {@link RecentsOrientedState}'s cached RecentsView rotation.
+ */
+ public void updateRecentsRotation() {
+ final int rotation = mActivity.getDisplay().getRotation();
+ mOrientationState.setRecentsRotation(rotation);
+ }
+
+ public void setLayoutRotation(int touchRotation, int displayRotation) {
+ if (mOrientationState.update(touchRotation, displayRotation)) {
+ updateOrientationHandler();
+ }
+ }
+
+ public RecentsOrientedState getPagedViewOrientedState() {
+ return mOrientationState;
+ }
+
+ public PagedOrientationHandler getPagedOrientationHandler() {
+ return mOrientationHandler;
+ }
+
+ @Nullable
+ public TaskView getNextTaskView() {
+ return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() + 1);
+ }
+
+ @Nullable
+ public TaskView getCurrentPageTaskView() {
+ return getTaskViewAtByAbsoluteIndex(getCurrentPage());
+ }
+
+ @Nullable
+ public TaskView getNextPageTaskView() {
+ return getTaskViewAtByAbsoluteIndex(getNextPage());
+ }
+
+ @Nullable
+ public TaskView getTaskViewNearestToCenterOfScreen() {
+ return getTaskViewAtByAbsoluteIndex(getPageNearestToCenterOfScreen());
+ }
+
+ /**
+ * Returns null instead of indexOutOfBoundsError when index is not in range
+ */
+ @Nullable
+ public TaskView getTaskViewAt(int index) {
+ return getTaskViewAtByAbsoluteIndex(index + mTaskViewStartIndex);
+ }
+
+ @Nullable
+ private TaskView getTaskViewAtByAbsoluteIndex(int index) {
+ if (index < getChildCount() && index >= 0) {
+ View child = getChildAt(index);
+ return child instanceof TaskView ? (TaskView) child : null;
+ }
+ return null;
+ }
+
+ public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
+ mOnEmptyMessageUpdatedListener = listener;
+ }
+
+ public void updateEmptyMessage() {
+ boolean isEmpty = getTaskViewCount() == 0;
+ boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
+ || mLastMeasureSize.y != getHeight();
+ if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
+ return;
+ }
+ setContentDescription(isEmpty ? mEmptyMessage : "");
+ mShowEmptyMessage = isEmpty;
+ updateEmptyStateUi(hasSizeChanged);
+ invalidate();
+
+ if (mOnEmptyMessageUpdatedListener != null) {
+ mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ updateEmptyStateUi(changed);
+
+ // Update the pivots such that when the task is scaled, it fills the full page
+ getTaskSize(mTempRect);
+ getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect,
+ mActivity.getDeviceProfile(), mTempPointF);
+ setPivotX(mTempPointF.x);
+ setPivotY(mTempPointF.y);
+ setTaskModalness(mTaskModalness);
+ mLastComputedTaskStartPushOutDistance = null;
+ mLastComputedTaskEndPushOutDistance = null;
+ updatePageOffsets();
+ setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+
+ private void updatePageOffsets() {
+ float offset = mAdjacentPageHorizontalOffset;
+ float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness);
+ int count = getChildCount();
+
+ TaskView runningTask = mRunningTaskId == -1 || !mRunningTaskTileHidden
+ ? null : getTaskView(mRunningTaskId);
+ int midpoint = runningTask == null ? -1 : indexOfChild(runningTask);
+ int modalMidpoint = getCurrentPage();
+
+ float midpointOffsetSize = 0;
+ float leftOffsetSize = midpoint - 1 >= 0
+ ? getHorizontalOffsetSize(midpoint - 1, midpoint, offset)
+ : 0;
+ float rightOffsetSize = midpoint + 1 < count
+ ? getHorizontalOffsetSize(midpoint + 1, midpoint, offset)
+ : 0;
+
+ boolean showAsGrid = showAsGrid();
+ float modalMidpointOffsetSize = 0;
+ float modalLeftOffsetSize = 0;
+ float modalRightOffsetSize = 0;
+ float gridOffsetSize = 0;
+
+ if (showAsGrid) {
+ // In grid, we only focus the task on the side. The reference index used for offset
+ // calculation is the task directly next to the focus task in the grid.
+ int referenceIndex = modalMidpoint == 0 ? 1 : 0;
+ gridOffsetSize = referenceIndex < count
+ ? getHorizontalOffsetSize(referenceIndex, modalMidpoint, modalOffset)
+ : 0;
+ } else {
+ modalLeftOffsetSize = modalMidpoint - 1 >= 0
+ ? getHorizontalOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset)
+ : 0;
+ modalRightOffsetSize = modalMidpoint + 1 < count
+ ? getHorizontalOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset)
+ : 0;
+ }
+
+ for (int i = 0; i < count; i++) {
+ float translation = i == midpoint
+ ? midpointOffsetSize
+ : i < midpoint
+ ? leftOffsetSize
+ : rightOffsetSize;
+ float modalTranslation = i == modalMidpoint
+ ? modalMidpointOffsetSize
+ : showAsGrid
+ ? gridOffsetSize
+ : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize;
+ float totalTranslation = translation + modalTranslation;
+ View child = getChildAt(i);
+ FloatProperty translationProperty = child instanceof TaskView
+ ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
+ : mOrientationHandler.getPrimaryViewTranslate();
+ translationProperty.set(child, totalTranslation);
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+ && i == getRunningTaskIndex()) {
+ mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = totalTranslation;
+ redrawLiveTile();
+ }
+ }
+ updateCurveProperties();
+ }
+
+ /**
+ * Computes the child position with persistent translation considered (see
+ * {@link TaskView#getPersistentTranslationX()}.
+ */
+ private void getPersistentChildPosition(int childIndex, int midPointScroll, RectF outRect) {
+ View child = getChildAt(childIndex);
+ outRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
+ if (child instanceof TaskView) {
+ TaskView taskView = (TaskView) child;
+ outRect.offset(taskView.getPersistentTranslationX(),
+ taskView.getPersistentTranslationY());
+ outRect.top += mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
+ }
+ outRect.offset(mOrientationHandler.getPrimaryValue(-midPointScroll, 0),
+ mOrientationHandler.getSecondaryValue(-midPointScroll, 0));
+ }
+
+ /**
+ * Computes the distance to offset the given child such that it is completely offscreen when
+ * translating away from the given midpoint.
+ * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
+ */
+ private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
+ if (offsetProgress == 0) {
+ // Don't bother calculating everything below if we won't offset anyway.
+ return 0;
+ }
+
+ // First, get the position of the task relative to the midpoint. If there is no midpoint
+ // then we just use the normal (centered) task position.
+ RectF taskPosition = mTempRectF;
+ // Whether the task should be shifted to start direction (i.e. left edge for portrait, top
+ // edge for landscape/seascape).
+ boolean isStartShift;
+ if (midpointIndex > -1) {
+ // When there is a midpoint reference task, adjacent tasks have less distance to travel
+ // to reach offscreen. Offset the task position to the task's starting point.
+ int midpointScroll = getScrollForPage(midpointIndex);
+ getPersistentChildPosition(midpointIndex, midpointScroll, taskPosition);
+ float midpointStart = mOrientationHandler.getStart(taskPosition);
+
+ getPersistentChildPosition(childIndex, midpointScroll, taskPosition);
+ // Assume child does not overlap with midPointChild.
+ isStartShift = mOrientationHandler.getStart(taskPosition) < midpointStart;
+ } else {
+ // Position the task at scroll position.
+ getPersistentChildPosition(childIndex, getScrollForPage(childIndex), taskPosition);
+ isStartShift = mIsRtl;
+ }
+
+ // Next, calculate the distance to move the task off screen. We also need to account for
+ // RecentsView scale, because it moves tasks based on its pivot. To do this, we move the
+ // task position to where it would be offscreen at scale = 1 (computed above), then we
+ // apply the scale via getMatrix() to determine how much that moves the task from its
+ // desired position, and adjust the computed distance accordingly.
+ float distanceToOffscreen;
+ if (isStartShift) {
+ float desiredStart = -mOrientationHandler.getPrimarySize(taskPosition);
+ distanceToOffscreen = -mOrientationHandler.getEnd(taskPosition);
+ if (mLastComputedTaskStartPushOutDistance == null) {
+ taskPosition.offsetTo(
+ mOrientationHandler.getPrimaryValue(desiredStart, 0f),
+ mOrientationHandler.getSecondaryValue(desiredStart, 0f));
+ getMatrix().mapRect(taskPosition);
+ mLastComputedTaskStartPushOutDistance = mOrientationHandler.getEnd(taskPosition)
+ / mOrientationHandler.getPrimaryScale(this);
+ }
+ distanceToOffscreen -= mLastComputedTaskStartPushOutDistance;
+ } else {
+ float desiredStart = mOrientationHandler.getPrimarySize(this);
+ distanceToOffscreen = desiredStart - mOrientationHandler.getStart(taskPosition);
+ if (mLastComputedTaskEndPushOutDistance == null) {
+ taskPosition.offsetTo(
+ mOrientationHandler.getPrimaryValue(desiredStart, 0f),
+ mOrientationHandler.getSecondaryValue(desiredStart, 0f));
+ getMatrix().mapRect(taskPosition);
+ mLastComputedTaskEndPushOutDistance = (mOrientationHandler.getStart(taskPosition)
+ - desiredStart) / mOrientationHandler.getPrimaryScale(this);
+ }
+ distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
+ }
+ return distanceToOffscreen * offsetProgress;
+ }
+
+ protected void setTaskViewsResistanceTranslation(float translation) {
+ mTaskViewsSecondaryTranslation = translation;
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView task = getTaskViewAt(i);
+ task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
+ }
+ mLiveTileTaskViewSimulator.recentsViewSecondaryTranslation.value = translation;
+ }
+
+ protected void setTaskViewsPrimarySplitTranslation(float translation) {
+ mTaskViewsPrimarySplitTranslation = translation;
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView task = getTaskViewAt(i);
+ task.getPrimarySplitTranslationProperty().set(task, translation);
+ }
+ }
+
+ protected void setTaskViewsSecondarySplitTranslation(float translation) {
+ mTaskViewsSecondarySplitTranslation = translation;
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView task = getTaskViewAt(i);
+ task.getSecondarySplitTranslationProperty().set(task, translation);
+ }
+ }
+
+ /**
+ * Resets the visuals when exit modal state.
+ */
+ public void resetModalVisuals() {
+ TaskView taskView = getCurrentPageTaskView();
+ if (taskView != null) {
+ taskView.getThumbnail().getTaskOverlay().resetModalVisuals();
+ }
+ }
+
+ public void initiateSplitSelect(TaskView taskView, SplitPositionOption splitPositionOption) {
+ mSplitHiddenTaskView = taskView;
+ SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController();
+ Rect initialBounds = new Rect(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
+ taskView.getBottom());
+ splitController.setInitialTaskSelect(taskView, splitPositionOption, initialBounds);
+ mSplitHiddenTaskViewIndex = indexOfChild(taskView);
+ mSplitPlaceholderView.setLayoutParams(
+ splitController.getLayoutParamsForActivePosition(getResources(),
+ mActivity.getDeviceProfile()));
+ mSplitPlaceholderView.setIcon(taskView.getIconView());
+ }
+
+ public PendingAnimation createSplitSelectInitAnimation() {
+ int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
+ return createTaskDismissAnimation(mSplitHiddenTaskView, true, false, duration);
+ }
+
+ public void confirmSplitSelect(TaskView taskView) {
+ mSplitPlaceholderView.getSplitController().setSecondTaskId(taskView);
+ resetTaskVisuals();
+ setTranslationY(0);
+ }
+
+ public PendingAnimation cancelSplitSelect(boolean animate) {
+ SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController();
+ SplitPositionOption splitOption = splitController.getActiveSplitPositionOption();
+ Rect initialBounds = splitController.getInitialBounds();
+ splitController.resetState();
+ int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
+ PendingAnimation pendingAnim = new PendingAnimation(duration);
+ if (!animate) {
+ resetFromSplitSelectionState();
+ return pendingAnim;
+ }
+
+ addViewInLayout(mSplitHiddenTaskView, mSplitHiddenTaskViewIndex,
+ mSplitHiddenTaskView.getLayoutParams());
+ mSplitHiddenTaskView.setAlpha(0);
+ int[] oldScroll = new int[getChildCount()];
+ getPageScrolls(oldScroll, false,
+ view -> view.getVisibility() != GONE && view != mSplitHiddenTaskView);
+
+ int[] newScroll = new int[getChildCount()];
+ getPageScrolls(newScroll, false, SIMPLE_SCROLL_LOGIC);
+
+ boolean needsCurveUpdates = false;
+ for (int i = mSplitHiddenTaskViewIndex; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (child == mSplitHiddenTaskView) {
+ TaskView taskView = (TaskView) child;
+
+ int dir = mOrientationHandler.getSplitTaskViewDismissDirection(splitOption,
+ mActivity.getDeviceProfile());
+ FloatProperty<TaskView> dismissingTaskViewTranslate;
+ Rect hiddenBounds = new Rect(taskView.getLeft(), taskView.getTop(),
+ taskView.getRight(), taskView.getBottom());
+ int distanceDelta = 0;
+ if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_SECONDARY_NEGATIVE) {
+ dismissingTaskViewTranslate = taskView
+ .getSecondaryDissmissTranslationProperty();
+ distanceDelta = initialBounds.top - hiddenBounds.top;
+ taskView.layout(initialBounds.left, hiddenBounds.top, initialBounds.right,
+ hiddenBounds.bottom);
+ } else {
+ dismissingTaskViewTranslate = taskView
+ .getPrimaryDismissTranslationProperty();
+ distanceDelta = initialBounds.left - hiddenBounds.left;
+ taskView.layout(hiddenBounds.left, initialBounds.top, hiddenBounds.right,
+ initialBounds.bottom);
+ if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_POSITIVE) {
+ distanceDelta *= -1;
+ }
+ }
+ pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView,
+ dismissingTaskViewTranslate,
+ distanceDelta));
+ pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, ALPHA, 1));
+ } else {
+ // If insertion is on last index (furthest from clear all), we directly add the view
+ // else we translate all views to the right of insertion index further right,
+ // ignore views to left
+ if (showAsGrid()) {
+ // TODO(b/186800707) handle more elegantly for grid
+ continue;
+ }
+ int scrollDiff = newScroll[i] - oldScroll[i];
+ if (scrollDiff != 0) {
+ FloatProperty translationProperty = child instanceof TaskView
+ ? ((TaskView) child).getPrimaryDismissTranslationProperty()
+ : mOrientationHandler.getPrimaryViewTranslate();
+
+ ResourceProvider rp = DynamicResource.provider(mActivity);
+ SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
+ .setDampingRatio(
+ rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
+ .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
+ pendingAnim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
+ .setDuration(duration), ACCEL, sp);
+ needsCurveUpdates = true;
+ }
+ }
+ }
+
+ if (needsCurveUpdates) {
+ pendingAnim.addOnFrameCallback(this::updateCurveProperties);
+ }
+
+ pendingAnim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ // TODO(b/186800707) Figure out how to undo for grid view
+ // Need to handle cases where dismissed task is
+ // * Top Row
+ // * Bottom Row
+ // * Focused Task
+ updateGridProperties();
+ resetFromSplitSelectionState();
+ }
+ });
+
+ return pendingAnim;
+ }
+
+ private void resetFromSplitSelectionState() {
+ mSplitHiddenTaskView.setTranslationY(0);
+ if (!showAsGrid()) {
+ // TODO(b/186800707)
+ int pageToSnapTo = mCurrentPage;
+ if (mSplitHiddenTaskViewIndex <= pageToSnapTo) {
+ pageToSnapTo += 1;
+ } else {
+ pageToSnapTo = mSplitHiddenTaskViewIndex;
+ }
+ snapToPageImmediately(pageToSnapTo);
+ }
+ onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
+ resetTaskVisuals();
+ mSplitHiddenTaskView = null;
+ mSplitHiddenTaskViewIndex = -1;
+ }
+
+ private void updateDeadZoneRects() {
+ // Get the deadzone rect surrounding the clear all button to not dismiss overview to home
+ mClearAllButtonDeadZoneRect.setEmpty();
+ if (mClearAllButton.getWidth() > 0) {
+ int verticalMargin = getResources()
+ .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin);
+ mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect);
+ mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin);
+ }
+
+ // Get the deadzone rect between the task views
+ mTaskViewDeadZoneRect.setEmpty();
+ int count = getTaskViewCount();
+ if (count > 0) {
+ final View taskView = getTaskViewAt(0);
+ getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
+ mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
+ taskView.getBottom());
+ }
+ }
+
+ private void updateEmptyStateUi(boolean sizeChanged) {
+ boolean hasValidSize = getWidth() > 0 && getHeight() > 0;
+ if (sizeChanged && hasValidSize) {
+ mEmptyTextLayout = null;
+ mLastMeasureSize.set(getWidth(), getHeight());
+ }
+
+ 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) {
+ AnimatorSet anim = new AnimatorSet();
+
+ int taskIndex = indexOfChild(tv);
+ int centerTaskIndex = getCurrentPage();
+ boolean launchingCenterTask = taskIndex == centerTaskIndex;
+
+ float toScale = getMaxScaleForFullScreen();
+ RecentsView recentsView = tv.getRecentsView();
+ if (launchingCenterTask) {
+ anim.play(ObjectAnimator.ofFloat(recentsView, RECENTS_SCALE_PROPERTY, toScale));
+ anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
+ } else {
+ // We are launching an adjacent task, so parallax the center and other adjacent task.
+ float displacementX = tv.getWidth() * (toScale - 1f);
+ float primaryTranslation = mIsRtl ? -displacementX : displacementX;
+ anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
+ mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation));
+ int runningTaskIndex = recentsView.getRunningTaskIndex();
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && runningTaskIndex != -1
+ && runningTaskIndex != taskIndex) {
+ anim.play(ObjectAnimator.ofFloat(
+ recentsView.getLiveTileTaskViewSimulator().taskPrimaryTranslation,
+ AnimatedFloat.VALUE,
+ primaryTranslation));
+ }
+
+ int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
+ if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
+ PropertyValuesHolder[] properties = new PropertyValuesHolder[3];
+ properties[0] = PropertyValuesHolder.ofFloat(
+ mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation);
+ properties[1] = PropertyValuesHolder.ofFloat(View.SCALE_X, 1);
+ properties[2] = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1);
+
+ anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex),
+ properties));
+ }
+ }
+ return anim;
+ }
+
+ /**
+ * Returns the scale up required on the view, so that it coves the screen completely
+ */
+ public float getMaxScaleForFullScreen() {
+ getTaskSize(mTempRect);
+ return getPagedViewOrientedState().getFullScreenScaleAndPivot(
+ mTempRect, mActivity.getDeviceProfile(), mTempPointF);
+ }
+
+ public PendingAnimation createTaskLaunchAnimation(
+ TaskView tv, long duration, Interpolator interpolator) {
+ if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
+ throw new IllegalStateException("Another pending animation is still running");
+ }
+
+ int count = getTaskViewCount();
+ if (count == 0) {
+ return new PendingAnimation(duration);
+ }
+
+ // When swiping down from overview to tasks, ensures the snapped page's scroll maintain
+ // invariant between quick switch and overview, to ensure a smooth animation transition.
+ updateGridProperties();
+ updateScrollSynchronously();
+
+ int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
+ final boolean[] passedOverviewThreshold = new boolean[] {false};
+ ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
+ progressAnim.addUpdateListener(animator -> {
+ // Once we pass a certain threshold, update the sysui flags to match the target
+ // tasks' flags
+ if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) {
+ mActivity.getSystemUiController().updateUiState(
+ UI_STATE_FULLSCREEN_TASK, targetSysUiFlags);
+ } else {
+ mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
+ }
+
+ // Passing the threshold from taskview to fullscreen app will vibrate
+ final boolean passed = animator.getAnimatedFraction() >=
+ SUCCESS_TRANSITION_PROGRESS;
+ if (passed != passedOverviewThreshold[0]) {
+ passedOverviewThreshold[0] = passed;
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+ });
+
+ AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
+
+ DepthController depthController = getDepthController();
+ if (depthController != null) {
+ ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController, DEPTH,
+ BACKGROUND_APP.getDepth(mActivity));
+ anim.play(depthAnimator);
+ }
+ anim.play(progressAnim);
+ anim.setInterpolator(interpolator);
+
+ mPendingAnimation = new PendingAnimation(duration);
+ mPendingAnimation.add(anim);
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
+ mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+ }
+ mPendingAnimation.addEndListener(isSuccess -> {
+ if (isSuccess) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && tv.isRunningTask()) {
+ finishRecentsAnimation(false /* toRecents */, null);
+ onTaskLaunchAnimationEnd(true /* success */);
+ } else {
+ tv.launchTask(this::onTaskLaunchAnimationEnd);
+ }
+ Task task = tv.getTask();
+ if (task != null) {
+ mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo())
+ .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
+ }
+ } else {
+ onTaskLaunchAnimationEnd(false);
+ }
+ mPendingAnimation = null;
+ });
+ return mPendingAnimation;
+ }
+
+ protected void onTaskLaunchAnimationEnd(boolean success) {
+ if (success) {
+ resetTaskVisuals();
+ }
+ }
+
+ @Override
+ protected void notifyPageSwitchListener(int prevPage) {
+ super.notifyPageSwitchListener(prevPage);
+ loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+ updateEnabledOverlays();
+ }
+
+ @Override
+ protected String getCurrentPageDescription() {
+ return "";
+ }
+
+ @Override
+ public void addChildrenForAccessibility(ArrayList<View> outChildren) {
+ // Add children in reverse order
+ for (int i = getChildCount() - 1; i >= 0; --i) {
+ outChildren.add(getChildAt(i));
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ final AccessibilityNodeInfo.CollectionInfo
+ collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
+ 1, getTaskViewCount(), false,
+ AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE);
+ info.setCollectionInfo(collectionInfo);
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+
+ final int taskViewCount = getTaskViewCount();
+ event.setScrollable(taskViewCount > 0);
+
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ final int[] visibleTasks = getVisibleChildrenRange();
+ event.setFromIndex(taskViewCount - visibleTasks[1]);
+ event.setToIndex(taskViewCount - visibleTasks[0]);
+ event.setItemCount(taskViewCount);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ // To hear position-in-list related feedback from Talkback.
+ return ListView.class.getName();
+ }
+
+ @Override
+ protected boolean isPageOrderFlipped() {
+ return true;
+ }
+
+ public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) {
+ mEnableDrawingLiveTile = enableDrawingLiveTile;
+ }
+
+ public void redrawLiveTile() {
+ if (mLiveTileParams.getTargetSet() != null) {
+ mLiveTileTaskViewSimulator.apply(mLiveTileParams);
+ }
+ }
+
+ public TaskViewSimulator getLiveTileTaskViewSimulator() {
+ return mLiveTileTaskViewSimulator;
+ }
+
+ public TransformParams getLiveTileParams() {
+ return mLiveTileParams;
+ }
+
+ // TODO: To be removed in a follow up CL
+ public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
+ RecentsAnimationTargets recentsAnimationTargets) {
+ mRecentsAnimationController = recentsAnimationController;
+ if (recentsAnimationTargets != null && recentsAnimationTargets.apps.length > 0) {
+ if (mSyncTransactionApplier != null) {
+ recentsAnimationTargets.addReleaseCheck(mSyncTransactionApplier);
+ }
+ mLiveTileTaskViewSimulator.setPreview(
+ recentsAnimationTargets.apps[recentsAnimationTargets.apps.length - 1]);
+ mLiveTileParams.setTargetSet(recentsAnimationTargets);
+ }
+ }
+
+ public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
+ finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete);
+ }
+
+ public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
+ Runnable onFinishComplete) {
+ if (!toRecents && ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ // Reset the minimized state since we force-toggled the minimized state when entering
+ // overview, but never actually finished the recents animation. This is a catch all for
+ // cases where we haven't already reset it.
+ SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate();
+ if (p != null) {
+ p.setSplitScreenMinimized(false);
+ }
+ }
+
+ if (mRecentsAnimationController == null) {
+ if (onFinishComplete != null) {
+ onFinishComplete.run();
+ }
+ return;
+ }
+
+ final boolean sendUserLeaveHint = toRecents && shouldPip;
+ if (sendUserLeaveHint) {
+ // Notify the SysUI to use fade-in animation when entering PiP from live tile.
+ final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext());
+ systemUiProxy.notifySwipeToHomeFinished();
+ systemUiProxy.setShelfHeight(true, mActivity.getDeviceProfile().hotseatBarSizePx);
+ }
+ mRecentsAnimationController.finish(toRecents, () -> {
+ if (onFinishComplete != null) {
+ onFinishComplete.run();
+ }
+ onRecentsAnimationComplete();
+ }, sendUserLeaveHint);
+ }
+
+ /**
+ * Called when a running recents animation has finished or canceled.
+ */
+ public void onRecentsAnimationComplete() {
+ // At this point, the recents animation is not running and if the animation was canceled
+ // by a display rotation then reset this state to show the screenshot
+ setRunningTaskViewShowScreenshot(true);
+ // After we finish the recents animation, the current task id should be correctly
+ // reset so that when the task is launched from Overview later, it goes through the
+ // flow of starting a new task instead of finishing recents animation to app. A
+ // typical example of this is (1) user swipes up from app to Overview (2) user
+ // taps on QSB (3) user goes back to Overview and launch the most recent task.
+ setCurrentTask(-1);
+ mRecentsAnimationController = null;
+ executeSideTaskLaunchCallback();
+ }
+
+ public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
+ if (mDisallowScrollToClearAll != disallowScrollToClearAll) {
+ mDisallowScrollToClearAll = disallowScrollToClearAll;
+ updateMinAndMaxScrollX();
+ }
+ }
+
+ /**
+ * Updates page scroll synchronously and layout child views.
+ */
+ public void updateScrollSynchronously() {
+ onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
+ updateMinAndMaxScrollX();
+ }
+
+ @Override
+ protected int computeMinScroll() {
+ if (getTaskViewCount() > 0) {
+ if (mIsRtl) {
+ // If we aren't showing the clear all button, use the rightmost task as the min
+ // scroll.
+ return getScrollForPage(mDisallowScrollToClearAll ? indexOfChild(
+ getTaskViewAt(getTaskViewCount() - 1)) : indexOfChild(mClearAllButton));
+ } else {
+ TaskView focusedTaskView = showAsGrid() ? getFocusedTaskView() : null;
+ return getScrollForPage(focusedTaskView != null ? indexOfChild(focusedTaskView)
+ : mTaskViewStartIndex);
+ }
+ }
+ return super.computeMinScroll();
+ }
+
+ @Override
+ protected int computeMaxScroll() {
+ if (getTaskViewCount() > 0) {
+ if (mIsRtl) {
+ TaskView focusedTaskView = showAsGrid() ? getFocusedTaskView() : null;
+ return getScrollForPage(focusedTaskView != null ? indexOfChild(focusedTaskView)
+ : mTaskViewStartIndex);
+ } else {
+ // If we aren't showing the clear all button, use the leftmost task as the min
+ // scroll.
+ return getScrollForPage(mDisallowScrollToClearAll ? indexOfChild(
+ getTaskViewAt(getTaskViewCount() - 1)) : indexOfChild(mClearAllButton));
+ }
+ }
+ return super.computeMaxScroll();
+ }
+
+ /**
+ * Returns page scroll of ClearAllButton.
+ */
+ public int getClearAllScroll() {
+ return getScrollForPage(indexOfChild(mClearAllButton));
+ }
+
+ @Override
+ protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
+ ComputePageScrollsLogic scrollLogic) {
+ int[] newPageScrolls = new int[outPageScrolls.length];
+ super.getPageScrolls(newPageScrolls, layoutChildren, scrollLogic);
+ boolean showAsFullscreen = showAsFullscreen();
+ boolean showAsGrid = showAsGrid();
+
+ // Align ClearAllButton to the left (RTL) or right (non-RTL), which is different from other
+ // TaskViews. This must be called after laying out ClearAllButton.
+ if (layoutChildren) {
+ int clearAllWidthDiff = mOrientationHandler.getPrimaryValue(mTaskWidth, mTaskHeight)
+ - mOrientationHandler.getPrimarySize(mClearAllButton);
+ mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff);
+ }
+
+ boolean pageScrollChanged = false;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ float scrollDiff = 0;
+ if (child instanceof TaskView) {
+ scrollDiff = ((TaskView) child).getScrollAdjustment(showAsFullscreen, showAsGrid);
+ } else if (child instanceof ClearAllButton) {
+ scrollDiff = ((ClearAllButton) child).getScrollAdjustment(showAsFullscreen,
+ showAsGrid);
+ }
+
+ final int pageScroll = newPageScrolls[i] + (int) scrollDiff;
+ if (outPageScrolls[i] != pageScroll) {
+ pageScrollChanged = true;
+ outPageScrolls[i] = pageScroll;
+ }
+ }
+ return pageScrollChanged;
+ }
+
+ @Override
+ protected int getChildOffset(int index) {
+ int childOffset = super.getChildOffset(index);
+ View child = getChildAt(index);
+ if (child instanceof TaskView) {
+ childOffset += ((TaskView) child).getOffsetAdjustment(showAsFullscreen(),
+ showAsGrid());
+ } else if (child instanceof ClearAllButton) {
+ childOffset += ((ClearAllButton) child).getOffsetAdjustment(mOverviewFullscreenEnabled,
+ showAsGrid());
+ }
+ return childOffset;
+ }
+
+ @Override
+ protected int getChildVisibleSize(int index) {
+ final TaskView taskView = getTaskViewAtByAbsoluteIndex(index);
+ if (taskView == null) {
+ return super.getChildVisibleSize(index);
+ }
+ return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
+ showAsFullscreen()));
+ }
+
+ public ClearAllButton getClearAllButton() {
+ return mClearAllButton;
+ }
+
+ /**
+ * @return How many pixels the running task is offset on the currently laid out dominant axis.
+ */
+ public int getScrollOffset() {
+ return getScrollOffset(getRunningTaskIndex());
+ }
+
+ /**
+ * Returns how many pixels the page is offset on the currently laid out dominant axis.
+ */
+ public int getScrollOffset(int pageIndex) {
+ if (pageIndex == -1) {
+ return 0;
+ }
+
+ int overScrollShift = getOverScrollShift();
+ if (mAdjacentPageHorizontalOffset > 0) {
+ // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so
+ // that the page can move freely given there's no visual indication why it shouldn't.
+ overScrollShift = (int) Utilities.mapRange(mAdjacentPageHorizontalOffset,
+ overScrollShift, getUndampedOverScrollShift());
+ }
+ return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this)
+ + overScrollShift;
+ }
+
+ /**
+ * Returns how many pixels the task is offset on the currently laid out secondary axis
+ * according to {@link #mGridProgress}.
+ */
+ public float getGridTranslationSecondary(int pageIndex) {
+ TaskView taskView = getTaskViewAtByAbsoluteIndex(pageIndex);
+ if (taskView == null) {
+ return 0;
+ }
+
+ return mOrientationHandler.getSecondaryValue(taskView.getGridTranslationX(),
+ taskView.getGridTranslationY());
+ }
+
+ public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
+ float degreesRotated;
+ if (navbarRotation == 0) {
+ degreesRotated = mOrientationHandler.getDegreesRotated();
+ } else {
+ degreesRotated = -navbarRotation;
+ }
+ if (degreesRotated == 0) {
+ return super::onTouchEvent;
+ }
+
+ // At this point the event coordinates have already been transformed, so we need to
+ // undo that transformation since PagedView also accommodates for the transformation via
+ // PagedOrientationHandler
+ return e -> {
+ if (navbarRotation != 0
+ && mOrientationState.isMultipleOrientationSupportedByDevice()
+ && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) {
+ mOrientationState.flipVertical(e);
+ super.onTouchEvent(e);
+ mOrientationState.flipVertical(e);
+ return;
+ }
+ mOrientationState.transformEvent(-degreesRotated, e, true);
+ super.onTouchEvent(e);
+ mOrientationState.transformEvent(-degreesRotated, e, false);
+ };
+ }
+
+ private void updateEnabledOverlays() {
+ int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
+ int taskCount = getTaskViewCount();
+ for (int i = mTaskViewStartIndex; i < mTaskViewStartIndex + taskCount; i++) {
+ getTaskViewAtByAbsoluteIndex(i).setOverlayEnabled(i == overlayEnabledPage);
+ }
+ }
+
+ public void setOverlayEnabled(boolean overlayEnabled) {
+ if (mOverlayEnabled != overlayEnabled) {
+ mOverlayEnabled = overlayEnabled;
+ updateEnabledOverlays();
+ }
+ }
+
+ public void setOverviewGridEnabled(boolean overviewGridEnabled) {
+ if (mOverviewGridEnabled != overviewGridEnabled) {
+ mOverviewGridEnabled = overviewGridEnabled;
+ // Request layout to ensure scroll position is recalculated with updated mGridProgress.
+ requestLayout();
+ }
+ }
+
+ public void setOverviewFullscreenEnabled(boolean overviewFullscreenEnabled) {
+ if (mOverviewFullscreenEnabled != overviewFullscreenEnabled) {
+ mOverviewFullscreenEnabled = overviewFullscreenEnabled;
+ // Request layout to ensure scroll position is recalculated with updated
+ // mFullscreenProgress.
+ requestLayout();
+ }
+ }
+
+ /**
+ * Switch the current running task view to static snapshot mode,
+ * capturing the snapshot at the same time.
+ */
+ public void switchToScreenshot(Runnable onFinishRunnable) {
+ if (mRecentsAnimationController == null) {
+ if (onFinishRunnable != null) {
+ onFinishRunnable.run();
+ }
+ return;
+ }
+ switchToScreenshot(mRunningTaskId == -1 ? null
+ : mRecentsAnimationController.screenshotTask(mRunningTaskId), onFinishRunnable);
+ }
+
+ /**
+ * Switch the current running task view to static snapshot mode, using the
+ * provided thumbnail data as the snapshot.
+ */
+ public void switchToScreenshot(ThumbnailData thumbnailData, Runnable onFinishRunnable) {
+ TaskView taskView = getRunningTaskView();
+ if (taskView != null) {
+ taskView.setShowScreenshot(true);
+ if (thumbnailData != null) {
+ taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
+ } else {
+ taskView.getThumbnail().refresh();
+ }
+ ViewUtils.postFrameDrawn(taskView, onFinishRunnable);
+ } else {
+ onFinishRunnable.run();
+ }
+ }
+
+ /**
+ * The current task is fully modal (modalness = 1) when it is shown on its own in a modal
+ * way. Modalness 0 means the task is shown in context with all the other tasks.
+ */
+ private void setTaskModalness(float modalness) {
+ mTaskModalness = modalness;
+ updatePageOffsets();
+ if (getCurrentPageTaskView() != null) {
+ getCurrentPageTaskView().setModalness(modalness);
+ }
+ // Only show actions view when it's modal for in-place landscape mode.
+ boolean inPlaceLandscape = !mOrientationState.canRecentsActivityRotate()
+ && mOrientationState.getTouchRotation() != ROTATION_0;
+ mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape);
+ mActionsView.setTaskModalness(modalness);
+ }
+
+ @Nullable
+ protected DepthController getDepthController() {
+ return null;
+ }
+
+ @Override
+ public void onSecondaryWindowBoundsChanged() {
+ // Invalidate the task view size
+ setInsets(mInsets);
+ }
+
+ /**
+ * Enables or disables modal state for RecentsView
+ * @param isModalState
+ */
+ public void setModalStateEnabled(boolean isModalState) { }
+
+ public TaskOverlayFactory getTaskOverlayFactory() {
+ return mTaskOverlayFactory;
+ }
+
+ public BaseActivityInterface getSizeStrategy() {
+ return mSizeStrategy;
+ }
+
+ /**
+ * Set all the task views to color tint scrim mode, dimming or tinting them all. Allows the
+ * tasks to be dimmed while other elements in the recents view are left alone.
+ */
+ public void showForegroundScrim(boolean show) {
+ if (!show && mColorTint == 0) {
+ if (mTintingAnimator != null) {
+ mTintingAnimator.cancel();
+ mTintingAnimator = null;
+ }
+ return;
+ }
+
+ mTintingAnimator = ObjectAnimator.ofFloat(this, COLOR_TINT, show ? 0.5f : 0f);
+ mTintingAnimator.setAutoCancel(true);
+ mTintingAnimator.start();
+ }
+
+ /** Tint the RecentsView and TaskViews in to simulate a scrim. */
+ // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView
+ private void setColorTint(float tintAmount) {
+ mColorTint = tintAmount;
+
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ getTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
+ }
+
+ Drawable scrimBg = mActivity.getScrimView().getBackground();
+ if (scrimBg != null) {
+ if (tintAmount == 0f) {
+ scrimBg.setTintList(null);
+ } else {
+ scrimBg.setTintBlendMode(BlendMode.SRC_OVER);
+ scrimBg.setTint(
+ ColorUtils.setAlphaComponent(mTintingColor, (int) (255 * tintAmount)));
+ }
+ }
+ }
+
+ private float getColorTint() {
+ return mColorTint;
+ }
+
+ /** Returns {@code true} if the overview tasks are displayed as a grid. */
+ public boolean showAsGrid() {
+ return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
+ && mSizeStrategy.stateFromGestureEndTarget(
+ mCurrentGestureEndTarget).displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
+ }
+
+ private boolean showAsFullscreen() {
+ return mOverviewFullscreenEnabled
+ && mCurrentGestureEndTarget != GestureState.GestureEndTarget.RECENTS;
+ }
+
+ public boolean shouldShowOverviewActionsForState(STATE_TYPE state) {
+ return !state.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())
+ || getFocusedTaskView() != null;
+ }
+
+ /**
+ * Used to register callbacks for when our empty message state changes.
+ *
+ * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener)
+ * @see #updateEmptyMessage()
+ */
+ public interface OnEmptyMessageUpdatedListener {
+ /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */
+ void onEmptyMessageUpdated(boolean isEmpty);
+ }
+
+ /**
+ * Adds a listener for scroll changes
+ */
+ public void addOnScrollChangedListener(OnScrollChangedListener listener) {
+ mScrollListeners.add(listener);
+ }
+
+ /**
+ * Removes a previously added scroll change listener
+ */
+ public void removeOnScrollChangedListener(OnScrollChangedListener listener) {
+ mScrollListeners.remove(listener);
+ }
+
+ /**
+ * @return Corner radius in pixel value for PiP window, which is updated via
+ * {@link #mIPipAnimationListener}
+ */
+ public int getPipCornerRadius() {
+ return mPipCornerRadius;
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ dispatchScrollChanged();
+ }
+
+ private void dispatchScrollChanged() {
+ mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
+ for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
+ mScrollListeners.get(i).onScrollChanged();
+ }
+ }
+
+ private static class PinnedStackAnimationListener<T extends BaseActivity> extends
+ IPipAnimationListener.Stub {
+ private T mActivity;
+ private RecentsView mRecentsView;
+
+ public void setActivityAndRecentsView(T activity, RecentsView recentsView) {
+ mActivity = activity;
+ mRecentsView = recentsView;
+ }
+
+ @Override
+ public void onPipAnimationStarted() {
+ MAIN_EXECUTOR.execute(() -> {
+ // Needed for activities that auto-enter PiP, which will not trigger a remote
+ // animation to be created
+ if (mActivity != null) {
+ mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+ }
+ });
+ }
+
+ @Override
+ public void onPipCornerRadiusChanged(int cornerRadius) {
+ if (mRecentsView != null) {
+ mRecentsView.mPipCornerRadius = cornerRadius;
+ }
+ }
+ }
+
+ /** Get the color used for foreground scrimming the RecentsView for sharing. */
+ public static int getForegroundScrimDimColor(Context context) {
+ int baseColor = Themes.getAttrColor(context, R.attr.overviewScrimColor);
+ // The Black blending is temporary until we have the proper color token.
+ return ColorUtils.blendARGB(Color.BLACK, baseColor, 0.25f);
+ }
+
+ /** Get the RecentsAnimationController */
+ @Nullable
+ public RecentsAnimationController getRecentsAnimationController() {
+ return mRecentsAnimationController;
+ }
+
+ /** Update the current activity locus id to show the enabled state of Overview */
+ public void updateLocusId() {
+ String locusId = "Overview";
+
+ if (mOverviewStateEnabled && mActivity.isStarted()) {
+ locusId += "|ENABLED";
+ } else {
+ locusId += "|DISABLED";
+ }
+
+ final LocusId id = new LocusId(locusId);
+ // Set locus context is a binder call, don't want it to happen during a transition
+ UI_HELPER_EXECUTOR.post(() -> mActivity.setLocusContext(id, Bundle.EMPTY));
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
deleted file mode 100644
index 19e278b..0000000
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.QUICK_SWITCH;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW;
-
-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.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.uioverrides.states.OverviewState;
-import com.android.launcher3.util.OnboardingPrefs;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.views.ScrimView;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
-import com.android.quickstep.util.LayoutUtils;
-
-/**
- * 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<BaseQuickstepLauncher>
- implements NavigationModeChangeListener {
-
- // If the progress is more than this, shelf follows the finger, otherwise it moves faster to
- // cover the whole screen
- private static final float SCRIM_CATCHUP_THRESHOLD = 0.2f;
-
- // Temporarily needed until android.R.attr.bottomDialogCornerRadius becomes public
- private static final float BOTTOM_CORNER_RADIUS_RATIO = 2f;
-
- // In transposed layout, we simply draw a flat color.
- private boolean mDrawingFlatColor;
-
- // For shelf mode
- private final int mEndAlpha;
- private final float mRadius;
- private final int mMaxScrimAlpha;
- private final Paint mPaint;
- private final OnboardingPrefs mOnboardingPrefs;
-
- // Mid point where the alpha changes
- private int mMidAlpha;
- private float mMidProgress;
-
- // The progress at which the drag handle starts moving up with the shelf.
- private float mDragHandleProgress;
-
- private Interpolator mBeforeMidProgressColorInterpolator = ACCEL;
- private Interpolator mAfterMidProgressColorInterpolator = ACCEL;
-
- private float mShiftRange;
-
- private float mTopOffset;
- private float mShelfTop;
- private float mShelfTopAtThreshold;
-
- private int mShelfColor;
- private int mRemainingScreenColor;
-
- private final Path mTempPath = new Path();
- private final Path mRemainingScreenPath = new Path();
- private boolean mRemainingScreenPathValid = false;
-
- private Mode mSysUINavigationMode;
- private boolean mIsTwoZoneSwipeModel;
-
- public ShelfScrimView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mMaxScrimAlpha = Math.round(OVERVIEW.getOverviewScrimAlpha(mLauncher) * 255);
-
- mEndAlpha = Color.alpha(mEndScrim);
- mRadius = BOTTOM_CORNER_RADIUS_RATIO * Themes.getDialogCornerRadius(context);
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mOnboardingPrefs = mLauncher.getOnboardingPrefs();
-
- // 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
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(getContext())
- .addModeChangeListener(this));
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- SysUINavigationMode.INSTANCE.get(getContext()).removeModeChangeListener(this);
- }
-
- @Override
- public void onNavigationModeChanged(Mode newMode) {
- mSysUINavigationMode = newMode;
- // Note that these interpolators are inverted because progress goes 1 to 0.
- if (mSysUINavigationMode == Mode.NO_BUTTON) {
- // Show the shelf more quickly before reaching overview progress.
- mBeforeMidProgressColorInterpolator = ACCEL_2;
- mAfterMidProgressColorInterpolator = ACCEL;
- mIsTwoZoneSwipeModel = FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get();
- } else {
- mBeforeMidProgressColorInterpolator = ACCEL;
- mAfterMidProgressColorInterpolator = Interpolators.clampToProgress(ACCEL, 0.5f, 1f);
- mIsTwoZoneSwipeModel = false;
- }
- }
-
- @Override
- public void reInitUi() {
- DeviceProfile dp = mLauncher.getDeviceProfile();
- mDrawingFlatColor = dp.isVerticalBarLayout();
-
- if (!mDrawingFlatColor) {
- mRemainingScreenPathValid = false;
- mShiftRange = mLauncher.getAllAppsController().getShiftRange();
-
- Context context = getContext();
- if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) {
- mDragHandleProgress = 1;
- if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()
- && SysUINavigationMode.removeShelfFromOverview(context)) {
- // Fade in all apps background quickly to distinguish from swiping from nav bar.
- mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha);
- mMidProgress = OverviewState.getDefaultVerticalProgress(mLauncher);
- } else {
- mMidAlpha = 0;
- mMidProgress = 1;
- }
- } else {
- mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha);
- mMidProgress = OVERVIEW.getVerticalProgress(mLauncher);
- Rect hotseatPadding = dp.getHotseatLayoutPadding();
- int hotseatSize = dp.hotseatBarSizePx + dp.getInsets().bottom
- + hotseatPadding.bottom + hotseatPadding.top;
- float dragHandleTop =
- Math.min(hotseatSize, LayoutUtils.getDefaultSwipeHeight(context, dp));
- mDragHandleProgress = 1 - (dragHandleTop / mShiftRange);
- }
- mTopOffset = dp.getInsets().top - mDragHandleSize.y;
- mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset;
- }
- updateColors();
- updateSysUiColors();
- updateDragHandleAlpha();
- invalidate();
- }
-
- @Override
- public void updateColors() {
- super.updateColors();
- mDragHandleOffset = 0;
- if (mDrawingFlatColor) {
- return;
- }
-
- if (mProgress < mDragHandleProgress) {
- mDragHandleOffset = mShiftRange * (mDragHandleProgress - mProgress);
- }
-
- if (mProgress >= SCRIM_CATCHUP_THRESHOLD) {
- mShelfTop = mShiftRange * mProgress + mTopOffset;
- } else {
- mShelfTop = Utilities.mapRange(mProgress / SCRIM_CATCHUP_THRESHOLD, -mRadius,
- mShelfTopAtThreshold);
- }
-
- if (mProgress >= 1) {
- mRemainingScreenColor = 0;
- mShelfColor = 0;
- LauncherState state = mLauncher.getStateManager().getState();
- if (mSysUINavigationMode == Mode.NO_BUTTON
- && (state == BACKGROUND_APP || state == QUICK_SWITCH)
- && mLauncher.getShelfPeekAnim().isPeeking()) {
- // Show the shelf background when peeking during swipe up.
- mShelfColor = setColorAlphaBound(mEndScrim, mMidAlpha);
- }
- } else if (mProgress >= mMidProgress) {
- mRemainingScreenColor = 0;
-
- int alpha = Math.round(Utilities.mapToRange(
- mProgress, mMidProgress, 1, mMidAlpha, 0, mBeforeMidProgressColorInterpolator));
- mShelfColor = setColorAlphaBound(mEndScrim, alpha);
- } else {
- // Note that these ranges and interpolators are inverted because progress goes 1 to 0.
- int alpha = Math.round(
- Utilities.mapToRange(mProgress, (float) 0, mMidProgress, (float) mEndAlpha,
- (float) mMidAlpha, mAfterMidProgressColorInterpolator));
- mShelfColor = setColorAlphaBound(mEndScrim, alpha);
-
- int remainingScrimAlpha = Math.round(
- Utilities.mapToRange(mProgress, (float) 0, mMidProgress, mMaxScrimAlpha,
- (float) 0, LINEAR));
- mRemainingScreenColor = setColorAlphaBound(mScrimColor, remainingScrimAlpha);
- }
- }
-
- @Override
- protected void updateSysUiColors() {
- if (mDrawingFlatColor) {
- super.updateSysUiColors();
- } else {
- // Use a light system UI (dark icons) if all apps is behind at least half of the
- // status bar.
- boolean forceChange = mShelfTop <= mLauncher.getDeviceProfile().getInsets().top / 2f;
- if (forceChange) {
- mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !mIsScrimDark);
- } else {
- mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
- }
- }
- }
-
- @Override
- protected boolean shouldDragHandleBeVisible() {
- boolean needsAllAppsEdu = mIsTwoZoneSwipeModel
- && !mOnboardingPrefs.hasReachedMaxCount(OnboardingPrefs.ALL_APPS_COUNT);
- return needsAllAppsEdu || super.shouldDragHandleBeVisible();
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- drawBackground(canvas);
- drawDragHandle(canvas);
- }
-
- private void drawBackground(Canvas canvas) {
- if (mDrawingFlatColor) {
- if (mCurrentFlatColor != 0) {
- canvas.drawColor(mCurrentFlatColor);
- }
- return;
- }
-
- if (Color.alpha(mShelfColor) == 0) {
- return;
- } else if (mProgress <= 0) {
- canvas.drawColor(mShelfColor);
- return;
- }
-
- int height = getHeight();
- int width = getWidth();
- // 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, height - mRadius, width, height + mRadius + 10,
- mRadius, mRadius, Direction.CW);
- mRemainingScreenPath.reset();
- mRemainingScreenPath.addRect(0, 0, width, height, Direction.CW);
- mRemainingScreenPath.op(mTempPath, Op.DIFFERENCE);
- }
-
- float offset = height - mRadius - mShelfTop;
- canvas.translate(0, -offset);
- mPaint.setColor(mRemainingScreenColor);
- canvas.drawPath(mRemainingScreenPath, mPaint);
- canvas.translate(0, offset);
- }
-
- mPaint.setColor(mShelfColor);
- canvas.drawRoundRect(0, mShelfTop, width, height + mRadius, mRadius, mRadius, mPaint);
- }
-
- @Override
- public float getVisualTop() {
- return mShelfTop;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/SplitPlaceholderView.java b/quickstep/src/com/android/quickstep/views/SplitPlaceholderView.java
new file mode 100644
index 0000000..bb8bc11
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/SplitPlaceholderView.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+
+import com.android.quickstep.util.SplitSelectStateController;
+
+public class SplitPlaceholderView extends FrameLayout {
+
+ public static final FloatProperty<SplitPlaceholderView> ALPHA_FLOAT =
+ new FloatProperty<SplitPlaceholderView>("SplitViewAlpha") {
+ @Override
+ public void setValue(SplitPlaceholderView splitPlaceholderView, float v) {
+ splitPlaceholderView.setVisibility(v != 0 ? VISIBLE : GONE);
+ splitPlaceholderView.setAlpha(v);
+ }
+
+ @Override
+ public Float get(SplitPlaceholderView splitPlaceholderView) {
+ return splitPlaceholderView.getAlpha();
+ }
+ };
+
+ private SplitSelectStateController mSplitController;
+ private IconView mIcon;
+
+ public SplitPlaceholderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void init(SplitSelectStateController controller) {
+ this.mSplitController = controller;
+ }
+
+ public SplitSelectStateController getSplitController() {
+ return mSplitController;
+ }
+
+ public void setIcon(IconView icon) {
+ if (mIcon == null) {
+ mIcon = new IconView(getContext());
+ addView(mIcon);
+ }
+ mIcon.setDrawable(icon.getDrawable());
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(icon.getLayoutParams());
+ params.gravity = Gravity.CENTER;
+ mIcon.setLayoutParams(params);
+ }
+}
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 0000000..906e854
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -0,0 +1,329 @@
+/*
+ * 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.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RectShape;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.TaskOverlayFactory;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.TaskCornerRadius;
+
+/**
+ * Contains options for a recent task when long-pressing its icon.
+ */
+public class TaskMenuView extends AbstractFloatingView implements OnScrollChangedListener {
+
+ private static final Rect sTempRect = new Rect();
+
+ private static final int REVEAL_OPEN_DURATION = 150;
+ private static final int REVEAL_CLOSE_DURATION = 100;
+ private final float mTaskInsetMargin;
+
+ private BaseDraggingActivity mActivity;
+ private TextView mTaskName;
+ private AnimatorSet mOpenCloseAnimator;
+ private TaskView mTaskView;
+ private LinearLayout mOptionLayout;
+
+ 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);
+ mTaskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTaskName = findViewById(R.id.task_name);
+ mOptionLayout = findViewById(R.id.menu_option_layout);
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ BaseDragLayer dl = 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
+ protected boolean isOfType(int type) {
+ return (type & TYPE_TASK_MENU) != 0;
+ }
+
+ @Override
+ public ViewOutlineProvider getOutlineProvider() {
+ return new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(),
+ TaskCornerRadius.get(view.getContext()));
+ }
+ };
+ }
+
+ private void setPosition(float x, float y, int overscrollShift) {
+ PagedOrientationHandler pagedOrientationHandler = mTaskView.getPagedOrientationHandler();
+ // Inset due to margin
+ PointF additionalInset = pagedOrientationHandler
+ .getAdditionalInsetForTaskMenu(mTaskInsetMargin);
+ int taskTopMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
+
+ float adjustedY = y + taskTopMargin - additionalInset.y;
+ float adjustedX = x - additionalInset.x;
+ // Changing pivot to make computations easier
+ // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
+ // which would render the X and Y position set here incorrect
+ setPivotX(0);
+ if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+ // In tablet, set pivotY to original position without mThumbnailTopMargin adjustment.
+ setPivotY(-taskTopMargin);
+ } else {
+ setPivotY(0);
+ }
+ setRotation(pagedOrientationHandler.getDegreesRotated());
+ setX(pagedOrientationHandler.getTaskMenuX(adjustedX,
+ mTaskView.getThumbnail(), overscrollShift));
+ setY(pagedOrientationHandler.getTaskMenuY(
+ adjustedY, mTaskView.getThumbnail(), overscrollShift));
+ }
+
+ public void onRotationChanged() {
+ if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
+ mOpenCloseAnimator.end();
+ }
+ if (mIsOpen) {
+ mOptionLayout.removeAllViews();
+ if (!populateAndLayoutMenu()) {
+ close(false);
+ }
+ }
+ }
+
+ 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;
+ if (!populateAndLayoutMenu()) {
+ return false;
+ }
+ post(this::animateOpen);
+ ((RecentsView) mActivity.getOverviewPanel()).addOnScrollChangedListener(this);
+ return true;
+ }
+
+ @Override
+ public void onScrollChanged() {
+ RecentsView rv = mTaskView.getRecentsView();
+ setPosition(mTaskView.getX() - rv.getScrollX(), mTaskView.getY() - rv.getScrollY(),
+ rv.getOverScrollShift());
+ }
+
+ /** @return true if successfully able to populate task view menu, false otherwise */
+ private boolean populateAndLayoutMenu() {
+ if (mTaskView.getTask().icon == null) {
+ // Icon may not be loaded
+ return false;
+ }
+ addMenuOptions(mTaskView);
+ orientAroundTaskView(mTaskView);
+ return true;
+ }
+
+ private void addMenuOptions(TaskView taskView) {
+ mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
+ mTaskName.setOnClickListener(v -> close(true));
+
+ TaskOverlayFactory.getEnabledShortcuts(taskView, mActivity.getDeviceProfile())
+ .forEach(this::addMenuOption);
+ }
+
+ private void addMenuOption(SystemShortcut menuOption) {
+ LinearLayout menuOptionView = (LinearLayout) mActivity.getLayoutInflater().inflate(
+ R.layout.task_view_menu_option, this, false);
+ menuOption.setIconAndLabelFor(
+ menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
+ LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
+ mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp,
+ menuOptionView, mActivity.getDeviceProfile());
+ menuOptionView.setEnabled(menuOption.isEnabled());
+ menuOptionView.setAlpha(menuOption.isEnabled() ? 1 : 0.5f);
+ menuOptionView.setOnClickListener(view -> {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && !menuOption.hasFinishRecentsInAction()) {
+ RecentsView recentsView = mTaskView.getRecentsView();
+ recentsView.switchToScreenshot(null,
+ () -> recentsView.finishRecentsAnimation(true /* toRecents */,
+ false /* shouldPip */,
+ () -> menuOption.onClick(view)));
+ } else {
+ menuOption.onClick(view);
+ }
+ });
+ mOptionLayout.addView(menuOptionView);
+ }
+
+ private void orientAroundTaskView(TaskView taskView) {
+ PagedOrientationHandler orientationHandler = taskView.getPagedOrientationHandler();
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ orientationHandler.setTaskMenuAroundTaskView(this, mTaskInsetMargin);
+
+ // Get Position
+ mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
+ Rect insets = mActivity.getDragLayer().getInsets();
+ BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
+ int padding = getResources()
+ .getDimensionPixelSize(R.dimen.task_menu_vertical_padding);
+ params.width = orientationHandler.getTaskMenuWidth(taskView.getThumbnail()) - (2 * padding);
+ // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
+ params.gravity = Gravity.LEFT;
+ setLayoutParams(params);
+ setScaleX(taskView.getScaleX());
+ setScaleY(taskView.getScaleY());
+
+ // Set divider spacing
+ ShapeDrawable divider = new ShapeDrawable(new RectShape());
+ divider.getPaint().setColor(getResources().getColor(android.R.color.transparent));
+ int dividerSpacing = (int) getResources().getDimension(R.dimen.task_menu_spacing);
+ mOptionLayout.setShowDividers(SHOW_DIVIDER_MIDDLE);
+
+ orientationHandler.setTaskOptionsMenuLayoutOrientation(
+ mActivity.getDeviceProfile(), mOptionLayout, dividerSpacing, divider);
+ setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top, 0);
+ }
+
+ private void animateOpen() {
+ animateOpenOrClosed(false);
+ mIsOpen = true;
+ }
+
+ private void animateClose() {
+ animateOpenOrClosed(true);
+ }
+
+ private void animateOpenOrClosed(boolean closing) {
+ if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
+ mOpenCloseAnimator.end();
+ }
+ mOpenCloseAnimator = new AnimatorSet();
+
+ final Animator revealAnimator = createOpenCloseOutlineProvider()
+ .createRevealAnimator(this, closing);
+ revealAnimator.setInterpolator(Interpolators.DEACCEL);
+ mOpenCloseAnimator.playTogether(revealAnimator,
+ ObjectAnimator.ofFloat(
+ mTaskView.getThumbnail(), DIM_ALPHA,
+ closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
+ ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+ mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setVisibility(VISIBLE);
+ }
+
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (closing) {
+ closeComplete();
+ }
+ }
+ });
+ mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION);
+ mOpenCloseAnimator.start();
+ }
+
+ private void closeComplete() {
+ mIsOpen = false;
+ mActivity.getDragLayer().removeView(this);
+ ((RecentsView) mActivity.getOverviewPanel()).removeOnScrollChangedListener(this);
+ }
+
+ private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
+ float radius = TaskCornerRadius.get(mContext);
+ Rect fromRect = new Rect(0, 0, getWidth(), 0);
+ Rect toRect = new Rect(0, 0, getWidth(), getHeight());
+ return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
+ }
+
+ public View findMenuItemByText(String text) {
+ for (int i = mOptionLayout.getChildCount() - 1; i >= 0; --i) {
+ final ViewGroup menuOptionView = (ViewGroup) mOptionLayout.getChildAt(i);
+ if (text.equals(menuOptionView.<TextView>findViewById(R.id.text).getText())) {
+ return menuOptionView;
+ }
+ }
+ return null;
+ }
+}
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 0000000..c70596d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -0,0 +1,650 @@
+/*
+ * 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.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+
+import static com.android.launcher3.Utilities.comp;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Property;
+import android.view.Surface;
+import android.view.View;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.SystemUiController;
+import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
+import com.android.quickstep.views.TaskView.FullscreenDrawParams;
+import com.android.systemui.plugins.OverviewScreenshotActions;
+import com.android.systemui.plugins.PluginListener;
+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 implements PluginListener<OverviewScreenshotActions> {
+ private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS =
+ new MainThreadInitializedObject<>(FullscreenDrawParams::new);
+
+ public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
+ new FloatProperty<TaskThumbnailView>("dimAlpha") {
+ @Override
+ public void setValue(TaskThumbnailView thumbnail, float dimAlpha) {
+ thumbnail.setDimAlpha(dimAlpha);
+ }
+
+ @Override
+ public Float get(TaskThumbnailView thumbnailView) {
+ return thumbnailView.mDimAlpha;
+ }
+ };
+
+ private final BaseActivity mActivity;
+ private TaskOverlay mOverlay;
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint mClearPaint = new Paint();
+ private final Paint mDimmingPaintAfterClearing = new Paint();
+ private final int mDimColor;
+
+ // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
+ private final Rect mPreviewRect = new Rect();
+ private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
+ private TaskView.FullscreenDrawParams mFullscreenParams;
+
+ private Task mTask;
+ private ThumbnailData mThumbnailData;
+ protected BitmapShader mBitmapShader;
+
+ /** How much this thumbnail is dimmed, 0 not dimmed at all, 1 totally dimmed. */
+ private float mDimAlpha = 0f;
+
+ private boolean mOverlayEnabled;
+ private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
+
+ 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);
+ mPaint.setFilterBitmap(true);
+ mBackgroundPaint.setColor(Color.WHITE);
+ mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ mActivity = BaseActivity.fromContext(context);
+ // Initialize with placeholder value. It is overridden later by TaskView
+ mFullscreenParams = TEMP_PARAMS.get(context);
+
+ mDimColor = RecentsView.getForegroundScrimDimColor(context);
+ mDimmingPaintAfterClearing.setColor(mDimColor);
+ }
+
+ /**
+ * Updates the thumbnail to draw the provided task
+ * @param task
+ */
+ public void bind(Task task) {
+ getTaskOverlay().reset();
+ mTask = task;
+ int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
+ mPaint.setColor(color);
+ mBackgroundPaint.setColor(color);
+ }
+
+ /**
+ * Updates the thumbnail.
+ * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
+ * In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)}
+ * version with {@code refreshNow} is true. The only exception is
+ * in the live tile case that we grab a screenshot when user enters Overview
+ * upon swipe up so that a usable screenshot is accessible immediately when
+ * recents animation needs to be finished / cancelled.
+ */
+ public void setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow) {
+ mTask = task;
+ mThumbnailData =
+ (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
+ if (refreshNow) {
+ refresh();
+ }
+ }
+
+ /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */
+ public void setThumbnail(Task task, ThumbnailData thumbnailData) {
+ setThumbnail(task, thumbnailData, true /* refreshNow */);
+ }
+
+ /** Updates the shader, paint, matrix to redraw. */
+ public void refresh() {
+ if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
+ Bitmap bm = mThumbnailData.thumbnail;
+ bm.prepareToDraw();
+ mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ mPaint.setShader(mBitmapShader);
+ updateThumbnailMatrix();
+ } else {
+ mBitmapShader = null;
+ mThumbnailData = null;
+ mPaint.setShader(null);
+ getTaskOverlay().reset();
+ }
+ if (mOverviewScreenshotActionsPlugin != null) {
+ mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
+ }
+ updateThumbnailPaintFilter();
+ }
+
+ /**
+ * Sets the alpha of the dim layer on top of this view.
+ * <p>
+ * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be the
+ * extracted background color.
+ *
+ */
+ public void setDimAlpha(float dimAlpha) {
+ mDimAlpha = dimAlpha;
+ updateThumbnailPaintFilter();
+ }
+
+ public TaskOverlay getTaskOverlay() {
+ if (mOverlay == null) {
+ mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this);
+ }
+ return mOverlay;
+ }
+
+ public float getDimAlpha() {
+ return mDimAlpha;
+ }
+
+ /**
+ * Get the scaled insets that are being used to draw the task view. This is a subsection of
+ * the full snapshot.
+ * @return the insets in snapshot bitmap coordinates.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ public Insets getScaledInsets() {
+ if (mThumbnailData == null) {
+ return Insets.NONE;
+ }
+
+ if (!TaskView.CLIP_STATUS_AND_NAV_BARS) {
+ return Insets.NONE;
+ }
+
+ RectF bitmapRect = new RectF(
+ 0, 0,
+ mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
+ RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
+
+ // The position helper matrix tells us how to transform the bitmap to fit the view, the
+ // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
+ // difference between the bitmap bounds and the projected view bounds.
+ Matrix boundsToBitmapSpace = new Matrix();
+ mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
+ RectF boundsInBitmapSpace = new RectF();
+ boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
+
+ return Insets.of(
+ Math.round(boundsInBitmapSpace.left),
+ Math.round(boundsInBitmapSpace.top),
+ Math.round(bitmapRect.right - boundsInBitmapSpace.right),
+ Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom));
+ }
+
+
+ public int getSysUiStatusNavFlags() {
+ if (mThumbnailData != null) {
+ int flags = 0;
+ flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_STATUS_BARS) != 0
+ ? SystemUiController.FLAG_LIGHT_STATUS
+ : SystemUiController.FLAG_DARK_STATUS;
+ flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0
+ ? SystemUiController.FLAG_LIGHT_NAV
+ : SystemUiController.FLAG_DARK_NAV;
+ return flags;
+ }
+ return 0;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets;
+ canvas.save();
+ canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale);
+ canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top);
+ // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
+ drawOnCanvas(canvas,
+ -currentDrawnInsets.left,
+ -currentDrawnInsets.top,
+ getMeasuredWidth() + currentDrawnInsets.right,
+ getMeasuredHeight() + currentDrawnInsets.bottom,
+ mFullscreenParams.mCurrentDrawnCornerRadius);
+ canvas.restore();
+ }
+
+ @Override
+ public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions,
+ Context context) {
+ mOverviewScreenshotActionsPlugin = overviewScreenshotActions;
+ mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
+ }
+
+ @Override
+ public void onPluginDisconnected(OverviewScreenshotActions plugin) {
+ if (mOverviewScreenshotActionsPlugin != null) {
+ mOverviewScreenshotActionsPlugin = null;
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ PluginManagerWrapper.INSTANCE.get(getContext())
+ .addPluginListener(this, OverviewScreenshotActions.class);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
+ }
+
+ public PreviewPositionHelper getPreviewPositionHelper() {
+ return mPreviewPositionHelper;
+ }
+
+ public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
+ mFullscreenParams = fullscreenParams;
+ invalidate();
+ }
+
+ public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
+ float cornerRadius) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
+ // TODO(b/189265196): Temporary fix to align the surface with the cutout perfectly.
+ // Round up only when the live tile task is displayed in Overview.
+ float rounding = comp(mFullscreenParams.mFullscreenProgress);
+ float left = x + rounding / 2;
+ float top = y + rounding / 2;
+ float right = width - rounding;
+ float bottom = height - rounding;
+
+ canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
+ mClearPaint);
+ canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
+ mDimmingPaintAfterClearing);
+ return;
+ }
+ }
+
+ // 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 || mThumbnailData.isTranslucent) {
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
+ if (drawBackgroundOnly) {
+ return;
+ }
+ }
+
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
+ }
+
+ public TaskView getTaskView() {
+ return (TaskView) getParent();
+ }
+
+ public void setOverlayEnabled(boolean overlayEnabled) {
+ if (mOverlayEnabled != overlayEnabled) {
+ mOverlayEnabled = overlayEnabled;
+
+ refreshOverlay();
+ }
+ }
+
+ /**
+ * Potentially re-init the task overlay. Be cautious when calling this as the overlay may
+ * do processing on initialization.
+ */
+ private void refreshOverlay() {
+ if (mOverlayEnabled) {
+ getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
+ mPreviewPositionHelper.mIsOrientationChanged);
+ } else {
+ getTaskOverlay().reset();
+ }
+ }
+
+ private void updateThumbnailPaintFilter() {
+ ColorFilter filter = getColorFilter(mDimAlpha);
+ mBackgroundPaint.setColorFilter(filter);
+ int alpha = (int) (mDimAlpha * 255);
+ mDimmingPaintAfterClearing.setAlpha(alpha);
+ if (mBitmapShader != null) {
+ mPaint.setColorFilter(filter);
+ } else {
+ mPaint.setColorFilter(null);
+ mPaint.setColor(ColorUtils.blendARGB(Color.BLACK, mDimColor, alpha));
+ }
+ invalidate();
+ }
+
+ private void updateThumbnailMatrix() {
+ mPreviewPositionHelper.mIsOrientationChanged = false;
+ if (mBitmapShader != null && mThumbnailData != null) {
+ mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
+ mThumbnailData.thumbnail.getHeight());
+ int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState()
+ .getRecentsActivityRotation();
+ boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
+ getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
+ currentRotation, isRtl);
+
+ mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
+ mPaint.setShader(mBitmapShader);
+ }
+ getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper);
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateThumbnailMatrix();
+
+ refreshOverlay();
+ }
+
+ private ColorFilter getColorFilter(float dimAmount) {
+ return Utilities.makeColorTintingColorFilter(mDimColor, dimAmount);
+ }
+
+ public Bitmap getThumbnail() {
+ if (mThumbnailData == null) {
+ return null;
+ }
+ return mThumbnailData.thumbnail;
+ }
+
+ /**
+ * Returns whether the snapshot is real. If the device is locked for the user of the task,
+ * the snapshot used will be an app-theme generated snapshot instead of a real snapshot.
+ */
+ public boolean isRealSnapshot() {
+ if (mThumbnailData == null) {
+ return false;
+ }
+ return mThumbnailData.isRealSnapshot && !mTask.isLocked;
+ }
+
+ /**
+ * Utility class to position the thumbnail in the TaskView
+ */
+ public static class PreviewPositionHelper {
+
+ // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
+ private final RectF mClippedInsets = new RectF();
+ private final Matrix mMatrix = new Matrix();
+ private boolean mIsOrientationChanged;
+
+ public Matrix getMatrix() {
+ return mMatrix;
+ }
+
+ /**
+ * Updates the matrix based on the provided parameters
+ */
+ public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
+ int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation,
+ boolean isRtl) {
+ boolean isRotated = false;
+ boolean isOrientationDifferent;
+
+ int thumbnailRotation = thumbnailData.rotation;
+ int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+ RectF thumbnailClipHint = TaskView.CLIP_STATUS_AND_NAV_BARS
+ ? new RectF(thumbnailData.insets) : new RectF();
+
+ float scale = thumbnailData.scale;
+ final float thumbnailScale;
+
+ // Landscape vs portrait change.
+ // Note: Disable rotation in grid layout.
+ boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
+ && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN
+ && !(dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
+ isOrientationDifferent = isOrientationChange(deltaRotate)
+ && windowingModeSupportsRotation;
+ if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
+ // If we haven't measured , skip the thumbnail drawing and only draw the background
+ // color
+ thumbnailScale = 0f;
+ } else {
+ // Rotate the screenshot if not in multi-window mode
+ isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
+
+ float surfaceWidth = thumbnailBounds.width() / scale;
+ float surfaceHeight = thumbnailBounds.height() / scale;
+ float availableWidth = surfaceWidth
+ - (thumbnailClipHint.left + thumbnailClipHint.right);
+ float availableHeight = surfaceHeight
+ - (thumbnailClipHint.top + thumbnailClipHint.bottom);
+
+ if (isRotated) {
+ float canvasAspect = canvasWidth / (float) canvasHeight;
+ float availableAspect = availableHeight / availableWidth;
+ // Do not rotate thumbnail if it would not improve fit
+ if (Utilities.isRelativePercentDifferenceGreaterThan(canvasAspect,
+ availableAspect, 0.1f)) {
+ isRotated = false;
+ isOrientationDifferent = false;
+ }
+ }
+
+ final float targetW, targetH;
+ if (isOrientationDifferent) {
+ targetW = canvasHeight;
+ targetH = canvasWidth;
+ } else {
+ targetW = canvasWidth;
+ targetH = canvasHeight;
+ }
+ float canvasAspect = targetW / targetH;
+
+ // Update the clipHint such that
+ // > the final clipped position has same aspect ratio as requested by canvas
+ // > the clipped region is within the task insets if possible
+ // > the clipped region is not scaled up when drawing. If that is not possible
+ // while staying within the taskInsets, move outside the insets.
+ float croppedWidth = availableWidth;
+ if (croppedWidth < targetW) {
+ croppedWidth = Math.min(targetW, surfaceWidth);
+ }
+
+ float croppedHeight = croppedWidth / canvasAspect;
+ if (croppedHeight > availableHeight) {
+ croppedHeight = availableHeight;
+ if (croppedHeight < targetH) {
+ croppedHeight = Math.min(targetH, surfaceHeight);
+ }
+ croppedWidth = croppedHeight * canvasAspect;
+
+ // One last check in case the task aspect radio messed up something
+ if (croppedWidth > surfaceWidth) {
+ croppedWidth = surfaceWidth;
+ croppedHeight = croppedWidth / canvasAspect;
+ }
+ }
+
+ // Update the clip hints. Align to 0,0, crop the remaining.
+ if (isRtl) {
+ thumbnailClipHint.left += availableWidth - croppedWidth;
+ if (thumbnailClipHint.right < 0) {
+ thumbnailClipHint.left += thumbnailClipHint.right;
+ thumbnailClipHint.right = 0;
+ }
+ } else {
+ thumbnailClipHint.right += availableWidth - croppedWidth;
+ if (thumbnailClipHint.left < 0) {
+ thumbnailClipHint.right += thumbnailClipHint.left;
+ thumbnailClipHint.left = 0;
+ }
+ }
+ thumbnailClipHint.bottom += availableHeight - croppedHeight;
+ if (thumbnailClipHint.top < 0) {
+ thumbnailClipHint.bottom += thumbnailClipHint.top;
+ thumbnailClipHint.top = 0;
+ } else if (thumbnailClipHint.bottom < 0) {
+ thumbnailClipHint.top += thumbnailClipHint.bottom;
+ thumbnailClipHint.bottom = 0;
+ }
+
+ thumbnailScale = targetW / (croppedWidth * scale);
+ }
+
+ Rect splitScreenInsets = dp.getInsets();
+ if (!isRotated) {
+ // No Rotation
+ if (dp.isMultiWindowMode) {
+ mClippedInsets.offsetTo(splitScreenInsets.left * scale,
+ splitScreenInsets.top * scale);
+ } else {
+ mClippedInsets.offsetTo(thumbnailClipHint.left * scale,
+ thumbnailClipHint.top * scale);
+ }
+ mMatrix.setTranslate(
+ -thumbnailClipHint.left * scale,
+ -thumbnailClipHint.top * scale);
+ } else {
+ setThumbnailRotation(deltaRotate, thumbnailClipHint, scale, thumbnailBounds);
+ }
+
+ final float widthWithInsets;
+ final float heightWithInsets;
+ if (isOrientationDifferent) {
+ widthWithInsets = thumbnailBounds.height() * thumbnailScale;
+ heightWithInsets = thumbnailBounds.width() * thumbnailScale;
+ } else {
+ widthWithInsets = thumbnailBounds.width() * thumbnailScale;
+ heightWithInsets = thumbnailBounds.height() * thumbnailScale;
+ }
+ mClippedInsets.left *= thumbnailScale;
+ mClippedInsets.top *= thumbnailScale;
+
+ if (dp.isMultiWindowMode) {
+ mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale;
+ mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale;
+ } else {
+ mClippedInsets.right = Math.max(0,
+ widthWithInsets - mClippedInsets.left - canvasWidth);
+ mClippedInsets.bottom = Math.max(0,
+ heightWithInsets - mClippedInsets.top - canvasHeight);
+ }
+
+ mMatrix.postScale(thumbnailScale, thumbnailScale);
+ mIsOrientationChanged = isOrientationDifferent;
+ }
+
+ private int getRotationDelta(int oldRotation, int newRotation) {
+ int delta = newRotation - oldRotation;
+ if (delta < 0) delta += 4;
+ return delta;
+ }
+
+ /**
+ * @param deltaRotation the number of 90 degree turns from the current orientation
+ * @return {@code true} if the change in rotation results in a shift from landscape to
+ * portrait or vice versa, {@code false} otherwise
+ */
+ private boolean isOrientationChange(int deltaRotation) {
+ return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+ }
+
+ private void setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale,
+ Rect thumbnailPosition) {
+ float newLeftInset = 0;
+ float newTopInset = 0;
+ float translateX = 0;
+ float translateY = 0;
+
+ mMatrix.setRotate(90 * deltaRotate);
+ switch (deltaRotate) { /* Counter-clockwise */
+ case Surface.ROTATION_90:
+ newLeftInset = thumbnailInsets.bottom;
+ newTopInset = thumbnailInsets.left;
+ translateX = thumbnailPosition.height();
+ break;
+ case Surface.ROTATION_270:
+ newLeftInset = thumbnailInsets.top;
+ newTopInset = thumbnailInsets.right;
+ translateY = thumbnailPosition.width();
+ break;
+ case Surface.ROTATION_180:
+ newLeftInset = -thumbnailInsets.top;
+ newTopInset = -thumbnailInsets.left;
+ translateX = thumbnailPosition.width();
+ translateY = thumbnailPosition.height();
+ break;
+ }
+ mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
+ mMatrix.postTranslate(translateX - mClippedInsets.left,
+ translateY - mClippedInsets.top);
+ }
+
+ /**
+ * Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
+ */
+ public RectF getInsetsToDrawInFullscreen() {
+ return mClippedInsets;
+ }
+ }
+}
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 0000000..2d322e9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -0,0 +1,1529 @@
+/*
+ * 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.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.Gravity.END;
+import static android.view.Gravity.START;
+import static android.view.Gravity.TOP;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+import static android.widget.Toast.LENGTH_SHORT;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
+import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
+import static com.android.launcher3.Utilities.comp;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.Toast;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.util.TransformingTouchDelegate;
+import com.android.launcher3.util.ViewPool.Reusable;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.RemoteAnimationTargets;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskIconCache;
+import com.android.quickstep.TaskOverlayFactory;
+import com.android.quickstep.TaskThumbnailCache;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.util.CancellableTask;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.TaskCornerRadius;
+import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.QuickStepContract;
+
+import java.lang.annotation.Retention;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A task in the Recents view.
+ */
+public class TaskView extends FrameLayout implements Reusable {
+
+ private static final String TAG = TaskView.class.getSimpleName();
+
+ public static final int FLAG_UPDATE_ICON = 1;
+ public static final int FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON << 1;
+
+ public static final int FLAG_UPDATE_ALL = FLAG_UPDATE_ICON | FLAG_UPDATE_THUMBNAIL;
+
+ /**
+ * Used in conjunction with {@link #onTaskListVisibilityChanged(boolean, int)}, providing more
+ * granularity on which components of this task require an update
+ */
+ @Retention(SOURCE)
+ @IntDef({FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL})
+ public @interface TaskDataChanges {}
+
+ /**
+ * Should the layout account for space for a proactive action (or chip) to be added under
+ * the task.
+ */
+ public static final boolean SHOW_PROACTIVE_ACTIONS = false;
+
+ /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
+ public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
+
+ /**
+ * Should the TaskView display clip off the status and navigation bars in recents. When this
+ * is false the overview shows the whole screen scaled down instead.
+ */
+ public static final boolean CLIP_STATUS_AND_NAV_BARS = false;
+
+ private static final float EDGE_SCALE_DOWN_FACTOR_CAROUSEL = 0.03f;
+ private static final float EDGE_SCALE_DOWN_FACTOR_GRID = 0.00f;
+
+ public static final long SCALE_ICON_DURATION = 120;
+ private static final long DIM_ANIM_DURATION = 700;
+
+ private static final Interpolator FULLSCREEN_INTERPOLATOR = ACCEL_DEACCEL;
+
+ /**
+ * This technically can be a vanilla {@link TouchDelegate} class, however that class requires
+ * setting the touch bounds at construction, so we'd repeatedly be created many instances
+ * unnecessarily as scrolling occurs, whereas {@link TransformingTouchDelegate} allows touch
+ * delegated bounds only to be updated.
+ */
+ private TransformingTouchDelegate mIconTouchDelegate;
+ private TransformingTouchDelegate mChipTouchDelegate;
+
+ private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
+ Collections.singletonList(new Rect());
+
+ private static final FloatProperty<TaskView> FOCUS_TRANSITION =
+ new FloatProperty<TaskView>("focusTransition") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setIconAndDimTransitionProgress(v, false /* invert */);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mFocusTransitionProgress;
+ }
+ };
+
+ private static final FloatProperty<TaskView> SPLIT_SELECT_TRANSLATION_X =
+ new FloatProperty<TaskView>("splitSelectTranslationX") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setSplitSelectTranslationX(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mSplitSelectTranslationX;
+ }
+ };
+
+ private static final FloatProperty<TaskView> SPLIT_SELECT_TRANSLATION_Y =
+ new FloatProperty<TaskView>("splitSelectTranslationY") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setSplitSelectTranslationY(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mSplitSelectTranslationY;
+ }
+ };
+
+ private static final FloatProperty<TaskView> DISMISS_TRANSLATION_X =
+ new FloatProperty<TaskView>("dismissTranslationX") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setDismissTranslationX(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mDismissTranslationX;
+ }
+ };
+
+ private static final FloatProperty<TaskView> DISMISS_TRANSLATION_Y =
+ new FloatProperty<TaskView>("dismissTranslationY") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setDismissTranslationY(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mDismissTranslationY;
+ }
+ };
+
+ private static final FloatProperty<TaskView> TASK_OFFSET_TRANSLATION_X =
+ new FloatProperty<TaskView>("taskOffsetTranslationX") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setTaskOffsetTranslationX(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mTaskOffsetTranslationX;
+ }
+ };
+
+ private static final FloatProperty<TaskView> TASK_OFFSET_TRANSLATION_Y =
+ new FloatProperty<TaskView>("taskOffsetTranslationY") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setTaskOffsetTranslationY(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mTaskOffsetTranslationY;
+ }
+ };
+
+ private static final FloatProperty<TaskView> TASK_RESISTANCE_TRANSLATION_X =
+ new FloatProperty<TaskView>("taskResistanceTranslationX") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setTaskResistanceTranslationX(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mTaskResistanceTranslationX;
+ }
+ };
+
+ private static final FloatProperty<TaskView> TASK_RESISTANCE_TRANSLATION_Y =
+ new FloatProperty<TaskView>("taskResistanceTranslationY") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setTaskResistanceTranslationY(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mTaskResistanceTranslationY;
+ }
+ };
+
+ private static final FloatProperty<TaskView> FULLSCREEN_TRANSLATION_X =
+ new FloatProperty<TaskView>("fullscreenTranslationX") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setFullscreenTranslationX(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mFullscreenTranslationX;
+ }
+ };
+
+ private static final FloatProperty<TaskView> FULLSCREEN_TRANSLATION_Y =
+ new FloatProperty<TaskView>("fullscreenTranslationY") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setFullscreenTranslationY(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mFullscreenTranslationY;
+ }
+ };
+
+ private static final FloatProperty<TaskView> NON_FULLSCREEN_TRANSLATION_X =
+ new FloatProperty<TaskView>("nonFullscreenTranslationX") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setNonFullscreenTranslationX(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mNonFullscreenTranslationX;
+ }
+ };
+
+ private static final FloatProperty<TaskView> NON_FULLSCREEN_TRANSLATION_Y =
+ new FloatProperty<TaskView>("nonFullscreenTranslationY") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setNonFullscreenTranslationY(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mNonFullscreenTranslationY;
+ }
+ };
+
+ private final TaskOutlineProvider mOutlineProvider;
+
+ private Task mTask;
+ private TaskThumbnailView mSnapshotView;
+ private IconView mIconView;
+ private final DigitalWellBeingToast mDigitalWellBeingToast;
+ private float mFullscreenProgress;
+ private float mGridProgress;
+ private float mFullscreenScale = 1;
+ private final FullscreenDrawParams mCurrentFullscreenParams;
+ private final StatefulActivity mActivity;
+
+ // Various causes of changing primary translation, which we aggregate to setTranslationX/Y().
+ private float mDismissTranslationX;
+ private float mDismissTranslationY;
+ private float mTaskOffsetTranslationX;
+ private float mTaskOffsetTranslationY;
+ private float mTaskResistanceTranslationX;
+ private float mTaskResistanceTranslationY;
+ // The following translation variables should only be used in the same orientation as Launcher.
+ private float mFullscreenTranslationX;
+ private float mFullscreenTranslationY;
+ // Applied as a complement to fullscreenTranslation, for adjusting the carousel overview, or the
+ // in transition carousel before forming the grid on tablets.
+ private float mNonFullscreenTranslationX;
+ private float mNonFullscreenTranslationY;
+ private float mBoxTranslationY;
+ // The following grid translations scales with mGridProgress.
+ private float mGridTranslationX;
+ private float mGridTranslationY;
+ // Used when in SplitScreenSelectState
+ private float mSplitSelectTranslationY;
+ private float mSplitSelectTranslationX;
+
+ private ObjectAnimator mIconAndDimAnimator;
+ private float mIconScaleAnimStartProgress = 0;
+ private float mFocusTransitionProgress = 1;
+ private float mModalness = 0;
+ private float mStableAlpha = 1;
+
+ private boolean mShowScreenshot;
+
+ // The current background requests to load the task thumbnail and icon
+ private CancellableTask mThumbnailLoadRequest;
+ private CancellableTask mIconLoadRequest;
+
+ private boolean mEndQuickswitchCuj;
+
+ private View mContextualChipWrapper;
+ private final float[] mIconCenterCoords = new float[2];
+ private final float[] mChipCenterCoords = new float[2];
+
+ private boolean mIsClickableAsLiveTile = true;
+
+ 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);
+ mActivity = StatefulActivity.fromContext(context);
+ setOnClickListener(this::onClick);
+
+ mCurrentFullscreenParams = new FullscreenDrawParams(context);
+ mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
+
+ mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams,
+ mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx);
+ setOutlineProvider(mOutlineProvider);
+ }
+
+ /**
+ * Builds proto for logging
+ */
+ public WorkspaceItemInfo getItemInfo() {
+ final Task task = getTask();
+ ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key);
+ WorkspaceItemInfo stubInfo = new WorkspaceItemInfo();
+ stubInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK;
+ stubInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
+ stubInfo.user = componentKey.user;
+ stubInfo.intent = new Intent().setComponent(componentKey.componentName);
+ stubInfo.title = task.title;
+ stubInfo.screenId = getRecentsView().indexOfChild(this);
+ return stubInfo;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mSnapshotView = findViewById(R.id.snapshot);
+ mIconView = findViewById(R.id.icon);
+ mIconTouchDelegate = new TransformingTouchDelegate(mIconView);
+ }
+
+ /**
+ * Whether the taskview should take the touch event from parent. Events passed to children
+ * that might require special handling.
+ */
+ public boolean offerTouchToChildren(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ computeAndSetIconTouchDelegate();
+ computeAndSetChipTouchDelegate();
+ }
+ if (mIconTouchDelegate != null && mIconTouchDelegate.onTouchEvent(event)) {
+ return true;
+ }
+ if (mChipTouchDelegate != null && mChipTouchDelegate.onTouchEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ private void computeAndSetIconTouchDelegate() {
+ float iconHalfSize = mIconView.getWidth() / 2f;
+ mIconCenterCoords[0] = mIconCenterCoords[1] = iconHalfSize;
+ getDescendantCoordRelativeToAncestor(mIconView, mActivity.getDragLayer(), mIconCenterCoords,
+ false);
+ mIconTouchDelegate.setBounds(
+ (int) (mIconCenterCoords[0] - iconHalfSize),
+ (int) (mIconCenterCoords[1] - iconHalfSize),
+ (int) (mIconCenterCoords[0] + iconHalfSize),
+ (int) (mIconCenterCoords[1] + iconHalfSize));
+ }
+
+ private void computeAndSetChipTouchDelegate() {
+ if (mContextualChipWrapper != null) {
+ float chipHalfWidth = mContextualChipWrapper.getWidth() / 2f;
+ float chipHalfHeight = mContextualChipWrapper.getHeight() / 2f;
+ mChipCenterCoords[0] = chipHalfWidth;
+ mChipCenterCoords[1] = chipHalfHeight;
+ getDescendantCoordRelativeToAncestor(mContextualChipWrapper, mActivity.getDragLayer(),
+ mChipCenterCoords,
+ false);
+ mChipTouchDelegate.setBounds(
+ (int) (mChipCenterCoords[0] - chipHalfWidth),
+ (int) (mChipCenterCoords[1] - chipHalfHeight),
+ (int) (mChipCenterCoords[0] + chipHalfWidth),
+ (int) (mChipCenterCoords[1] + chipHalfHeight));
+ }
+ }
+
+ /**
+ * The modalness of this view is how it should be displayed when it is shown on its own in the
+ * modal state of overview.
+ *
+ * @param modalness [0, 1] 0 being in context with other tasks, 1 being shown on its own.
+ */
+ public void setModalness(float modalness) {
+ if (mModalness == modalness) {
+ return;
+ }
+ mModalness = modalness;
+ mIconView.setAlpha(comp(modalness));
+ if (mContextualChipWrapper != null) {
+ mContextualChipWrapper.setScaleX(comp(modalness));
+ mContextualChipWrapper.setScaleY(comp(modalness));
+ }
+ mDigitalWellBeingToast.updateBannerOffset(modalness,
+ mCurrentFullscreenParams.mCurrentDrawnInsets.top
+ + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom);
+ }
+
+ public DigitalWellBeingToast getDigitalWellBeingToast() {
+ return mDigitalWellBeingToast;
+ }
+
+ /**
+ * Updates this task view to the given {@param task}.
+ *
+ * TODO(b/142282126) Re-evaluate if we need to pass in isMultiWindowMode after
+ * that issue is fixed
+ */
+ public void bind(Task task, RecentsOrientedState orientedState) {
+ cancelPendingLoadTasks();
+ mTask = task;
+ mSnapshotView.bind(task);
+ setOrientationState(orientedState);
+ }
+
+ public Task getTask() {
+ return mTask;
+ }
+
+ public boolean hasTaskId(int taskId) {
+ return mTask != null && mTask.key != null && mTask.key.id == taskId;
+ }
+
+ public TaskThumbnailView getThumbnail() {
+ return mSnapshotView;
+ }
+
+ public IconView getIconView() {
+ return mIconView;
+ }
+
+ private void onClick(View view) {
+ if (getTask() == null) {
+ return;
+ }
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
+ if (!mIsClickableAsLiveTile) {
+ return;
+ }
+
+ // Reset the minimized state since we force-toggled the minimized state when entering
+ // overview, but never actually finished the recents animation
+ SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate();
+ if (p != null) {
+ p.setSplitScreenMinimized(false);
+ }
+
+ mIsClickableAsLiveTile = false;
+ RecentsView recentsView = getRecentsView();
+ final RemoteAnimationTargets targets = recentsView.getLiveTileParams().getTargetSet();
+ if (targets == null) {
+ // If the recents animation is cancelled somehow between the parent if block and
+ // here, try to launch the task as a non live tile task.
+ launcherNonLiveTileTask();
+ return;
+ }
+
+ AnimatorSet anim = new AnimatorSet();
+ TaskViewUtils.composeRecentsLaunchAnimator(
+ anim, this, targets.apps,
+ targets.wallpapers, targets.nonApps, true /* launcherClosing */,
+ mActivity.getStateManager(), recentsView,
+ recentsView.getDepthController());
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ recentsView.getLiveTileTaskViewSimulator().setDrawsBelowRecents(false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ recentsView.getLiveTileTaskViewSimulator().setDrawsBelowRecents(true);
+ mIsClickableAsLiveTile = true;
+ }
+ });
+ anim.start();
+ } else {
+ launcherNonLiveTileTask();
+ }
+ mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
+ .log(LAUNCHER_TASK_LAUNCH_TAP);
+ }
+
+ private void launcherNonLiveTileTask() {
+ if (mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
+ // User tapped to select second split screen app
+ getRecentsView().confirmSplitSelect(this);
+ } else {
+ launchTaskAnimated();
+ }
+ }
+
+ /**
+ * Starts the task associated with this view and animates the startup.
+ * @return CompletionStage to indicate the animation completion or null if the launch failed.
+ */
+ public RunnableList launchTaskAnimated() {
+ if (mTask != null) {
+ TestLogging.recordEvent(
+ TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
+ ActivityOptionsWrapper opts = mActivity.getActivityLaunchOptions(this, null);
+ if (ActivityManagerWrapper.getInstance()
+ .startActivityFromRecents(mTask.key, opts.options)) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && getRecentsView().getRunningTaskId() != -1) {
+ // Return a fresh callback in the live tile case, so that it's not accidentally
+ // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner.
+ RunnableList callbackList = new RunnableList();
+ getRecentsView().addSideTaskLaunchCallback(callbackList);
+ return callbackList;
+ }
+ return opts.onEndCallback;
+ } else {
+ notifyTaskLaunchFailed(TAG);
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Starts the task associated with this view without any animation
+ */
+ public void launchTask(@NonNull Consumer<Boolean> callback) {
+ launchTask(callback, false /* freezeTaskList */);
+ }
+
+ /**
+ * Starts the task associated with this view without any animation
+ */
+ public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+ if (mTask != null) {
+ TestLogging.recordEvent(
+ TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
+
+ // Indicate success once the system has indicated that the transition has started
+ ActivityOptions opts = ActivityOptionsCompat.makeCustomAnimation(
+ getContext(), 0, 0, () -> callback.accept(true), MAIN_EXECUTOR.getHandler());
+ if (freezeTaskList) {
+ ActivityOptionsCompat.setFreezeRecentTasksList(opts);
+ }
+ Task.TaskKey key = mTask.key;
+ UI_HELPER_EXECUTOR.execute(() -> {
+ if (!ActivityManagerWrapper.getInstance().startActivityFromRecents(key, opts)) {
+ // If the call to start activity failed, then post the result immediately,
+ // otherwise, wait for the animation start callback from the activity options
+ // above
+ MAIN_EXECUTOR.post(() -> {
+ notifyTaskLaunchFailed(TAG);
+ callback.accept(false);
+ });
+ }
+ });
+ } else {
+ callback.accept(false);
+ }
+ }
+
+ /**
+ * See {@link TaskDataChanges}
+ * @param visible If this task view will be visible to the user in overview or hidden
+ */
+ public void onTaskListVisibilityChanged(boolean visible) {
+ onTaskListVisibilityChanged(visible, FLAG_UPDATE_ALL);
+ }
+
+ /**
+ * See {@link TaskDataChanges}
+ * @param visible If this task view will be visible to the user in overview or hidden
+ */
+ public void onTaskListVisibilityChanged(boolean visible, @TaskDataChanges int changes) {
+ if (mTask == null) {
+ return;
+ }
+ cancelPendingLoadTasks();
+ if (visible) {
+ // These calls are no-ops if the data is already loaded, try and load the high
+ // resolution thumbnail if the state permits
+ RecentsModel model = RecentsModel.INSTANCE.get(getContext());
+ TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
+ TaskIconCache iconCache = model.getIconCache();
+
+ if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
+ mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground(
+ mTask, thumbnail -> {
+ mSnapshotView.setThumbnail(mTask, thumbnail);
+ });
+ }
+ if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
+ mIconLoadRequest = iconCache.updateIconInBackground(mTask,
+ (task) -> {
+ setIcon(task.icon);
+ mDigitalWellBeingToast.initialize(mTask);
+ });
+ }
+ } else {
+ if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
+ mSnapshotView.setThumbnail(null, null);
+ // Reset the task thumbnail reference as well (it will be fetched from the cache or
+ // reloaded next time we need it)
+ mTask.thumbnail = null;
+ }
+ if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
+ setIcon(null);
+ }
+ }
+ }
+
+ private boolean needsUpdate(@TaskDataChanges int dataChange, @TaskDataChanges int flag) {
+ return (dataChange & flag) == flag;
+ }
+
+ private void cancelPendingLoadTasks() {
+ if (mThumbnailLoadRequest != null) {
+ mThumbnailLoadRequest.cancel();
+ mThumbnailLoadRequest = null;
+ }
+ if (mIconLoadRequest != null) {
+ mIconLoadRequest.cancel();
+ mIconLoadRequest = null;
+ }
+ }
+
+ private boolean showTaskMenu() {
+ if (getRecentsView().mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
+ // Don't show menu when selecting second split screen app
+ return true;
+ }
+
+ if (!getRecentsView().isClearAllHidden()) {
+ getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
+ return false;
+ } else {
+ mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
+ .log(LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS);
+ return TaskMenuView.showForTask(this);
+ }
+ }
+
+ private void setIcon(Drawable icon) {
+ if (icon != null) {
+ mIconView.setDrawable(icon);
+ mIconView.setOnClickListener(v -> showTaskMenu());
+ mIconView.setOnLongClickListener(v -> {
+ requestDisallowInterceptTouchEvent(true);
+ return showTaskMenu();
+ });
+ } else {
+ mIconView.setDrawable(null);
+ mIconView.setOnClickListener(null);
+ mIconView.setOnLongClickListener(null);
+ }
+ }
+
+ public void setOrientationState(RecentsOrientedState orientationState) {
+ PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
+ boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+ snapshotParams.topMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
+ int taskIconMargin = deviceProfile.overviewTaskMarginPx;
+ int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
+ LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
+ switch (orientationHandler.getRotation()) {
+ case ROTATION_90:
+ iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
+ iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2;
+ iconParams.leftMargin = 0;
+ iconParams.topMargin = snapshotParams.topMargin / 2;
+ break;
+ case ROTATION_180:
+ iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
+ iconParams.bottomMargin = -snapshotParams.topMargin;
+ iconParams.leftMargin = iconParams.rightMargin = 0;
+ iconParams.topMargin = taskIconMargin;
+ break;
+ case ROTATION_270:
+ iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
+ iconParams.leftMargin = -taskIconHeight - taskIconMargin / 2;
+ iconParams.rightMargin = 0;
+ iconParams.topMargin = snapshotParams.topMargin / 2;
+ break;
+ case Surface.ROTATION_0:
+ default:
+ iconParams.gravity = TOP | CENTER_HORIZONTAL;
+ iconParams.leftMargin = iconParams.rightMargin = 0;
+ iconParams.topMargin = taskIconMargin;
+ break;
+ }
+ mSnapshotView.setLayoutParams(snapshotParams);
+ iconParams.width = iconParams.height = taskIconHeight;
+ mIconView.setLayoutParams(iconParams);
+ mIconView.setRotation(orientationHandler.getDegreesRotated());
+ snapshotParams.topMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
+ mSnapshotView.setLayoutParams(snapshotParams);
+ getThumbnail().getTaskOverlay().updateOrientationState(orientationState);
+ }
+
+ private void setIconAndDimTransitionProgress(float progress, boolean invert) {
+ if (invert) {
+ progress = 1 - progress;
+ }
+ mFocusTransitionProgress = progress;
+ float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION;
+ float lowerClamp = invert ? 1f - iconScalePercentage : 0;
+ float upperClamp = invert ? 1 : iconScalePercentage;
+ float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp)
+ .getInterpolation(progress);
+ mIconView.setAlpha(scale);
+ if (mContextualChipWrapper != null && mContextualChipWrapper != null) {
+ mContextualChipWrapper.setAlpha(scale);
+ mContextualChipWrapper.setScaleX(Math.min(scale, comp(mModalness)));
+ mContextualChipWrapper.setScaleY(Math.min(scale, comp(mModalness)));
+ }
+ mDigitalWellBeingToast.updateBannerOffset(1f - scale,
+ mCurrentFullscreenParams.mCurrentDrawnInsets.top
+ + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom);
+ }
+
+ public void setIconScaleAnimStartProgress(float startProgress) {
+ mIconScaleAnimStartProgress = startProgress;
+ }
+
+ public void animateIconScaleAndDimIntoView() {
+ if (mIconAndDimAnimator != null) {
+ mIconAndDimAnimator.cancel();
+ }
+ mIconAndDimAnimator = ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1);
+ mIconAndDimAnimator.setCurrentFraction(mIconScaleAnimStartProgress);
+ mIconAndDimAnimator.setDuration(DIM_ANIM_DURATION).setInterpolator(LINEAR);
+ mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIconAndDimAnimator = null;
+ }
+ });
+ mIconAndDimAnimator.start();
+ }
+
+ protected void setIconScaleAndDim(float iconScale) {
+ setIconScaleAndDim(iconScale, false);
+ }
+
+ private void setIconScaleAndDim(float iconScale, boolean invert) {
+ if (mIconAndDimAnimator != null) {
+ mIconAndDimAnimator.cancel();
+ }
+ setIconAndDimTransitionProgress(iconScale, invert);
+ }
+
+ protected void resetViewTransforms() {
+ // fullscreenTranslation and accumulatedTranslation should not be reset, as
+ // resetViewTransforms is called during Quickswitch scrolling.
+ mDismissTranslationX = mTaskOffsetTranslationX = mTaskResistanceTranslationX =
+ mSplitSelectTranslationX = 0f;
+ mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY =
+ mSplitSelectTranslationY = 0f;
+ applyTranslationX();
+ applyTranslationY();
+ setTranslationZ(0);
+ setAlpha(mStableAlpha);
+ setIconScaleAndDim(1);
+ setColorTint(0, 0);
+ }
+
+ public void setStableAlpha(float parentAlpha) {
+ mStableAlpha = parentAlpha;
+ setAlpha(mStableAlpha);
+ }
+
+ @Override
+ public void onRecycle() {
+ mFullscreenTranslationX = mFullscreenTranslationY = mNonFullscreenTranslationX =
+ mNonFullscreenTranslationY = mGridTranslationX = mGridTranslationY =
+ mBoxTranslationY = 0f;
+ resetViewTransforms();
+ // Clear any references to the thumbnail (it will be re-read either from the cache or the
+ // system on next bind)
+ mSnapshotView.setThumbnail(mTask, null);
+ setOverlayEnabled(false);
+ onTaskListVisibilityChanged(false);
+ }
+
+ /**
+ * Sets the contextual chip.
+ *
+ * @param view Wrapper view containing contextual chip.
+ */
+ public void setContextualChip(View view) {
+ if (mContextualChipWrapper != null) {
+ removeView(mContextualChipWrapper);
+ }
+ if (view != null) {
+ mContextualChipWrapper = view;
+ LayoutParams layoutParams = new LayoutParams(((View) getParent()).getMeasuredWidth(),
+ LayoutParams.WRAP_CONTENT);
+ layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
+ int expectedChipHeight = getExpectedViewHeight(view);
+ float chipOffset = getResources().getDimension(R.dimen.chip_hint_vertical_offset);
+ layoutParams.bottomMargin = -expectedChipHeight - (int) chipOffset;
+ mContextualChipWrapper.setScaleX(0f);
+ mContextualChipWrapper.setScaleY(0f);
+ addView(view, getChildCount(), layoutParams);
+ if (mContextualChipWrapper != null) {
+ float scale = comp(mModalness);
+ mContextualChipWrapper.animate().scaleX(scale).scaleY(scale).setDuration(50);
+ mChipTouchDelegate = new TransformingTouchDelegate(mContextualChipWrapper);
+ }
+ }
+ }
+
+ public float getTaskCornerRadius() {
+ return TaskCornerRadius.get(mActivity);
+ }
+
+ /**
+ * Clears the contextual chip from TaskView.
+ *
+ * @return The contextual chip wrapper view to be recycled.
+ */
+ public View clearContextualChip() {
+ if (mContextualChipWrapper != null) {
+ removeView(mContextualChipWrapper);
+ }
+ View oldContextualChipWrapper = mContextualChipWrapper;
+ mContextualChipWrapper = null;
+ mChipTouchDelegate = null;
+ return oldContextualChipWrapper;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+ setPivotX(getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : right - left);
+ setPivotY(mSnapshotView.getTop());
+ } else {
+ setPivotX((right - left) * 0.5f);
+ setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
+ }
+ if (Utilities.ATLEAST_Q) {
+ SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight());
+ setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
+ }
+ }
+
+ /**
+ * How much to scale down pages near the edge of the screen.
+ */
+ public static float getEdgeScaleDownFactor(DeviceProfile deviceProfile) {
+ if (deviceProfile.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+ return EDGE_SCALE_DOWN_FACTOR_GRID;
+ } else {
+ return EDGE_SCALE_DOWN_FACTOR_CAROUSEL;
+ }
+ }
+
+ private void setFullscreenScale(float fullscreenScale) {
+ mFullscreenScale = fullscreenScale;
+ applyScale();
+ }
+
+ public float getFullscreenScale() {
+ return mFullscreenScale;
+ }
+
+ /**
+ * Moves TaskView between carousel and 2 row grid.
+ *
+ * @param gridProgress 0 = carousel; 1 = 2 row grid.
+ */
+ public void setGridProgress(float gridProgress) {
+ mGridProgress = gridProgress;
+ applyTranslationX();
+ applyTranslationY();
+ applyScale();
+ }
+
+ private void applyScale() {
+ float scale = 1;
+ float fullScreenProgress = FULLSCREEN_INTERPOLATOR.getInterpolation(mFullscreenProgress);
+ scale *= Utilities.mapRange(fullScreenProgress, 1f, mFullscreenScale);
+ setScaleX(scale);
+ setScaleY(scale);
+ }
+
+ private void setSplitSelectTranslationX(float x) {
+ mSplitSelectTranslationX = x;
+ applyTranslationX();
+ }
+
+ private void setSplitSelectTranslationY(float y) {
+ mSplitSelectTranslationY = y;
+ applyTranslationY();
+ }
+ private void setDismissTranslationX(float x) {
+ mDismissTranslationX = x;
+ applyTranslationX();
+ }
+
+ private void setDismissTranslationY(float y) {
+ mDismissTranslationY = y;
+ applyTranslationY();
+ }
+
+ private void setTaskOffsetTranslationX(float x) {
+ mTaskOffsetTranslationX = x;
+ applyTranslationX();
+ }
+
+ private void setTaskOffsetTranslationY(float y) {
+ mTaskOffsetTranslationY = y;
+ applyTranslationY();
+ }
+
+ private void setTaskResistanceTranslationX(float x) {
+ mTaskResistanceTranslationX = x;
+ applyTranslationX();
+ }
+
+ private void setTaskResistanceTranslationY(float y) {
+ mTaskResistanceTranslationY = y;
+ applyTranslationY();
+ }
+
+ private void setFullscreenTranslationX(float fullscreenTranslationX) {
+ mFullscreenTranslationX = fullscreenTranslationX;
+ applyTranslationX();
+ }
+
+ private void setFullscreenTranslationY(float fullscreenTranslationY) {
+ mFullscreenTranslationY = fullscreenTranslationY;
+ applyTranslationY();
+ }
+
+ private void setNonFullscreenTranslationX(float nonFullscreenTranslationX) {
+ mNonFullscreenTranslationX = nonFullscreenTranslationX;
+ applyTranslationX();
+ }
+
+ private void setNonFullscreenTranslationY(float nonFullscreenTranslationY) {
+ mNonFullscreenTranslationY = nonFullscreenTranslationY;
+ applyTranslationY();
+ }
+
+ public void setGridTranslationX(float gridTranslationX) {
+ mGridTranslationX = gridTranslationX;
+ applyTranslationX();
+ }
+
+ public float getGridTranslationX() {
+ return mGridTranslationX;
+ }
+
+ public void setGridTranslationY(float gridTranslationY) {
+ mGridTranslationY = gridTranslationY;
+ applyTranslationY();
+ }
+
+ public float getGridTranslationY() {
+ return mGridTranslationY;
+ }
+
+ public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
+ float scrollAdjustment = 0;
+ if (fullscreenEnabled) {
+ scrollAdjustment += getPrimaryFullscreenTranslationProperty().get(this);
+ } else {
+ scrollAdjustment += getPrimaryNonFullscreenTranslationProperty().get(this);
+ }
+ if (gridEnabled) {
+ scrollAdjustment += mGridTranslationX;
+ }
+ return scrollAdjustment;
+ }
+
+ public float getOffsetAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
+ return getScrollAdjustment(fullscreenEnabled, gridEnabled);
+ }
+
+ public float getSizeAdjustment(boolean fullscreenEnabled) {
+ float sizeAdjustment = 1;
+ if (fullscreenEnabled) {
+ sizeAdjustment *= mFullscreenScale;
+ }
+ return sizeAdjustment;
+ }
+
+ private void setBoxTranslationY(float boxTranslationY) {
+ mBoxTranslationY = boxTranslationY;
+ applyTranslationY();
+ }
+
+ private void applyTranslationX() {
+ setTranslationX(mDismissTranslationX + mTaskOffsetTranslationX + mTaskResistanceTranslationX
+ + mSplitSelectTranslationX + getPersistentTranslationX());
+ }
+
+ private void applyTranslationY() {
+ setTranslationY(mDismissTranslationY + mTaskOffsetTranslationY + mTaskResistanceTranslationY
+ + mSplitSelectTranslationY + getPersistentTranslationY());
+ }
+
+ /**
+ * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does not
+ * change according to a temporary state (e.g. task offset).
+ */
+ public float getPersistentTranslationX() {
+ return getFullscreenTrans(mFullscreenTranslationX)
+ + getNonFullscreenTrans(mNonFullscreenTranslationX)
+ + getGridTrans(mGridTranslationX);
+ }
+
+ /**
+ * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does not
+ * change according to a temporary state (e.g. task offset).
+ */
+ public float getPersistentTranslationY() {
+ return mBoxTranslationY
+ + getFullscreenTrans(mFullscreenTranslationY)
+ + getNonFullscreenTrans(mNonFullscreenTranslationY)
+ + getGridTrans(mGridTranslationY);
+ }
+
+ public FloatProperty<TaskView> getPrimarySplitTranslationProperty() {
+ return getPagedOrientationHandler().getPrimaryValue(
+ SPLIT_SELECT_TRANSLATION_X, SPLIT_SELECT_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getSecondarySplitTranslationProperty() {
+ return getPagedOrientationHandler().getSecondaryValue(
+ SPLIT_SELECT_TRANSLATION_X, SPLIT_SELECT_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getPrimaryDismissTranslationProperty() {
+ return getPagedOrientationHandler().getPrimaryValue(
+ DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getSecondaryDissmissTranslationProperty() {
+ return getPagedOrientationHandler().getSecondaryValue(
+ DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getPrimaryTaskOffsetTranslationProperty() {
+ return getPagedOrientationHandler().getPrimaryValue(
+ TASK_OFFSET_TRANSLATION_X, TASK_OFFSET_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getTaskResistanceTranslationProperty() {
+ return getPagedOrientationHandler().getSecondaryValue(
+ TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getPrimaryFullscreenTranslationProperty() {
+ return getPagedOrientationHandler().getPrimaryValue(
+ FULLSCREEN_TRANSLATION_X, FULLSCREEN_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getSecondaryFullscreenTranslationProperty() {
+ return getPagedOrientationHandler().getSecondaryValue(
+ FULLSCREEN_TRANSLATION_X, FULLSCREEN_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getPrimaryNonFullscreenTranslationProperty() {
+ return getPagedOrientationHandler().getPrimaryValue(
+ NON_FULLSCREEN_TRANSLATION_X, NON_FULLSCREEN_TRANSLATION_Y);
+ }
+
+ public FloatProperty<TaskView> getSecondaryNonFullscreenTranslationProperty() {
+ return getPagedOrientationHandler().getSecondaryValue(
+ NON_FULLSCREEN_TRANSLATION_X, NON_FULLSCREEN_TRANSLATION_Y);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
+ return false;
+ }
+
+ public boolean isEndQuickswitchCuj() {
+ return mEndQuickswitchCuj;
+ }
+
+ public void setEndQuickswitchCuj(boolean endQuickswitchCuj) {
+ mEndQuickswitchCuj = endQuickswitchCuj;
+ }
+
+ private static final class TaskOutlineProvider extends ViewOutlineProvider {
+
+ private int mMarginTop;
+ private FullscreenDrawParams mFullscreenParams;
+
+ TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams, int topMargin) {
+ mMarginTop = topMargin;
+ mFullscreenParams = fullscreenParams;
+ }
+
+ public void updateParams(FullscreenDrawParams params, int topMargin) {
+ mFullscreenParams = params;
+ mMarginTop = topMargin;
+ }
+
+ @Override
+ public void getOutline(View view, Outline outline) {
+ RectF insets = mFullscreenParams.mCurrentDrawnInsets;
+ float scale = mFullscreenParams.mScale;
+ outline.setRoundRect(0,
+ (int) (mMarginTop * scale),
+ (int) ((insets.left + view.getWidth() + insets.right) * scale),
+ (int) ((insets.top + view.getHeight() + insets.bottom) * scale),
+ mFullscreenParams.mCurrentDrawnCornerRadius);
+ }
+ }
+
+ private int getExpectedViewHeight(View view) {
+ int expectedHeight;
+ int h = view.getLayoutParams().height;
+ if (h > 0) {
+ expectedHeight = h;
+ } else {
+ int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
+ view.measure(m, m);
+ expectedHeight = view.getMeasuredHeight();
+ }
+ return expectedHeight;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+
+ info.addAction(
+ new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close,
+ getContext().getText(R.string.accessibility_close)));
+
+ final Context context = getContext();
+ for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+ mActivity.getDeviceProfile())) {
+ info.addAction(s.createAccessibilityAction(context));
+ }
+
+ if (mDigitalWellBeingToast.hasLimit()) {
+ info.addAction(
+ new AccessibilityNodeInfo.AccessibilityAction(
+ R.string.accessibility_app_usage_settings,
+ getContext().getText(R.string.accessibility_app_usage_settings)));
+ }
+
+ final RecentsView recentsView = getRecentsView();
+ final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
+ AccessibilityNodeInfo.CollectionItemInfo.obtain(
+ 0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1,
+ 1, false);
+ info.setCollectionItemInfo(itemInfo);
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ if (action == R.string.accessibility_close) {
+ getRecentsView().dismissTask(this, true /*animateTaskView*/,
+ true /*removeTask*/);
+ return true;
+ }
+
+ if (action == R.string.accessibility_app_usage_settings) {
+ mDigitalWellBeingToast.openAppUsageSettings(this);
+ return true;
+ }
+
+ for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+ mActivity.getDeviceProfile())) {
+ if (s.hasHandlerForAction(action)) {
+ s.onClick(this);
+ return true;
+ }
+ }
+
+ return super.performAccessibilityAction(action, arguments);
+ }
+
+ public RecentsView getRecentsView() {
+ return (RecentsView) getParent();
+ }
+
+ PagedOrientationHandler getPagedOrientationHandler() {
+ return getRecentsView().mOrientationState.getOrientationHandler();
+ }
+
+ private 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();
+ }
+
+ /**
+ * Hides the icon and shows insets when this TaskView is about to be shown fullscreen.
+ *
+ * @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets.
+ */
+ public void setFullscreenProgress(float progress) {
+ progress = Utilities.boundToRange(progress, 0, 1);
+ mFullscreenProgress = progress;
+ mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
+ getThumbnail().getTaskOverlay().setFullscreenProgress(progress);
+
+ applyTranslationX();
+ applyTranslationY();
+ applyScale();
+
+ TaskThumbnailView thumbnail = getThumbnail();
+ updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
+
+ if (!getRecentsView().isTaskIconScaledDown(this)) {
+ // Some of the items in here are dependent on the current fullscreen params, but don't
+ // update them if the icon is supposed to be scaled down.
+ setIconScaleAndDim(progress, true /* invert */);
+ }
+
+ thumbnail.setFullscreenParams(mCurrentFullscreenParams);
+ mOutlineProvider.updateParams(
+ mCurrentFullscreenParams,
+ mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx);
+ invalidateOutline();
+ }
+
+ void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
+ if (getRecentsView() == null) {
+ return;
+ }
+ mCurrentFullscreenParams.setProgress(
+ mFullscreenProgress,
+ getRecentsView().getScaleX(),
+ getWidth(), mActivity.getDeviceProfile(),
+ previewPositionHelper);
+ }
+
+ /**
+ * Updates TaskView scaling and translation required to support variable width if enabled, while
+ * ensuring TaskView fits into screen in fullscreen.
+ */
+ void updateTaskSize() {
+ ViewGroup.LayoutParams params = getLayoutParams();
+ float fullscreenScale;
+ float boxTranslationY;
+ int expectedWidth;
+ int expectedHeight;
+ if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+ final int thumbnailPadding =
+ mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
+ final Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize();
+ final int taskWidth = lastComputedTaskSize.width();
+ final int taskHeight = lastComputedTaskSize.height();
+
+ int boxWidth;
+ int boxHeight;
+ float thumbnailRatio;
+ boolean isFocusedTask = isFocusedTask();
+ if (isFocusedTask) {
+ // Task will be focused and should use focused task size. Use focusTaskRatio
+ // that is associated with the original orientation of the focused task.
+ boxWidth = taskWidth;
+ boxHeight = taskHeight;
+ thumbnailRatio = getRecentsView().getFocusedTaskRatio();
+ } else {
+ // Otherwise task is in grid, and should use lastComputedGridTaskSize.
+ Rect lastComputedGridTaskSize = getRecentsView().getLastComputedGridTaskSize();
+ boxWidth = lastComputedGridTaskSize.width();
+ boxHeight = lastComputedGridTaskSize.height();
+ thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio(
+ TaskView.CLIP_STATUS_AND_NAV_BARS) : 0f;
+ }
+ int boxLength = Math.max(boxWidth, boxHeight);
+
+ // Bound width/height to the box size.
+ if (thumbnailRatio == 0f) {
+ expectedWidth = boxWidth;
+ expectedHeight = boxHeight + thumbnailPadding;
+ } else if (thumbnailRatio > 1) {
+ expectedWidth = boxLength;
+ expectedHeight = (int) (boxLength / thumbnailRatio) + thumbnailPadding;
+ } else {
+ expectedWidth = (int) (boxLength * thumbnailRatio);
+ expectedHeight = boxLength + thumbnailPadding;
+ }
+
+ // Scale to to fit task Rect.
+ fullscreenScale = taskWidth / (float) boxWidth;
+
+ // In full screen, scale back TaskView to original size.
+ if (expectedWidth > boxWidth) {
+ fullscreenScale *= boxWidth / (float) expectedWidth;
+ } else if (expectedHeight - thumbnailPadding > boxHeight) {
+ fullscreenScale *= boxHeight / (float) (expectedHeight - thumbnailPadding);
+ }
+
+ // Align to top of task Rect.
+ boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f;
+ } else {
+ fullscreenScale = 1f;
+ boxTranslationY = 0f;
+ expectedWidth = ViewGroup.LayoutParams.MATCH_PARENT;
+ expectedHeight = ViewGroup.LayoutParams.MATCH_PARENT;
+ }
+
+ setFullscreenScale(fullscreenScale);
+ setBoxTranslationY(boxTranslationY);
+ if (params.width != expectedWidth || params.height != expectedHeight) {
+ params.width = expectedWidth;
+ params.height = expectedHeight;
+ setLayoutParams(params);
+ }
+ }
+
+ private float getFullscreenTrans(float endTranslation) {
+ float progress = FULLSCREEN_INTERPOLATOR.getInterpolation(mFullscreenProgress);
+ return Utilities.mapRange(progress, 0, endTranslation);
+ }
+
+ private float getNonFullscreenTrans(float endTranslation) {
+ return endTranslation - getFullscreenTrans(endTranslation);
+ }
+
+ private float getGridTrans(float endTranslation) {
+ float progress = ACCEL_DEACCEL.getInterpolation(mGridProgress);
+ return Utilities.mapRange(progress, 0, endTranslation);
+ }
+
+ public boolean isRunningTask() {
+ if (getRecentsView() == null) {
+ return false;
+ }
+ return this == getRecentsView().getRunningTaskView();
+ }
+
+ public boolean isFocusedTask() {
+ if (getRecentsView() == null) {
+ return false;
+ }
+ return this == getRecentsView().getFocusedTaskView();
+ }
+
+ public void setShowScreenshot(boolean showScreenshot) {
+ mShowScreenshot = showScreenshot;
+ }
+
+ public boolean showScreenshot() {
+ if (!isRunningTask()) {
+ return true;
+ }
+ return mShowScreenshot;
+ }
+
+ public void setOverlayEnabled(boolean overlayEnabled) {
+ mSnapshotView.setOverlayEnabled(overlayEnabled);
+ }
+
+ public void initiateSplitSelect(SplitPositionOption splitPositionOption) {
+ AbstractFloatingView.closeOpenViews(mActivity, false, TYPE_TASK_MENU);
+ getRecentsView().initiateSplitSelect(this, splitPositionOption);
+ }
+
+ /**
+ * Set a color tint on the snapshot and supporting views.
+ */
+ public void setColorTint(float amount, int tintColor) {
+ mSnapshotView.setDimAlpha(amount);
+ mIconView.setIconColorTint(tintColor, amount);
+ mDigitalWellBeingToast.setBannerColorTint(tintColor, amount);
+ }
+
+ /**
+ * We update and subsequently draw these in {@link #setFullscreenProgress(float)}.
+ */
+ public static class FullscreenDrawParams {
+
+ private final float mCornerRadius;
+ private final float mWindowCornerRadius;
+
+ public float mFullscreenProgress;
+ public RectF mCurrentDrawnInsets = new RectF();
+ public float mCurrentDrawnCornerRadius;
+ /** The current scale we apply to the thumbnail to adjust for new left/right insets. */
+ public float mScale = 1;
+
+ public FullscreenDrawParams(Context context) {
+ mCornerRadius = TaskCornerRadius.get(context);
+ mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
+
+ mCurrentDrawnCornerRadius = mCornerRadius;
+ }
+
+ /**
+ * Sets the progress in range [0, 1]
+ */
+ public void setProgress(float fullscreenProgress, float parentScale, int previewWidth,
+ DeviceProfile dp, PreviewPositionHelper pph) {
+ mFullscreenProgress = fullscreenProgress;
+ RectF insets = pph.getInsetsToDrawInFullscreen();
+
+ float currentInsetsLeft = insets.left * fullscreenProgress;
+ float currentInsetsRight = insets.right * fullscreenProgress;
+ mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress,
+ currentInsetsRight, insets.bottom * fullscreenProgress);
+ float fullscreenCornerRadius = dp.isMultiWindowMode ? 0 : mWindowCornerRadius;
+
+ mCurrentDrawnCornerRadius =
+ Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius)
+ / parentScale;
+
+ // We scaled the thumbnail to fit the content (excluding insets) within task view width.
+ // Now that we are drawing left/right insets again, we need to scale down to fit them.
+ if (previewWidth > 0) {
+ mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight);
+ }
+ }
+
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
index a412b39..dc73a9a 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
@@ -16,7 +16,15 @@
package com.android.quickstep;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.quickstep.views.RecentsView;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
@@ -31,4 +39,49 @@
outerRule(new NavigationModeSwitchRule(mLauncher)).
around(super.getRulesInsideActivityMonitor());
}
+
+ @Override
+ protected void onLauncherActivityClose(Launcher launcher) {
+ RecentsView recentsView = launcher.getOverviewPanel();
+ if (recentsView != null) {
+ recentsView.finishRecentsAnimation(true, null);
+ }
+ }
+
+ @Override
+ protected void checkLauncherState(Launcher launcher, ContainerType expectedContainerType,
+ boolean isResumed, boolean isStarted) {
+ if (!isInLiveTileMode(launcher, expectedContainerType)) {
+ super.checkLauncherState(launcher, expectedContainerType, isResumed, isStarted);
+ } else {
+ assertTrue("[Live Tile] hasBeenResumed() == isStarted(), hasBeenResumed(): "
+ + isResumed, isResumed != isStarted);
+ }
+ }
+
+ @Override
+ protected void checkLauncherStateInOverview(Launcher launcher,
+ ContainerType expectedContainerType, boolean isStarted, boolean isResumed) {
+ if (!isInLiveTileMode(launcher, expectedContainerType)) {
+ super.checkLauncherStateInOverview(launcher, expectedContainerType, isStarted,
+ isResumed);
+ } else {
+ assertTrue(
+ "[Live Tile] Launcher is not started or has been resumed in state: "
+ + expectedContainerType,
+ isStarted && !isResumed);
+ }
+ }
+
+ private boolean isInLiveTileMode(Launcher launcher,
+ LauncherInstrumentation.ContainerType expectedContainerType) {
+ if (!ENABLE_QUICKSTEP_LIVE_TILE.get()
+ || expectedContainerType != LauncherInstrumentation.ContainerType.OVERVIEW) {
+ return false;
+ }
+
+ RecentsView recentsView = launcher.getOverviewPanel();
+ return recentsView.getSizeStrategy().isInLiveTileMode()
+ && recentsView.getRunningTaskId() != -1;
+ }
}
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
deleted file mode 100644
index 5904fcd..0000000
--- a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.quickstep;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.app.prediction.AppTargetId;
-import android.content.ComponentName;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
-import android.os.Process;
-import android.view.View;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.appprediction.PredictionRowView;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.model.AppLaunchTracker;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class AppPredictionsUITests extends AbstractQuickStepTest {
-
- private LauncherActivityInfo mSampleApp1;
- private LauncherActivityInfo mSampleApp2;
- private LauncherActivityInfo mSampleApp3;
-
- private AppPredictor.Callback mCallback;
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- List<LauncherActivityInfo> activities = mTargetContext.getSystemService(LauncherApps.class)
- .getActivityList(null, Process.myUserHandle());
- mSampleApp1 = activities.get(0);
- mSampleApp2 = activities.get(1);
- mSampleApp3 = activities.get(2);
-
- // Disable app tracker
- AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
- PredictionUiStateManager.INSTANCE.initializeForTesting(null);
-
- mCallback = PredictionUiStateManager.INSTANCE.get(mTargetContext).appPredictorCallback(
- Client.HOME);
-
- mDevice.setOrientationNatural();
- }
-
- @After
- public void tearDown() throws Throwable {
- AppLaunchTracker.INSTANCE.initializeForTesting(null);
- PredictionUiStateManager.INSTANCE.initializeForTesting(null);
- mDevice.unfreezeRotation();
- }
-
- /**
- * Test that prediction UI is updated as soon as we get predictions from the system
- */
- @Test
- public void testPredictionExistsInAllApps() {
- mLauncher.pressHome().switchToAllApps();
-
- // Dispatch an update
- sendPredictionUpdate(mSampleApp1, mSampleApp2);
- // The first update should apply immediately.
- waitForLauncherCondition("Predictions were not updated in loading state",
- launcher -> getPredictedApp(launcher).size() == 2);
- }
-
- /**
- * Test that prediction update is deferred if it is already visible
- */
- @Test
- public void testPredictionsDeferredUntilHome() {
- mDevice.pressHome();
- sendPredictionUpdate(mSampleApp1, mSampleApp2);
- mLauncher.pressHome().switchToAllApps();
- waitForLauncherCondition("Predictions were not updated in loading state",
- launcher -> getPredictedApp(launcher).size() == 2);
-
- // Update predictions while all-apps is visible
- sendPredictionUpdate(mSampleApp1, mSampleApp2, mSampleApp3);
- assertEquals(2, getFromLauncher(this::getPredictedApp).size());
-
- // Go home and go back to all-apps
- mLauncher.pressHome().switchToAllApps();
- assertEquals(3, getFromLauncher(this::getPredictedApp).size());
- }
-
- @Test
- public void testPredictionsDisabled() {
- mDevice.pressHome();
- sendPredictionUpdate();
- mLauncher.pressHome().switchToAllApps();
-
- waitForLauncherCondition("Predictions were not updated in loading state",
- launcher -> launcher.getAppsView().getFloatingHeaderView()
- .findFixedRowByType(PredictionRowView.class).getVisibility() == View.GONE);
- assertFalse(PredictionUiStateManager.INSTANCE.get(mTargetContext)
- .getCurrentState().isEnabled);
- }
-
- public ArrayList<BubbleTextView> getPredictedApp(Launcher launcher) {
- PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
- .findFixedRowByType(PredictionRowView.class);
-
- ArrayList<BubbleTextView> predictedAppViews = new ArrayList<>();
- for (int i = 0; i < container.getChildCount(); i++) {
- View view = container.getChildAt(i);
- if (view instanceof BubbleTextView && view.getVisibility() == View.VISIBLE) {
- predictedAppViews.add((BubbleTextView) view);
- }
- }
- return predictedAppViews;
- }
-
- private void sendPredictionUpdate(LauncherActivityInfo... activities) {
- getOnUiThread(() -> {
- List<AppTarget> targets = new ArrayList<>(activities.length);
- for (LauncherActivityInfo info : activities) {
- ComponentName cn = info.getComponentName();
- AppTarget target = new AppTarget.Builder(
- new AppTargetId("app:" + cn), cn.getPackageName(), info.getUser())
- .setClassName(cn.getClassName())
- .build();
- targets.add(target);
- }
- mCallback.onTargetsAvailable(targets);
- return null;
- });
- }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index ccfa3fc..3e84a76 100644
--- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -11,7 +11,6 @@
import android.app.PendingIntent;
import android.app.usage.UsageStatsManager;
import android.content.Intent;
-import android.os.Build;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
@@ -34,9 +33,6 @@
@Test
public void testToast() throws Exception {
- // b/150303529
- if (Build.MODEL.contains("Cuttlefish")) return;
-
startAppFast(CALCULATOR_PACKAGE);
final UsageStatsManager usageStatsManager =
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index b9e0f62..a683d01 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -23,6 +23,7 @@
import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage;
import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_ACTIVITY_TIMEOUT;
+import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_BROADCAST_TIMEOUT_SECS;
import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT;
import static com.android.launcher3.ui.AbstractLauncherUiTest.resolveSystemApp;
import static com.android.launcher3.ui.AbstractLauncherUiTest.startAppFast;
@@ -41,6 +42,7 @@
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.RemoteException;
+import android.util.Log;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
@@ -54,11 +56,13 @@
import com.android.launcher3.tapl.OverviewTask;
import com.android.launcher3.tapl.TestHelpers;
import com.android.launcher3.testcomponent.TestCommandReceiver;
+import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.rule.FailureRewriterRule;
import com.android.launcher3.util.rule.FailureWatcher;
import com.android.quickstep.views.RecentsView;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
@@ -66,6 +70,8 @@
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -100,9 +106,8 @@
}
mOrderSensitiveRules = RuleChain
- .outerRule(new FailureRewriterRule())
- .around(new NavigationModeSwitchRule(mLauncher))
- .around(new FailureWatcher(mDevice));
+ .outerRule(new NavigationModeSwitchRule(mLauncher))
+ .around(new FailureWatcher(mDevice, mLauncher));
mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
getHomeIntentInPackage(context),
@@ -112,11 +117,16 @@
@Override
public void evaluate() throws Throwable {
TestCommandReceiver.callCommand(TestCommandReceiver.ENABLE_TEST_LAUNCHER);
+ OverviewUpdateHandler updateHandler =
+ MAIN_EXECUTOR.submit(OverviewUpdateHandler::new).get();
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
getLauncherCommand(mOtherLauncherActivity));
+ updateHandler.mChangeCounter
+ .await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
try {
base.evaluate();
} finally {
+ MAIN_EXECUTOR.submit(updateHandler::destroy).get();
TestCommandReceiver.callCommand(TestCommandReceiver.DISABLE_TEST_LAUNCHER);
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
getLauncherCommand(getLauncherInMyProcess()));
@@ -131,10 +141,21 @@
TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()).
getString("result"));
}
- // b/143488140
- mDevice.pressHome();
- mDevice.waitForIdle();
- startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+ }
+
+ @Before
+ public void setUp() {
+ mLauncher.onTestStart();
+ }
+
+ @After
+ public void tearDown() {
+ try {
+ // Limits UI tests affecting tests running after them.
+ AbstractQuickStepTest.checkDetectedLeaks(mLauncher);
+ } finally {
+ mLauncher.onTestFinish();
+ }
}
// b/143488140
@@ -166,9 +187,15 @@
protected <T> T getFromRecents(Function<RecentsActivity, T> f) {
if (!TestHelpers.isInLauncherProcess()) return null;
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.FALLBACK_ACTIVITY_NO_SET, "getFromRecents");
+ }
Object[] result = new Object[1];
Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> {
RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.FALLBACK_ACTIVITY_NO_SET, "activity=" + activity);
+ }
if (activity == null) {
return false;
}
@@ -194,8 +221,9 @@
() -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
BaseOverview overview = mLauncher.getBackground().switchToOverview();
- executeOnRecents(recents ->
- assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3));
+ executeOnRecents(recents -> {
+ assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3);
+ });
// Test flinging forward and backward.
overview.flingForward();
@@ -213,7 +241,7 @@
OverviewTask task = overview.getCurrentTask();
assertNotNull("overview.getCurrentTask() returned null (1)", task);
assertNotNull("OverviewTask.open returned null", task.open());
- assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject(
+ assertTrue("Test activity didn't open from Overview", TestHelpers.wait(Until.hasObject(
By.pkg(getAppPackageName()).text("TestActivity2")),
DEFAULT_UI_TIMEOUT));
@@ -230,7 +258,7 @@
// Test dismissing all tasks.
pressHomeAndGoToOverview().dismissAllTasks();
- assertTrue("Fallback Launcher not visible", mDevice.wait(Until.hasObject(By.pkg(
+ assertTrue("Fallback Launcher not visible", TestHelpers.wait(Until.hasObject(By.pkg(
mOtherLauncherActivity.packageName)), WAIT_TIME_MS));
}
@@ -241,4 +269,30 @@
private int getTaskCount(RecentsActivity recents) {
return recents.<RecentsView>getOverviewPanel().getTaskViewCount();
}
+
+ private class OverviewUpdateHandler {
+
+ final RecentsAnimationDeviceState mRads;
+ final OverviewComponentObserver mObserver;
+ final CountDownLatch mChangeCounter;
+
+ OverviewUpdateHandler() {
+ Context ctx = getInstrumentation().getTargetContext();
+ mRads = new RecentsAnimationDeviceState(ctx);
+ mObserver = new OverviewComponentObserver(ctx, mRads);
+ mChangeCounter = new CountDownLatch(1);
+ if (mObserver.getHomeIntent().getComponent()
+ .getPackageName().equals(mOtherLauncherActivity.packageName)) {
+ // Home already same
+ mChangeCounter.countDown();
+ } else {
+ mObserver.setOverviewChangeListener(b -> mChangeCounter.countDown());
+ }
+ }
+
+ void destroy() {
+ mObserver.onDestroy();
+ mRads.destroy();
+ }
+ }
}
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 40265c4..67840d1 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -168,7 +168,7 @@
Log.d(TAG, "setActiveOverlay: " + overlayPackage + "...");
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
- "cmd overlay enable-exclusive " + overlayPackage);
+ "cmd overlay enable-exclusive --category " + overlayPackage);
if (currentSysUiNavigationMode() != expectedMode) {
final CountDownLatch latch = new CountDownLatch(1);
@@ -188,7 +188,7 @@
SYS_UI_NAVIGATION_MODE.removeModeChangeListener(listener));
Wait.atMost(() -> "Navigation mode didn't change to " + expectedMode,
- () -> currentSysUiNavigationMode() == expectedMode, 60000 /* b/148422894 */,
+ () -> currentSysUiNavigationMode() == expectedMode, WAIT_TIME_MS,
launcher);
// b/139137636
// assertTrue(launcher, "Navigation mode didn't change to " + expectedMode,
@@ -200,9 +200,9 @@
() -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher);
Wait.atMost(() -> "Switching nav mode: "
- + launcher.getNavigationModeMismatchError(),
- () -> launcher.getNavigationModeMismatchError() == null,
- 60000 /* b/148422894 */, launcher);
+ + launcher.getNavigationModeMismatchError(false),
+ () -> launcher.getNavigationModeMismatchError(false) == null,
+ WAIT_TIME_MS, launcher);
AbstractLauncherUiTest.checkDetectedLeaks(launcher);
return true;
}
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 9732cdc..6e19436 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -100,6 +100,7 @@
// The test action.
mLauncher.getBackground().switchToOverview();
}
+ closeLauncherActivity();
mLauncher.pressHome();
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index bf093fd..a5038a1 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertTrue;
import android.content.Intent;
+import android.util.Log;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
@@ -31,13 +32,12 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.tapl.AllApps;
-import com.android.launcher3.tapl.AllAppsFromOverview;
import com.android.launcher3.tapl.Background;
import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
import com.android.launcher3.tapl.Overview;
+import com.android.launcher3.tapl.OverviewActions;
import com.android.launcher3.tapl.OverviewTask;
-import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import com.android.quickstep.views.RecentsView;
@@ -68,11 +68,14 @@
});
}
- private void startTestApps() throws Exception {
+ public static void startTestApps() throws Exception {
startAppFast(getAppPackageName());
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
startTestActivity(2);
+ }
+ private void startTestAppsWithCheck() throws Exception {
+ startTestApps();
executeOnLauncher(launcher -> assertTrue(
"Launcher activity is the top activity; expecting another activity to be the top "
+ "one",
@@ -90,22 +93,9 @@
}
@Test
- public void testAllAppsFromOverview() throws Exception {
- if (!mLauncher.hasAllAppsInOverview()) {
- return;
- }
-
- // Test opening all apps from Overview.
- assertNotNull("switchToAllApps() returned null",
- mLauncher.getWorkspace().switchToOverview().switchToAllApps());
-
- TaplTestsLauncher3.runAllAppsTest(this, mLauncher.getAllAppsFromOverview());
- }
-
- @Test
@PortraitLandscape
public void testOverview() throws Exception {
- startTestApps();
+ startTestAppsWithCheck();
// mLauncher.pressHome() also tests an important case of pressing home while in background.
Overview overview = mLauncher.pressHome().switchToOverview();
assertTrue("Launcher internal state didn't switch to Overview",
@@ -155,28 +145,6 @@
launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview",
numTasks - 1, getTaskCount(launcher)));
- if (mLauncher.hasAllAppsInOverview() && (!TestHelpers.isInLauncherProcess()
- || getFromLauncher(launcher -> !launcher.getDeviceProfile().isLandscape))) {
- // Test switching to all apps and back.
- final AllAppsFromOverview allApps = overview.switchToAllApps();
- assertNotNull("overview.switchToAllApps() returned null (1)", allApps);
- assertTrue("Launcher internal state is not All Apps (1)",
- isInState(() -> LauncherState.ALL_APPS));
-
- overview = allApps.switchBackToOverview();
- assertNotNull("allApps.switchBackToOverview() returned null", overview);
- assertTrue("Launcher internal state didn't switch to Overview",
- isInState(() -> LauncherState.OVERVIEW));
-
- // Test UIDevice.pressBack()
- overview.switchToAllApps();
- assertNotNull("overview.switchToAllApps() returned null (2)", allApps);
- assertTrue("Launcher internal state is not All Apps (2)",
- isInState(() -> LauncherState.ALL_APPS));
- mDevice.pressBack();
- mLauncher.getOverview();
- }
-
// Test UIDevice.pressHome, once we are in AllApps.
mDevice.pressHome();
waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
@@ -189,6 +157,26 @@
0, getTaskCount(launcher)));
}
+ /**
+ * Smoke test for action buttons: Presses all the buttons and makes sure no crashes occur.
+ */
+ @Test
+ @NavigationModeSwitch
+ @PortraitLandscape
+ public void testOverviewActions() throws Exception {
+ // Experimenting for b/165029151:
+ final Overview overview = mLauncher.pressHome().switchToOverview();
+ if (overview.hasTasks()) overview.dismissAllTasks();
+ mLauncher.pressHome();
+ //
+
+ startTestAppsWithCheck();
+ OverviewActions actionsView =
+ mLauncher.pressHome().switchToOverview().getOverviewActions();
+ actionsView.clickAndDismissScreenshot();
+ actionsView.clickAndDismissShare();
+ }
+
private int getCurrentOverviewPage(Launcher launcher) {
return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
}
@@ -198,20 +186,6 @@
}
@Test
- public void testAppIconLaunchFromAllAppsFromOverview() throws Exception {
- if (!mLauncher.hasAllAppsInOverview()) {
- return;
- }
-
- final AllApps allApps =
- mLauncher.getWorkspace().switchToOverview().switchToAllApps();
- assertTrue("Launcher internal state is not All Apps",
- isInState(() -> LauncherState.ALL_APPS));
-
- TaplTestsLauncher3.runIconLaunchFromAllAppsTest(this, allApps);
- }
-
- @Test
@NavigationModeSwitch
@PortraitLandscape
public void testSwitchToOverview() throws Exception {
@@ -284,6 +258,10 @@
assertTrue("The second app we should have quick switched to is not running",
isTestActivityRunning(2));
}
+ background = getAndAssertBackground();
+ background.quickSwitchToPreviousAppSwipeLeft();
+ assertTrue("The 2nd app we should have quick switched to is not running",
+ isTestActivityRunning(3));
getAndAssertBackground();
}
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index 115294a..f33abb0 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -49,7 +49,6 @@
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.tapl.Background;
@@ -58,6 +57,7 @@
import com.android.launcher3.testcomponent.TestCommandReceiver;
import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import org.junit.Before;
@@ -71,7 +71,7 @@
/**
* Test to verify view inflation does not happen during swipe up.
- * To verify view inflation, we setup a dummy ViewConfiguration and check if any call to that class
+ * To verify view inflation, we setup a stub ViewConfiguration and check if any call to that class
* does from a View.init method or not.
*
* Alternative approaches considered:
@@ -79,10 +79,7 @@
* directly (ex: new LinearLayout)
* Using ExtendedMockito: Mocking static methods from platform classes (loaded in zygote) makes
* the main thread extremely slow and untestable
- *
- * Suppressed until b/141579810 is resolved
*/
-@Suppress
@LargeTest
@RunWith(AndroidJUnit4.class)
public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest {
@@ -117,6 +114,7 @@
@Test
@NavigationModeSwitch(mode = ZERO_BUTTON)
+ @Suppress // until b/190618549 is fixed
public void testSwipeUpFromApp() throws Exception {
try {
// Go to overview once so that all views are initialized and cached
@@ -137,18 +135,20 @@
@Test
@NavigationModeSwitch(mode = ZERO_BUTTON)
+ @Suppress // until b/190729479 is fixed
public void testSwipeUpFromApp_widget_update() {
- String dummyText = "Some random dummy text";
+ String stubText = "Some random stub text";
executeSwipeUpTestWithWidget(
widgetId -> { },
widgetId -> AppWidgetManager.getInstance(getContext())
- .updateAppWidget(widgetId, createMainWidgetViews(dummyText)),
- dummyText);
+ .updateAppWidget(widgetId, createMainWidgetViews(stubText)),
+ stubText);
}
@Test
@NavigationModeSwitch(mode = ZERO_BUTTON)
+ @Suppress // until b/190729479 is fixed
public void testSwipeUp_with_list_widgets() {
SimpleViewsFactory viewFactory = new SimpleViewsFactory();
viewFactory.viewCount = 1;
@@ -188,6 +188,11 @@
LauncherSettings.Settings.call(mResolver,
LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
+ // Make sure the widget is big enough to show a list of items
+ info.minSpanX = 2;
+ info.minSpanY = 2;
+ info.spanX = 2;
+ info.spanY = 2;
LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
addItemToScreen(item);
diff --git a/res/animator-v23/discovery_bounce.xml b/res/animator-v23/discovery_bounce.xml
deleted file mode 100644
index f554853..0000000
--- a/res/animator-v23/discovery_bounce.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="2166"
- android:repeatCount="5">
- <propertyValuesHolder
- android:propertyName="progress"
- android:valueType="floatType">
- <keyframe
- android:fraction="0"
- android:value="1f" />
- <keyframe
- android:fraction="0.246"
- android:value="1f" />
- <keyframe
- android:fraction=".423"
- android:interpolator="@interpolator/disco_bounce"
- android:value="0.9738f" />
- <keyframe
- android:fraction="0.754"
- android:interpolator="@interpolator/disco_bounce"
- android:value="1f" />
- <keyframe
- android:fraction="1"
- android:value="1f" />
- </propertyValuesHolder>
-</objectAnimator>
diff --git a/res/animator/discovery_bounce.xml b/res/animator/discovery_bounce.xml
index f02ebdb..f554853 100644
--- a/res/animator/discovery_bounce.xml
+++ b/res/animator/discovery_bounce.xml
@@ -16,20 +16,28 @@
** limitations under the License.
*/
-->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:ordering="sequentially">
- <objectAnimator
- android:duration="166"
- android:interpolator="@interpolator/disco_bounce"
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="2166"
+ android:repeatCount="5">
+ <propertyValuesHolder
android:propertyName="progress"
- android:startOffset="750"
- android:valueFrom="1f"
- android:valueTo="0.9438f"
- android:valueType="floatType" />
- <objectAnimator
- android:duration="500"
- android:interpolator="@interpolator/disco_bounce"
- android:propertyName="progress"
- android:valueTo="1f"
- android:valueType="floatType" />
-</set>
+ android:valueType="floatType">
+ <keyframe
+ android:fraction="0"
+ android:value="1f" />
+ <keyframe
+ android:fraction="0.246"
+ android:value="1f" />
+ <keyframe
+ android:fraction=".423"
+ android:interpolator="@interpolator/disco_bounce"
+ android:value="0.9738f" />
+ <keyframe
+ android:fraction="0.754"
+ android:interpolator="@interpolator/disco_bounce"
+ android:value="1f" />
+ <keyframe
+ android:fraction="1"
+ android:value="1f" />
+ </propertyValuesHolder>
+</objectAnimator>
diff --git a/res/color-night-v31/all_apps_tab_background_selected.xml b/res/color-night-v31/all_apps_tab_background_selected.xml
new file mode 100644
index 0000000..b7c9ff6
--- /dev/null
+++ b/res/color-night-v31/all_apps_tab_background_selected.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_accent2_100"/>
+</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/all_apps_tab_text.xml b/res/color-night-v31/all_apps_tab_text.xml
new file mode 100644
index 0000000..83237b4
--- /dev/null
+++ b/res/color-night-v31/all_apps_tab_text.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_50" android:state_selected="true"/>
+ <item android:color="@android:color/system_neutral2_700"/>
+</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/folder_background_dark.xml b/res/color-night-v31/folder_background_dark.xml
new file mode 100644
index 0000000..a5bd636
--- /dev/null
+++ b/res/color-night-v31/folder_background_dark.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="@android:color/system_neutral2_50"
+ android:lStar="30" />
+</selector>
diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml
new file mode 100644
index 0000000..ba74128
--- /dev/null
+++ b/res/color-night-v31/popup_shade_first.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="?attr/popupColorPrimary"
+ android:lStar="20" />
+</selector>
diff --git a/res/color-night-v31/popup_shade_second.xml b/res/color-night-v31/popup_shade_second.xml
new file mode 100644
index 0000000..efc6205
--- /dev/null
+++ b/res/color-night-v31/popup_shade_second.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="?attr/popupColorPrimary"
+ android:lStar="15" />
+</selector>
diff --git a/res/color-night-v31/popup_shade_third.xml b/res/color-night-v31/popup_shade_third.xml
new file mode 100644
index 0000000..591c7ed
--- /dev/null
+++ b/res/color-night-v31/popup_shade_third.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="?attr/popupColorPrimary"
+ android:lStar="10" />
+</selector>
diff --git a/res/color-night-v31/surface.xml b/res/color-night-v31/surface.xml
new file mode 100644
index 0000000..fbc9e43
--- /dev/null
+++ b/res/color-night-v31/surface.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_800" />
+</selector>
diff --git a/res/color-night-v31/widgets_picker_scrim.xml b/res/color-night-v31/widgets_picker_scrim.xml
new file mode 100644
index 0000000..be7010b
--- /dev/null
+++ b/res/color-night-v31/widgets_picker_scrim.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_900" android:alpha="0.8" />
+</selector>
diff --git a/res/color-night/all_apps_tab_background_selected.xml b/res/color-night/all_apps_tab_background_selected.xml
new file mode 100644
index 0000000..b22bc8b
--- /dev/null
+++ b/res/color-night/all_apps_tab_background_selected.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#BFEBE3"/>
+</selector>
\ No newline at end of file
diff --git a/res/color-night/all_apps_tab_text.xml b/res/color-night/all_apps_tab_text.xml
new file mode 100644
index 0000000..183af01
--- /dev/null
+++ b/res/color-night/all_apps_tab_text.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#F0F0F0" android:state_selected="true"/>
+ <item android:color="#464646"/>
+</selector>
\ No newline at end of file
diff --git a/res/color-v31/all_apps_tab_background_selected.xml b/res/color-v31/all_apps_tab_background_selected.xml
new file mode 100644
index 0000000..dac8fa2
--- /dev/null
+++ b/res/color-v31/all_apps_tab_background_selected.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_accent1_100"/>
+</selector>
\ No newline at end of file
diff --git a/res/color-v31/all_apps_tab_text.xml b/res/color-v31/all_apps_tab_text.xml
new file mode 100644
index 0000000..c3520a7
--- /dev/null
+++ b/res/color-v31/all_apps_tab_text.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_900" android:state_selected="true"/>
+ <item android:color="@android:color/system_neutral2_700"/>
+</selector>
\ No newline at end of file
diff --git a/res/color-v31/all_apps_tabs_background.xml b/res/color-v31/all_apps_tabs_background.xml
new file mode 100644
index 0000000..30757b0
--- /dev/null
+++ b/res/color-v31/all_apps_tabs_background.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_500" android:lStar="97" />
+</selector>
\ No newline at end of file
diff --git a/res/color-v31/folder_background_light.xml b/res/color-v31/folder_background_light.xml
new file mode 100644
index 0000000..e3c7e7d
--- /dev/null
+++ b/res/color-v31/folder_background_light.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="@android:color/system_neutral1_50"
+ android:lStar="98" />
+</selector>
diff --git a/res/color-v31/overview_scrim.xml b/res/color-v31/overview_scrim.xml
new file mode 100644
index 0000000..8079995
--- /dev/null
+++ b/res/color-v31/overview_scrim.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral2_500" android:lStar="87" />
+</selector>
diff --git a/res/color-v31/overview_scrim_dark.xml b/res/color-v31/overview_scrim_dark.xml
new file mode 100644
index 0000000..b8ed774
--- /dev/null
+++ b/res/color-v31/overview_scrim_dark.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_800" />
+</selector>
diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml
new file mode 100644
index 0000000..28d9155
--- /dev/null
+++ b/res/color-v31/popup_shade_first.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="?attr/popupColorPrimary"
+ android:lStar="98" />
+</selector>
diff --git a/res/color-v31/popup_shade_second.xml b/res/color-v31/popup_shade_second.xml
new file mode 100644
index 0000000..dec562c
--- /dev/null
+++ b/res/color-v31/popup_shade_second.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="?attr/popupColorPrimary"
+ android:lStar="95" />
+</selector>
diff --git a/res/color-v31/popup_shade_third.xml b/res/color-v31/popup_shade_third.xml
new file mode 100644
index 0000000..582232c
--- /dev/null
+++ b/res/color-v31/popup_shade_third.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:color="?attr/popupColorPrimary"
+ android:lStar="90" />
+</selector>
diff --git a/res/color-v31/surface.xml b/res/color-v31/surface.xml
new file mode 100644
index 0000000..30f3032
--- /dev/null
+++ b/res/color-v31/surface.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_500" android:lStar="98"/>
+</selector>
diff --git a/res/color-v31/widgets_picker_scrim.xml b/res/color-v31/widgets_picker_scrim.xml
new file mode 100644
index 0000000..648824a
--- /dev/null
+++ b/res/color-v31/widgets_picker_scrim.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_200" android:alpha="0.8" />
+</selector>
diff --git a/res/color-v24/all_apps_bg_hand_fill.xml b/res/color/all_apps_bg_hand_fill.xml
similarity index 100%
rename from res/color-v24/all_apps_bg_hand_fill.xml
rename to res/color/all_apps_bg_hand_fill.xml
diff --git a/res/color-v24/all_apps_bg_hand_fill_dark.xml b/res/color/all_apps_bg_hand_fill_dark.xml
similarity index 100%
rename from res/color-v24/all_apps_bg_hand_fill_dark.xml
rename to res/color/all_apps_bg_hand_fill_dark.xml
diff --git a/res/color/all_apps_tab_background_selected.xml b/res/color/all_apps_tab_background_selected.xml
new file mode 100644
index 0000000..5cb9bd8
--- /dev/null
+++ b/res/color/all_apps_tab_background_selected.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#8DF5E3"/>
+</selector>
\ No newline at end of file
diff --git a/res/color/all_apps_tab_text.xml b/res/color/all_apps_tab_text.xml
index f0c6310..dace380 100644
--- a/res/color/all_apps_tab_text.xml
+++ b/res/color/all_apps_tab_text.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="?android:attr/colorAccent" android:state_selected="true"/>
- <item android:color="?android:attr/textColorTertiary"/>
+ <item android:color="#1B1B1B" android:state_selected="true"/>
+ <item android:color="#464646"/>
</selector>
\ No newline at end of file
diff --git a/res/color/all_apps_tabs_background.xml b/res/color/all_apps_tabs_background.xml
new file mode 100644
index 0000000..a4b7d1f
--- /dev/null
+++ b/res/color/all_apps_tabs_background.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#F6F6F6"/>
+</selector>
\ No newline at end of file
diff --git a/res/color/arrow_tip_view_bg.xml b/res/color/arrow_tip_view_bg.xml
new file mode 100644
index 0000000..91eed50
--- /dev/null
+++ b/res/color/arrow_tip_view_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?android:attr/colorAccent" />
+</selector>
diff --git a/res/color/arrow_tip_view_content.xml b/res/color/arrow_tip_view_content.xml
new file mode 100644
index 0000000..7d7f98b
--- /dev/null
+++ b/res/color/arrow_tip_view_content.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?android:attr/textColorPrimaryInverse" />
+</selector>
diff --git a/res/color/button_bg.xml b/res/color/button_bg.xml
new file mode 100644
index 0000000..91eed50
--- /dev/null
+++ b/res/color/button_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?android:attr/colorAccent" />
+</selector>
diff --git a/res/color/button_text.xml b/res/color/button_text.xml
new file mode 100644
index 0000000..7d7f98b
--- /dev/null
+++ b/res/color/button_text.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?android:attr/textColorPrimaryInverse" />
+</selector>
diff --git a/res/color/cell_layout_bg_color_active.xml b/res/color/cell_layout_bg_color_active.xml
new file mode 100644
index 0000000..d1a3d7c
--- /dev/null
+++ b/res/color/cell_layout_bg_color_active.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:alpha="0.3"
+ android:color="?android:attr/colorAccent"/>
+</selector>
diff --git a/res/color/cell_layout_bg_color_inactive.xml b/res/color/cell_layout_bg_color_inactive.xml
new file mode 100644
index 0000000..0632100
--- /dev/null
+++ b/res/color/cell_layout_bg_color_inactive.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:alpha="0.25"
+ android:color="?android:attr/colorAccent"/>
+</selector>
\ No newline at end of file
diff --git a/res/color/drop_target_text.xml b/res/color/drop_target_text.xml
new file mode 100644
index 0000000..18d78e7
--- /dev/null
+++ b/res/color/drop_target_text.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?workspaceAccentColor" android:state_selected="false" />
+ <item android:color="?dropTargetHoverTextColor" android:state_selected="true" />
+</selector>
\ No newline at end of file
diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml
index 6ac36bf..aa48b78 100644
--- a/res/color/overview_button.xml
+++ b/res/color/overview_button.xml
@@ -2,10 +2,10 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:alpha="1"
- android:color="?attr/workspaceTextColor"
+ android:color="?android:attr/textColorPrimary"
android:state_enabled="true" />
<item
android:alpha="?android:disabledAlpha"
- android:color="?attr/workspaceTextColor"
+ android:color="?android:attr/textColorPrimary"
android:state_enabled="false" />
</selector>
\ No newline at end of file
diff --git a/res/color/overview_scrim.xml b/res/color/overview_scrim.xml
new file mode 100644
index 0000000..48cf576
--- /dev/null
+++ b/res/color/overview_scrim.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#30000000" />
+</selector>
diff --git a/res/color/overview_scrim_dark.xml b/res/color/overview_scrim_dark.xml
new file mode 100644
index 0000000..48cf576
--- /dev/null
+++ b/res/color/overview_scrim_dark.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#30000000" />
+</selector>
diff --git a/res/color/popup_shade_first.xml b/res/color/popup_shade_first.xml
new file mode 100644
index 0000000..151190b
--- /dev/null
+++ b/res/color/popup_shade_first.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:color="?attr/popupShadeFirst" />
+</selector>
diff --git a/res/color/popup_shade_second.xml b/res/color/popup_shade_second.xml
new file mode 100644
index 0000000..8660850
--- /dev/null
+++ b/res/color/popup_shade_second.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:color="?attr/popupShadeSecond" />
+</selector>
diff --git a/res/color/popup_shade_third.xml b/res/color/popup_shade_third.xml
new file mode 100644
index 0000000..9544728
--- /dev/null
+++ b/res/color/popup_shade_third.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:color="?attr/popupShadeThird" />
+</selector>
\ No newline at end of file
diff --git a/res/color/surface.xml b/res/color/surface.xml
new file mode 100644
index 0000000..6150110
--- /dev/null
+++ b/res/color/surface.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?android:attr/colorBackgroundFloating" />
+</selector>
diff --git a/res/color/widgets_picker_scrim.xml b/res/color/widgets_picker_scrim.xml
new file mode 100644
index 0000000..1cf97f6
--- /dev/null
+++ b/res/color/widgets_picker_scrim.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#000000" android:alpha="0.32" />
+</selector>
diff --git a/res/drawable-hdpi/widget_resize_frame.9.png b/res/drawable-hdpi/widget_resize_frame.9.png
deleted file mode 100644
index a710932..0000000
--- a/res/drawable-hdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/widget_resize_shadow.9.png b/res/drawable-hdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 7cb5214..0000000
--- a/res/drawable-hdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/widget_resize_frame.9.png b/res/drawable-mdpi/widget_resize_frame.9.png
deleted file mode 100644
index 252482f..0000000
--- a/res/drawable-mdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/widget_resize_shadow.9.png b/res/drawable-mdpi/widget_resize_shadow.9.png
deleted file mode 100644
index a2010e2..0000000
--- a/res/drawable-mdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-v24/drag_handle_indicator_shadow.xml b/res/drawable-v24/drag_handle_indicator_shadow.xml
deleted file mode 100644
index 774bc38..0000000
--- a/res/drawable-v24/drag_handle_indicator_shadow.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<com.android.launcher3.graphics.ShadowDrawable
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:src="@drawable/drag_handle_indicator_no_shadow"
- android:elevation="@dimen/vertical_drag_handle_elevation" />
diff --git a/res/drawable-v26/ic_deepshortcut_placeholder.xml b/res/drawable-v26/ic_deepshortcut_placeholder.xml
deleted file mode 100644
index 3fa8506..0000000
--- a/res/drawable-v26/ic_deepshortcut_placeholder.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="?attr/popupColorSecondary"/>
- <foreground android:drawable="?attr/popupColorSecondary"/>
-</adaptive-icon>
diff --git a/res/drawable-v26/ic_launcher_home.xml b/res/drawable-v26/ic_launcher_home.xml
deleted file mode 100644
index 7038775..0000000
--- a/res/drawable-v26/ic_launcher_home.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="@color/icon_background" />
- <foreground>
- <bitmap android:src="@mipmap/ic_launcher_home_foreground"/>
- </foreground>
-</adaptive-icon>
diff --git a/res/drawable-v28/bg_celllayout.xml b/res/drawable-v28/bg_celllayout.xml
new file mode 100644
index 0000000..c68bdec
--- /dev/null
+++ b/res/drawable-v28/bg_celllayout.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_active="true" >
+ <shape android:shape="rectangle" >
+ <stroke
+ android:width="@dimen/spring_loaded_panel_border"
+ android:color="?android:attr/colorAccent" />
+ <solid android:color="@color/cell_layout_bg_color_active"/>
+ <corners android:radius="?android:attr/dialogCornerRadius" />
+
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle" >
+ <solid android:color="@color/cell_layout_bg_color_inactive"/>
+ <corners android:radius="?android:attr/dialogCornerRadius" />
+ </shape>
+ </item>
+</selector>
diff --git a/res/drawable-v28/widgets_bottom_sheet_background.xml b/res/drawable-v28/widgets_bottom_sheet_background.xml
new file mode 100644
index 0000000..7fb8681
--- /dev/null
+++ b/res/drawable-v28/widgets_bottom_sheet_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/surface" />
+ <corners
+ android:topLeftRadius="?android:attr/dialogCornerRadius"
+ android:topRightRadius="?android:attr/dialogCornerRadius"
+ android:bottomLeftRadius="0dp"
+ android:bottomRightRadius="0dp"
+ />
+</shape>
\ No newline at end of file
diff --git a/res/drawable-xhdpi/widget_resize_frame.9.png b/res/drawable-xhdpi/widget_resize_frame.9.png
deleted file mode 100644
index 563c75d..0000000
--- a/res/drawable-xhdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/widget_resize_shadow.9.png b/res/drawable-xhdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 2b1ac05..0000000
--- a/res/drawable-xhdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/widget_resize_frame.9.png b/res/drawable-xxhdpi/widget_resize_frame.9.png
deleted file mode 100644
index ea527f4..0000000
--- a/res/drawable-xxhdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/widget_resize_shadow.9.png b/res/drawable-xxhdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 5412168..0000000
--- a/res/drawable-xxhdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/widget_resize_frame.9.png b/res/drawable-xxxhdpi/widget_resize_frame.9.png
deleted file mode 100644
index 4644e9a..0000000
--- a/res/drawable-xxxhdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/widget_resize_shadow.9.png b/res/drawable-xxxhdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 63cea84..0000000
--- a/res/drawable-xxxhdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml
new file mode 100644
index 0000000..c3a8269
--- /dev/null
+++ b/res/drawable/add_item_dialog_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid android:color="@color/surface" />
+ <corners
+ android:topLeftRadius="?android:attr/dialogCornerRadius"
+ android:topRightRadius="?android:attr/dialogCornerRadius" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/all_apps_divider.xml b/res/drawable/all_apps_divider.xml
index 4bd274d..53d3522 100644
--- a/res/drawable/all_apps_divider.xml
+++ b/res/drawable/all_apps_divider.xml
@@ -15,6 +15,6 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
- <solid android:color="?android:attr/colorControlHighlight" />
- <size android:height="1dp" />
+ <solid android:color="@android:color/transparent" />
+ <size android:height="2dp" />
</shape>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml
new file mode 100644
index 0000000..aea2e7a
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:enterFadeDuration="100">
+ <item
+ android:id="@+id/unselected"
+ android:state_selected="false">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
+ <solid android:color="@color/all_apps_tabs_background" />
+ </shape>
+ </item>
+
+ <item
+ android:id="@+id/selected"
+ android:state_selected="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
+ <solid android:color="@color/all_apps_tab_background_selected" />
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/arrow_toast_rounded_background.xml b/res/drawable/arrow_toast_rounded_background.xml
index 52cc6fc..1206ddd 100644
--- a/res/drawable/arrow_toast_rounded_background.xml
+++ b/res/drawable/arrow_toast_rounded_background.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
- <solid android:color="?android:attr/colorAccent" />
- <corners android:radius="8dp" />
+ <solid android:color="@color/arrow_tip_view_bg" />
+ <corners android:radius="@dimen/dialogCornerRadius" />
</shape>
diff --git a/res/drawable/bg_celllayout.xml b/res/drawable/bg_celllayout.xml
index b81b37f..4e7e82f 100644
--- a/res/drawable/bg_celllayout.xml
+++ b/res/drawable/bg_celllayout.xml
@@ -23,13 +23,16 @@
<shape android:shape="rectangle" >
<stroke
android:width="@dimen/spring_loaded_panel_border"
- android:color="@color/spring_loaded_highlighted_panel_border_color" />
- <solid android:color="@color/spring_loaded_panel_color" />
+ android:color="?android:attr/colorAccent" />
+ <solid android:color="@color/cell_layout_bg_color_active"/>
+ <corners android:radius="@dimen/bg_round_rect_radius" />
+
</shape>
</item>
<item>
<shape android:shape="rectangle" >
- <solid android:color="@color/spring_loaded_panel_color" />
+ <solid android:color="@color/cell_layout_bg_color_inactive"/>
+ <corners android:radius="@dimen/bg_round_rect_radius" />
</shape>
</item>
</selector>
diff --git a/res/drawable/bg_rounded_corner_bottom_sheet.xml b/res/drawable/bg_rounded_corner_bottom_sheet.xml
new file mode 100644
index 0000000..aa49bce
--- /dev/null
+++ b/res/drawable/bg_rounded_corner_bottom_sheet.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid android:color="@color/surface" />
+ <corners
+ android:topLeftRadius="@dimen/dialogCornerRadius"
+ android:topRightRadius="@dimen/dialogCornerRadius" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_picker_handle.xml b/res/drawable/bg_widgets_picker_handle.xml
new file mode 100644
index 0000000..68681a6
--- /dev/null
+++ b/res/drawable/bg_widgets_picker_handle.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground" />
+ <padding android:top="16dp"/>
+ </shape>
+ </item>
+ <item android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/textColorSecondary" />
+ <size android:width="48dp" android:height="2dp" />
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_searchbox.xml b/res/drawable/bg_widgets_searchbox.xml
new file mode 100644
index 0000000..dc6d868
--- /dev/null
+++ b/res/drawable/bg_widgets_searchbox.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/surface" />
+ <corners android:radius="24dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/button_bottom_rounded_colored_ripple.xml b/res/drawable/button_bottom_rounded_colored_ripple.xml
new file mode 100644
index 0000000..95f5234
--- /dev/null
+++ b/res/drawable/button_bottom_rounded_colored_ripple.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:topLeftRadius="4dp"
+ android:topRightRadius="4dp"
+ android:bottomLeftRadius="12dp"
+ android:bottomRightRadius="12dp" />
+ <solid android:color="@color/button_bg"/>
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/button_rounded_colored_ripple.xml b/res/drawable/button_rounded_colored_ripple.xml
new file mode 100644
index 0000000..f6d689f
--- /dev/null
+++ b/res/drawable/button_rounded_colored_ripple.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="12dp"/>
+ <solid android:color="@color/button_bg"/>
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml
new file mode 100644
index 0000000..f15a4a0
--- /dev/null
+++ b/res/drawable/button_top_rounded_bordered_ripple.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:topLeftRadius="12dp"
+ android:topRightRadius="12dp"
+ android:bottomLeftRadius="4dp"
+ android:bottomRightRadius="4dp" />
+ <solid android:color="@color/surface"/>
+ <stroke
+ android:width="2dp"
+ android:color="@color/button_bg"/>
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/deep_shortcuts_text_placeholder.xml b/res/drawable/deep_shortcuts_text_placeholder.xml
index 99da50f..5820d6b 100644
--- a/res/drawable/deep_shortcuts_text_placeholder.xml
+++ b/res/drawable/deep_shortcuts_text_placeholder.xml
@@ -15,7 +15,7 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
- <solid android:color="?attr/popupColorSecondary" />
+ <solid android:color="?attr/popupColorPrimary" />
<corners android:radius="8dp" />
<size android:height="16dp" />
</shape>
diff --git a/res/drawable/drag_handle_indicator_no_shadow.xml b/res/drawable/drag_handle_indicator_no_shadow.xml
deleted file mode 100644
index 341e60c..0000000
--- a/res/drawable/drag_handle_indicator_no_shadow.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/vertical_drag_handle_width"
- android:height="@dimen/vertical_drag_handle_height"
- android:viewportWidth="18.0"
- android:viewportHeight="6.0"
- android:tint="?attr/workspaceTextColor" >
-
- <path
- android:pathData="M17,6c-0.15,0-0.3-0.03-0.45-0.11L9,2.12L1.45,5.89c-0.5,0.25-1.09,
- 0.05-1.34-0.45S0.06,4.35,0.55,4.11l8-4c0.28-0.14,0.61-0.14,0.89,0l8,4c0.49,0.25,0.69,
- 0.85,0.45,1.34C17.72,5.8,17.37,6,17,6z"
- android:fillColor="@android:color/white" />
-</vector>
diff --git a/res/drawable/drop_target_background.xml b/res/drawable/drop_target_background.xml
new file mode 100644
index 0000000..7e07bf5
--- /dev/null
+++ b/res/drawable/drop_target_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true"
+ android:drawable="@drawable/drop_target_frame_hover" />
+ <item android:state_selected="false"
+ android:drawable="@drawable/drop_target_frame" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable/drop_target_frame.xml b/res/drawable/drop_target_frame.xml
new file mode 100644
index 0000000..666a96e
--- /dev/null
+++ b/res/drawable/drop_target_frame.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/transparent" />
+ <corners android:radius="28dp" />
+ <stroke android:width="2dp" android:color="?attr/workspaceAccentColor" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/drop_target_frame_hover.xml b/res/drawable/drop_target_frame_hover.xml
new file mode 100644
index 0000000..ddf3a4d
--- /dev/null
+++ b/res/drawable/drop_target_frame_hover.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/workspaceAccentColor" />
+ <corners android:radius="28dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/full_rounded_colored_ripple.xml b/res/drawable/full_rounded_colored_ripple.xml
new file mode 100644
index 0000000..d89537c
--- /dev/null
+++ b/res/drawable/full_rounded_colored_ripple.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/button_bg"/>
+ <corners android:radius="28dp"/>
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/full_rounded_transparent_ripple.xml b/res/drawable/full_rounded_transparent_ripple.xml
new file mode 100644
index 0000000..a1e7943
--- /dev/null
+++ b/res/drawable/full_rounded_transparent_ripple.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="oval">
+ <size android:height="24dp" android:width="24dp"/>
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/gesture_tutorial_motion_overview_light_mode.xml b/res/drawable/gesture_tutorial_motion_overview_light_mode.xml
new file mode 100644
index 0000000..75887c9
--- /dev/null
+++ b/res/drawable/gesture_tutorial_motion_overview_light_mode.xml
@@ -0,0 +1,1587 @@
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="_R_G_L_4_G_N_3_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1050"
+ android:pathData="M 206,446C 206,446 206,395 206,395"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="217">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_4_G_N_3_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1050"
+ android:propertyName="scaleX"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.6"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1050"
+ android:propertyName="scaleY"
+ android:startOffset="217"
+ android:valueFrom="1"
+ android:valueTo="0.6"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_4_G_N_3_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="3400"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_27_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_26_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_25_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_24_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_23_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_22_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_21_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_20_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_19_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_18_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_17_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_16_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_15_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_14_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_13_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_12_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_11_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_10_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_9_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_8_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_7_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_6_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_5_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_4_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_3_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_2_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_L_0_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:pathData="M 206,395C 206,403.5 206,437.5 206,446"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="2083">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleX"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleX"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleY"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_3_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0"
+ android:valueTo="0.6"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_D_0_P_0_G_0_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="scaleX"
+ android:startOffset="2567"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="scaleY"
+ android:startOffset="2567"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:pathData="M 206,395C 206,403.5 206,437.5 206,446"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="2083">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleX"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleX"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleY"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_2_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="2567"
+ android:valueFrom="0"
+ android:valueTo="0.6"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="250"
+ android:pathData="M -556.176,-7.307C -556.176,-7.307 -421.176,-7.307 -421.176,-7.307"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="1350">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.272,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="417"
+ android:pathData="M -421.176,-7.307C -421.176,-7.307 -429.51,-7.307 -429.51,-7.307"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="1600">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:pathData="M 206,395C 206,403.5 206,437.5 206,446"
+ android:propertyName="translateXY"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY"
+ android:startOffset="2083">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleX"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="350"
+ android:propertyName="scaleY"
+ android:startOffset="2083"
+ android:valueFrom="0.6"
+ android:valueTo="0.72718"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.34,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleX"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="scaleY"
+ android:startOffset="2433"
+ android:valueFrom="0.72718"
+ android:valueTo="0.72"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.51,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_1_G_N_2_T_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="0"
+ android:propertyName="scaleY"
+ android:startOffset="1350"
+ android:valueFrom="0"
+ android:valueTo="0.6"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1833"
+ android:propertyName="fillAlpha"
+ android:startOffset="217"
+ android:valueFrom="0.75"
+ android:valueTo="0.75"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="2050"
+ android:valueFrom="0.75"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="217"
+ android:propertyName="pathData"
+ android:startOffset="0"
+ android:valueFrom="M0 406 C21.54,406 39,423.46 39,445 C39,466.54 21.54,484 0,484 C-21.54,484 -39,466.54 -39,445 C-39,423.46 -21.54,406 0,406c "
+ android:valueTo="M0 395 C27.61,395 50,417.39 50,445 C50,472.61 27.61,495 0,495 C-27.61,495 -50,472.61 -50,445 C-50,417.39 -27.61,395 0,395c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1050"
+ android:propertyName="pathData"
+ android:startOffset="217"
+ android:valueFrom="M0 395 C27.61,395 50,417.39 50,445 C50,472.61 27.61,495 0,495 C-27.61,495 -50,472.61 -50,445 C-50,417.39 -27.61,395 0,395c "
+ android:valueTo="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="783"
+ android:propertyName="pathData"
+ android:startOffset="1267"
+ android:valueFrom="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueTo="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="pathData"
+ android:startOffset="2050"
+ android:valueFrom="M0 166 C27.61,166 50,188.39 50,216 C50,243.61 27.61,266 0,266 C-27.61,266 -50,243.61 -50,216 C-50,188.39 -27.61,166 0,166c "
+ android:valueTo="M0 180 C19.88,180 36,196.12 36,216 C36,235.88 19.88,252 0,252 C-19.88,252 -36,235.88 -36,216 C-36,196.12 -19.88,180 0,180c "
+ android:valueType="pathType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="2750"
+ android:propertyName="translateX"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="412dp"
+ android:height="892dp"
+ android:viewportHeight="892"
+ android:viewportWidth="412">
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_5_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="@color/fake_wallpaper_color_light_mode"
+ android:fillType="nonZero"
+ android:pathData=" M206 -446 C206,-446 206,446 206,446 C206,446 -206,446 -206,446 C-206,446 -206,-446 -206,-446 C-206,-446 206,-446 206,-446c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_N_3_T_0"
+ android:scaleX="1"
+ android:scaleY="1"
+ android:translateX="206"
+ android:translateY="446">
+ <group
+ android:name="_R_G_L_4_G"
+ android:translateX="-206"
+ android:translateY="-446">
+ <group android:name="_R_G_L_4_G_L_0_G">
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_27_G"
+ android:translateX="206"
+ android:translateY="422.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_27_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -395.5 C206,-395.5 206,395.5 206,395.5 C206,395.5 -206,395.5 -206,395.5 C-206,395.5 -206,-395.5 -206,-395.5 C-206,-395.5 206,-395.5 206,-395.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_26_G"
+ android:translateX="206"
+ android:translateY="496.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_26_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -377.5 C206,-377.5 206,377.5 206,377.5 C206,387.43 197.93,395.5 188,395.5 C188,395.5 -188,395.5 -188,395.5 C-197.93,395.5 -206,387.43 -206,377.5 C-206,377.5 -206,-377.5 -206,-377.5 C-206,-387.43 -197.93,-395.5 -188,-395.5 C-188,-395.5 188,-395.5 188,-395.5 C197.93,-395.5 206,-387.43 206,-377.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_25_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_25_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -23.5 C206,-23.5 206,50.5 206,50.5 C206,50.5 -206,50.5 -206,50.5 C-206,50.5 -206,-23.5 -206,-23.5 C-206,-23.5 206,-23.5 206,-23.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_24_G"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_24_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_23_G"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_23_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_22_G"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_22_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_21_G"
+ android:translateX="148.5"
+ android:translateY="148">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_21_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M46.5 -5 C46.5,-5 46.5,5 46.5,5 C46.5,7.21 44.71,9 42.5,9 C42.5,9 -42.5,9 -42.5,9 C-44.71,9 -46.5,7.21 -46.5,5 C-46.5,5 -46.5,-5 -46.5,-5 C-46.5,-7.21 -44.71,-9 -42.5,-9 C-42.5,-9 42.5,-9 42.5,-9 C44.71,-9 46.5,-7.21 46.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_20_G"
+ android:translateX="186"
+ android:translateY="169">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_20_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M84 -4 C84,-4 84,4 84,4 C84,6.21 82.21,8 80,8 C80,8 -80,8 -80,8 C-82.21,8 -84,6.21 -84,4 C-84,4 -84,-4 -84,-4 C-84,-6.21 -82.21,-8 -80,-8 C-80,-8 80,-8 80,-8 C82.21,-8 84,-6.21 84,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_19_G"
+ android:translateX="54"
+ android:translateY="245">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_19_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_18_G"
+ android:translateX="162"
+ android:translateY="236">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_18_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60 -5 C60,-5 60,5 60,5 C60,7.21 58.21,9 56,9 C56,9 -56,9 -56,9 C-58.21,9 -60,7.21 -60,5 C-60,5 -60,-5 -60,-5 C-60,-7.21 -58.21,-9 -56,-9 C-56,-9 56,-9 56,-9 C58.21,-9 60,-7.21 60,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_17_G"
+ android:translateX="171.5"
+ android:translateY="257">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_17_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M69.5 -4 C69.5,-4 69.5,4 69.5,4 C69.5,6.21 67.71,8 65.5,8 C65.5,8 -65.5,8 -65.5,8 C-67.71,8 -69.5,6.21 -69.5,4 C-69.5,4 -69.5,-4 -69.5,-4 C-69.5,-6.21 -67.71,-8 -65.5,-8 C-65.5,-8 65.5,-8 65.5,-8 C67.71,-8 69.5,-6.21 69.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_16_G"
+ android:translateX="54"
+ android:translateY="333">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_16_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_15_G"
+ android:translateX="158"
+ android:translateY="324">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_15_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M56 -5 C56,-5 56,5 56,5 C56,7.21 54.21,9 52,9 C52,9 -52,9 -52,9 C-54.21,9 -56,7.21 -56,5 C-56,5 -56,-5 -56,-5 C-56,-7.21 -54.21,-9 -52,-9 C-52,-9 52,-9 52,-9 C54.21,-9 56,-7.21 56,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_14_G"
+ android:translateX="217.5"
+ android:translateY="345">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_14_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M115.5 -4 C115.5,-4 115.5,4 115.5,4 C115.5,6.21 113.71,8 111.5,8 C111.5,8 -111.5,8 -111.5,8 C-113.71,8 -115.5,6.21 -115.5,4 C-115.5,4 -115.5,-4 -115.5,-4 C-115.5,-6.21 -113.71,-8 -111.5,-8 C-111.5,-8 111.5,-8 111.5,-8 C113.71,-8 115.5,-6.21 115.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_13_G"
+ android:translateX="54"
+ android:translateY="421">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_13_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_12_G"
+ android:translateX="170"
+ android:translateY="412">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_12_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M68 -5 C68,-5 68,5 68,5 C68,7.21 66.21,9 64,9 C64,9 -64,9 -64,9 C-66.21,9 -68,7.21 -68,5 C-68,5 -68,-5 -68,-5 C-68,-7.21 -66.21,-9 -64,-9 C-64,-9 64,-9 64,-9 C66.21,-9 68,-7.21 68,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_11_G"
+ android:translateX="198.5"
+ android:translateY="433">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_10_G"
+ android:translateX="54"
+ android:translateY="509">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_9_G"
+ android:translateX="135"
+ android:translateY="500">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M33 -5 C33,-5 33,5 33,5 C33,7.21 31.21,9 29,9 C29,9 -29,9 -29,9 C-31.21,9 -33,7.21 -33,5 C-33,5 -33,-5 -33,-5 C-33,-7.21 -31.21,-9 -29,-9 C-29,-9 29,-9 29,-9 C31.21,-9 33,-7.21 33,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_8_G"
+ android:translateX="185.5"
+ android:translateY="521">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M83.5 -4 C83.5,-4 83.5,4 83.5,4 C83.5,6.21 81.71,8 79.5,8 C79.5,8 -79.5,8 -79.5,8 C-81.71,8 -83.5,6.21 -83.5,4 C-83.5,4 -83.5,-4 -83.5,-4 C-83.5,-6.21 -81.71,-8 -79.5,-8 C-79.5,-8 79.5,-8 79.5,-8 C81.71,-8 83.5,-6.21 83.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_7_G"
+ android:translateX="54"
+ android:translateY="597">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_6_G"
+ android:translateX="168.5"
+ android:translateY="588">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M66.5 -5 C66.5,-5 66.5,5 66.5,5 C66.5,7.21 64.71,9 62.5,9 C62.5,9 -62.5,9 -62.5,9 C-64.71,9 -66.5,7.21 -66.5,5 C-66.5,5 -66.5,-5 -66.5,-5 C-66.5,-7.21 -64.71,-9 -62.5,-9 C-62.5,-9 62.5,-9 62.5,-9 C64.71,-9 66.5,-7.21 66.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_5_G"
+ android:translateX="198.5"
+ android:translateY="609">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_4_G"
+ android:translateX="54"
+ android:translateY="685">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_3_G"
+ android:translateX="162.5"
+ android:translateY="676">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60.5 -5 C60.5,-5 60.5,5 60.5,5 C60.5,7.21 58.71,9 56.5,9 C56.5,9 -56.5,9 -56.5,9 C-58.71,9 -60.5,7.21 -60.5,5 C-60.5,5 -60.5,-5 -60.5,-5 C-60.5,-7.21 -58.71,-9 -56.5,-9 C-56.5,-9 56.5,-9 56.5,-9 C58.71,-9 60.5,-7.21 60.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_2_G"
+ android:translateX="174"
+ android:translateY="697">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M72 -4 C72,-4 72,4 72,4 C72,6.21 70.21,8 68,8 C68,8 -68,8 -68,8 C-70.21,8 -72,6.21 -72,4 C-72,4 -72,-4 -72,-4 C-72,-6.21 -70.21,-8 -68,-8 C-68,-8 68,-8 68,-8 C70.21,-8 72,-6.21 72,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_1_G"
+ android:translateX="313.5"
+ android:translateY="798">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M74.5 0 C74.5,0 74.5,0 74.5,0 C74.5,15.45 61.95,28 46.5,28 C46.5,28 -46.5,28 -46.5,28 C-61.95,28 -74.5,15.45 -74.5,0 C-74.5,0 -74.5,0 -74.5,0 C-74.5,-15.45 -61.95,-28 -46.5,-28 C-46.5,-28 46.5,-28 46.5,-28 C61.95,-28 74.5,-15.45 74.5,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_4_G_L_0_G_L_0_G"
+ android:translateX="205.5"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_4_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#f8f9fa"
+ android:fillType="nonZero"
+ android:pathData=" M171.5 -14 C171.5,-14 171.5,14 171.5,14 C171.5,16.21 169.71,18 167.5,18 C167.5,18 -167.5,18 -167.5,18 C-169.71,18 -171.5,16.21 -171.5,14 C-171.5,14 -171.5,-14 -171.5,-14 C-171.5,-16.21 -169.71,-18 -167.5,-18 C-167.5,-18 167.5,-18 167.5,-18 C169.71,-18 171.5,-16.21 171.5,-14c " />
+ </group>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_3_G_N_2_T_0"
+ android:scaleX="0.6"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="395">
+ <group
+ android:name="_R_G_L_3_G"
+ android:translateX="-206"
+ android:translateY="-446">
+ <group
+ android:name="_R_G_L_3_G_L_0_G"
+ android:scaleY="0">
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_27_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="422.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_27_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -395.5 C206,-395.5 206,395.5 206,395.5 C206,395.5 -206,395.5 -206,395.5 C-206,395.5 -206,-395.5 -206,-395.5 C-206,-395.5 206,-395.5 206,-395.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_26_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="496.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_26_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#dadce0"
+ android:fillType="nonZero"
+ android:pathData=" M206 -377.5 C206,-377.5 206,377.5 206,377.5 C206,387.43 197.93,395.5 188,395.5 C188,395.5 -188,395.5 -188,395.5 C-197.93,395.5 -206,387.43 -206,377.5 C-206,377.5 -206,-377.5 -206,-377.5 C-206,-387.43 -197.93,-395.5 -188,-395.5 C-188,-395.5 188,-395.5 188,-395.5 C197.93,-395.5 206,-387.43 206,-377.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_25_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_25_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -23.5 C206,-23.5 206,50.5 206,50.5 C206,50.5 -206,50.5 -206,50.5 C-206,50.5 -206,-23.5 -206,-23.5 C-206,-23.5 206,-23.5 206,-23.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_24_G"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="50.5">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_24_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#e8eaed"
+ android:fillType="nonZero"
+ android:pathData=" M206 -32.5 C206,-32.5 206,32.5 206,32.5 C206,42.43 197.93,50.5 188,50.5 C188,50.5 -188,50.5 -188,50.5 C-197.93,50.5 -206,42.43 -206,32.5 C-206,32.5 -206,-32.5 -206,-32.5 C-206,-42.43 -197.93,-50.5 -188,-50.5 C-188,-50.5 188,-50.5 188,-50.5 C197.93,-50.5 206,-42.43 206,-32.5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_23_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_23_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_22_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="157">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_22_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_21_G"
+ android:scaleY="0"
+ android:translateX="148.5"
+ android:translateY="148">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_21_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M46.5 -5 C46.5,-5 46.5,5 46.5,5 C46.5,7.21 44.71,9 42.5,9 C42.5,9 -42.5,9 -42.5,9 C-44.71,9 -46.5,7.21 -46.5,5 C-46.5,5 -46.5,-5 -46.5,-5 C-46.5,-7.21 -44.71,-9 -42.5,-9 C-42.5,-9 42.5,-9 42.5,-9 C44.71,-9 46.5,-7.21 46.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_20_G"
+ android:scaleY="0"
+ android:translateX="186"
+ android:translateY="169">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_20_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M84 -4 C84,-4 84,4 84,4 C84,6.21 82.21,8 80,8 C80,8 -80,8 -80,8 C-82.21,8 -84,6.21 -84,4 C-84,4 -84,-4 -84,-4 C-84,-6.21 -82.21,-8 -80,-8 C-80,-8 80,-8 80,-8 C82.21,-8 84,-6.21 84,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_19_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="245">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_19_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_18_G"
+ android:scaleY="0"
+ android:translateX="162"
+ android:translateY="236">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_18_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60 -5 C60,-5 60,5 60,5 C60,7.21 58.21,9 56,9 C56,9 -56,9 -56,9 C-58.21,9 -60,7.21 -60,5 C-60,5 -60,-5 -60,-5 C-60,-7.21 -58.21,-9 -56,-9 C-56,-9 56,-9 56,-9 C58.21,-9 60,-7.21 60,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_17_G"
+ android:scaleY="0"
+ android:translateX="171.5"
+ android:translateY="257">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_17_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M69.5 -4 C69.5,-4 69.5,4 69.5,4 C69.5,6.21 67.71,8 65.5,8 C65.5,8 -65.5,8 -65.5,8 C-67.71,8 -69.5,6.21 -69.5,4 C-69.5,4 -69.5,-4 -69.5,-4 C-69.5,-6.21 -67.71,-8 -65.5,-8 C-65.5,-8 65.5,-8 65.5,-8 C67.71,-8 69.5,-6.21 69.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_16_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="333">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_16_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_15_G"
+ android:scaleY="0"
+ android:translateX="158"
+ android:translateY="324">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_15_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M56 -5 C56,-5 56,5 56,5 C56,7.21 54.21,9 52,9 C52,9 -52,9 -52,9 C-54.21,9 -56,7.21 -56,5 C-56,5 -56,-5 -56,-5 C-56,-7.21 -54.21,-9 -52,-9 C-52,-9 52,-9 52,-9 C54.21,-9 56,-7.21 56,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_14_G"
+ android:scaleY="0"
+ android:translateX="217.5"
+ android:translateY="345">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_14_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M115.5 -4 C115.5,-4 115.5,4 115.5,4 C115.5,6.21 113.71,8 111.5,8 C111.5,8 -111.5,8 -111.5,8 C-113.71,8 -115.5,6.21 -115.5,4 C-115.5,4 -115.5,-4 -115.5,-4 C-115.5,-6.21 -113.71,-8 -111.5,-8 C-111.5,-8 111.5,-8 111.5,-8 C113.71,-8 115.5,-6.21 115.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_13_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="421">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_13_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_12_G"
+ android:scaleY="0"
+ android:translateX="170"
+ android:translateY="412">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_12_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M68 -5 C68,-5 68,5 68,5 C68,7.21 66.21,9 64,9 C64,9 -64,9 -64,9 C-66.21,9 -68,7.21 -68,5 C-68,5 -68,-5 -68,-5 C-68,-7.21 -66.21,-9 -64,-9 C-64,-9 64,-9 64,-9 C66.21,-9 68,-7.21 68,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_11_G"
+ android:scaleY="0"
+ android:translateX="198.5"
+ android:translateY="433">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_11_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_10_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="509">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_10_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_9_G"
+ android:scaleY="0"
+ android:translateX="135"
+ android:translateY="500">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_9_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M33 -5 C33,-5 33,5 33,5 C33,7.21 31.21,9 29,9 C29,9 -29,9 -29,9 C-31.21,9 -33,7.21 -33,5 C-33,5 -33,-5 -33,-5 C-33,-7.21 -31.21,-9 -29,-9 C-29,-9 29,-9 29,-9 C31.21,-9 33,-7.21 33,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_8_G"
+ android:scaleY="0"
+ android:translateX="185.5"
+ android:translateY="521">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_8_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M83.5 -4 C83.5,-4 83.5,4 83.5,4 C83.5,6.21 81.71,8 79.5,8 C79.5,8 -79.5,8 -79.5,8 C-81.71,8 -83.5,6.21 -83.5,4 C-83.5,4 -83.5,-4 -83.5,-4 C-83.5,-6.21 -81.71,-8 -79.5,-8 C-79.5,-8 79.5,-8 79.5,-8 C81.71,-8 83.5,-6.21 83.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_7_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="597">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_7_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_6_G"
+ android:scaleY="0"
+ android:translateX="168.5"
+ android:translateY="588">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_6_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M66.5 -5 C66.5,-5 66.5,5 66.5,5 C66.5,7.21 64.71,9 62.5,9 C62.5,9 -62.5,9 -62.5,9 C-64.71,9 -66.5,7.21 -66.5,5 C-66.5,5 -66.5,-5 -66.5,-5 C-66.5,-7.21 -64.71,-9 -62.5,-9 C-62.5,-9 62.5,-9 62.5,-9 C64.71,-9 66.5,-7.21 66.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_5_G"
+ android:scaleY="0"
+ android:translateX="198.5"
+ android:translateY="609">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_5_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M96.5 -4 C96.5,-4 96.5,4 96.5,4 C96.5,6.21 94.71,8 92.5,8 C92.5,8 -92.5,8 -92.5,8 C-94.71,8 -96.5,6.21 -96.5,4 C-96.5,4 -96.5,-4 -96.5,-4 C-96.5,-6.21 -94.71,-8 -92.5,-8 C-92.5,-8 92.5,-8 92.5,-8 C94.71,-8 96.5,-6.21 96.5,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_4_G"
+ android:scaleY="0"
+ android:translateX="54"
+ android:translateY="685">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_4_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M28 0 C28,15.46 15.46,28 0,28 C-15.46,28 -28,15.46 -28,0 C-28,-15.46 -15.46,-28 0,-28 C15.46,-28 28,-15.46 28,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_3_G"
+ android:scaleY="0"
+ android:translateX="162.5"
+ android:translateY="676">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_3_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M60.5 -5 C60.5,-5 60.5,5 60.5,5 C60.5,7.21 58.71,9 56.5,9 C56.5,9 -56.5,9 -56.5,9 C-58.71,9 -60.5,7.21 -60.5,5 C-60.5,5 -60.5,-5 -60.5,-5 C-60.5,-7.21 -58.71,-9 -56.5,-9 C-56.5,-9 56.5,-9 56.5,-9 C58.71,-9 60.5,-7.21 60.5,-5c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_2_G"
+ android:scaleY="0"
+ android:translateX="174"
+ android:translateY="697">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M72 -4 C72,-4 72,4 72,4 C72,6.21 70.21,8 68,8 C68,8 -68,8 -68,8 C-70.21,8 -72,6.21 -72,4 C-72,4 -72,-4 -72,-4 C-72,-6.21 -70.21,-8 -68,-8 C-68,-8 68,-8 68,-8 C70.21,-8 72,-6.21 72,-4c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_1_G"
+ android:scaleY="0"
+ android:translateX="313.5"
+ android:translateY="798">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#bdc1c6"
+ android:fillType="nonZero"
+ android:pathData=" M74.5 0 C74.5,0 74.5,0 74.5,0 C74.5,15.45 61.95,28 46.5,28 C46.5,28 -46.5,28 -46.5,28 C-61.95,28 -74.5,15.45 -74.5,0 C-74.5,0 -74.5,0 -74.5,0 C-74.5,-15.45 -61.95,-28 -46.5,-28 C-46.5,-28 46.5,-28 46.5,-28 C61.95,-28 74.5,-15.45 74.5,0c " />
+ </group>
+ <group
+ android:name="_R_G_L_3_G_L_0_G_L_0_G"
+ android:scaleY="0"
+ android:translateX="205.5"
+ android:translateY="61">
+ <path
+ android:name="_R_G_L_3_G_L_0_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#f8f9fa"
+ android:fillType="nonZero"
+ android:pathData=" M171.5 -14 C171.5,-14 171.5,14 171.5,14 C171.5,16.21 169.71,18 167.5,18 C167.5,18 -167.5,18 -167.5,18 C-169.71,18 -171.5,16.21 -171.5,14 C-171.5,14 -171.5,-14 -171.5,-14 C-171.5,-16.21 -169.71,-18 -167.5,-18 C-167.5,-18 167.5,-18 167.5,-18 C169.71,-18 171.5,-16.21 171.5,-14c " />
+ </group>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_2_G_N_2_T_0"
+ android:scaleX="0.6"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="395">
+ <group
+ android:name="_R_G_L_2_G"
+ android:scaleX="1.3767699999999998"
+ android:scaleY="1.3767699999999998"
+ android:translateY="-508.163">
+ <group
+ android:name="_R_G_L_2_G_D_0_P_0_G_0_T_0"
+ android:scaleX="0"
+ android:scaleY="0">
+ <path
+ android:name="_R_G_L_2_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#9aa0a6"
+ android:fillType="nonZero"
+ android:pathData=" M0 25 C13.81,25 25,13.81 25,0 C25,-13.81 13.81,-25 0,-25 C-13.81,-25 -25,-13.81 -25,0 C-25,13.81 -13.81,25 0,25c " />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_1_G_N_2_T_0"
+ android:scaleX="0.6"
+ android:scaleY="0"
+ android:translateX="206"
+ android:translateY="395">
+ <group
+ android:name="_R_G_L_1_G"
+ android:scaleX="1.39"
+ android:scaleY="1.39"
+ android:translateX="-556.176"
+ android:translateY="-7.307">
+ <path
+ android:name="_R_G_L_1_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="@color/gesture_tutorial_fake_previous_task_view_color"
+ android:fillType="nonZero"
+ android:pathData=" M135 -301 C135,-301 135,311 135,311 C135,319.28 128.28,326 120,326 C120,326 -120,326 -120,326 C-128.28,326 -135,319.28 -135,311 C-135,311 -135,-301 -135,-301 C-135,-309.28 -128.28,-316 -120,-316 C-120,-316 120,-316 120,-316 C128.28,-316 135,-309.28 135,-301c " />
+ </group>
+ </group>
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="206"
+ android:translateY="446">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="0"
+ android:fillColor="#84ba69"
+ android:fillType="nonZero"
+ android:pathData=" M0 406 C21.54,406 39,423.46 39,445 C39,466.54 21.54,484 0,484 C-21.54,484 -39,466.54 -39,445 C-39,423.46 -21.54,406 0,406c " />
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/res/drawable/gm_edit_24.xml b/res/drawable/gm_edit_24.xml
new file mode 100644
index 0000000..59a0dc2
--- /dev/null
+++ b/res/drawable/gm_edit_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M20.41,4.94l-1.35,-1.35c-0.78,-0.78 -2.05,-0.78 -2.83,0L3,16.82L3,21h4.18L20.41,7.77c0.79,-0.78 0.79,-2.05 0,-2.83zM6.41,19.06L5,19v-1.36l9.82,-9.82 1.41,1.41 -9.82,9.83z"/>
+</vector>
diff --git a/res/drawable/ic_allapps_search.xml b/res/drawable/ic_allapps_search.xml
index 2aeb947..dbed824 100644
--- a/res/drawable/ic_allapps_search.xml
+++ b/res/drawable/ic_allapps_search.xml
@@ -17,8 +17,9 @@
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
- android:viewportWidth="24.0">
+ android:viewportWidth="24.0"
+ android:autoMirrored="true">
<path
- android:fillColor="?android:attr/colorAccent"
+ android:fillColor="?android:attr/textColorTertiary"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>
diff --git a/res/drawable/ic_block_no_shadow.xml b/res/drawable/ic_block_no_shadow.xml
index edeb4c6..6ac61f4 100644
--- a/res/drawable/ic_block_no_shadow.xml
+++ b/res/drawable/ic_block_no_shadow.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="20dp"
+ android:height="20dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:tint="?android:attr/textColorPrimary">
diff --git a/res/drawable-v24/ic_block_shadow.xml b/res/drawable/ic_block_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_block_shadow.xml
rename to res/drawable/ic_block_shadow.xml
diff --git a/res/drawable/ic_conversations_widget_category.xml b/res/drawable/ic_conversations_widget_category.xml
new file mode 100644
index 0000000..7b13b23
--- /dev/null
+++ b/res/drawable/ic_conversations_widget_category.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"
+ android:fillColor="#81C995"/>
+ <path
+ android:pathData="M27,34C23.134,34 20,30.866 20,27C20,23.134 23.134,20 27,20C30.866,20 34,23.134 34,27C34,28.4872 33.5362,29.8662 32.7453,31"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#3C4043"/>
+ <path
+ android:pathData="M35,33l-8,0l-0,2l8,0z"
+ android:fillColor="#3C4043"/>
+ <path
+ android:pathData="M21,21m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
+ android:fillColor="#81C995"/>
+ <path
+ android:pathData="M16,25h5v2h-5z"
+ android:fillColor="#81C995"/>
+ <path
+ android:pathData="M21,28C24.866,28 28,24.866 28,21C28,17.134 24.866,14 21,14C17.134,14 14,17.134 14,21C14,22.4872 14.4638,23.8662 15.2547,25"
+ android:strokeWidth="2"
+ android:fillColor="#00000000"
+ android:strokeColor="#ffffff"/>
+ <path
+ android:pathData="M13,27h8v2h-8z"
+ android:fillColor="#ffffff"/>
+</vector>
diff --git a/res/drawable/ic_corp_off.xml b/res/drawable/ic_corp_off.xml
index 62a9787..117258e 100644
--- a/res/drawable/ic_corp_off.xml
+++ b/res/drawable/ic_corp_off.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,10 +15,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0"
- android:tint="?android:attr/textColorHint" >
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorHint">
<path
- android:pathData="M22 7.95c.05-1.11-.84-2-1.95-1.95H16V3.95c0-1.11-.84-2-1.95-1.95h-4C8.94 1.95 8 2.84 8 3.95v.32l14 14V7.95zM14 6h-4V4h4v2zm7.54 14.28l-7.56-7.56v.01l-1.7-1.7h.01L7.21 5.95 3.25 1.99 1.99 3.27 4.69 6h-.64c-1.11 0-1.99.86-1.99 1.97l-.01 11.02c0 1.11.89 2.01 2 2.01h15.64l2.05 2.02L23 21.75l-1.46-1.47z"
- android:fillColor="@android:color/white"/>
+ android:fillColor="@android:color/white"
+ android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v1.17L10.83,8L20,8v9.17l1.98,1.98c0,-0.05 0.02,-0.1 0.02,-0.16L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2zM19,19L8,8 6,6 2.81,2.81 1.39,4.22 3.3,6.13C2.54,6.41 2.01,7.14 2.01,8L2,19c0,1.11 0.89,2 2,2h14.17l1.61,1.61 1.41,-1.41 -0.37,-0.37L19,19zM4,19L4,8h1.17l11,11L4,19z" />
</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_deepshortcut_placeholder.xml b/res/drawable/ic_deepshortcut_placeholder.xml
index 85a9694..16e7cf3 100644
--- a/res/drawable/ic_deepshortcut_placeholder.xml
+++ b/res/drawable/ic_deepshortcut_placeholder.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ 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.
@@ -13,10 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="oval">
- <solid android:color="?attr/popupColorSecondary" />
- <size
- android:height="32dp"
- android:width="32dp" />
-</shape>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="?attr/popupColorPrimary"/>
+ <foreground android:drawable="?attr/popupColorPrimary"/>
+</adaptive-icon>
diff --git a/res/drawable/ic_drag_handle.xml b/res/drawable/ic_drag_handle.xml
index 0181ff1..9db75f4 100644
--- a/res/drawable/ic_drag_handle.xml
+++ b/res/drawable/ic_drag_handle.xml
@@ -19,7 +19,7 @@
android:height="@dimen/deep_shortcut_drag_handle_size"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
- android:tint="?android:attr/textColorHint" >
+ android:tint="?android:attr/textColorPrimary" >
<path
android:pathData="M20,9H4v2h16V9z M4,15h16v-2H4V15z"
diff --git a/res/drawable/ic_expand_less.xml b/res/drawable/ic_expand_less.xml
new file mode 100644
index 0000000..cc16083
--- /dev/null
+++ b/res/drawable/ic_expand_less.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorSecondary">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18.59,16.41L20,15l-8,-8 -8,8 1.41,1.41L12,9.83"/>
+</vector>
diff --git a/res/drawable/ic_expand_more.xml b/res/drawable/ic_expand_more.xml
new file mode 100644
index 0000000..ecbce7f
--- /dev/null
+++ b/res/drawable/ic_expand_more.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorSecondary">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M5.41,7.59L4,9l8,8 8,-8 -1.41,-1.41L12,14.17"/>
+</vector>
diff --git a/res/drawable/ic_gm_close_24.xml b/res/drawable/ic_gm_close_24.xml
new file mode 100644
index 0000000..2c9c932
--- /dev/null
+++ b/res/drawable/ic_gm_close_24.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?android:attr/textColorTertiary"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z"/>
+</vector>
diff --git a/res/drawable/ic_launcher_home.xml b/res/drawable/ic_launcher_home.xml
index a6f2519..7038775 100644
--- a/res/drawable/ic_launcher_home.xml
+++ b/res/drawable/ic_launcher_home.xml
@@ -13,6 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<bitmap
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:src="@mipmap/ic_launcher_home" />
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/icon_background" />
+ <foreground>
+ <bitmap android:src="@mipmap/ic_launcher_home_foreground"/>
+ </foreground>
+</adaptive-icon>
diff --git a/res/drawable/ic_remove_no_shadow.xml b/res/drawable/ic_remove_no_shadow.xml
index 2c706db..10f1e43 100644
--- a/res/drawable/ic_remove_no_shadow.xml
+++ b/res/drawable/ic_remove_no_shadow.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="20dp"
+ android:height="20dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:tint="?android:attr/textColorPrimary">
diff --git a/res/drawable-v24/ic_remove_shadow.xml b/res/drawable/ic_remove_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_remove_shadow.xml
rename to res/drawable/ic_remove_shadow.xml
diff --git a/res/drawable-v24/ic_setup_shadow.xml b/res/drawable/ic_setup_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_setup_shadow.xml
rename to res/drawable/ic_setup_shadow.xml
diff --git a/quickstep/res/drawable/ic_split_screen.xml b/res/drawable/ic_split_screen.xml
similarity index 100%
rename from quickstep/res/drawable/ic_split_screen.xml
rename to res/drawable/ic_split_screen.xml
diff --git a/res/drawable/ic_uninstall_no_shadow.xml b/res/drawable/ic_uninstall_no_shadow.xml
index 6aff102..fbabdd2 100644
--- a/res/drawable/ic_uninstall_no_shadow.xml
+++ b/res/drawable/ic_uninstall_no_shadow.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="20dp"
+ android:height="20dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary" >
diff --git a/res/drawable-v24/ic_uninstall_shadow.xml b/res/drawable/ic_uninstall_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_uninstall_shadow.xml
rename to res/drawable/ic_uninstall_shadow.xml
diff --git a/res/drawable/ic_widget_height_decrease.xml b/res/drawable/ic_widget_height_decrease.xml
new file mode 100644
index 0000000..df704ba
--- /dev/null
+++ b/res/drawable/ic_widget_height_decrease.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorPrimary">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M8,19h3v3h2v-3h3l-4,-4 -4,4zM16,4h-3L13,1h-2v3L8,4l4,4 4,-4zM4,9v2h16L20,9L4,9zM4,12h16v2H4z"/>
+</vector>
diff --git a/res/drawable/ic_widget_height_increase.xml b/res/drawable/ic_widget_height_increase.xml
new file mode 100644
index 0000000..c263a4b
--- /dev/null
+++ b/res/drawable/ic_widget_height_increase.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorPrimary">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M4,20h16v2L4,22zM4,2h16v2L4,4zM13,9h3l-4,-4 -4,4h3v6L8,15l4,4 4,-4h-3z"/>
+</vector>
diff --git a/res/drawable/ic_widget_width_decrease.xml b/res/drawable/ic_widget_width_decrease.xml
new file mode 100644
index 0000000..2a2fad7
--- /dev/null
+++ b/res/drawable/ic_widget_width_decrease.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<rotate
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/ic_widget_height_decrease"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fromDegrees="90"
+ android:toDegrees="90" />
diff --git a/res/drawable/ic_widget_width_increase.xml b/res/drawable/ic_widget_width_increase.xml
new file mode 100644
index 0000000..89b9f40
--- /dev/null
+++ b/res/drawable/ic_widget_width_increase.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<rotate
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/ic_widget_height_increase"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fromDegrees="90"
+ android:toDegrees="90" />
diff --git a/res/drawable/middle_item_primary.xml b/res/drawable/middle_item_primary.xml
new file mode 100644
index 0000000..0c04ea1
--- /dev/null
+++ b/res/drawable/middle_item_primary.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/popupColorPrimary"/>
+ <corners android:radius="@dimen/popup_smaller_radius" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/notification_circle.xml b/res/drawable/notification_circle.xml
new file mode 100644
index 0000000..65fbaea
--- /dev/null
+++ b/res/drawable/notification_circle.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <solid android:color="?attr/popupNotificationDotColor"/>
+
+ <size
+ android:width="@dimen/notification_circle_icon_size"
+ android:height="@dimen/notification_circle_icon_size"/>
+</shape>
\ No newline at end of file
diff --git a/res/drawable/padded_rounded_action_button.xml b/res/drawable/padded_rounded_action_button.xml
new file mode 100644
index 0000000..6432efd
--- /dev/null
+++ b/res/drawable/padded_rounded_action_button.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item
+ android:left="@dimen/padded_rounded_button_padding"
+ android:top="@dimen/padded_rounded_button_padding"
+ android:right="@dimen/padded_rounded_button_padding"
+ android:bottom="@dimen/padded_rounded_button_padding">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/rounded_button_radius" />
+ <stroke android:width="1dp"
+ android:color="?androidprv:attr/colorAccentPrimaryVariant" />
+ </shape>
+ </item>
+</layer-list>
+
diff --git a/res/drawable/personal_work_tabs_ripple.xml b/res/drawable/personal_work_tabs_ripple.xml
new file mode 100644
index 0000000..2e57b80
--- /dev/null
+++ b/res/drawable/personal_work_tabs_ripple.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/transparent" />
+ <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
+ </shape>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
new file mode 100644
index 0000000..f043893
--- /dev/null
+++ b/res/drawable/rounded_action_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/rounded_button_radius" />
+ <stroke android:width="1dp" android:color="@color/all_apps_tab_background_selected" />
+ <padding
+ android:left="@dimen/rounded_button_padding"
+ android:right="@dimen/rounded_button_padding" />
+</shape>
+
diff --git a/res/drawable/single_item_primary.xml b/res/drawable/single_item_primary.xml
new file mode 100644
index 0000000..1c0889b
--- /dev/null
+++ b/res/drawable/single_item_primary.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/popupColorPrimary"/>
+ <corners android:radius="@dimen/popup_single_item_radius" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/top_round_rect_primary.xml b/res/drawable/top_round_rect_primary.xml
deleted file mode 100644
index 1caaa02..0000000
--- a/res/drawable/top_round_rect_primary.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <solid android:color="?android:attr/colorPrimary" />
- <corners
- android:topLeftRadius="@dimen/bg_round_rect_radius"
- android:topRightRadius="@dimen/bg_round_rect_radius"
- android:bottomLeftRadius="0dp"
- android:bottomRightRadius="0dp"
- />
-</shape>
diff --git a/res/drawable/widget_reconfigure_button_frame.xml b/res/drawable/widget_reconfigure_button_frame.xml
new file mode 100644
index 0000000..37d93ad
--- /dev/null
+++ b/res/drawable/widget_reconfigure_button_frame.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:width="@dimen/widget_reconfigure_button_size"
+ android:height="@dimen/widget_reconfigure_button_size">
+ <shape
+ android:shape="rectangle">
+ <solid android:color="?android:attr/colorAccent" />
+ <corners android:radius="@dimen/widget_reconfigure_button_corner_radius" />
+ </shape>
+ </item>
+ <item
+ android:gravity="center"
+ android:padding="@dimen/widget_reconfigure_button_padding"
+ android:drawable="@drawable/gm_edit_24"
+ android:color="?android:attr/colorPrimary" />
+</layer-list>
diff --git a/res/drawable/widget_resize_frame.xml b/res/drawable/widget_resize_frame.xml
new file mode 100644
index 0000000..9426de4
--- /dev/null
+++ b/res/drawable/widget_resize_frame.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/transparent" />
+ <corners android:radius="@android:dimen/system_app_widget_background_radius" />
+ <stroke android:width="2dp" android:color="?attr/workspaceAccentColor" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/widgets_bottom_sheet_background.xml b/res/drawable/widgets_bottom_sheet_background.xml
new file mode 100644
index 0000000..b877546
--- /dev/null
+++ b/res/drawable/widgets_bottom_sheet_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/surface" />
+ <corners
+ android:topLeftRadius="@dimen/default_dialog_corner_radius"
+ android:topRightRadius="@dimen/default_dialog_corner_radius"
+ android:bottomLeftRadius="0dp"
+ android:bottomRightRadius="0dp"
+ />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/widgets_recommendation_background.xml b/res/drawable/widgets_recommendation_background.xml
new file mode 100644
index 0000000..0550a34
--- /dev/null
+++ b/res/drawable/widgets_recommendation_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<shape android:shape="rectangle"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/surface" />
+ <corners android:radius="@dimen/widget_list_top_bottom_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/res/drawable/widgets_tray_expand_button.xml b/res/drawable/widgets_tray_expand_button.xml
new file mode 100644
index 0000000..8316e0f
--- /dev/null
+++ b/res/drawable/widgets_tray_expand_button.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true"
+ android:drawable="@drawable/ic_expand_less" />
+ <item android:state_checked="false"
+ android:drawable="@drawable/ic_expand_more" />
+</selector>
diff --git a/res/drawable/work_apps_toggle_background.xml b/res/drawable/work_apps_toggle_background.xml
new file mode 100644
index 0000000..a47c8fe
--- /dev/null
+++ b/res/drawable/work_apps_toggle_background.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/work_fab_radius" />
+ <solid android:color="?android:attr/colorControlHighlight" />
+ <padding
+ android:left="@dimen/work_profile_footer_padding"
+ android:right="@dimen/work_profile_footer_padding" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/work_fab_radius" />
+ <solid android:color="@color/all_apps_tab_background_selected" />
+ <padding
+ android:left="@dimen/work_profile_footer_padding"
+ android:right="@dimen/work_profile_footer_padding" />
+ </shape>
+ </item>
+</selector>
diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml
new file mode 100644
index 0000000..4a66cac
--- /dev/null
+++ b/res/drawable/work_card.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorSurface" />
+ <corners android:radius="@dimen/work_edu_card_radius" />
+</shape>
+
diff --git a/res/layout/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml
index 830255b..0e06690 100644
--- a/res/layout/add_item_confirmation_activity.xml
+++ b/res/layout/add_item_confirmation_activity.xml
@@ -16,74 +16,94 @@
** limitations under the License.
*/
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.launcher3.dragndrop.AddItemDragLayer
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/add_item_drag_layer"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical">
- <ScrollView
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:importantForAccessibility="no">
+
+ <com.android.launcher3.widget.AddItemWidgetsBottomSheet
+ android:id="@+id/add_item_bottom_sheet"
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:clipToPadding="false">
+ android:layout_height="wrap_content"
+ android:theme="?attr/widgetsTheme"
+ android:layout_gravity="bottom"
+ android:orientation="vertical">
<LinearLayout
+ android:id="@+id/add_item_bottom_sheet_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:padding="24dp"
+ android:background="@drawable/add_item_dialog_background"
+ android:orientation="vertical" >
+
+ <TextView
+ style="@style/TextHeadline"
+ android:id="@+id/widget_appName"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="24sp"
+ android:ellipsize="end"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:maxLines="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingBottom="20dp"
- android:paddingLeft="24dp"
- android:paddingRight="24dp"
- android:paddingTop="4dp"
- android:text="@string/add_item_request_drag_hint" />
+ android:gravity="center_horizontal"
+ android:paddingTop="8dp"
+ android:text="@string/add_item_request_drag_hint"
+ android:textSize="14sp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:alpha="0.7"/>
- <FrameLayout
+ <include layout="@layout/widget_cell"
+ android:id="@+id/widget_cell"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_marginVertical="16dp" />
+
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?android:attr/colorPrimaryDark"
- android:theme="?attr/widgetsTheme">
-
- <com.android.launcher3.dragndrop.LivePreviewWidgetCell
- android:id="@+id/widget_cell"
+ android:gravity="center_vertical|end"
+ android:paddingVertical="8dp"
+ android:orientation="horizontal">
+ <Button
+ style="@style/Button.FullRounded.Colored"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_weight="1"
- android:background="?android:attr/colorPrimaryDark"
- android:focusable="true"
- android:gravity="center_horizontal"
- android:orientation="vertical" >
+ android:layout_height="36dp"
+ android:paddingHorizontal="16dp"
+ android:textSize="14sp"
+ android:textColor="@color/button_text"
+ android:text="@android:string/cancel"
+ android:onClick="onCancelClick"/>
- <include layout="@layout/widget_cell_content" />
+ <Space
+ android:layout_width="8dp"
+ android:layout_height="wrap_content" />
- </com.android.launcher3.dragndrop.LivePreviewWidgetCell>
- </FrameLayout>
+ <Button
+ style="@style/Button.FullRounded.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="36dp"
+ android:paddingHorizontal="16dp"
+ android:textSize="14sp"
+ android:textColor="@color/button_text"
+ android:text="@string/add_to_home_screen"
+ android:onClick="onPlaceAutomaticallyClick"/>
+ </LinearLayout>
</LinearLayout>
- </ScrollView>
+ </com.android.launcher3.widget.AddItemWidgetsBottomSheet>
- <LinearLayout
- style="?android:attr/buttonBarStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="end"
- android:paddingBottom="4dp"
- android:paddingEnd="12dp"
- android:paddingStart="12dp"
- android:paddingTop="4dp" >
- <Button
- style="?android:attr/buttonBarButtonStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:onClick="onCancelClick"
- android:text="@android:string/cancel" />
- <Button
- style="?android:attr/buttonBarButtonStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:onClick="onPlaceAutomaticallyClick"
- android:text="@string/place_automatically" />
- </LinearLayout>
-</LinearLayout>
+</com.android.launcher3.dragndrop.AddItemDragLayer>
+
+
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 67ec664..9ac6ed0 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,8 +15,7 @@
<!-- The top and bottom paddings are defined in this container, but since we want
the list view to span the full width (for touch interception purposes), we
will bake the left/right padding into that view's background itself. -->
-<com.android.launcher3.allapps.LauncherAllAppsContainerView
- xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.launcher3.allapps.LauncherAllAppsContainerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/apps_view"
android:theme="?attr/allAppsTheme"
android:layout_width="match_parent"
@@ -38,43 +36,15 @@
android:layout_below="@id/search_container_all_apps"
android:clipToPadding="false"
android:paddingTop="@dimen/all_apps_header_top_padding"
+ android:paddingBottom="@dimen/all_apps_header_bottom_padding"
android:orientation="vertical" >
<include layout="@layout/floating_header_content" />
- <com.android.launcher3.allapps.PersonalWorkSlidingTabStrip
- android:id="@+id/tabs"
- android:layout_width="match_parent"
- android:layout_height="@dimen/all_apps_header_tab_height"
- android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
- android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
- android:orientation="horizontal"
- style="@style/TextHeadline">
-
- <Button
- android:id="@+id/tab_personal"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="?android:attr/selectableItemBackground"
- android:text="@string/all_apps_personal_tab"
- android:textColor="@color/all_apps_tab_text"
- android:textSize="14sp" />
-
- <Button
- android:id="@+id/tab_work"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="?android:attr/selectableItemBackground"
- android:text="@string/all_apps_work_tab"
- android:textColor="@color/all_apps_tab_text"
- android:textSize="14sp" />
- </com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
+ <include layout="@layout/all_apps_personal_work_tabs" />
</com.android.launcher3.allapps.FloatingHeaderView>
<include
- android:id="@id/search_container_all_apps"
layout="@layout/search_container_all_apps"/>
<include layout="@layout/all_apps_fast_scroller" />
diff --git a/res/layout/all_apps_icon.xml b/res/layout/all_apps_icon.xml
index 79fb612..069954c 100644
--- a/res/layout/all_apps_icon.xml
+++ b/res/layout/all_apps_icon.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,16 +12,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher3.BubbleTextView
- xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.launcher3.BubbleTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
- style="@style/BaseIcon"
+ style="@style/BaseIcon.AllApps"
android:id="@+id/icon"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:stateListAnimator="@animator/all_apps_fastscroll_icon_anim"
launcher:iconDisplay="all_apps"
- launcher:centerVertically="true"
- android:paddingLeft="@dimen/dynamic_grid_cell_padding_x"
- android:paddingRight="@dimen/dynamic_grid_cell_padding_x" />
+ launcher:centerVertically="true" />
diff --git a/res/layout/all_apps_personal_work_tabs.xml b/res/layout/all_apps_personal_work_tabs.xml
new file mode 100644
index 0000000..11143fb
--- /dev/null
+++ b/res/layout/all_apps_personal_work_tabs.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/all_apps_header_pill_height"
+ android:layout_gravity="center_horizontal"
+ android:orientation="horizontal"
+ style="@style/TextHeadline">
+
+ <Button
+ android:id="@+id/tab_personal"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginEnd="@dimen/all_apps_tabs_button_horizontal_padding"
+ android:layout_marginVertical="@dimen/all_apps_tabs_vertical_padding"
+ android:layout_weight="1"
+ android:background="@drawable/all_apps_tabs_background"
+ android:text="@string/all_apps_personal_tab"
+ android:textColor="@color/all_apps_tab_text"
+ android:textSize="14sp"
+ style="?android:attr/borderlessButtonStyle" />
+
+ <Button
+ android:id="@+id/tab_work"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/all_apps_tabs_button_horizontal_padding"
+ android:layout_marginVertical="@dimen/all_apps_tabs_vertical_padding"
+ android:layout_weight="1"
+ android:background="@drawable/all_apps_tabs_background"
+ android:text="@string/all_apps_work_tab"
+ android:textColor="@color/all_apps_tab_text"
+ android:textSize="14sp"
+ style="?android:attr/borderlessButtonStyle" />
+</com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
\ No newline at end of file
diff --git a/res/layout/all_apps_tabs.xml b/res/layout/all_apps_tabs.xml
index 2accd2d..de4a69d 100644
--- a/res/layout/all_apps_tabs.xml
+++ b/res/layout/all_apps_tabs.xml
@@ -22,7 +22,7 @@
android:layout_height="match_parent"
android:layout_below="@id/search_container_all_apps"
android:layout_gravity="center_horizontal|top"
- android:layout_marginTop="@dimen/all_apps_header_tab_height"
+ android:layout_marginTop="@dimen/all_apps_header_pill_height"
android:clipChildren="true"
android:clipToPadding="false"
android:descendantFocusability="afterDescendants"
diff --git a/res/layout/app_widget_resize_frame.xml b/res/layout/app_widget_resize_frame.xml
index 12561b6..ff07a91 100644
--- a/res/layout/app_widget_resize_frame.xml
+++ b/res/layout/app_widget_resize_frame.xml
@@ -18,50 +18,73 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@drawable/widget_resize_shadow"
- android:foreground="@drawable/widget_resize_frame"
- android:foregroundTint="?attr/workspaceTextColor"
android:padding="0dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent" >
+ <!-- Frame -->
+ <ImageView
+ android:id="@+id/widget_resize_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:layout_margin="@dimen/resize_frame_margin"
+ android:src="@drawable/widget_resize_frame" />
+
<!-- Left -->
<ImageView
+ android:id="@+id/widget_resize_left_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|center_vertical"
android:layout_marginLeft="@dimen/widget_handle_margin"
android:src="@drawable/ic_widget_resize_handle"
- android:tint="?attr/workspaceTextColor" />
+ android:tint="?attr/workspaceAccentColor" />
<!-- Top -->
<ImageView
+ android:id="@+id/widget_resize_top_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:layout_marginTop="@dimen/widget_handle_margin"
android:src="@drawable/ic_widget_resize_handle"
- android:tint="?attr/workspaceTextColor" />
+ android:tint="?attr/workspaceAccentColor" />
<!-- Right -->
<ImageView
+ android:id="@+id/widget_resize_right_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|center_vertical"
android:layout_marginRight="@dimen/widget_handle_margin"
android:src="@drawable/ic_widget_resize_handle"
- android:tint="?attr/workspaceTextColor" />
+ android:tint="?attr/workspaceAccentColor" />
<!-- Bottom -->
<ImageView
+ android:id="@+id/widget_resize_bottom_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="@dimen/widget_handle_margin"
android:src="@drawable/ic_widget_resize_handle"
- android:tint="?attr/workspaceTextColor" />
+ android:tint="?attr/workspaceAccentColor" />
+
+ <ImageButton
+ android:id="@+id/widget_reconfigure_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/widget_reconfigure_button_padding"
+ android:layout_gravity="bottom|end"
+ android:layout_marginBottom="@dimen/widget_reconfigure_button_margin"
+ android:layout_marginEnd="@dimen/widget_reconfigure_button_margin"
+ android:src="@drawable/widget_reconfigure_button_frame"
+ android:background="?android:attr/selectableItemBackground"
+ android:visibility="gone"
+ android:contentDescription="@string/widget_reconfigure_button_content_description" />
</FrameLayout>
</com.android.launcher3.AppWidgetResizeFrame>
\ No newline at end of file
diff --git a/res/layout/arrow_toast.xml b/res/layout/arrow_toast.xml
index 0ec9981..9a6f8c3 100644
--- a/res/layout/arrow_toast.xml
+++ b/res/layout/arrow_toast.xml
@@ -19,46 +19,23 @@
android:layout_height="wrap_content"
android:layout_width="wrap_content">
- <LinearLayout
+ <TextView
+ android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingStart="24dp"
- android:paddingEnd="4dp"
- android:background="@drawable/arrow_toast_rounded_background"
android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:padding="16dp"
+ android:background="@drawable/arrow_toast_rounded_background"
android:elevation="2dp"
- android:orientation="horizontal">
-
- <TextView
- android:id="@+id/text"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:paddingTop="5dp"
- android:paddingBottom="5dp"
- android:gravity="center"
- android:layout_gravity="center_vertical"
- android:textColor="@android:color/white"
- android:textSize="16sp"/>
- <ImageView
- android:id="@+id/dismiss"
- android:layout_width="40dp"
- android:layout_height="40dp"
- android:layout_gravity="center_vertical"
- android:padding="10dp"
- android:layout_marginStart="2dp"
- android:layout_marginEnd="2dp"
- android:alpha="0.7"
- android:src="@drawable/ic_remove_no_shadow"
- android:tint="@android:color/white"
- android:background="?android:attr/selectableItemBackgroundBorderless"
- android:contentDescription="@string/accessibility_close"/>
- </LinearLayout>
+ android:outlineProvider="none"
+ android:textColor="@color/arrow_tip_view_content"
+ android:textSize="14sp"/>
<View
android:id="@+id/arrow"
android:elevation="2dp"
- android:layout_width="10dp"
- android:layout_height="8dp"
- android:layout_marginTop="-2dp"/>
+ android:outlineProvider="none"
+ android:layout_width="@dimen/arrow_toast_arrow_width"
+ android:layout_height="10dp"/>
</merge>
diff --git a/res/layout/deep_shortcut.xml b/res/layout/deep_shortcut.xml
index 840a8b7..b175d17 100644
--- a/res/layout/deep_shortcut.xml
+++ b/res/layout/deep_shortcut.xml
@@ -19,6 +19,8 @@
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="@dimen/bg_popup_item_width"
android:layout_height="@dimen/bg_popup_item_height"
+ android:elevation="@dimen/deep_shortcuts_elevation"
+ android:background="@drawable/middle_item_primary"
android:theme="@style/PopupItem" >
<com.android.launcher3.shortcuts.DeepShortcutTextView
@@ -31,6 +33,8 @@
android:paddingEnd="@dimen/popup_padding_end"
android:drawableEnd="@drawable/ic_drag_handle"
android:drawablePadding="@dimen/deep_shortcut_drawable_padding"
+ android:singleLine="true"
+ android:ellipsize="end"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary"
launcher:layoutHorizontal="true"
@@ -45,12 +49,4 @@
android:layout_gravity="start|center_vertical"
android:background="@drawable/ic_deepshortcut_placeholder"/>
- <View
- android:id="@+id/divider"
- android:layout_width="@dimen/deep_shortcuts_divider_width"
- android:layout_height="@dimen/popup_item_divider_height"
- android:layout_gravity="end|bottom"
- android:visibility="gone"
- android:background="?attr/popupColorTertiary" />
-
</com.android.launcher3.shortcuts.DeepShortcutView>
diff --git a/res/layout/floating_surface_view.xml b/res/layout/floating_surface_view.xml
new file mode 100644
index 0000000..434e84f
--- /dev/null
+++ b/res/layout/floating_surface_view.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.launcher3.views.FloatingSurfaceView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
diff --git a/res/layout/floating_widget_view.xml b/res/layout/floating_widget_view.xml
new file mode 100644
index 0000000..eea7a92
--- /dev/null
+++ b/res/layout/floating_widget_view.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.quickstep.views.FloatingWidgetView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layoutDirection="ltr" />
diff --git a/res/layout/folder_application.xml b/res/layout/folder_application.xml
index 32a5419..1cdee08 100644
--- a/res/layout/folder_application.xml
+++ b/res/layout/folder_application.xml
@@ -21,4 +21,5 @@
android:textColor="?attr/folderTextColor"
android:includeFontPadding="false"
android:hapticFeedbackEnabled="false"
- launcher:iconDisplay="folder" />
+ launcher:iconDisplay="folder"
+ launcher:centerVertically="true" />
diff --git a/res/layout/home_settings.xml b/res/layout/home_settings.xml
new file mode 100644
index 0000000..0f2461a
--- /dev/null
+++ b/res/layout/home_settings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <EditText
+ android:id="@+id/filter_box"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/developer_options_filter_margins"
+ android:hint="@string/developer_options_filter_hint"
+ android:visibility="gone"
+ />
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@android:id/list_container"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/keyboard_drag_and_drop.xml b/res/layout/keyboard_drag_and_drop.xml
new file mode 100644
index 0000000..e9463c4
--- /dev/null
+++ b/res/layout/keyboard_drag_and_drop.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.launcher3.keyboard.KeyboardDragAndDropView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:elevation="6dp">
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:background="?attr/folderFillColor"
+ android:padding="8dp"
+ android:textColor="?attr/folderTextColor"
+ />
+
+</com.android.launcher3.keyboard.KeyboardDragAndDropView>
\ No newline at end of file
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index a137908..039d8d3 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -28,6 +28,12 @@
android:clipToPadding="false"
android:importantForAccessibility="no">
+ <com.android.launcher3.views.AccessibilityActionsView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/home_screen"
+ />
+
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
@@ -43,11 +49,6 @@
android:id="@+id/hotseat"
layout="@layout/hotseat" />
- <include
- android:id="@+id/overview_panel"
- layout="@layout/overview_panel" />
-
-
<!-- Keep these behind the workspace so that they are not visible when
we go into AllApps -->
<com.android.launcher3.pageindicators.WorkspacePageIndicator
@@ -61,9 +62,11 @@
android:id="@+id/drop_target_bar"
layout="@layout/drop_target_bar" />
- <include
+ <com.android.launcher3.views.ScrimView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:id="@+id/scrim_view"
- layout="@layout/scrim_view" />
+ android:background="@android:color/transparent" />
<include
android:id="@+id/apps_view"
@@ -71,6 +74,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
+ <include
+ android:id="@+id/overview_panel"
+ layout="@layout/overview_panel" />
+
</com.android.launcher3.dragndrop.DragLayer>
</com.android.launcher3.LauncherRootView>
diff --git a/res/layout/launcher_preview_layout.xml b/res/layout/launcher_preview_layout.xml
index 3fd02e3..cf2f2c7 100644
--- a/res/layout/launcher_preview_layout.xml
+++ b/res/layout/launcher_preview_layout.xml
@@ -13,11 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher3.InsettableFrameLayout
+<view class="com.android.launcher3.graphics.LauncherPreviewRenderer$LauncherPreviewLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:focusable="false">
<com.android.launcher3.CellLayout
android:id="@+id/workspace"
@@ -28,23 +29,8 @@
launcher:containerType="workspace"
launcher:pageIndicator="@+id/page_indicator"/>
- <com.android.launcher3.Hotseat
+ <include
android:id="@+id/hotseat"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:importantForAccessibility="no"
- android:theme="@style/HomeScreenElementTheme"
- launcher:containerType="hotseat" />
+ layout="@layout/hotseat" />
- <com.android.launcher3.InsettableFrameLayout
- android:id="@+id/apps_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
-
- <include
- android:id="@id/search_container_all_apps"
- layout="@layout/search_container_all_apps"/>
-
- </com.android.launcher3.InsettableFrameLayout>
-
-</com.android.launcher3.InsettableFrameLayout>
\ No newline at end of file
+</view>
\ No newline at end of file
diff --git a/res/layout/longpress_options_menu.xml b/res/layout/longpress_options_menu.xml
index 20bb5b8..fbe28d8 100644
--- a/res/layout/longpress_options_menu.xml
+++ b/res/layout/longpress_options_menu.xml
@@ -15,12 +15,10 @@
-->
<com.android.launcher3.views.OptionsPopupView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/deep_shortcuts_container"
+ android:id="@+id/popup_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="?attr/popupColorPrimary"
android:clipToPadding="false"
android:clipChildren="false"
- android:elevation="@dimen/deep_shortcuts_elevation"
android:importantForAccessibility="yes"
android:orientation="vertical" />
diff --git a/res/layout/notification_content.xml b/res/layout/notification_content.xml
index d01be01..84822a6 100644
--- a/res/layout/notification_content.xml
+++ b/res/layout/notification_content.xml
@@ -23,35 +23,36 @@
<FrameLayout
android:id="@+id/header"
android:layout_width="match_parent"
- android:layout_height="@dimen/notification_header_height"
- android:paddingEnd="@dimen/notification_padding_end"
- android:paddingStart="@dimen/notification_padding_start">
+ android:layout_height="wrap_content"
+ android:paddingEnd="@dimen/notification_padding"
+ android:paddingStart="@dimen/notification_padding">
<TextView
android:id="@+id/notification_text"
+ android:paddingTop="@dimen/notification_padding"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="start"
- android:gravity="center_vertical"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|start"
android:text="@string/notifications_header"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="@dimen/notification_header_text_size" />
+ android:textSize="@dimen/notification_header_text_size"
+ style="@style/TextHeadline"/>
<TextView
android:id="@+id/notification_count"
- android:layout_width="@dimen/notification_icon_size"
- android:layout_height="match_parent"
- android:layout_gravity="end"
- android:fontFamily="sans-serif-medium"
+ android:layout_width="@dimen/notification_circle_icon_size"
+ android:layout_height="@dimen/notification_circle_icon_size"
+ android:background="@drawable/notification_circle"
+ android:layout_gravity="bottom|end"
android:gravity="center"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="@dimen/notification_header_count_text_size" />
+ android:textSize="@dimen/notification_header_count_text_size"
+ style="@style/TextHeadline"/>
</FrameLayout>
<!-- Main view -->
<com.android.launcher3.notification.NotificationMainView
android:id="@+id/main_view"
android:layout_width="match_parent"
- android:layout_height="@dimen/notification_main_height"
- android:background="@drawable/bg_notification_content"
+ android:layout_height="wrap_content"
android:focusable="true" >
<LinearLayout
@@ -61,28 +62,28 @@
android:background="?attr/popupColorPrimary"
android:gravity="center_vertical"
android:orientation="vertical"
- android:paddingBottom="14dp"
- android:paddingEnd="@dimen/notification_main_text_padding_end"
- android:paddingStart="@dimen/notification_padding_start">
+ android:paddingTop="@dimen/notification_padding"
+ android:paddingBottom="@dimen/notification_padding"
+ android:paddingEnd="@dimen/notification_padding"
+ android:paddingStart="@dimen/notification_main_text_padding_start">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
- android:fontFamily="sans-serif"
android:lines="1"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="@dimen/notification_main_title_size" />
+ android:textSize="@dimen/notification_main_title_size"
+ style="@style/TextHeadline" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
- android:fontFamily="sans-serif"
android:lines="1"
- android:textColor="?android:attr/textColorSecondary"
+ android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/notification_main_text_size" />
</LinearLayout>
@@ -90,45 +91,9 @@
android:id="@+id/popup_item_icon"
android:layout_width="@dimen/notification_icon_size"
android:layout_height="@dimen/notification_icon_size"
- android:layout_gravity="center_vertical|end"
- android:layout_marginBottom="7dp"
- android:layout_marginEnd="@dimen/notification_padding_end" />
+ android:layout_gravity="start"
+ android:layout_marginTop="@dimen/notification_padding"
+ android:layout_marginStart="@dimen/notification_icon_padding" />
</com.android.launcher3.notification.NotificationMainView>
-
- <!-- Divider -->
- <View
- android:id="@+id/divider"
- android:layout_width="match_parent"
- android:layout_height="@dimen/popup_item_divider_height"
- android:layout_below="@id/main_view"
- android:background="?attr/popupColorTertiary" />
-
- <!-- Footer -->
- <com.android.launcher3.notification.NotificationFooterLayout
- android:id="@+id/footer"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_footer_height"
- android:layout_gravity="center_vertical"
- android:clipChildren="false">
-
- <LinearLayout
- android:id="@+id/icon_row"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:gravity="end|center_vertical"
- android:orientation="horizontal"
- android:padding="@dimen/notification_footer_icon_row_padding"/>
-
- <View
- android:id="@+id/overflow"
- android:layout_width="@dimen/horizontal_ellipsis_size"
- android:layout_height="@dimen/horizontal_ellipsis_size"
- android:layout_gravity="start|center_vertical"
- android:layout_marginStart="@dimen/horizontal_ellipsis_offset"
- android:background="@drawable/horizontal_ellipsis" />
-
- </com.android.launcher3.notification.NotificationFooterLayout>
</merge>
\ No newline at end of file
diff --git a/res/layout/notification_gutter.xml b/res/layout/notification_gutter.xml
index 10e7f7d..9a3e55a 100644
--- a/res/layout/notification_gutter.xml
+++ b/res/layout/notification_gutter.xml
@@ -16,6 +16,5 @@
<View
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="4dp"
- android:layout_marginTop="4dp"
- android:background="@drawable/bg_notification_content" />
\ No newline at end of file
+ android:layout_height="0dp"
+ android:layout_marginTop="@dimen/popup_margin" />
\ No newline at end of file
diff --git a/res/layout/overview_actions_container.xml b/res/layout/overview_actions_container.xml
index 5946bf6..f152c7c 100644
--- a/res/layout/overview_actions_container.xml
+++ b/res/layout/overview_actions_container.xml
@@ -14,6 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<!-- NOTE! don't add dimensions for margins / gravity to root view in this file, they need to be
+ loaded at runtime. -->
<Space
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
diff --git a/res/layout/popup_container.xml b/res/layout/popup_container.xml
index c737407..18014bb 100644
--- a/res/layout/popup_container.xml
+++ b/res/layout/popup_container.xml
@@ -16,11 +16,27 @@
<com.android.launcher3.popup.PopupContainerWithArrow
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/deep_shortcuts_container"
+ android:id="@+id/popup_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="?attr/popupColorPrimary"
android:clipToPadding="false"
android:clipChildren="false"
- android:elevation="@dimen/deep_shortcuts_elevation"
- android:orientation="vertical" />
\ No newline at end of file
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/deep_shortcuts_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:tag="@string/popup_container_iterate_children"
+ android:elevation="@dimen/deep_shortcuts_elevation"
+ android:orientation="vertical"/>
+
+ <LinearLayout
+ android:id="@+id/notification_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:background="?attr/popupColorPrimary"
+ android:elevation="@dimen/deep_shortcuts_elevation"
+ android:orientation="vertical"/>
+</com.android.launcher3.popup.PopupContainerWithArrow>
\ No newline at end of file
diff --git a/res/layout/scrim_view.xml b/res/layout/scrim_view.xml
deleted file mode 100644
index a604d56..0000000
--- a/res/layout/scrim_view.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<com.android.launcher3.views.ScrimView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:id="@+id/scrim_view" />
\ No newline at end of file
diff --git a/res/layout/search_container_hotseat.xml b/res/layout/search_container_hotseat.xml
new file mode 100644
index 0000000..8f12ca0
--- /dev/null
+++ b/res/layout/search_container_hotseat.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<View
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="0dp" />
\ No newline at end of file
diff --git a/res/layout/secondary_launcher.xml b/res/layout/secondary_launcher.xml
index fdf4446..b15a320 100644
--- a/res/layout/secondary_launcher.xml
+++ b/res/layout/secondary_launcher.xml
@@ -67,12 +67,10 @@
android:paddingTop="@dimen/all_apps_header_top_padding"
android:orientation="vertical" >
- <com.android.launcher3.allapps.PersonalWorkSlidingTabStrip
+ <com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
android:id="@+id/tabs"
android:layout_width="match_parent"
- android:layout_height="@dimen/all_apps_header_tab_height"
- android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
- android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
+ android:layout_height="@dimen/all_apps_header_pill_height"
android:orientation="horizontal"
style="@style/TextHeadline">
@@ -97,7 +95,7 @@
android:textAllCaps="true"
android:textColor="@color/all_apps_tab_text"
android:textSize="14sp" />
- </com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
+ </com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
</com.android.launcher3.allapps.FloatingHeaderView>
<com.android.launcher3.allapps.search.AppsSearchContainerLayout
diff --git a/res/layout/settings_activity.xml b/res/layout/settings_activity.xml
new file mode 100644
index 0000000..5edd2df
--- /dev/null
+++ b/res/layout/settings_activity.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:fitsSystemWindows="true">
+
+ <Toolbar
+ android:id="@+id/action_bar"
+ style="?android:attr/actionBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="?android:attr/actionBarTheme" />
+
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/switch_preference_with_settings.xml b/res/layout/switch_preference_with_settings.xml
index d319561..cd51833 100644
--- a/res/layout/switch_preference_with_settings.xml
+++ b/res/layout/switch_preference_with_settings.xml
@@ -26,7 +26,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_setting"
- android:tint="@android:color/black"
+ android:forceDarkAllowed="true"
android:padding="12dp"
android:background="?android:attr/selectableItemBackgroundBorderless" />
diff --git a/res/layout/system_shortcut.xml b/res/layout/system_shortcut.xml
index 881df1b..de091c5 100644
--- a/res/layout/system_shortcut.xml
+++ b/res/layout/system_shortcut.xml
@@ -19,6 +19,8 @@
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="@dimen/bg_popup_item_width"
android:layout_height="@dimen/bg_popup_item_height"
+ android:elevation="@dimen/deep_shortcuts_elevation"
+ android:background="@drawable/middle_item_primary"
android:theme="@style/PopupItem" >
<com.android.launcher3.BubbleTextView
@@ -30,7 +32,9 @@
android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
android:paddingEnd="@dimen/popup_padding_end"
android:textSize="14sp"
+ android:minLines="1"
android:maxLines="2"
+ android:ellipsize="end"
android:textColor="?android:attr/textColorPrimary"
launcher:iconDisplay="shortcut_popup"
launcher:layoutHorizontal="true"
@@ -42,14 +46,6 @@
android:layout_height="@dimen/system_shortcut_icon_size"
android:layout_marginStart="@dimen/system_shortcut_margin_start"
android:layout_gravity="start|center_vertical"
- android:backgroundTint="?android:attr/textColorTertiary"/>
-
- <View
- android:id="@+id/divider"
- android:layout_width="@dimen/deep_shortcuts_divider_width"
- android:layout_height="@dimen/popup_item_divider_height"
- android:layout_gravity="end|bottom"
- android:visibility="gone"
- android:background="?attr/popupColorTertiary" />
+ android:backgroundTint="?android:attr/textColorPrimary"/>
</com.android.launcher3.shortcuts.DeepShortcutView>
diff --git a/res/layout/system_shortcut_icons.xml b/res/layout/system_shortcut_icons.xml
index a340f4f..775a45f 100644
--- a/res/layout/system_shortcut_icons.xml
+++ b/res/layout/system_shortcut_icons.xml
@@ -21,7 +21,8 @@
android:layout_height="@dimen/system_shortcut_header_height"
android:orientation="horizontal"
android:gravity="end|center_vertical"
- android:background="?attr/popupColorSecondary"
+ android:background="@drawable/single_item_primary"
+ android:elevation="@dimen/deep_shortcuts_elevation"
android:clipToPadding="true">
<Space android:layout_width="0dp"
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 923352e..11eea60 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -18,8 +18,6 @@
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@drawable/round_rect_folder"
- android:elevation="5dp"
android:orientation="vertical" >
<com.android.launcher3.folder.FolderPagedView
@@ -27,15 +25,12 @@
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingLeft="8dp"
- android:paddingRight="8dp"
- android:paddingTop="16dp"
launcher:pageIndicator="@+id/folder_page_indicator" />
<LinearLayout
android:id="@+id/folder_footer"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="48dp"
android:clipChildren="false"
android:orientation="horizontal"
android:paddingLeft="12dp"
@@ -49,17 +44,13 @@
style="@style/TextHeadline"
android:layout_weight="1"
android:background="@android:color/transparent"
- android:textStyle="bold"
android:gravity="center_horizontal"
android:hint="@string/folder_hint_text"
android:imeOptions="flagNoExtractUi"
- android:paddingBottom="@dimen/folder_label_padding_bottom"
- android:paddingTop="@dimen/folder_label_padding_top"
android:singleLine="true"
android:textColor="?attr/folderTextColor"
android:textColorHighlight="?android:attr/colorControlHighlight"
- android:textColorHint="?attr/folderHintColor"
- android:textSize="@dimen/folder_label_text_size" />
+ android:textColorHint="?attr/folderHintColor"/>
<com.android.launcher3.pageindicators.PageIndicatorDots
android:id="@+id/folder_page_indicator"
diff --git a/res/layout/widget_cell.xml b/res/layout/widget_cell.xml
index 148a99b..55dd1de 100644
--- a/res/layout/widget_cell.xml
+++ b/res/layout/widget_cell.xml
@@ -15,12 +15,13 @@
-->
<com.android.launcher3.widget.WidgetCell
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/widget_cell_horizontal_padding"
+ android:paddingVertical="@dimen/widget_cell_vertical_padding"
android:layout_weight="1"
android:orientation="vertical"
android:focusable="true"
- android:background="?android:attr/colorPrimaryDark"
android:gravity="center_horizontal">
<include layout="@layout/widget_cell_content" />
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 64f2362..b27b505 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -17,47 +17,57 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <LinearLayout
+ <com.android.launcher3.widget.WidgetCellPreview
+ android:id="@+id/widget_preview_container"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:importantForAccessibility="no"
+ android:layout_marginVertical="8dp">
+ <!-- The image of the widget. This view does not support padding. Any placement adjustment
+ should be done using margins. Width & height are set at runtime after scaling the
+ preview image. -->
+ <com.android.launcher3.widget.WidgetImageView
+ android:id="@+id/widget_preview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no"
+ android:layout_gravity="fill"/>
+ </com.android.launcher3.widget.WidgetCellPreview>
+
+ <!-- The name of the widget. -->
+ <TextView
+ android:id="@+id/widget_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingTop="@dimen/widget_preview_label_vertical_padding"
- android:paddingBottom="@dimen/widget_preview_label_vertical_padding"
- android:paddingLeft="@dimen/widget_preview_label_horizontal_padding"
- android:paddingRight="@dimen/widget_preview_label_horizontal_padding"
- android:orientation="horizontal">
+ android:ellipsize="end"
+ android:fadingEdge="horizontal"
+ android:gravity="center_horizontal"
+ android:singleLine="true"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/widget_cell_font_size" />
- <!-- The name of the widget. -->
- <TextView
- android:id="@+id/widget_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:ellipsize="end"
- android:fadingEdge="horizontal"
- android:gravity="start"
- android:singleLine="true"
- android:maxLines="1"
- android:textColor="?android:attr/textColorSecondary"
- android:textSize="14sp" />
-
- <!-- The original dimensions of the widget (can't be the same text as above due to different
- style. -->
- <TextView
- android:id="@+id/widget_dims"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="5dp"
- android:layout_marginLeft="5dp"
- android:textColor="?android:attr/textColorSecondary"
- android:textSize="14sp"
- android:alpha="0.8" />
- </LinearLayout>
-
- <!-- The image of the widget. This view does not support padding. Any placement adjustment
- should be done using margins. -->
- <com.android.launcher3.widget.WidgetImageView
- android:id="@+id/widget_preview"
+ <!-- The original dimensions of the widget -->
+ <TextView
+ android:id="@+id/widget_dims"
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/widget_cell_font_size"
+ android:alpha="0.7" />
+
+ <TextView
+ android:id="@+id/widget_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textSize="@dimen/widget_cell_font_size"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:fadingEdge="horizontal"
+ android:alpha="0.7" />
+
</merge>
\ No newline at end of file
diff --git a/res/layout/widget_shortcut_container.xml b/res/layout/widget_shortcut_container.xml
new file mode 100644
index 0000000..a4d8eb4
--- /dev/null
+++ b/res/layout/widget_shortcut_container.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/system_shortcut_full"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/system_shortcut_header_height"
+ android:orientation="horizontal"
+ android:gravity="end|center_vertical"
+ android:elevation="@dimen/deep_shortcuts_elevation"
+ android:tag="@string/popup_container_iterate_children"
+ android:clipToPadding="true">
+</LinearLayout>
diff --git a/res/layout/widgets_bottom_sheet.xml b/res/layout/widgets_bottom_sheet.xml
index 3fdfc96..bbb08fa 100644
--- a/res/layout/widgets_bottom_sheet.xml
+++ b/res/layout/widgets_bottom_sheet.xml
@@ -19,36 +19,9 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingTop="28dp"
- android:background="@drawable/top_round_rect_primary"
- android:elevation="@dimen/deep_shortcuts_elevation"
android:layout_gravity="bottom"
android:theme="?attr/widgetsTheme">
- <TextView
- style="@style/TextHeadline"
- android:id="@+id/title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:textColor="?android:attr/textColorPrimary"
- android:textSize="24sp"/>
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:paddingTop="4dp"
- android:fontFamily="sans-serif"
- android:textColor="?android:attr/textColorTertiary"
- android:textSize="14sp"
- android:text="@string/long_press_widget_to_add"/>
-
- <include layout="@layout/widgets_scroll_container"
- android:id="@+id/widgets"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="45dp"
- android:layout_marginBottom="40dp"/>
+ <include layout="@layout/widgets_bottom_sheet_content" />
</com.android.launcher3.widget.WidgetsBottomSheet>
\ No newline at end of file
diff --git a/res/layout/widgets_bottom_sheet_content.xml b/res/layout/widgets_bottom_sheet_content.xml
new file mode 100644
index 0000000..3b3ff8b
--- /dev/null
+++ b/res/layout/widgets_bottom_sheet_content.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <LinearLayout
+ android:id="@+id/widgets_bottom_sheet"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/widgets_bottom_sheet_background"
+ android:paddingTop="16dp"
+ android:orientation="vertical">
+ <View
+ android:id="@+id/collapse_handle"
+ android:layout_width="48dp"
+ android:layout_height="2dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="16dp"
+ android:visibility="gone"
+ android:background="?android:attr/textColorSecondary"/>
+ <TextView
+ style="@style/TextHeadline"
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="24sp"/>
+
+ <ScrollView
+ android:id="@+id/widgets_table_scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fadeScrollbars="false"
+ android:layout_marginVertical="16dp">
+ <include layout="@layout/widgets_table_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal" />
+ </ScrollView>
+ </LinearLayout>
+</merge>
diff --git a/res/layout/widgets_edu.xml b/res/layout/widgets_edu.xml
new file mode 100644
index 0000000..280c095
--- /dev/null
+++ b/res/layout/widgets_edu.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.launcher3.views.WidgetsEduView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:gravity="bottom"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/edu_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/bg_rounded_corner_bottom_sheet"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/bottom_sheet_edu_padding"
+ android:paddingTop="@dimen/bottom_sheet_edu_padding">
+
+ <TextView
+ style="@style/TextHeadline"
+ android:id="@+id/edu_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/widget_education_header"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="24sp"
+ android:layout_marginBottom="16dp"/>
+
+ <TextView
+ android:id="@+id/edu_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/widget_education_content"
+ android:textSize="14sp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_marginBottom="24dp"/>
+
+ <Button
+ android:id="@+id/edu_close_button"
+ style="@style/Button.Rounded.Colored"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:text="@string/widget_education_close_button"
+ android:textSize="16sp"
+ android:textColor="@color/button_text"
+ android:layout_marginBottom="8dp"/>
+ </LinearLayout>
+</com.android.launcher3.views.WidgetsEduView>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index f507a88..1b4f3b9 100644
--- a/res/layout/widgets_full_sheet.xml
+++ b/res/layout/widgets_full_sheet.xml
@@ -13,8 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher3.widget.WidgetsFullSheet
+<com.android.launcher3.widget.picker.WidgetsFullSheet
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -24,14 +25,17 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?android:attr/colorPrimary"
- android:elevation="4dp">
+ android:background="?android:attr/colorBackground">
- <com.android.launcher3.widget.WidgetsRecyclerView
- android:id="@+id/widgets_list_view"
+ <TextView
+ android:id="@+id/no_widgets_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:clipToPadding="false" />
+ android:gravity="center"
+ android:visibility="gone"
+ android:fontFamily="sans-serif-medium"
+ android:textSize="20sp"
+ tools:text="No widgets available" />
<!-- Fast scroller popup -->
<TextView
@@ -48,5 +52,13 @@
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="@dimen/fastscroll_end_margin" />
+
+ <com.android.launcher3.widget.picker.WidgetsRecyclerView
+ android:id="@+id/search_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:clipToPadding="false" />
+
</com.android.launcher3.views.TopRoundedCornerView>
-</com.android.launcher3.widget.WidgetsFullSheet>
\ No newline at end of file
+</com.android.launcher3.widget.picker.WidgetsFullSheet>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
new file mode 100644
index 0000000..f0ddc2b
--- /dev/null
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+ <com.android.launcher3.workprofile.PersonalWorkPagedView
+ android:id="@+id/widgets_view_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:paddingTop="@dimen/widget_picker_view_pager_top_padding"
+ android:descendantFocusability="afterDescendants"
+ launcher:pageIndicator="@+id/tabs">
+
+ <com.android.launcher3.widget.picker.WidgetsRecyclerView
+ android:id="@+id/primary_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false" />
+
+ <com.android.launcher3.widget.picker.WidgetsRecyclerView
+ android:id="@+id/work_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false" />
+
+ </com.android.launcher3.workprofile.PersonalWorkPagedView>
+
+ <include layout="@layout/widgets_personal_work_tabs"/>
+</merge>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
new file mode 100644
index 0000000..fbe559c
--- /dev/null
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.launcher3.widget.picker.WidgetsRecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/primary_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false" />
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_search_and_recommendations.xml b/res/layout/widgets_full_sheet_search_and_recommendations.xml
new file mode 100644
index 0000000..4a3e20d
--- /dev/null
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.launcher3.widget.picker.SearchAndRecommendationsView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_and_recommendations_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/collapse_handle"
+ android:layout_width="match_parent"
+ android:layout_height="18dp"
+ android:elevation="0.1dp"
+ android:background="@drawable/bg_widgets_picker_handle"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textSize="24sp"
+ android:layout_marginTop="24dp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:text="@string/widget_button_text"/>
+
+ <FrameLayout
+ android:id="@+id/search_bar_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:elevation="0.1dp"
+ android:background="?android:attr/colorBackground"
+ android:paddingBottom="8dp"
+ android:clipToPadding="false">
+ <include layout="@layout/widgets_search_bar" />
+ </FrameLayout>
+
+ <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
+ android:id="@+id/recommended_widget_table"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
+ android:layout_marginTop="8dp"
+ android:background="@drawable/widgets_recommendation_background"
+ android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
+ android:visibility="gone" />
+</com.android.launcher3.widget.picker.SearchAndRecommendationsView>
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
new file mode 100644
index 0000000..7f84050
--- /dev/null
+++ b/res/layout/widgets_list_row_header.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.launcher3.widget.picker.WidgetsListHeader xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/widgets_list_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
+ android:paddingVertical="@dimen/widget_list_header_view_vertical_padding"
+ android:orientation="horizontal"
+ launcher:appIconSize="48dp">
+
+ <ImageView
+ android:id="@+id/app_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="16dp"
+ android:importantForAccessibility="no"
+ tools:src="@drawable/ic_corp"/>
+
+ <LinearLayout
+ android:id="@+id/app_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:focusable="true"
+ android:descendantFocusability="afterDescendants">
+
+ <TextView
+ android:id="@+id/app_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|center_vertical"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ tools:text="App name" />
+
+ <TextView
+ android:id="@+id/app_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorSecondary"
+ android:alpha="0.7"
+ tools:text="m widgets, n shortcuts" />
+
+ </LinearLayout>
+
+ <!-- This checkbox is not clickable. The outermost LinearLayout is responsible to handle all
+ click event and update the checkbox state. -->
+ <CheckBox
+ android:id="@+id/toggle"
+ android:layout_marginHorizontal="16dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_alignParentEnd="true"
+ android:enabled="false"
+ android:clickable="false"
+ android:importantForAccessibility="no"
+ android:button="@drawable/widgets_tray_expand_button"/>
+
+</com.android.launcher3.widget.picker.WidgetsListHeader>
\ No newline at end of file
diff --git a/res/layout/widgets_list_row_view.xml b/res/layout/widgets_list_row_view.xml
index eec57a5..5942ba6 100644
--- a/res/layout/widgets_list_row_view.xml
+++ b/res/layout/widgets_list_row_view.xml
@@ -45,5 +45,5 @@
launcher:iconSizeOverride="@dimen/widget_section_icon_size"
launcher:layoutHorizontal="true" />
- <include layout="@layout/widgets_scroll_container" />
+ <include layout="@layout/widgets_table_container" />
</LinearLayout>
diff --git a/res/layout/widgets_personal_work_tabs.xml b/res/layout/widgets_personal_work_tabs.xml
new file mode 100644
index 0000000..532c422
--- /dev/null
+++ b/res/layout/widgets_personal_work_tabs.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/all_apps_header_pill_height"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:layout_marginHorizontal="@dimen/widget_tabs_horizontal_margin"
+ style="@style/TextHeadline">
+
+ <Button
+ android:id="@+id/tab_personal"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
+ android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
+ android:layout_weight="1"
+ android:background="@drawable/all_apps_tabs_background"
+ android:text="@string/widgets_full_sheet_personal_tab"
+ android:textColor="@color/all_apps_tab_text"
+ android:textSize="14sp"
+ style="?android:attr/borderlessButtonStyle" />
+
+ <Button
+ android:id="@+id/tab_work"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
+ android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
+ android:layout_weight="1"
+ android:background="@drawable/all_apps_tabs_background"
+ android:text="@string/widgets_full_sheet_work_tab"
+ android:textColor="@color/all_apps_tab_text"
+ android:textSize="14sp"
+ style="?android:attr/borderlessButtonStyle" />
+</com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
\ No newline at end of file
diff --git a/res/layout/widgets_scroll_container.xml b/res/layout/widgets_scroll_container.xml
deleted file mode 100644
index fc509d1..0000000
--- a/res/layout/widgets_scroll_container.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<HorizontalScrollView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/widgets_scroll_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="?android:attr/colorPrimaryDark"
- android:scrollbars="none">
- <LinearLayout
- android:id="@+id/widgets_cell_list"
- style="@style/TextTitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingStart="0dp"
- android:paddingEnd="0dp"
- android:orientation="horizontal"
- android:showDividers="none"/>
-</HorizontalScrollView>
\ No newline at end of file
diff --git a/res/layout/widgets_search_bar.xml b/res/layout/widgets_search_bar.xml
new file mode 100644
index 0000000..cb27f4f
--- /dev/null
+++ b/res/layout/widgets_search_bar.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.launcher3.widget.picker.search.LauncherWidgetsSearchBar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widgets_search_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginTop="24dp"
+ android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
+ android:background="@drawable/bg_widgets_searchbox">
+
+ <com.android.launcher3.ExtendedEditText
+ android:id="@+id/widgets_search_bar_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="12dp"
+ android:paddingStart="12dp"
+ android:paddingEnd="0dp"
+ android:drawablePadding="8dp"
+ android:drawableStart="@drawable/ic_allapps_search"
+ android:background="@null"
+ android:hint="@string/widgets_full_sheet_search_bar_hint"
+ android:maxLines="1"
+ android:layout_weight="1"
+ android:inputType="text"
+ android:imeOptions="actionSearch"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textColorHint="?android:attr/textColorSecondary"/>
+
+ <ImageButton
+ android:id="@+id/widgets_search_cancel_button"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:padding="12dp"
+ android:src="@drawable/ic_gm_close_24"
+ android:background="@drawable/full_rounded_transparent_ripple"
+ android:layout_gravity="center"
+ android:contentDescription="@string/widgets_full_sheet_cancel_button_description"
+ android:visibility="gone"/>
+</com.android.launcher3.widget.picker.search.LauncherWidgetsSearchBar>
\ No newline at end of file
diff --git a/res/layout/widgets_table_container.xml b/res/layout/widgets_table_container.xml
new file mode 100644
index 0000000..ab470d8
--- /dev/null
+++ b/res/layout/widgets_table_container.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.launcher3.widget.picker.WidgetsListTableView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widgets_table"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin" />
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
new file mode 100644
index 0000000..1517087
--- /dev/null
+++ b/res/layout/work_apps_edu.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.launcher3.allapps.WorkEduCard xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:gravity="center">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/work_card_padding_horizontal"
+ android:paddingVertical="@dimen/work_card_padding_horizontal"
+ android:background="@drawable/work_card"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:id="@+id/wrapper">
+
+ <TextView
+ style="@style/PrimaryHeadline"
+ android:textColor="?android:attr/textColorPrimary"
+ android:id="@+id/work_apps_paused_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/work_card_padding_horizontal"
+ android:text="@string/work_profile_edu_work_apps"
+ android:textAlignment="center"
+ android:textSize="20sp" />
+
+ <Button
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/work_card_button_height"
+ android:id="@+id/action_btn"
+ android:textColor="?attr/workProfileOverlayTextColor"
+ android:text="@string/work_profile_edu_accept"
+ android:textAlignment="center"
+ android:background="@drawable/rounded_action_button"
+
+ android:textSize="14sp" />
+ </LinearLayout>
+</com.android.launcher3.allapps.WorkEduCard>
\ No newline at end of file
diff --git a/res/layout/work_apps_paused.xml b/res/layout/work_apps_paused.xml
index 7f1107f..ec34b47 100644
--- a/res/layout/work_apps_paused.xml
+++ b/res/layout/work_apps_paused.xml
@@ -12,13 +12,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.launcher3.allapps.WorkPausedCard xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?attr/allAppsScrimColor"
- android:padding="48dp"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/work_edu_card_margin"
android:orientation="vertical"
- android:gravity="center">
+ android:gravity="center_horizontal">
<TextView
style="@style/PrimaryHeadline"
@@ -26,18 +25,31 @@
android:id="@+id/work_apps_paused_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- android:layout_marginBottom="8dp"
+ android:layout_marginTop="40dp"
android:text="@string/work_apps_paused_title"
android:textAlignment="center"
- android:textSize="20sp" />
+ android:textSize="22sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/work_apps_paused_content"
- android:textColor="?attr/workProfileOverlayTextColor"
+ android:textColor="?android:attr/textColorSecondary"
android:text="@string/work_apps_paused_body"
android:textAlignment="center"
- android:textSize="16sp" />
-</LinearLayout>
\ No newline at end of file
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="24dp"
+ android:textSize="14sp" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/rounded_button_height"
+ android:id="@+id/enable_work_apps"
+ android:textColor="?attr/workProfileOverlayTextColor"
+ android:text="@string/work_apps_enable_btn_text"
+ android:textAlignment="center"
+ android:background="@drawable/rounded_action_button"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:textSize="14sp" />
+</com.android.launcher3.allapps.WorkPausedCard>
\ No newline at end of file
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
new file mode 100644
index 0000000..04faa15
--- /dev/null
+++ b/res/layout/work_mode_fab.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!-- 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.
+-->
+<com.android.launcher3.allapps.WorkModeSwitch xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/TextHeadline"
+ android:id="@+id/work_mode_toggle"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_height="@dimen/work_fab_height"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:includeFontPadding="false"
+ android:drawableTint="@color/all_apps_tab_text"
+ android:textColor="@color/all_apps_tab_text"
+ android:textSize="14sp"
+ android:background="@drawable/work_apps_toggle_background"
+ android:drawablePadding="8dp"
+ android:drawableStart="@drawable/ic_corp_off"
+ android:layout_marginBottom="@dimen/work_fab_margin"
+ android:layout_marginEnd="@dimen/work_fab_margin"
+ android:text="@string/work_apps_pause_btn_text" />
\ No newline at end of file
diff --git a/res/layout/work_mode_switch.xml b/res/layout/work_mode_switch.xml
deleted file mode 100644
index 31953c7..0000000
--- a/res/layout/work_mode_switch.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<com.android.launcher3.allapps.WorkModeSwitch
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- style="@style/PrimaryHeadline"
- android:id="@+id/work_mode_toggle"
- android:drawableStart="@drawable/ic_corp"
- android:drawablePadding="16dp"
- android:drawableTint="?attr/workProfileOverlayTextColor"
- android:textColor="?attr/workProfileOverlayTextColor"
- android:layout_alignParentBottom="true"
- android:ellipsize="end"
- android:elevation="10dp"
- android:gravity="start"
- android:lines="1"
- android:showText="false"
- android:textSize="@dimen/work_profile_footer_text_size"
- android:background="?attr/allAppsScrimColor"
- android:text="@string/work_profile_toggle_label"
- android:paddingBottom="@dimen/work_profile_footer_padding"
- android:paddingLeft="@dimen/work_profile_footer_padding"
- android:paddingRight="@dimen/work_profile_footer_padding"
- android:paddingTop="@dimen/work_profile_footer_padding"
-/>
diff --git a/res/layout/work_profile_edu.xml b/res/layout/work_profile_edu.xml
deleted file mode 100644
index c3c7010..0000000
--- a/res/layout/work_profile_edu.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<com.android.launcher3.views.WorkEduView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- android:gravity="bottom"
- android:orientation="vertical">
-
- <View
- android:layout_width="match_parent"
- android:layout_height="32dp"
- android:background="@drawable/bottom_sheet_top_border"
- android:backgroundTint="?attr/eduHalfSheetBGColor" />
-
- <LinearLayout
- android:id="@+id/view_wrapper"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="?attr/eduHalfSheetBGColor"
- android:orientation="vertical"
- android:paddingLeft="@dimen/bottom_sheet_edu_padding"
- android:paddingRight="@dimen/bottom_sheet_edu_padding">
-
- <TextView
- android:id="@+id/content_text"
- style="@style/TextHeadline"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="48dp"
- android:layout_marginBottom="48dp"
- android:gravity="center"
- android:text="@string/work_profile_edu_personal_apps"
- android:textAlignment="center"
- android:textColor="@android:color/white"
- android:textSize="20sp" />
-
- <Button
- android:id="@+id/proceed"
- android:layout_width="wrap_content"
- android:layout_height="48dp"
- android:layout_gravity="end"
- android:background="?android:attr/selectableItemBackground"
- android:gravity="center"
- android:text="@string/work_profile_edu_next"
- android:textAlignment="center"
- android:textColor="@android:color/white" />
- </LinearLayout>
-</com.android.launcher3.views.WorkEduView>
\ No newline at end of file
diff --git a/res/mipmap-hdpi/ic_launcher_home.png b/res/mipmap-hdpi/ic_launcher_home.png
deleted file mode 100644
index d068d92..0000000
--- a/res/mipmap-hdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_home.png b/res/mipmap-mdpi/ic_launcher_home.png
deleted file mode 100644
index 16c8ec2..0000000
--- a/res/mipmap-mdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_home.png b/res/mipmap-xhdpi/ic_launcher_home.png
deleted file mode 100644
index 8b2671b..0000000
--- a/res/mipmap-xhdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xxhdpi/ic_launcher_home.png b/res/mipmap-xxhdpi/ic_launcher_home.png
deleted file mode 100644
index 43d8b7d..0000000
--- a/res/mipmap-xxhdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/raw/downgrade_schema.json b/res/raw/downgrade_schema.json
index 8f1780e..bc25cec 100644
--- a/res/raw/downgrade_schema.json
+++ b/res/raw/downgrade_schema.json
@@ -2,8 +2,14 @@
// Note: Comments are not supported in JSON schema, but android parser is lenient.
// Maximum DB version supported by this schema
- "version" : 28,
+ "version" : 29,
+ "downgrade_to_28" : [
+ "ALTER TABLE favorites RENAME TO temp_favorites;",
+ "CREATE TABLE favorites(_id INTEGER PRIMARY KEY, title TEXT, intent TEXT, container INTEGER, screen INTEGER, cellX INTEGER, cellY INTEGER, spanX INTEGER, spanY INTEGER, itemType INTEGER, appWidgetId INTEGER NOT NULL DEFAULT - 1, iconPackage TEXT, iconResource TEXT, icon BLOB, appWidgetProvider TEXT, modified INTEGER NOT NULL DEFAULT 0, restored INTEGER NOT NULL DEFAULT 0, profileId INTEGER DEFAULT 0, rank INTEGER NOT NULL DEFAULT 0, options INTEGER NOT NULL DEFAULT 0);",
+ "INSERT INTO favorites SELECT _id, title, intent, container, screen, cellX, cellY, spanX, spanY, itemType, appWidgetId, iconPackage, iconResource, icon, appWidgetProvider, modified, restored, profileId, rank, options FROM temp_favorites;",
+ "DROP TABLE temp_favorites;"
+ ],
"downgrade_to_27" : [
"CREATE TABLE workspaceScreens (_id INTEGER PRIMARY KEY,screenRank INTEGER,modified INTEGER NOT NULL DEFAULT 0)",
"insert into workspaceScreens (_id, screenRank) select screen as _id, screen as screenRank from favorites where container = -100 group by screen order by screen"
diff --git a/res/values-night-v26/styles.xml b/res/values-night-v26/styles.xml
deleted file mode 100644
index 510e1f4..0000000
--- a/res/values-night-v26/styles.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* 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.
-*/
--->
-
-<resources>
-
- <style name="AppItemActivityTheme" parent="@android:style/Theme.DeviceDefault.Dialog.Alert">
- <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
- </style>
-
-</resources>
\ No newline at end of file
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
new file mode 100644
index 0000000..d41eb7e
--- /dev/null
+++ b/res/values-night/styles.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2008 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+
+<resources>
+ <style name="AddItemActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+ <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
+ <item name="android:windowTranslucentStatus">true</item>
+ </style>
+</resources>
diff --git a/res/values-sw340dp/dimens.xml b/res/values-sw340dp/dimens.xml
index c9f2981..33b06f5 100644
--- a/res/values-sw340dp/dimens.xml
+++ b/res/values-sw340dp/dimens.xml
@@ -19,6 +19,5 @@
<!-- Drag padding to add to the bottom of drop targets -->
<dimen name="drop_target_drag_padding">20dp</dimen>
- <dimen name="drop_target_text_size">16sp</dimen>
</resources>
\ No newline at end of file
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
deleted file mode 100644
index 09bdaaf..0000000
--- a/res/values-sw600dp/config.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<resources>
- <bool name="allow_rotation">true</bool>
-</resources>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index ead666c..47a88f2 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -3,7 +3,7 @@
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ You may obtain a copy of the License at`
http://www.apache.org/licenses/LICENSE-2.0
@@ -15,13 +15,6 @@
-->
<resources>
-<!-- All Apps -->
- <dimen name="all_apps_background_canvas_width">850dp</dimen>
- <dimen name="all_apps_background_canvas_height">525dp</dimen>
-
-<!-- Widget tray -->
- <dimen name="widget_section_indent">56dp</dimen>
-
<!-- DragController -->
<dimen name="drag_flingToDeleteMinVelocity">-1000dp</dimen>
</resources>
diff --git a/res/values-sw720dp/config.xml b/res/values-sw720dp/config.xml
deleted file mode 100644
index 1f401c4..0000000
--- a/res/values-sw720dp/config.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<resources>
- <bool name="config_largeHeap">true</bool>
-
-<!-- All Apps & Widgets -->
- <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
- <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
-
-<!-- Hotseat -->
- <bool name="hotseat_transpose_layout_with_orientation">false</bool>
-</resources>
diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml
deleted file mode 100644
index 691219a..0000000
--- a/res/values-sw720dp/dimens.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<resources>
- <!-- All Apps -->
- <dimen name="all_apps_empty_search_message_top_offset">64dp</dimen>
- <dimen name="all_apps_empty_search_bg_top_offset">180dp</dimen>
-
- <!-- Fast scroll -->
- <dimen name="fastscroll_popup_width">75dp</dimen>
- <dimen name="fastscroll_popup_height">62dp</dimen>
- <dimen name="fastscroll_popup_padding">13dp</dimen>
- <dimen name="fastscroll_popup_text_size">32dp</dimen>
-
-</resources>
diff --git a/res/values-sw720dp/styles.xml b/res/values-sw720dp/styles.xml
deleted file mode 100644
index f322e9f..0000000
--- a/res/values-sw720dp/styles.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright (C) 2008 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
--->
-
-<resources>
-
- <style name="BaseLauncherTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
- <item name="android:windowBackground">@android:color/transparent</item>
- <item name="android:colorBackgroundCacheHint">@null</item>
- <item name="android:windowShowWallpaper">true</item>
- <item name="android:windowNoTitle">true</item>
- <item name="android:windowActionModeOverlay">true</item>
- <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
- </style>
-
- <!-- Workspace -->
- <style name="DropTargetButton" parent="DropTargetButtonBase">
- <item name="android:paddingLeft">60dp</item>
- <item name="android:paddingRight">60dp</item>
- <item name="android:shadowDx">0.0</item>
- <item name="android:shadowDy">0.0</item>
- <item name="android:shadowRadius">2.0</item>
- </style>
-
-</resources>
\ No newline at end of file
diff --git a/res/values-v22/styles.xml b/res/values-v22/styles.xml
deleted file mode 100644
index f86db7a..0000000
--- a/res/values-v22/styles.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* 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.
-*/
--->
-
-<resources>
-
- <style name="AppItemActivityTheme" parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
- <item name="widgetsTheme">@style/WidgetContainerTheme</item>
- </style>
-
-</resources>
\ No newline at end of file
diff --git a/res/values-v26/bools.xml b/res/values-v26/bools.xml
deleted file mode 100644
index ad8c7a1..0000000
--- a/res/values-v26/bools.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 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.
-*/
--->
-
-<resources>
- <bool name="notification_dots_enabled">true</bool>
-
- <bool name="enable_install_shortcut_api">false</bool>
-</resources>
\ No newline at end of file
diff --git a/res/values-v26/styles.xml b/res/values-v26/styles.xml
deleted file mode 100644
index d2f0802..0000000
--- a/res/values-v26/styles.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* 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.
-*/
--->
-<resources>
- <!-- Theme for the widget container. -->
- <style name="WidgetContainerTheme" parent="@android:style/Theme.DeviceDefault.Settings">
- <item name="android:colorPrimaryDark">#E8EAED</item>
- <item name="android:textColorSecondary">?android:attr/textColorPrimary</item>
- <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
- </style>
- <style name="WidgetContainerTheme.Dark" parent="AppTheme.Dark">
- <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
- <item name="android:colorPrimaryDark">#616161</item> <!-- Gray 700 -->
- </style>
-
-</resources>
diff --git a/res/values-v28/dimens.xml b/res/values-v28/dimens.xml
new file mode 100644
index 0000000..3f118cd
--- /dev/null
+++ b/res/values-v28/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <dimen name="dialogCornerRadius">?android:attr/dialogCornerRadius</dimen>
+</resources>
diff --git a/res/values-v28/styles.xml b/res/values-v28/styles.xml
deleted file mode 100644
index 7df9ce5..0000000
--- a/res/values-v28/styles.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright (C) 2019 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
--->
-<resources>
- <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" >
- <item name="android:textFontWeight">400</item>
- </style>
-</resources>
diff --git a/res/values-v29/styles.xml b/res/values-v29/styles.xml
index 7590594..5370b79 100644
--- a/res/values-v29/styles.xml
+++ b/res/values-v29/styles.xml
@@ -26,7 +26,6 @@
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowShowWallpaper">true</item>
- <item name="folderTextColor">?attr/workspaceTextColor</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
diff --git a/res/values-v30/styles.xml b/res/values-v30/styles.xml
index 71740a9..ec5c113 100644
--- a/res/values-v30/styles.xml
+++ b/res/values-v30/styles.xml
@@ -26,7 +26,6 @@
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowShowWallpaper">true</item>
- <item name="folderTextColor">?attr/workspaceTextColor</item>
<item name="android:windowLayoutInDisplayCutoutMode">always</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
new file mode 100644
index 0000000..d73ee57
--- /dev/null
+++ b/res/values-v31/colors.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="popup_color_primary_light">@android:color/system_accent2_50</color>
+ <color name="popup_color_secondary_light">@android:color/system_neutral2_100</color>
+ <color name="popup_color_tertiary_light">@android:color/system_neutral2_300</color>
+ <color name="popup_color_neutral_dark">@android:color/system_neutral1_1000</color>
+ <color name="popup_color_primary_dark">@android:color/system_neutral2_800</color>
+ <color name="popup_color_secondary_dark">@android:color/system_neutral1_900</color>
+ <color name="popup_color_tertiary_dark">@android:color/system_neutral2_700</color>
+
+ <color name="popup_notification_dot_light">@android:color/system_accent1_100</color>
+ <color name="popup_notification_dot_dark">@android:color/system_accent2_600</color>
+
+ <color name="workspace_text_color_light">@android:color/system_neutral1_0</color>
+ <color name="workspace_text_color_dark">@android:color/system_neutral1_1000</color>
+
+ <color name="folder_hint_text_color_light">@android:color/system_neutral1_50</color>
+ <color name="folder_hint_text_color_dark">@android:color/system_neutral2_700</color>
+
+ <color name="text_color_primary_dark">@android:color/system_neutral1_50</color>
+ <color name="text_color_secondary_dark">@android:color/system_neutral2_200</color>
+ <color name="text_color_tertiary_dark">@android:color/system_neutral2_400</color>
+
+ <color name="wallpaper_popup_scrim">@android:color/system_neutral1_900</color>
+
+ <color name="folder_dot_color">@android:color/system_accent2_50</color>
+
+ <color name="workspace_accent_color_light">@android:color/system_accent1_100</color>
+ <color name="workspace_accent_color_dark">@android:color/system_accent2_600</color>
+</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 26e6cba..00cf31c 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
/* Copyright 2008, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,23 +19,27 @@
<!-- Attributes used for launcher theme -->
<attr name="allAppsScrimColor" format="color" />
- <attr name="allAppsInterimScrimAlpha" format="integer" />
+ <attr name="allappsHeaderProtectionColor" format="color" />
<attr name="allAppsNavBarScrimColor" format="color" />
<attr name="allAppsTheme" format="reference" />
<attr name="popupColorPrimary" format="color" />
<attr name="popupColorSecondary" format="color" />
<attr name="popupColorTertiary" format="color" />
+ <attr name="popupShadeFirst" format="color" />
+ <attr name="popupShadeSecond" format="color" />
+ <attr name="popupShadeThird" format="color" />
<attr name="isMainColorDark" format="boolean" />
<attr name="isWorkspaceDarkText" format="boolean" />
<attr name="workspaceTextColor" format="color" />
<attr name="workspaceShadowColor" format="color" />
- <attr name="workspaceAmbientShadowColor" format="color"/>
+ <attr name="workspaceAmbientShadowColor" format="color" />
<attr name="workspaceKeyShadowColor" format="color" />
<attr name="workspaceStatusBarScrim" format="reference" />
<attr name="widgetsTheme" format="reference" />
- <attr name="loadingIconColor" format="color" />
- <attr name="iconOnlyShortcutColor" format="color"/>
- <attr name="eduHalfSheetBGColor" format="color"/>
+ <attr name="iconOnlyShortcutColor" format="color" />
+ <attr name="eduHalfSheetBGColor" format="color" />
+ <attr name="overviewScrimColor" format="color" />
+ <attr name="popupNotificationDotColor" format="color" />
<attr name="folderDotColor" format="color" />
<attr name="folderFillColor" format="color" />
@@ -44,8 +47,10 @@
<attr name="folderIconBorderColor" format="color" />
<attr name="folderTextColor" format="color" />
<attr name="folderHintColor" format="color" />
+ <attr name="isFolderDarkText" format="boolean" />
<attr name="workProfileOverlayTextColor" format="color" />
- <attr name="disabledIconAlpha" format="float" />
+ <attr name="workspaceAccentColor" format="color" />
+ <attr name="dropTargetHoverTextColor" format="color" />
<!-- BubbleTextView specific attributes. -->
<declare-styleable name="BubbleTextView">
@@ -57,6 +62,9 @@
<enum name="folder" value="2" />
<enum name="widget_section" value="3" />
<enum name="shortcut_popup" value="4" />
+ <enum name="taskbar" value="5" />
+ <enum name="search_result_tall" value="6" />
+ <enum name="search_result_small" value="7" />
</attr>
<attr name="centerVertically" format="boolean" />
</declare-styleable>
@@ -68,12 +76,19 @@
<attr name="folderDotColor" />
</declare-styleable>
+ <declare-styleable name="SearchResultSuggestion">
+ <attr name="customIcon" format="reference" />
+ <attr name="matchTextInsetWithQuery" format="boolean" />
+ </declare-styleable>
+
+
<declare-styleable name="ShadowInfo">
<attr name="ambientShadowColor" format="color" />
<attr name="ambientShadowBlur" format="dimension" />
<attr name="keyShadowColor" format="color" />
<attr name="keyShadowBlur" format="dimension" />
- <attr name="keyShadowOffset" format="dimension" />
+ <attr name="keyShadowOffsetX" format="dimension" />
+ <attr name="keyShadowOffsetY" format="dimension" />
</declare-styleable>
<!-- PagedView specific attributes. These attributes are used to customize
@@ -119,13 +134,34 @@
<!-- numFolderRows & numFolderColumns defaults to numRows & numColumns, if not specified -->
<attr name="numFolderRows" format="integer" />
<attr name="numFolderColumns" format="integer" />
- <!-- numHotseatIcons defaults to numColumns, if not specified -->
- <attr name="numHotseatIcons" format="integer" />
- <attr name="dbFile" format="string" />
<!-- numAllAppsColumns defaults to numColumns, if not specified -->
<attr name="numAllAppsColumns" format="integer" />
+ <!-- Number of columns to use when extending the all-apps size,
+ defaults to 2 * numAllAppsColumns -->
+ <attr name="numExtendedAllAppsColumns" format="integer" />
+
+ <!-- numHotseatIcons defaults to numColumns, if not specified -->
+ <attr name="numHotseatIcons" format="integer" />
+ <!-- Number of icons to use when extending the hotseat size,
+ defaults to 2 * numHotseatIcons -->
+ <attr name="numExtendedHotseatIcons" format="integer" />
+
+ <attr name="dbFile" format="string" />
<attr name="defaultLayoutId" format="reference" />
<attr name="demoModeLayoutId" format="reference" />
+ <attr name="isScalable" format="boolean" />
+ <attr name="devicePaddingId" format="reference" />
+
+ </declare-styleable>
+
+ <declare-styleable name="DevicePadding">
+ <attr name="maxEmptySpace" format="dimension" />
+ </declare-styleable>
+
+ <declare-styleable name="DevicePaddingFormula">
+ <attr name="a" format="float|dimension" />
+ <attr name="b" format="float|dimension" />
+ <attr name="c" format="float|dimension" />
</declare-styleable>
<declare-styleable name="ProfileDisplayOption">
@@ -133,18 +169,31 @@
<attr name="minWidthDps" format="float" />
<attr name="minHeightDps" format="float" />
+ <!-- These min cell values are only used if GridDisplayOption#isScalable is true-->
+ <attr name="minCellHeightDps" format="float" />
+ <attr name="minCellWidthDps" format="float" />
+
+ <attr name="borderSpacingDps" format="float" />
+
<attr name="iconImageSize" format="float" />
<!-- landscapeIconSize defaults to iconSize, if not specified -->
<attr name="landscapeIconSize" format="float" />
<attr name="iconTextSize" format="float" />
- <!-- If true, this display option is used to determine the default grid -->
- <attr name="canBeDefault" format="boolean" />
+ <!-- landscapeIconTextSize defaults to iconTextSize, if not specified -->
+ <attr name="landscapeIconTextSize" format="float" />
+
+ <!-- If set, this display option is used to determine the default grid -->
+ <attr name="canBeDefault" format="boolean|integer" >
+ <!-- The profile can be default on split display devices -->
+ <flag name="split_display" value="0x2" />
+ </attr>
<!-- The following values are only enabled if grid is supported. -->
<!-- allAppsIconSize defaults to iconSize, if not specified -->
<attr name="allAppsIconSize" format="float" />
<!-- allAppsIconTextSize defaults to iconTextSize, if not specified -->
<attr name="allAppsIconTextSize" format="float" />
+
</declare-styleable>
<declare-styleable name="CellLayout">
@@ -159,15 +208,27 @@
<attr name="android:src" />
<attr name="android:shadowColor" />
<attr name="android:elevation" />
- <attr name="darkTintColor" format="color"/>
+ <attr name="darkTintColor" format="color" />
</declare-styleable>
<declare-styleable name="RecyclerViewFastScroller">
<attr name="canThumbDetach" format="boolean" />
</declare-styleable>
+ <declare-styleable name="LoggablePref">
+ <attr name="android:key" />
+ <attr name="android:defaultValue" />
+ <!-- Ground truth of this Pref integer can be found in StatsLogManager -->
+ <attr name="logIdOn" format="integer" />
+ <attr name="logIdOff" format="integer" />
+ </declare-styleable>
+
<declare-styleable name="PreviewFragment">
<attr name="android:name" />
<attr name="android:id" />
</declare-styleable>
+
+ <declare-styleable name="WidgetsListRowHeader">
+ <attr name="appIconSize" format="dimension" />
+ </declare-styleable>
</resources>
diff --git a/res/values/bools.xml b/res/values/bools.xml
deleted file mode 100644
index bc2c678..0000000
--- a/res/values/bools.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 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.
-*/
--->
-
-<resources>
- <bool name="notification_dots_enabled">false</bool>
-
- <bool name="enable_install_shortcut_api">true</bool>
-</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 043ad9a..1b68fb6 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,7 +17,8 @@
** limitations under the License.
*/
-->
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<!-- The color tints to apply to the text and drag view when hovering
over the delete target or the info target -->
<color name="delete_target_hover_tint">#FFC1C1C1</color>
@@ -35,11 +36,51 @@
<color name="icon_background">#E0E0E0</color> <!-- Gray 300 -->
- <color name="all_apps_bg_hand_fill">#E5E5E5</color>
- <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
-
<color name="gesture_tutorial_ripple_color">#A0C2F9</color> <!-- Light Blue -->
<color name="gesture_tutorial_fake_task_view_color">#6DA1FF</color> <!-- Light Blue -->
- <color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
- <color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
+ <color name="fake_wallpaper_color_dark_mode">#000000</color> <!-- Black -->
+ <color name="fake_wallpaper_color_light_mode">#f9f9f9</color> <!-- White -->
+ <!-- Must contrast fake_wallpaper_color_dark_mode and fake_wallpaper_color_light_mode -->
+ <color name="gesture_tutorial_fake_previous_task_view_color">#3C4043</color> <!-- Gray -->
+ <color name="gesture_tutorial_action_button_label_color">#FF000000</color>
+ <color name="gesture_tutorial_primary_color">#B7F29F</color> <!-- Light Green -->
+
+ <color name="popup_color_primary_light">#FFF</color>
+ <color name="popup_color_secondary_light">#F1F3F4</color>
+ <color name="popup_color_tertiary_light">#E0E0E0</color> <!-- Gray 300 -->
+ <color name="popup_color_neutral_dark">#3C4043</color> <!-- Gray 800 -->
+ <color name="popup_color_primary_dark">#3C4043</color> <!-- Gray 800 -->
+ <color name="popup_color_secondary_dark">#202124</color>
+ <color name="popup_color_tertiary_dark">#757575</color> <!-- Gray 600 -->
+
+ <color name="popup_shade_first_light">#F9F9F9</color>
+ <color name="popup_shade_second_light">#F1F1F1</color>
+ <color name="popup_shade_third_light">#E2E2E2</color>
+ <color name="popup_shade_first_dark">#303030</color>
+ <color name="popup_shade_second_dark">#262626</color>
+ <color name="popup_shade_third_dark">#1B1B1B</color>
+
+ <color name="popup_notification_dot_light">#FFF</color>
+ <color name="popup_notification_dot_dark">#757575</color>
+
+ <color name="workspace_text_color_light">#FFF</color>
+ <color name="workspace_text_color_dark">#FF000000</color>
+
+ <color name="folder_hint_text_color_light">#FFF</color>
+ <color name="folder_hint_text_color_dark">#FF000000</color>
+
+ <color name="folder_background_light">#F9F9F9</color>
+ <color name="folder_background_dark">#464746</color>
+
+ <color name="folder_dot_color">?attr/colorPrimary</color>
+
+ <color name="text_color_primary_dark">#FFFFFFFF</color>
+ <color name="text_color_secondary_dark">#FFFFFFFF</color>
+ <color name="text_color_tertiary_dark">#CCFFFFFF</color>
+
+ <color name="wallpaper_popup_scrim">?android:attr/colorAccent</color>
+ <color name="wallpaper_scrim_color">#0D878787</color>
+
+ <color name="workspace_accent_color_light">#ff8df5e3</color>
+ <color name="workspace_accent_color_dark">#ff3d665f</color>
</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index ca25325..04c359e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -1,7 +1,6 @@
<resources>
<!-- Miscellaneous -->
<bool name="config_largeHeap">false</bool>
- <bool name="allow_rotation">false</bool>
<integer name="extracted_color_gradient_alpha">153</integer>
@@ -24,7 +23,7 @@
<!-- AllApps & Launcher transitions -->
<!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
- <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
+ <integer name="config_workspaceSpringLoadShrinkPercentage">85</integer>
<!-- The duration of the animation from search hint to text entry -->
<integer name="config_searchHintAnimationDuration">50</integer>
@@ -32,13 +31,16 @@
<!-- View tag key used to store SpringAnimation data. -->
<item type="id" name="spring_animation_tag" />
+ <!-- View tag key used to determine if we should fade in the child views.. -->
+ <string name="popup_container_iterate_children" translatable="false">popup_container_iterate_children</string>
+
<!-- Workspace -->
<!-- The duration (in ms) of the fade animation on the object outlines, used when
we are dragging objects around on the home screen. -->
- <integer name="config_dragOutlineFadeTime">900</integer>
+ <integer name="config_dragOutlineFadeTime">500</integer>
<!-- The alpha value at which to show the most recent drop visualization outline. -->
- <integer name="config_dragOutlineMaxAlpha">128</integer>
+ <integer name="config_dragOutlineMaxAlpha">255</integer>
<!-- Parameters controlling the animation for when an item is dropped on the home screen,
and it animates from its old position to the new one. -->
@@ -55,21 +57,15 @@
<!-- The duration of the caret animation -->
<integer name="config_caretAnimationDuration">200</integer>
- <!-- Hotseat -->
- <bool name="hotseat_transpose_layout_with_orientation">true</bool>
-
<!-- Various classes overriden by projects/build flavors. -->
- <string name="app_filter_class" translatable="false"></string>
- <string name="user_event_dispatcher_class" translatable="false"></string>
<string name="folder_name_provider_class" translatable="false"></string>
<string name="stats_log_manager_class" translatable="false"></string>
- <string name="app_transition_manager_class" translatable="false"></string>
<string name="instant_app_resolver_class" translatable="false"></string>
<string name="main_process_initializer_class" translatable="false"></string>
<string name="app_launch_tracker_class" translatable="false"></string>
<string name="test_information_handler_class" translatable="false"></string>
<string name="launcher_activity_logic_class" translatable="false"></string>
- <string name="prediction_model_class" translatable="false"></string>
+ <string name="model_delegate_class" translatable="false"></string>
<!-- View ID to use for QSB widget -->
<item type="id" name="qsb_widget" />
@@ -80,18 +76,15 @@
<!-- View IDs to store item highlight information -->
<item type="id" name="view_unhighlight_background" />
+ <!-- view ID used to restore work tab state -->
+ <item type="id" name="work_tab_state_id" />
+
<!-- Menu id for feature flags -->
<item type="id" name="menu_apply_flags" />
- <!-- Popup items -->
- <integer name="config_popupOpenCloseDuration">150</integer>
- <integer name="config_popupArrowOpenCloseDuration">40</integer>
- <integer name="config_removeNotificationViewDuration">300</integer>
-
<!-- Default packages -->
<string name="wallpaper_picker_package" translatable="false"></string>
- <string name="calendar_component_name" translatable="false"></string>
- <string name="clock_component_name" translatable="false"></string>
+ <string name="local_colors_extraction_class" translatable="false"></string>
<!-- Accessibility actions -->
<item type="id" name="action_remove" />
@@ -113,6 +106,7 @@
<!-- QSB IDs. DO not change -->
<item type="id" name="search_container_workspace" />
<item type="id" name="search_container_all_apps" />
+ <item type="id" name="search_container_hotseat" />
<!-- Recents -->
<item type="id" name="overview_panel"/>
@@ -137,12 +131,49 @@
<item name="swipe_up_rect_scale_stiffness" type="dimen" format="float">200</item>
<item name="swipe_up_rect_xy_fling_friction" type="dimen" format="float">1.5</item>
+
+ <item name="swipe_up_scale_start" type="dimen" format="float">0.88</item>
+ <item name="swipe_up_duration" type="dimen" format="float">400</item>
+
+ <item name="swipe_up_trans_y_dp" type="dimen" format="float">4.5</item>
+ <item name="swipe_up_trans_y_dp_per_s" type="dimen" format="float">3</item>
+
+ <item name="swipe_up_trans_y_damping" type="dimen" format="float">0.45</item>
+ <item name="swipe_up_trans_y_stiffness" type="dimen" format="float">200</item>
+
<item name="swipe_up_rect_xy_damping_ratio" type="dimen" format="float">0.8</item>
<item name="swipe_up_rect_xy_stiffness" type="dimen" format="float">200</item>
+
+ <item name="swipe_up_rect_2_x_damping_ratio" type="dimen" format="float">1</item>
+ <item name="swipe_up_rect_2_x_stiffness" type="dimen" format="float">250</item>
+
+ <item name="swipe_up_rect_2_y_damping_ratio" type="dimen" format="float">1</item>
+ <item name="swipe_up_rect_2_y_stiffness" type="dimen" format="float">600</item>
+
+ <item name="swipe_up_rect_2_y_stiffness_low_swipe_multiplier" type="dimen" format="float">0.8</item>
+ <item name="swipe_up_low_swipe_duration_multiplier" type="dimen" format="float">1</item>
+
+ <item name="swipe_up_launcher_alpha_max_progress" type="dimen" format="float">0.85</item>
+
+
+ <item name="c1_a" type="dimen" format="float">0.05</item>
+ <item name="c1_b" type="dimen" format="float">0</item>
+ <item name="c1_c" type="dimen" format="float">0.133333</item>
+ <item name="c1_d" type="dimen" format="float">0.06</item>
+
+ <item name="mp_x" type="dimen" format="float">0.166666</item>
+ <item name="mp_y" type="dimen" format="float">.4</item>
+
+ <item name="c2_a" type="dimen" format="float">0.208333</item>
+ <item name="c2_b" type="dimen" format="float">.82</item>
+ <item name="c2_c" type="dimen" format="float">.25</item>
+ <item name="c2_d" type="dimen" format="float">1</item>
+
+
<item name="staggered_damping_ratio" type="dimen" format="float">0.7</item>
<item name="staggered_stiffness" type="dimen" format="float">150</item>
- <dimen name="unlock_staggered_velocity_dp_per_s">3dp</dimen>
+ <dimen name="unlock_staggered_velocity_dp_per_s">4dp</dimen>
<item name="hint_scale_damping_ratio" type="dimen" format="float">0.7</item>
<item name="hint_scale_stiffness" type="dimen" format="float">200</item>
@@ -154,37 +185,37 @@
<dimen name="swipe_up_max_workspace_trans_y">-60dp</dimen>
<array name="dynamic_resources">
- <item>@dimen/all_apps_spring_damping_ratio</item>
- <item>@dimen/all_apps_spring_stiffness</item>
+ <item>@dimen/swipe_up_duration</item>
+ <item>@dimen/swipe_up_scale_start</item>
+ <item>@dimen/swipe_up_trans_y_dp</item>
+ <item>@dimen/swipe_up_trans_y_dp_per_s</item>
+ <item>@dimen/swipe_up_trans_y_damping</item>
+ <item>@dimen/swipe_up_trans_y_stiffness</item>
+ <item>@dimen/swipe_up_rect_2_x_damping_ratio</item>
+ <item>@dimen/swipe_up_rect_2_x_stiffness</item>
+ <item>@dimen/swipe_up_rect_2_y_damping_ratio</item>
+ <item>@dimen/swipe_up_rect_2_y_stiffness</item>
+ <item>@dimen/swipe_up_launcher_alpha_max_progress</item>
+ <item>@dimen/swipe_up_rect_2_y_stiffness_low_swipe_multiplier</item>
+ <item>@dimen/swipe_up_low_swipe_duration_multiplier</item>
- <item>@dimen/dismiss_task_trans_y_damping_ratio</item>
- <item>@dimen/dismiss_task_trans_y_stiffness</item>
+ <item>@dimen/c1_a</item>
+ <item>@dimen/c1_b</item>
+ <item>@dimen/c1_c</item>
+ <item>@dimen/c1_d</item>
- <item>@dimen/dismiss_task_trans_x_damping_ratio</item>
- <item>@dimen/dismiss_task_trans_x_stiffness</item>
+ <item>@dimen/mp_x</item>
+ <item>@dimen/mp_y</item>
- <item>@dimen/horizontal_spring_damping_ratio</item>
- <item>@dimen/horizontal_spring_stiffness</item>
-
- <item>@dimen/swipe_up_rect_scale_damping_ratio</item>
- <item>@dimen/swipe_up_rect_scale_stiffness</item>
-
- <item>@dimen/swipe_up_rect_xy_fling_friction</item>
- <item>@dimen/swipe_up_rect_xy_damping_ratio</item>
- <item>@dimen/swipe_up_rect_xy_stiffness</item>
-
- <item>@dimen/staggered_damping_ratio</item>
- <item>@dimen/staggered_stiffness</item>
- <item>@dimen/unlock_staggered_velocity_dp_per_s</item>
-
- <item>@dimen/swipe_up_fling_min_visible_change</item>
- <item>@dimen/swipe_up_y_overshoot</item>
-
- <item>@dimen/hint_scale_damping_ratio</item>
- <item>@dimen/hint_scale_stiffness</item>
- <item>@dimen/hint_scale_velocity_dp_per_s</item>
+ <item>@dimen/c2_a</item>
+ <item>@dimen/c2_b</item>
+ <item>@dimen/c2_c</item>
+ <item>@dimen/c2_d</item>
</array>
- <string-array name="live_wallpapers_remove_sysui_scrims">
- </string-array>
+ <string-array name="filtered_components" ></string-array>
+
+ <!-- Name of the class used to generate colors from the wallpaper colors. Must be implementing the LauncherAppWidgetHostView.ColorGenerator interface. -->
+ <string name="color_generator_class" translatable="false"/>
+
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 947e635..f434644 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,13 +20,17 @@
<!-- Dynamic Grid -->
<dimen name="dynamic_grid_edge_margin">8dp</dimen>
- <dimen name="dynamic_grid_icon_drawable_padding">8dp</dimen>
+ <dimen name="dynamic_grid_left_right_margin">8dp</dimen>
+ <dimen name="dynamic_grid_icon_drawable_padding">7dp</dimen>
<!-- Minimum space between workspace and hotseat in spring loaded mode -->
<dimen name="dynamic_grid_min_spring_loaded_space">8dp</dimen>
+ <dimen name="dynamic_grid_cell_border_spacing">16dp</dimen>
<dimen name="dynamic_grid_cell_layout_padding">5.5dp</dimen>
<dimen name="dynamic_grid_cell_padding_x">8dp</dimen>
+ <dimen name="two_panel_home_side_padding">18dp</dimen>
+
<!-- Hotseat -->
<dimen name="dynamic_grid_hotseat_top_padding">8dp</dimen>
<dimen name="dynamic_grid_hotseat_bottom_padding">2dp</dimen>
@@ -35,26 +39,30 @@
<dimen name="dynamic_grid_hotseat_extra_vertical_size">34dp</dimen>
<dimen name="dynamic_grid_hotseat_side_padding">0dp</dimen>
+ <!-- Scalable Grid -->
+ <dimen name="scalable_grid_left_right_margin">22dp</dimen>
+ <dimen name="scalable_grid_qsb_bottom_margin">42dp</dimen>
+
<!-- Workspace page indicator -->
<dimen name="workspace_page_indicator_height">24dp</dimen>
<dimen name="workspace_page_indicator_line_height">1dp</dimen>
<dimen name="workspace_page_indicator_overlap_workspace">0dp</dimen>
- <!-- Hotseat/all-apps scrim -->
- <dimen name="all_apps_scrim_blur">4dp</dimen>
- <dimen name="vertical_drag_handle_width">18dp</dimen>
- <dimen name="vertical_drag_handle_height">6dp</dimen>
- <dimen name="vertical_drag_handle_elevation">1dp</dimen>
- <dimen name="vertical_drag_handle_touch_size">48dp</dimen>
- <dimen name="vertical_drag_handle_padding_in_vertical_bar_layout">16dp</dimen>
-
<!-- Drop target bar -->
- <dimen name="dynamic_grid_drop_target_size">48dp</dimen>
+ <dimen name="dynamic_grid_drop_target_size">52dp</dimen>
<dimen name="drop_target_vertical_gap">20dp</dimen>
<!-- App Widget resize frame -->
<dimen name="widget_handle_margin">13dp</dimen>
<dimen name="resize_frame_background_padding">24dp</dimen>
+ <dimen name="resize_frame_margin">22dp</dimen>
+
+ <!-- App widget reconfigure button -->
+ <dimen name="widget_reconfigure_button_corner_radius">14dp</dimen>
+ <dimen name="widget_reconfigure_button_padding">6dp</dimen>
+ <dimen name="widget_reconfigure_button_margin">32dp</dimen>
+ <dimen name="widget_reconfigure_button_size">36dp</dimen>
+ <dimen name="widget_reconfigure_tip_top_margin">16dp</dimen>
<!-- Fast scroll -->
<dimen name="fastscroll_track_min_width">6dp</dimen>
@@ -78,24 +86,30 @@
<dimen name="fastscroll_end_margin">-26dp</dimen>
<!-- All Apps -->
+ <dimen name="all_apps_open_vertical_translate">320dp</dimen>
<dimen name="all_apps_search_bar_field_height">48dp</dimen>
<dimen name="all_apps_search_bar_bottom_padding">30dp</dimen>
<dimen name="all_apps_empty_search_message_top_offset">40dp</dimen>
<dimen name="all_apps_empty_search_bg_top_offset">144dp</dimen>
<dimen name="all_apps_background_canvas_width">700dp</dimen>
<dimen name="all_apps_background_canvas_height">475dp</dimen>
- <dimen name="all_apps_header_tab_height">50dp</dimen>
+ <dimen name="all_apps_header_pill_height">48dp</dimen>
+ <dimen name="all_apps_header_pill_corner_radius">12dp</dimen>
+ <dimen name="all_apps_header_tab_height">48dp</dimen>
<dimen name="all_apps_tabs_indicator_height">2dp</dimen>
<dimen name="all_apps_header_top_padding">36dp</dimen>
+ <dimen name="all_apps_header_bottom_padding">6dp</dimen>
<dimen name="all_apps_work_profile_tab_footer_top_padding">16dp</dimen>
<dimen name="all_apps_work_profile_tab_footer_bottom_padding">20dp</dimen>
- <dimen name="all_apps_tabs_side_padding">12dp</dimen>
- <dimen name="all_apps_divider_height">1dp</dimen>
+ <dimen name="all_apps_tabs_button_horizontal_padding">4dp</dimen>
+ <dimen name="all_apps_tabs_vertical_padding">6dp</dimen>
+ <dimen name="all_apps_divider_height">2dp</dimen>
+ <dimen name="all_apps_divider_width">128dp</dimen>
<dimen name="all_apps_tip_bottom_margin">8dp</dimen>
<!-- The size of corner radius of the arrow in the arrow toast. -->
<dimen name="arrow_toast_corner_radius">2dp</dimen>
-
+ <dimen name="arrow_toast_arrow_width">10dp</dimen>
<!-- Search bar in All Apps -->
<dimen name="all_apps_header_max_elevation">3dp</dimen>
@@ -104,12 +118,47 @@
<dimen name="all_apps_divider_margin_vertical">8dp</dimen>
+<!-- Floating action button inside work tab to toggle work profile -->
+ <dimen name="work_fab_height">56dp</dimen>
+ <dimen name="work_fab_radius">28dp</dimen>
+ <dimen name="work_card_padding_horizontal">24dp</dimen>
+ <dimen name="work_card_button_height">52dp</dimen>
+ <dimen name="work_fab_margin">16dp</dimen>
<dimen name="work_profile_footer_padding">20dp</dimen>
<dimen name="work_profile_footer_text_size">16sp</dimen>
+ <dimen name="work_edu_card_margin">16dp</dimen>
+ <dimen name="work_edu_card_radius">28dp</dimen>
-<!-- Widget tray -->
- <dimen name="widget_preview_label_vertical_padding">8dp</dimen>
- <dimen name="widget_preview_label_horizontal_padding">16dp</dimen>
+ <!-- rounded button shown inside card views, and snack bars -->
+ <dimen name="padded_rounded_button_height">48dp</dimen>
+ <dimen name="rounded_button_height">32dp</dimen>
+ <dimen name="rounded_button_radius">16dp</dimen>
+ <dimen name="rounded_button_padding">8dp</dimen>
+
+
+ <!-- Widget tray -->
+ <dimen name="widget_cell_vertical_padding">8dp</dimen>
+ <dimen name="widget_cell_horizontal_padding">16dp</dimen>
+ <dimen name="widget_cell_font_size">14sp</dimen>
+
+ <dimen name="widget_tabs_button_horizontal_padding">4dp</dimen>
+ <dimen name="widget_tabs_horizontal_margin">32dp</dimen>
+ <dimen name="widget_apps_header_pill_height">48dp</dimen>
+ <dimen name="widget_apps_tabs_vertical_padding">6dp</dimen>
+
+ <dimen name="recommended_widgets_table_vertical_padding">8dp</dimen>
+
+ <!-- Bottom margin for the search and recommended widgets container without work profile -->
+ <dimen name="search_and_recommended_widgets_container_bottom_margin">16dp</dimen>
+ <!-- Bottom margin for the search and recommended widgets container with work profile -->
+ <dimen name="search_and_recommended_widgets_container_small_bottom_margin">10dp</dimen>
+
+ <dimen name="widget_list_top_bottom_corner_radius">28dp</dimen>
+ <dimen name="widget_list_content_corner_radius">4dp</dimen>
+
+ <dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
+ <dimen name="widget_list_entry_spacing">2dp</dimen>
+ <dimen name="widget_list_horizontal_margin">16dp</dimen>
<dimen name="widget_preview_shadow_blur">0.5dp</dimen>
<dimen name="widget_preview_key_shadow_distance">1dp</dimen>
@@ -121,21 +170,32 @@
<dimen name="widget_section_icon_size">40dp</dimen>
<dimen name="widget_section_vertical_padding">8dp</dimen>
<dimen name="widget_section_horizontal_padding">16dp</dimen>
- <dimen name="widget_section_indent">0dp</dimen>
<dimen name="widget_row_padding">8dp</dimen>
<dimen name="widget_row_divider">2dp</dimen>
+ <dimen name="widget_picker_education_tip_max_width">308dp</dimen>
+ <dimen name="widget_picker_education_tip_min_margin">4dp</dimen>
+
+ <dimen name="widget_picker_view_pager_top_padding">10dp</dimen>
+
<!-- Padding applied to shortcut previews -->
<dimen name="shortcut_preview_padding_left">0dp</dimen>
<dimen name="shortcut_preview_padding_right">0dp</dimen>
<dimen name="shortcut_preview_padding_top">0dp</dimen>
+<!-- Pin widget dialog -->
+ <dimen name="pin_widget_button_padding_horizontal">8dp</dimen>
+ <dimen name="pin_widget_button_padding_vertical">4dp</dimen>
+ <dimen name="pin_widget_button_inset_horizontal">4dp</dimen>
+ <dimen name="pin_widget_button_inset_vertical">6dp</dimen>
+
<!-- Dragging -->
<!-- Drag padding to add to the bottom of drop targets -->
<dimen name="drop_target_drag_padding">14dp</dimen>
- <dimen name="drop_target_text_size">14sp</dimen>
+ <dimen name="drop_target_text_size">16sp</dimen>
<dimen name="drop_target_shadow_elevation">2dp</dimen>
+ <dimen name="drop_target_bar_margin_horizontal">4dp</dimen>
<!-- the distance an icon must be dragged before button drop targets accept it -->
<dimen name="drag_distanceThreshold">30dp</dimen>
@@ -146,17 +206,20 @@
<dimen name="drag_flingToDeleteMinVelocity">-1500dp</dimen>
- <dimen name="spring_loaded_panel_border">1dp</dimen>
+ <dimen name="spring_loaded_panel_border">2dp</dimen>
+ <dimen name="keyboard_drag_stroke_width">4dp</dimen>
<!-- Folders -->
<dimen name="page_indicator_dot_size">8dp</dimen>
<dimen name="folder_cell_x_padding">9dp</dimen>
<dimen name="folder_cell_y_padding">6dp</dimen>
- <dimen name="folder_child_text_size">13sp</dimen>
- <dimen name="folder_label_padding_top">4dp</dimen>
- <dimen name="folder_label_padding_bottom">12dp</dimen>
- <dimen name="folder_label_text_size">14sp</dimen>
+ <!-- label text size = workspace text size multiplied by this scale -->
+ <dimen name="folder_label_text_scale">1.14</dimen>
+ <dimen name="folder_label_height">48dp</dimen>
+
+ <dimen name="folder_content_padding_left_right">8dp</dimen>
+ <dimen name="folder_content_padding_top">16dp</dimen>
<!-- Sizes for managed profile badges -->
<dimen name="profile_badge_size">24dp</dimen>
@@ -174,63 +237,53 @@
<dimen name="pending_widget_elevation">2dp</dimen>
<!-- Deep shortcuts -->
- <dimen name="deep_shortcuts_elevation">9dp</dimen>
- <dimen name="bg_popup_item_width">220dp</dimen>
+ <dimen name="deep_shortcuts_elevation">2dp</dimen>
+ <dimen name="bg_popup_padding">2dp</dimen>
+ <dimen name="bg_popup_item_width">216dp</dimen>
<dimen name="bg_popup_item_height">56dp</dimen>
- <dimen name="bg_popup_item_condensed_height">48dp</dimen>
<dimen name="pre_drag_view_scale">6dp</dimen>
<!-- an icon with shortcuts must be dragged this far before the container is removed. -->
<dimen name="deep_shortcuts_start_drag_threshold">16dp</dimen>
- <dimen name="deep_shortcut_icon_size">36dp</dimen>
- <dimen name="deep_shortcut_drawable_padding">8dp</dimen>
+ <dimen name="deep_shortcut_icon_size">32dp</dimen>
+ <dimen name="popup_margin">2dp</dimen>
+ <dimen name="popup_single_item_radius">100dp</dimen>
+ <dimen name="popup_smaller_radius">4dp</dimen>
+ <dimen name="deep_shortcut_drawable_padding">12dp</dimen>
<dimen name="deep_shortcut_drag_handle_size">16dp</dimen>
<dimen name="popup_padding_start">10dp</dimen>
<dimen name="popup_padding_end">16dp</dimen>
<dimen name="popup_vertical_padding">4dp</dimen>
- <dimen name="popup_arrow_width">10dp</dimen>
- <dimen name="popup_arrow_height">8dp</dimen>
- <dimen name="popup_arrow_vertical_offset">-2dp</dimen>
+ <dimen name="popup_arrow_width">12dp</dimen>
+ <dimen name="popup_arrow_height">10dp</dimen>
+ <dimen name="popup_arrow_vertical_offset">-1dp</dimen>
<!-- popup_padding_start + deep_shortcut_icon_size / 2 -->
- <dimen name="popup_arrow_horizontal_center_start">28dp</dimen>
- <!-- popup_padding_end + deep_shortcut_drag_handle_size / 2 -->
- <dimen name="popup_arrow_horizontal_center_end">24dp</dimen>
+ <dimen name="popup_arrow_horizontal_center_offset">26dp</dimen>
<dimen name="popup_arrow_corner_radius">2dp</dimen>
<!-- popup_padding_start + icon_size + 10dp -->
- <dimen name="deep_shortcuts_text_padding_start">56dp</dimen>
- <!-- popup_item_width - deep_shortcuts_text_padding_start -->
- <dimen name="deep_shortcuts_divider_width">164dp</dimen>
+ <dimen name="deep_shortcuts_text_padding_start">52dp</dimen>
<dimen name="system_shortcut_icon_size">24dp</dimen>
- <!-- popup_arrow_center_start - system_shortcut_icon_size / 2 -->
+ <!-- popup_arrow_horizontal_center_offset - system_shortcut_icon_size / 2 -->
<dimen name="system_shortcut_margin_start">16dp</dimen>
- <dimen name="system_shortcut_header_height">48dp</dimen>
+ <dimen name="system_shortcut_header_height">56dp</dimen>
<dimen name="system_shortcut_header_icon_touch_size">48dp</dimen>
<!-- (touch_size - icon_size) / 2 -->
<dimen name="system_shortcut_header_icon_padding">12dp</dimen>
<!-- Notifications -->
<dimen name="bg_round_rect_radius">8dp</dimen>
- <dimen name="notification_padding_start">16dp</dimen>
- <dimen name="notification_padding_end">12dp</dimen>
- <!-- notification_padding_end + (icon_size - footer_icon_size) / 2 -->
- <dimen name="notification_footer_icon_row_padding">15dp</dimen>
- <dimen name="notification_header_height">36dp</dimen>
- <dimen name="notification_main_height">84dp</dimen>
- <dimen name="notification_footer_height">32dp</dimen>
- <!-- How much space to keep as padding for the last notification when the footer collapses -->
- <dimen name="notification_empty_footer_height">6dp</dimen>
- <dimen name="notification_header_text_size">13sp</dimen>
+ <dimen name="notification_padding">16dp</dimen>
+ <dimen name="notification_padding_top">18dp</dimen>
+ <dimen name="notification_header_text_size">14sp</dimen>
<dimen name="notification_header_count_text_size">12sp</dimen>
- <dimen name="notification_main_title_size">16sp</dimen>
+ <dimen name="notification_main_title_size">14sp</dimen>
<dimen name="notification_main_text_size">14sp</dimen>
- <dimen name="notification_icon_size">24dp</dimen>
- <dimen name="notification_footer_icon_size">18dp</dimen>
- <!-- notification_icon_size + notification_padding_end + 16dp padding between icon and text -->
- <dimen name="notification_main_text_padding_end">52dp</dimen>
+ <dimen name="notification_circle_icon_size">24dp</dimen>
+ <dimen name="notification_icon_size">32dp</dimen>
+ <!-- Space between edge and icon and icon and text -->
+ <dimen name="notification_icon_padding">12dp</dimen>
+ <!-- notification_icon_padding + notification_icon_size + notification_icon_padding -->
+ <dimen name="notification_main_text_padding_start">56dp</dimen>
<dimen name="horizontal_ellipsis_size">18dp</dimen>
- <!-- arrow_horizontal_offset_start - (ellipsis_size - arrow_width) / 2 -->
- <dimen name="horizontal_ellipsis_offset">19dp</dimen>
- <dimen name="popup_item_divider_height">0.5dp</dimen>
- <dimen name="swipe_helper_falsing_threshold">70dp</dimen>
<!-- Overview -->
<dimen name="options_menu_icon_size">24dp</dimen>
@@ -247,10 +300,35 @@
<dimen name="snackbar_min_text_size">12sp</dimen>
<dimen name="snackbar_max_text_size">14sp</dimen>
+<!-- Developer Options -->
+ <dimen name="developer_options_filter_margins">10dp</dimen>
+
<!-- Theming related -->
<dimen name="default_dialog_corner_radius">8dp</dimen>
+ <dimen name="dialogCornerRadius">@dimen/default_dialog_corner_radius</dimen>
<!-- Onboarding bottomsheet related -->
<dimen name="bottom_sheet_edu_padding">24dp</dimen>
+<!-- Taskbar related (placeholders to compile in Launcher3 without Quickstep) -->
+ <dimen name="taskbar_size">0dp</dimen>
+
+ <!-- Size of the maximum radius for the enforced rounded rectangles. -->
+ <dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
+
+<!-- Overview placeholder to compile in Launcer3 without Quickstep -->
+ <dimen name="task_thumbnail_icon_size">0dp</dimen>
+ <dimen name="task_thumbnail_icon_size_grid">0dp</dimen>
+ <dimen name="overview_task_margin">0dp</dimen>
+ <dimen name="overview_actions_bottom_margin_gesture">0dp</dimen>
+ <dimen name="overview_actions_bottom_margin_three_button">0dp</dimen>
+
+<!-- Workspace grid visualization parameters -->
+ <dimen name="grid_visualization_rounding_radius">22dp</dimen>
+ <dimen name="grid_visualization_cell_spacing">6dp</dimen>
+
+<!-- Search results related parameters -->
+ <dimen name="search_row_icon_size">48dp</dimen>
+ <dimen name="search_row_small_icon_size">32dp</dimen>
+ <dimen name="padded_rounded_button_padding">8dp</dimen>
</resources>
diff --git a/res/values/drawables.xml b/res/values/drawables.xml
deleted file mode 100644
index 7d63142..0000000
--- a/res/values/drawables.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<resources>
- <drawable name="ic_setup_shadow">@drawable/ic_setting</drawable>
- <drawable name="ic_remove_shadow">@drawable/ic_remove_no_shadow</drawable>
- <drawable name="ic_uninstall_shadow">@drawable/ic_uninstall_no_shadow</drawable>
- <drawable name="ic_block_shadow">@drawable/ic_block_no_shadow</drawable>
- <drawable name="all_apps_arrow_shadow">@drawable/drag_handle_indicator_no_shadow</drawable>
-</resources>
\ No newline at end of file
diff --git a/res/values/id.xml b/res/values/id.xml
new file mode 100644
index 0000000..1709c59
--- /dev/null
+++ b/res/values/id.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <item type="id" name="apps_list_view_work" />
+ <item type="id" name="tag_widget_entry" />
+ <item type="id" name="view_type_widgets_list" />
+ <item type="id" name="view_type_widgets_header" />
+ <item type="id" name="view_type_widgets_search_header" />
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 935bb40..d1774e5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -36,24 +36,92 @@
<!-- Message shown when a shortcut is not available. It could have been temporarily disabled and may start working again after some time. -->
<string name="shortcut_not_available">Shortcut isn\'t available</string>
<!-- User visible name for the launcher/home screen. [CHAR_LIMIT=30] -->
- <string name="home_screen">Home screen</string>
- <!-- Label for showing custom action list of a shortcut or widget. [CHAR_LIMIT=30] -->
- <string name="custom_actions">Custom actions</string>
+ <string name="home_screen">Home</string>
+
+ <!-- Options for recent tasks -->
+ <!-- Title for an option to enter split screen mode for a given app -->
+ <string name="recent_task_option_split_screen">Split screen</string>
+ <string translatable="false" name="split_screen_position_top">Pin to top</string>
+ <string translatable="false" name="split_screen_position_left">Pin to left</string>
+ <string translatable="false" name="split_screen_position_right">Pin to right</string>
<!-- Widgets -->
<!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->
- <string name="long_press_widget_to_add">Touch & hold to pick up a widget.</string>
+ <string name="long_press_widget_to_add">Touch & hold to move a widget.</string>
<!-- Accessibility spoken hint message in widget picker, which allows user to add a widget. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=100] -->
- <string name="long_accessible_way_to_add">Double-tap & hold to pick up a widget or use custom actions.</string>
+ <string name="long_accessible_way_to_add">Double-tap & hold to move a widget or use custom actions.</string>
<!-- The format string for the dimensions of a widget in the drawer -->
<!-- There is a special version of this format string for Farsi -->
<string name="widget_dims_format">%1$d \u00d7 %2$d</string>
<!-- Accessibility spoken message format for the dimensions of a widget in the drawer -->
<string name="widget_accessible_dims_format">%1$d wide by %2$d high</string>
- <!-- Message to tell the user to press and hold a widget/icon to add it -->
- <string name="add_item_request_drag_hint">Touch & hold to place manually</string>
- <!-- Button label to automatically add icon on home screen [CHAR_LIMIT=50] -->
- <string name="place_automatically">Add automatically</string>
+ <!-- Spoken text for a screen reader. The placeholder text is the widget name.
+ [CHAR_LIMIT=none]-->
+ <string name="widget_preview_context_description"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget</string>
+ <!-- Message to tell the user to press and hold a widget/icon to add it to the home screen.
+ [CHAR LIMIT=NONE] -->
+ <string name="add_item_request_drag_hint">Touch & hold the widget to move it around the Home screen</string>
+ <!-- Button label to automatically add a widget to home screen [CHAR_LIMIT=50] -->
+ <string name="add_to_home_screen">Add to Home screen</string>
+ <!-- Accessibility spoken message announced when a widget gets added to the home screen using a
+ button in a dialog. [CHAR_LIMIT=none] -->
+ <string name="added_to_home_screen_accessibility_text"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget added to home screen</string>
+ <!-- Label for showing the number of widgets an app has in the full widgets picker.
+ [CHAR_LIMIT=25] -->
+ <plurals name="widgets_count">
+ <item quantity="one"><xliff:g id="widgets_count" example="1">%1$d</xliff:g> widget</item>
+ <item quantity="other"><xliff:g id="widgets_count" example="2">%1$d</xliff:g> widgets</item>
+ </plurals>
+ <!-- Label for showing the number of shortcut an app has in the full widgets picker.
+ [CHAR_LIMIT=25] -->
+ <plurals name="shortcuts_count">
+ <item quantity="one"><xliff:g id="shortcuts_count" example="1">%1$d</xliff:g> shortcut</item>
+ <item quantity="other"><xliff:g id="shortcuts_count" example="2">%1$d</xliff:g> shortcuts</item>
+ </plurals>
+ <!-- Label for showing both the number of widgets and shortcuts an app has in the full widgets
+ picker. [CHAR_LIMIT=50] -->
+ <string name="widgets_and_shortcuts_count"><xliff:g id="widgets_count" example="5 widgets">%1$s</xliff:g>, <xliff:g id="shortcuts_count" example="1 shortcut">%2$s</xliff:g></string>
+
+ <!-- Text for both the tile of a popup view, which shows all available widgets installed on
+ the device, and the text of a button, which opens this popup view. [CHAR LIMIT=30]-->
+ <string name="widget_button_text">Widgets</string>
+ <!-- Search bar text shown in the popup view showing all available widgets installed on the
+ device. [CHAR_LIMIT=50] -->
+ <string name="widgets_full_sheet_search_bar_hint">Search</string>
+ <!-- Spoken text for screen readers. This text lets a user know that the button is used to clear
+ the text that the user entered in the search box. [CHAR_LIMIT=none] -->
+ <string name="widgets_full_sheet_cancel_button_description">Clear text from search box</string>
+ <!-- Text shown when there are no widgets or shortcuts that can be added to the device.
+ [CHAR_LIMIT=none] -->
+ <string name="no_widgets_available">Widgets and shortcuts aren\'t available</string>
+ <!-- Text shown when there are no matching widget or shortcut search results for user's search
+ query. [CHAR_LIMIT=none] -->
+ <string name="no_search_results">No widgets or shortcuts found</string>
+ <!-- Tab label. A user can tap this tab to access their personal widgets. [CHAR_LIMIT=25] -->
+ <string name="widgets_full_sheet_personal_tab">Personal</string>
+ <!-- Tab label. A user can tap this tab to access their work widgets. [CHAR_LIMIT=25] -->
+ <string name="widgets_full_sheet_work_tab">Work</string>
+
+ <!-- A widget category label for grouping widgets related to conversations. [CHAR_LIMIT=30] -->
+ <string name="widget_category_conversations">Conversations</string>
+
+ <!-- Title of a dialog. This dialog lets a user know how they can use widgets on their phone.
+ [CHAR_LIMIT=NONE] -->
+ <string name="widget_education_header">Useful info at your fingertips</string>
+ <!-- Dialog text. This dialog lets a user know how they can use widgets on their phone.
+ [CHAR_LIMIT=NONE] -->
+ <string name="widget_education_content">To get info without opening apps, you can add widgets to your Home screen</string>
+
+ <!-- Text on an educational tip on widget informing users that they can change widget settings.
+ [CHAR_LIMIT=NONE] -->
+ <string name="reconfigurable_widget_education_tip">Tap to change widget settings</string>
+
+ <!-- Text on the button that closes the education dialog about widgets. [CHAR_LIMIT=50] -->
+ <string name="widget_education_close_button">Got it</string>
+
+ <!-- Spoken text for screen readers. This text is for an icon that lets the user change a
+ widget's settings. [CHAR_LIMIT=50] -->
+ <string name="widget_reconfigure_button_content_description">Change widget settings</string>
<!-- All Apps -->
<!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
@@ -73,13 +141,13 @@
<!-- Drag and drop -->
<!-- Message to tell the user to press and hold on a shortcut to add it [CHAR_LIMIT=50] -->
- <string name="long_press_shortcut_to_add">Touch & hold to pick up a shortcut.</string>
+ <string name="long_press_shortcut_to_add">Touch & hold to move a shortcut.</string>
<!-- Accessibility spoken hint message in deep shortcut menu, which allows user to add a shortcut. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=200] -->
- <string name="long_accessible_way_to_add_shortcut">Double-tap & hold to pick up a shortcut or use custom actions.</string>
+ <string name="long_accessible_way_to_add_shortcut">Double-tap & hold to move a shortcut or use custom actions.</string>
<skip />
- <!-- Error message when user has filled a home screen -->
- <string name="out_of_space">No more room on this Home screen.</string>
+ <!-- Error message when a user can't add more apps, widgets, or shortcuts to a Home screen. -->
+ <string name="out_of_space">No room on this Home screen</string>
<!-- Error message when user has filled the hotseat -->
<string name="hotseat_out_of_space">No more room in the Favorites tray</string>
@@ -88,10 +156,6 @@
<string name="all_apps_button_personal_label">Personal apps list</string>
<string name="all_apps_button_work_label">Work apps list</string>
- <!-- Label for button in all applications label to go back home (to the workspace / desktop)
- for accessibilty (spoken when the button gets focus). -->
- <string name="all_apps_home_button_label">Home</string>
-
<!-- Label for remove drop target (from the homescreen only).
May appear next to uninstall_drop_target_label [CHAR_LIMIT=20] -->
<string name="remove_drop_target_label">Remove</string>
@@ -132,11 +196,11 @@
<!-- Widgets: -->
<skip />
- <!-- Text to show user in place of a gadget when we can't display it properly -->
- <string name="gadget_error_text">Problem loading widget</string>
+ <!-- Error text that lets a user know that the widget can't load. -->
+ <string name="gadget_error_text">Can\'t load widget</string>
- <!-- Text to show user in place of a gadget when it is not yet initialized. -->
- <string name="gadget_setup_text">Setup</string>
+ <!-- Instructional text to encourage a user to finish setting up the widget. -->
+ <string name="gadget_setup_text">Tap to finish setup</string>
<!-- Text to inform the user that they can't uninstall a system application -->
<string name="uninstall_system_app_text">This is a system app and can\'t be uninstalled.</string>
@@ -178,13 +242,11 @@
<string name="folder_name_format_overflow">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> or more items</string>
<!-- Strings for the customization mode -->
- <!-- Text for widget add button -->
- <string name="widget_button_text">Widgets</string>
- <!-- Text for wallpaper change button -->
+ <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
<string name="wallpaper_button_text">Wallpapers</string>
- <!-- Text for wallpaper change button -->
- <string name="styles_wallpaper_button_text">Styles & wallpapers</string>
- <!-- Text for settings button [CHAR LIMIT=20]-->
+ <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
+ <string name="styles_wallpaper_button_text">Wallpaper & style</string>
+ <!-- Text for settings button [CHAR LIMIT=30]-->
<string name="settings_button_text">Home settings</string>
<!-- Message shown when a feature is disabled by the administrator -->
<string name="msg_disabled_by_admin">Disabled by your admin</string>
@@ -228,14 +290,14 @@
<string name="abandoned_promise_explanation">The app for this icon isn\'t installed.
You can remove it, or search for the app and install it manually.
</string>
+ <!-- Title for an app which is being installed. -->
+ <string name="app_installing_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> installing, <xliff:g id="progress" example="30%">%2$s</xliff:g> complete</string>
<!-- Title for an app which is being downloaded. -->
<string name="app_downloading_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> downloading, <xliff:g id="progress" example="30%">%2$s</xliff:g> complete</string>
<!-- Title for an app whose download has been started. -->
<string name="app_waiting_download_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> waiting to install</string>
<!-- Strings for widgets & more in the popup container/bottom sheet -->
- <!-- Title for a bottom sheet that shows widgets for a particular app -->
- <string name="widgets_bottom_sheet_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> widgets</string>
<!-- Accessibility title for the popup containing a list of widgets. [CHAR_LIMIT=50] -->
<string name="widgets_list">Widgets list</string>
@@ -332,26 +394,29 @@
<!-- This string is in the work profile tab when a user has All Apps open on their phone. This is a label for a toggle to turn the work profile on and off. "Work profile" means a separate profile on a user's phone that's specifically for their work apps and managed by their company. "Work" is used as an adjective.-->
<string name="work_profile_toggle_label">Work profile</string>
- <!--- User onboarding title for personal apps -->
- <string name="work_profile_edu_personal_apps">Personal data is separate & hidden from work apps</string>
<!--- User onboarding title for work profile apps -->
- <string name="work_profile_edu_work_apps">Work apps & data are visible to your IT admin</string>
- <!-- Action label to proceed to the next work profile edu section-->
- <string name="work_profile_edu_next">Next</string>
+ <string name="work_profile_edu_work_apps">Work apps are badged and visible to your IT admin</string>
<!-- Action label to finish work profile edu-->
<string name="work_profile_edu_accept">Got it</string>
<!--- heading shown when user opens work apps tab while work apps are paused -->
- <string name="work_apps_paused_title">Work profile is paused</string>
+ <string name="work_apps_paused_title">Work apps are paused</string>
<!--- body shown when user opens work apps tab while work apps are paused -->
- <string name="work_apps_paused_body">Work apps can\'t send you notifications, use your battery, or access your location</string>
+ <string name="work_apps_paused_body">Your work apps can’t send you notifications, use your battery, or access your location</string>
<!-- content description for paused work apps list -->
- <string name="work_apps_paused_content_description">Work profile is paused. Work apps can\’t send you notifications, use your battery, or access your location</string>
+ <string name="work_apps_paused_content_description">Work apps are off. Your work apps can’t send you notifications, use your battery, or access your location</string>
+ <!-- string shown in educational banner about work profile -->
+ <string name="work_apps_paused_edu_banner">Work apps are badged and visible to your IT admin</string>
+ <!-- button string shown to dismiss work tab education -->
+ <string name="work_apps_paused_edu_accept">Got it</string>
+ <!-- button string shown pause work profile -->
+ <string name="work_apps_pause_btn_text">Turn off work apps</string>
+ <!-- button string shown enable work profile -->
+ <string name="work_apps_enable_btn_text">Turn on work apps</string>
-
- <!-- A tip shown pointing at work toggle -->
- <string name="work_switch_tip">Pause work apps and notifications</string>
+ <!-- A hint shown in launcher settings develop options filter box -->
+ <string name="developer_options_filter_hint">Filter</string>
<!-- Failed action error message: e.g. Failed: Pause -->
<string name="remote_action_failed">Failed: <xliff:g id="what" example="Pause">%1$s</xliff:g></string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 25f21f3..d30b80c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -26,36 +26,43 @@
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowShowWallpaper">true</item>
- <item name="folderTextColor">?attr/workspaceTextColor</item>
</style>
<style name="LauncherTheme" parent="@style/BaseLauncherTheme">
<item name="android:textColorSecondary">#DE000000</item>
- <item name="allAppsScrimColor">#FFFFFFFF</item>
- <item name="allAppsInterimScrimAlpha">46</item>
+ <item name="allAppsScrimColor">?android:attr/colorBackgroundFloating</item>
+ <item name="allappsHeaderProtectionColor">@color/popup_color_tertiary_light</item>
<item name="allAppsNavBarScrimColor">#66FFFFFF</item>
<item name="allAppsTheme">@style/AllAppsTheme</item>
- <item name="popupColorPrimary">#FFF</item>
- <item name="popupColorSecondary">#F1F3F4</item>
- <item name="popupColorTertiary">#E0E0E0</item> <!-- Gray 300 -->
+ <item name="popupColorPrimary">@color/popup_color_primary_light</item>
+ <item name="popupColorSecondary">@color/popup_color_secondary_light</item>
+ <item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
+ <item name="popupShadeFirst">@color/popup_shade_first_light</item>
+ <item name="popupShadeSecond">@color/popup_shade_second_light</item>
+ <item name="popupShadeThird">@color/popup_shade_third_light</item>
+ <item name="popupNotificationDotColor">@color/popup_notification_dot_light</item>
<item name="isMainColorDark">false</item>
<item name="isWorkspaceDarkText">false</item>
- <item name="workspaceTextColor">@android:color/white</item>
+ <item name="workspaceTextColor">@color/workspace_text_color_light</item>
<item name="workspaceShadowColor">#B0000000</item>
- <item name="workspaceAmbientShadowColor">#33000000</item>
- <item name="workspaceKeyShadowColor">#44000000</item>
+ <item name="workspaceAmbientShadowColor">#40000000</item>
+ <item name="workspaceKeyShadowColor">#89000000</item>
<item name="workspaceStatusBarScrim">@drawable/workspace_bg</item>
<item name="widgetsTheme">@style/WidgetContainerTheme</item>
- <item name="folderDotColor">?android:attr/colorPrimary</item>
- <item name="folderFillColor">#CDFFFFFF</item>
+ <item name="folderDotColor">@color/folder_dot_color</item>
+ <item name="folderFillColor">@color/folder_background_light</item>
<item name="folderIconBorderColor">?android:attr/colorPrimary</item>
- <item name="folderTextColor">#FF212121</item>
- <item name="folderHintColor">#89616161</item>
+ <item name="folderTextColor">@color/workspace_text_color_dark</item>
+ <item name="isFolderDarkText">true</item>
+ <item name="folderHintColor">@color/folder_hint_text_color_dark</item>
<item name="loadingIconColor">#CCFFFFFF</item>
<item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
<item name="workProfileOverlayTextColor">#FF212121</item>
<item name="eduHalfSheetBGColor">?android:attr/colorAccent</item>
<item name="disabledIconAlpha">.54</item>
+ <item name="workspaceAccentColor">@color/workspace_accent_color_light</item>
+ <item name="dropTargetHoverTextColor">@color/workspace_text_color_dark</item>
+ <item name="overviewScrimColor">@color/overview_scrim</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
@@ -67,70 +74,67 @@
</style>
<style name="LauncherTheme.DarkMainColor" parent="@style/LauncherTheme">
- <item name="folderFillColor">#FF3C4043</item> <!-- 100% GM2 800 -->
- <item name="folderTextColor">?attr/workspaceTextColor</item>
<item name="disabledIconAlpha">.254</item>
</style>
<style name="LauncherTheme.DarkText" parent="@style/LauncherTheme">
- <item name="workspaceTextColor">#FF212121</item>
- <item name="allAppsInterimScrimAlpha">128</item>
+ <item name="workspaceTextColor">@color/workspace_text_color_dark</item>
<item name="workspaceShadowColor">@android:color/transparent</item>
<item name="workspaceAmbientShadowColor">@android:color/transparent</item>
<item name="workspaceKeyShadowColor">@android:color/transparent</item>
<item name="isWorkspaceDarkText">true</item>
<item name="workspaceStatusBarScrim">@null</item>
- <item name="folderDotColor">#FF464646</item>
- <item name="folderFillColor">#CDFFFFFF</item>
- <item name="folderIconBorderColor">#FF80868B</item>
- <item name="folderTextColor">?attr/workspaceTextColor</item>
+ <item name="workspaceAccentColor">@color/workspace_accent_color_dark</item>
+ <item name="dropTargetHoverTextColor">@color/workspace_text_color_light</item>
</style>
<style name="LauncherTheme.Dark" parent="@style/LauncherTheme">
- <item name="android:textColorPrimary">#FFFFFFFF</item>
- <item name="android:textColorSecondary">#FFFFFFFF</item>
- <item name="android:textColorTertiary">#CCFFFFFF</item>
+ <item name="android:textColorPrimary">@color/text_color_primary_dark</item>
+ <item name="android:textColorSecondary">@color/text_color_secondary_dark</item>
+ <item name="android:textColorTertiary">@color/text_color_tertiary_dark</item>
<item name="android:textColorHint">#A0FFFFFF</item>
- <item name="android:colorControlHighlight">#A0FFFFFF</item>
+ <item name="android:colorControlHighlight">#19FFFFFF</item>
<item name="android:colorPrimary">#FF212121</item>
- <item name="allAppsScrimColor">#FF000000</item>
- <item name="allAppsInterimScrimAlpha">102</item>
+ <item name="allAppsScrimColor">?android:attr/colorBackgroundFloating</item>
<item name="allAppsNavBarScrimColor">#80000000</item>
<item name="allAppsTheme">@style/AllAppsTheme.Dark</item>
- <item name="popupColorPrimary">#3C4043</item> <!-- Gray 800 -->
- <item name="popupColorSecondary">#202124</item>
- <item name="popupColorTertiary">#757575</item> <!-- Gray 600 -->
+ <item name="popupColorPrimary">@color/popup_color_primary_dark</item>
+ <item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
+ <item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
+ <item name="popupNotificationDotColor">@color/popup_notification_dot_dark</item>
+ <item name="popupShadeFirst">@color/popup_shade_first_dark</item>
+ <item name="popupShadeSecond">@color/popup_shade_second_dark</item>
+ <item name="popupShadeThird">@color/popup_shade_third_dark</item>
<item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
- <item name="folderDotColor">#FF464646</item>
- <item name="folderFillColor">#DD3C4043</item> <!-- 87% GM2 800 -->
- <item name="folderIconBorderColor">#FF80868B</item>
- <item name="folderTextColor">@android:color/white</item>
- <item name="folderHintColor">#89CCCCCC</item>
+ <item name="folderDotColor">@color/folder_dot_color</item>
+ <item name="folderFillColor">@color/folder_background_dark</item>
+ <item name="folderIconBorderColor">?android:attr/colorPrimary</item>
+ <item name="folderTextColor">@color/workspace_text_color_light</item>
+ <item name="isFolderDarkText">false</item>
+ <item name="folderHintColor">@color/folder_hint_text_color_light</item>
<item name="isMainColorDark">true</item>
<item name="loadingIconColor">#99FFFFFF</item>
<item name="iconOnlyShortcutColor">#B3FFFFFF</item>
<item name="workProfileOverlayTextColor">@android:color/white</item>
<item name="eduHalfSheetBGColor">#DD000000</item>
+ <item name="overviewScrimColor">@color/overview_scrim_dark</item>
</style>
<style name="LauncherTheme.Dark.DarkMainColor" parent="@style/LauncherTheme.Dark">
- <item name="folderFillColor">#FF3C4043</item> <!-- 100% GM2 800 -->
- <item name="folderTextColor">@android:color/white</item>
<item name="disabledIconAlpha">.54</item>
</style>
<style name="LauncherTheme.Dark.DarkText" parent="@style/LauncherTheme.Dark">
- <item name="android:colorControlHighlight">#75212121</item>
- <item name="allAppsInterimScrimAlpha">25</item>
- <item name="folderFillColor">#CDFFFFFF</item>
- <item name="folderTextColor">?attr/workspaceTextColor</item>
- <item name="workspaceTextColor">#FF212121</item>
+ <item name="android:colorControlHighlight">#19212121</item>
+ <item name="workspaceTextColor">@color/workspace_text_color_dark</item>
<item name="workspaceShadowColor">@android:color/transparent</item>
<item name="workspaceAmbientShadowColor">@android:color/transparent</item>
<item name="workspaceKeyShadowColor">@android:color/transparent</item>
<item name="isWorkspaceDarkText">true</item>
<item name="workspaceStatusBarScrim">@null</item>
+ <item name="workspaceAccentColor">@color/workspace_accent_color_dark</item>
+ <item name="dropTargetHoverTextColor">@color/workspace_text_color_light</item>
</style>
<!-- A derivative project can extend these themes to customize the application theme without
@@ -143,12 +147,19 @@
<style name="AppTheme.Dark.DarkMainColor" parent="@style/LauncherTheme.Dark.DarkMainColor" />
<style name="AppTheme.Dark.DarkText" parent="@style/LauncherTheme.Dark.DarkText" />
- <style name="AppItemActivityTheme" parent="@android:style/Theme.Material.Light.Dialog.Alert">
- <item name="widgetsTheme">@style/WidgetContainerTheme</item>
+ <style name="HomeSettingsTheme" parent="@android:style/Theme.DeviceDefault.Settings">
+ <item name="android:navigationBarColor">?android:colorPrimaryDark</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="preferenceTheme">@style/HomeSettingsPreferenceTheme</item>
</style>
- <style name="HomeSettingsTheme" parent="@android:style/Theme.DeviceDefault.Settings">
- <item name="android:navigationBarColor">@android:color/transparent</item>
+ <style name="HomeSettingsPreferenceTheme" parent="@style/PreferenceThemeOverlay.v14.Material">
+ <item name="preferenceFragmentCompatStyle">@style/HomeSettingsFragmentCompatStyle</item>
+ </style>
+
+ <style name="HomeSettingsFragmentCompatStyle" parent="@style/PreferenceFragment.Material">
+ <item name="android:layout">@layout/home_settings</item>
</style>
<!--
@@ -162,14 +173,16 @@
<item name="android:textColorSecondary">?attr/workspaceTextColor</item>
</style>
- <!-- Theme for the widget container. Overridden on API 26. -->
+ <!-- Theme for the widget container. -->
<style name="WidgetContainerTheme" parent="@android:style/Theme.DeviceDefault.Settings">
- <item name="android:colorEdgeEffect">?android:attr/textColorSecondaryInverse</item>
- <item name="android:textColorPrimary">?android:attr/textColorPrimaryInverse</item>
- <item name="android:textColorSecondary">?android:attr/textColorSecondaryInverse</item>
+ <item name="android:colorPrimaryDark">#E8EAED</item>
+ <item name="android:textColorSecondary">?android:attr/textColorPrimary</item>
+ <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
</style>
-
- <style name="WidgetContainerTheme.Dark" />
+ <style name="WidgetContainerTheme.Dark" parent="AppTheme.Dark">
+ <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
+ <item name="android:colorPrimaryDark">#616161</item> <!-- Gray 700 -->
+ </style>
<style name="FastScrollerPopup" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
<item name="android:layout_width">wrap_content</item>
@@ -181,7 +194,7 @@
<item name="android:alpha">0</item>
<item name="android:elevation">3dp</item>
<item name="android:saveEnabled">false</item>
- <item name="android:textColor">@android:color/white</item>
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
<item name="android:includeFontPadding">false</item>
<item name="android:importantForAccessibility">no</item>
</style>
@@ -194,8 +207,9 @@
<item name="disabledIconAlpha">.54</item>
</style>
+ <style name="BaseIconRoot" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
- <style name="BaseIconUnBounded" parent="@android:style/TextAppearance.DeviceDefault">
+ <style name="BaseIconUnBounded" parent="BaseIconRoot">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_gravity">center</item>
@@ -212,15 +226,30 @@
<item name="android:lines">1</item>
</style>
+ <!-- Base theme for AllApps BubbleTextViews -->
+ <style name="BaseIcon.AllApps" parent="BaseIcon">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:stateListAnimator">@animator/all_apps_fastscroll_icon_anim</item>
+ <item name="android:paddingLeft">@dimen/dynamic_grid_cell_padding_x</item>
+ <item name="android:paddingRight">@dimen/dynamic_grid_cell_padding_x</item>
+ </style>
+
+
<!-- Icon displayed on the workspace -->
- <style name="BaseIcon.Workspace" >
+ <style name="BaseIcon.Workspace.Shadows" parent="BaseIcon">
<item name="android:shadowRadius">2.0</item>
<item name="android:shadowColor">?attr/workspaceShadowColor</item>
<item name="ambientShadowColor">?attr/workspaceAmbientShadowColor</item>
- <item name="ambientShadowBlur">2.5dp</item>
+ <item name="ambientShadowBlur">1.5dp</item>
<item name="keyShadowColor">?attr/workspaceKeyShadowColor</item>
- <item name="keyShadowBlur">1dp</item>
- <item name="keyShadowOffset">.5dp</item>
+ <item name="keyShadowBlur">.5dp</item>
+ <item name="keyShadowOffsetX">.5dp</item>
+ <item name="keyShadowOffsetY">.5dp</item>
+ </style>
+
+ <!-- Intentionally empty so we can override -->
+ <style name="BaseIcon.Workspace" parent="BaseIcon.Workspace.Shadows">
</style>
<!-- Theme for the popup container -->
@@ -230,34 +259,57 @@
<!-- Drop targets -->
<style name="DropTargetButtonBase" parent="@android:style/TextAppearance.DeviceDefault">
- <item name="android:drawablePadding">7.5dp</item>
- <item name="android:paddingLeft">16dp</item>
- <item name="android:paddingRight">16dp</item>
- <item name="android:textColor">?attr/workspaceTextColor</item>
+ <item name="android:drawablePadding">8dp</item>
+ <item name="android:padding">14dp</item>
+ <item name="android:textColor">@color/drop_target_text</item>
<item name="android:textSize">@dimen/drop_target_text_size</item>
<item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item>
- <item name="android:shadowColor">?attr/workspaceShadowColor</item>
- <item name="android:shadowDx">0.0</item>
- <item name="android:shadowDy">1.0</item>
- <item name="android:shadowRadius">4.0</item>
+ <item name="android:background">@drawable/drop_target_background</item>
</style>
<style name="DropTargetButton" parent="DropTargetButtonBase" />
<style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
+
<style name="PrimaryHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
<style name="TextTitle" parent="@android:style/TextAppearance.DeviceDefault" />
<style name="AllAppsEmptySearchBackground">
<item name="android:colorPrimary">#E0E0E0</item>
- <item name="android:colorControlHighlight">#BDBDBD</item>
+ <item name="android:colorControlHighlight">#19BDBDBD</item>
<item name="android:colorForeground">@color/all_apps_bg_hand_fill</item>
</style>
<style name="AllAppsEmptySearchBackground.Dark">
<item name="android:colorPrimary">#9AA0A6</item>
- <item name="android:colorControlHighlight">#DFE1E5</item>
+ <item name="android:colorControlHighlight">#19DFE1E5</item>
<item name="android:colorForeground">@color/all_apps_bg_hand_fill_dark</item>
</style>
+
+ <style name="Button.TopRounded.Bordered" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">@drawable/button_top_rounded_bordered_ripple</item>
+ <item name="android:stateListAnimator">@null</item>
+ </style>
+
+ <style name="Button.BottomRounded.Colored" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">@drawable/button_bottom_rounded_colored_ripple</item>
+ <item name="android:stateListAnimator">@null</item>
+ </style>
+
+ <style name="Button.Rounded.Colored" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">@drawable/button_rounded_colored_ripple</item>
+ <item name="android:stateListAnimator">@null</item>
+ </style>
+
+ <style name="Button.FullRounded.Colored" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">@drawable/full_rounded_colored_ripple</item>
+ <item name="android:stateListAnimator">@null</item>
+ </style>
+
+ <style name="AddItemActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+ <item name="widgetsTheme">@style/WidgetContainerTheme</item>
+ <item name="android:windowLightStatusBar">true</item>
+ <item name="android:windowTranslucentStatus">true</item>
+ </style>
</resources>
diff --git a/res/xml/device_profiles.xml b/res/xml/device_profiles.xml
index 1c99dfc..256999c 100644
--- a/res/xml/device_profiles.xml
+++ b/res/xml/device_profiles.xml
@@ -116,6 +116,14 @@
launcher:canBeDefault="true" />
<display-option
+ launcher:name="Large Phone Split Display"
+ launcher:minWidthDps="406"
+ launcher:minHeightDps="694"
+ launcher:iconImageSize="56"
+ launcher:iconTextSize="14.4"
+ launcher:canBeDefault="split_display" />
+
+ <display-option
launcher:name="Shorter Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="400"
diff --git a/res/xml/dynamic_resources.xml b/res/xml/dynamic_resources.xml
index f5d2628..3a3e239 100644
--- a/res/xml/dynamic_resources.xml
+++ b/res/xml/dynamic_resources.xml
@@ -4,6 +4,6 @@
<entry id="@color/delete_target_hover_tint" />
<entry id="@color/delete_target_hover_tint" />
<entry id="@color/delete_target_hover_tint" />
+ <entry id="@color/wallpaper_scrim_color" />
-
-</DynamicResources>
\ No newline at end of file
+</DynamicResources>
diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml
index 3455cb8..90de498 100644
--- a/res/xml/launcher_preferences.xml
+++ b/res/xml/launcher_preferences.xml
@@ -15,40 +15,40 @@
-->
<androidx.preference.PreferenceScreen
- xmlns:android="http://schemas.android.com/apk/res/android">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto">
<com.android.launcher3.settings.NotificationDotsPreference
android:key="pref_icon_badging"
android:title="@string/notification_dots_title"
android:persistent="false"
- android:widgetLayout="@layout/notification_pref_warning" >
- <intent android:action="android.settings.NOTIFICATION_SETTINGS">
- <!-- This extra highlights the "Allow notification dots" field in Notification settings -->
- <extra
- android:name=":settings:fragment_args_key"
- android:value="notification_badging" />
- </intent>
- </com.android.launcher3.settings.NotificationDotsPreference>
+ android:widgetLayout="@layout/notification_pref_warning" />
+ <!--
+ LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED(613)
+ LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_DISABLED(614)
+ -->
<SwitchPreference
android:key="pref_add_icon_to_home"
android:title="@string/auto_add_shortcuts_label"
android:summary="@string/auto_add_shortcuts_description"
android:defaultValue="true"
- android:persistent="true" />
+ android:persistent="true"
+ launcher:logIdOn="613"
+ launcher:logIdOff="614" />
+ <!--
+ LAUNCHER_HOME_SCREEN_ROTATION_ENABLED(615)
+ LAUNCHER_HOME_SCREEN_ROTATION_DISABLED(616)
+ -->
<SwitchPreference
android:key="pref_allowRotation"
android:title="@string/allow_rotation_title"
android:summary="@string/allow_rotation_desc"
- android:defaultValue="@bool/allow_rotation"
- android:persistent="true" />
-
- <SwitchPreference
- android:key="pref_grid_options"
- android:title="Enable grid options"
android:defaultValue="false"
- android:persistent="true" />
+ android:persistent="true"
+ launcher:logIdOn="615"
+ launcher:logIdOff="616" />
<androidx.preference.PreferenceScreen
android:key="pref_developer_options"
diff --git a/res/xml/size_limits_80x104.xml b/res/xml/size_limits_80x104.xml
new file mode 100644
index 0000000..4178664
--- /dev/null
+++ b/res/xml/size_limits_80x104.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<device-paddings xmlns:launcher="http://schemas.android.com/apk/res-auto" >
+
+ <device-padding
+ launcher:maxEmptySpace="88dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ <workspaceBottomPadding
+ launcher:a="0.52"
+ launcher:b="0"/>
+ <hotseatBottomPadding
+ launcher:a="0.48"
+ launcher:b="0"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="100dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="9dp"/>
+ <workspaceBottomPadding
+ launcher:a="0.40"
+ launcher:b="0"
+ launcher:c="9dp"/>
+ <hotseatBottomPadding
+ launcher:a="0.60"
+ launcher:b="0"
+ launcher:c="9dp"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="103dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="26dp"/>
+ <workspaceBottomPadding
+ launcher:a="0"
+ launcher:b="20dp"/>
+ <hotseatBottomPadding
+ launcher:a="1"
+ launcher:b="0"
+ launcher:c="46dp"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="107dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="9dp"/>
+ <workspaceBottomPadding
+ launcher:a="0"
+ launcher:b="34dp"/>
+ <hotseatBottomPadding
+ launcher:a="1"
+ launcher:b="0"
+ launcher:c="43dp"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="120dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="16dp"/>
+ <workspaceBottomPadding
+ launcher:a="1"
+ launcher:c="72dp"/>
+ <hotseatBottomPadding
+ launcher:a="0"
+ launcher:b="56dp"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="135dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="39dp"/>
+ <workspaceBottomPadding
+ launcher:a="1"
+ launcher:c="95dp"/>
+ <hotseatBottomPadding
+ launcher:a="0"
+ launcher:b="56dp"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="9999dp">
+ <workspaceTopPadding
+ launcher:a="0.40"
+ launcher:c="36dp"/>
+ <workspaceBottomPadding
+ launcher:a="0.60"
+ launcher:c="36dp"/>
+ <hotseatBottomPadding
+ launcher:a="0"
+ launcher:b="36dp"/>
+ </device-padding>
+</device-paddings>
\ No newline at end of file
diff --git a/robolectric_tests/Android.bp b/robolectric_tests/Android.bp
new file mode 100644
index 0000000..9ed26ff
--- /dev/null
+++ b/robolectric_tests/Android.bp
@@ -0,0 +1,63 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//
+// Launcher Robolectric test target.
+//
+// "robolectric_android-all-stub", not needed, we write our own stubs
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "packages_apps_Launcher3_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["packages_apps_Launcher3_license"],
+}
+
+filegroup {
+ name: "launcher3-robolectric-resources",
+ path: "resources",
+ srcs: ["resources/*"],
+}
+
+filegroup {
+ name: "launcher3-robolectric-src",
+ srcs: ["src/**/*.java"],
+}
+
+android_robolectric_test {
+ name: "LauncherRoboTests",
+ srcs: [
+ ":launcher3-robolectric-src",
+ ":launcher3-test-src-common",
+ ],
+ java_resources: [":launcher3-robolectric-resources"],
+ static_libs: [
+ "truth-prebuilt",
+ "androidx.test.espresso.contrib",
+ "androidx.test.espresso.core",
+ "androidx.test.espresso.intents",
+ "androidx.test.ext.junit",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "mockito-robolectric-prebuilt",
+ "SystemUISharedLib",
+ ],
+ robolectric_prebuilt_version: "4.5.1",
+ instrumentation_for: "Launcher3",
+
+ test_options: {
+ timeout: 36000,
+ },
+}
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
deleted file mode 100644
index 3fa9b0a..0000000
--- a/robolectric_tests/Android.mk
+++ /dev/null
@@ -1,62 +0,0 @@
-# Copyright (C) 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-#############################################
-# Launcher Robolectric test target. #
-#############################################
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := LauncherRoboTests
-LOCAL_MODULE_CLASS := JAVA_LIBRARIES
-
-LOCAL_SDK_VERSION := system_current
-LOCAL_SRC_FILES := \
- $(call all-java-files-under, src) \
- $(call all-java-files-under, ../tests/src_common)
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- androidx.test.runner \
- androidx.test.rules \
- mockito-robolectric-prebuilt \
- truth-prebuilt
-LOCAL_JAVA_LIBRARIES := \
- platform-robolectric-4.3.1-prebuilt
-
-LOCAL_JAVA_RESOURCE_DIRS := resources config
-
-LOCAL_INSTRUMENTATION_FOR := Launcher3
-LOCAL_MODULE_TAGS := optional
-
-# Generate test_config.properties
-include external/robolectric-shadows/gen_test_config.mk
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-############################################
-# Target to run the previous target. #
-############################################
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := RunLauncherRoboTests
-LOCAL_SDK_VERSION := system_current
-LOCAL_JAVA_LIBRARIES := LauncherRoboTests
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_TEST_PACKAGE := Launcher3
-LOCAL_INSTRUMENT_SOURCE_DIRS := packages/apps/Launcher3/src
-
-LOCAL_ROBOTEST_TIMEOUT := 36000
-
-include prebuilts/misc/common/robolectric/4.3.1/run_robotests.mk
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
deleted file mode 100644
index a8e0cb3..0000000
--- a/robolectric_tests/config/robolectric.properties
+++ /dev/null
@@ -1,19 +0,0 @@
-sdk=29
-shadows= \
- com.android.launcher3.shadows.LShadowApplicationPackageManager \
- com.android.launcher3.shadows.LShadowAppPredictionManager \
- com.android.launcher3.shadows.LShadowAppWidgetManager \
- com.android.launcher3.shadows.LShadowBackupManager \
- com.android.launcher3.shadows.LShadowBitmap \
- com.android.launcher3.shadows.LShadowDisplay \
- com.android.launcher3.shadows.LShadowLauncherApps \
- com.android.launcher3.shadows.LShadowTypeface \
- com.android.launcher3.shadows.LShadowUserManager \
- com.android.launcher3.shadows.LShadowWallpaperManager \
- com.android.launcher3.shadows.ShadowDeviceFlag \
- com.android.launcher3.shadows.ShadowLooperExecutor \
- com.android.launcher3.shadows.ShadowMainThreadInitializedObject \
- com.android.launcher3.shadows.ShadowOverrides \
- com.android.launcher3.shadows.ShadowSurfaceTransactionApplier \
-
-application=com.android.launcher3.util.LauncherTestApplication
\ No newline at end of file
diff --git a/robolectric_tests/resources/robolectric.properties b/robolectric_tests/resources/robolectric.properties
new file mode 100644
index 0000000..abb6968
--- /dev/null
+++ b/robolectric_tests/resources/robolectric.properties
@@ -0,0 +1,15 @@
+sdk=30
+
+shadows= \
+ com.android.launcher3.shadows.LShadowAppPredictionManager \
+ com.android.launcher3.shadows.LShadowAppWidgetManager \
+ com.android.launcher3.shadows.LShadowBackupManager \
+ com.android.launcher3.shadows.LShadowDisplay \
+ com.android.launcher3.shadows.LShadowLauncherApps \
+ com.android.launcher3.shadows.ShadowDeviceFlag \
+ com.android.launcher3.shadows.ShadowLooperExecutor \
+ com.android.launcher3.shadows.ShadowMainThreadInitializedObject \
+ com.android.launcher3.shadows.ShadowOverrides \
+ com.android.launcher3.shadows.ShadowSurfaceTransactionApplier \
+
+application=com.android.launcher3.util.LauncherTestApplication
diff --git a/robolectric_tests/resources/widgets_predication_update_task_data.txt b/robolectric_tests/resources/widgets_predication_update_task_data.txt
new file mode 100644
index 0000000..941d195
--- /dev/null
+++ b/robolectric_tests/resources/widgets_predication_update_task_data.txt
@@ -0,0 +1,24 @@
+# Model data used by WidgetsPredictionUpdateTasksTest
+
+classMap s com.android.launcher3.model.data.WorkspaceItemInfo
+classMap w com.android.launcher3.model.data.LauncherAppWidgetInfo
+
+# Items for the BgDataModel
+
+# App shortcuts
+bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
+bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
+bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
+bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
+
+# Promise icons for app3
+bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
+bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
+bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7
+
+# Promise icon for app4
+bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8
+
+# Widget
+bgItem w providerName=app4/provider1 id=9
+bgItem w providerName=app5/provider1 id=10
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
index b7ba106..2a94d9b 100644
--- a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
@@ -25,16 +25,20 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.Executors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
import java.util.ArrayList;
@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
public final class FolderNameProviderTest {
private Context mContext;
private WorkspaceItemInfo mItem1;
@@ -58,18 +62,20 @@
}
@Test
- public void getSuggestedFolderName_workAssignedToEnd() {
+ public void getSuggestedFolderName_workAssignedToEnd() throws Exception {
ArrayList<WorkspaceItemInfo> list = new ArrayList<>();
list.add(mItem1);
list.add(mItem2);
FolderNameInfos nameInfos = new FolderNameInfos();
- new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos);
+ Executors.MODEL_EXECUTOR.submit(() ->
+ new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos)).get();
assertEquals("Work", nameInfos.getLabels()[0]);
nameInfos.setLabel(0, "candidate1", 1.0f);
nameInfos.setLabel(1, "candidate2", 1.0f);
nameInfos.setLabel(2, "candidate3", 1.0f);
- new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos);
+ Executors.MODEL_EXECUTOR.submit(() ->
+ new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos)).get();
assertEquals("Work", nameInfos.getLabels()[3]);
assertTrue(nameInfos.hasSuggestions());
assertTrue(nameInfos.hasPrimary());
diff --git a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
index c892618..01b23ba 100644
--- a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
+++ b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -9,8 +9,8 @@
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.util.Scheduler;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
import java.io.File;
import java.io.PrintWriter;
@@ -21,11 +21,10 @@
* Tests for {@link FileLog}
*/
@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
public class FileLogTest {
private File mTempDir;
- private boolean mTestActive;
-
@Before
public void setUp() {
int count = 0;
@@ -35,14 +34,6 @@
} while (!mTempDir.mkdir());
FileLog.setDir(mTempDir);
-
- mTestActive = true;
- Scheduler scheduler = Shadows.shadowOf(FileLog.getHandler().getLooper()).getScheduler();
- new Thread(() -> {
- while (mTestActive) {
- scheduler.advanceToLastPostedRunnable();
- }
- }).start();
}
@After
@@ -52,8 +43,6 @@
new File(mTempDir, "log-" + i).delete();
}
mTempDir.delete();
-
- mTestActive = false;
}
@Test
diff --git a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
index 90313ab..34a8025 100644
--- a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
@@ -35,13 +35,13 @@
import android.content.pm.PackageInstaller;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.provider.RestoreDbTask;
import com.android.launcher3.shadows.LShadowBackupManager;
-import com.android.launcher3.shadows.LShadowUserManager;
import com.android.launcher3.util.LauncherModelHelper;
import org.junit.Before;
@@ -51,6 +51,7 @@
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowUserManager;
/**
* Tests to verify backup and restore flow.
@@ -64,17 +65,11 @@
private static final long OLD_WORK_PROFILE_ID = 11;
private static final int WORK_PROFILE_ID = 10;
- private static final int SYSTEM_USER = 0;
- private static final int FLAG_SYSTEM = 0x00000800;
- private static final int FLAG_PROFILE = 0x00001000;
-
- private LShadowUserManager mUserManager;
+ private ShadowUserManager mUserManager;
private BackupManager mBackupManager;
private LauncherModelHelper mModelHelper;
private SQLiteDatabase mDb;
private InvariantDeviceProfile mIdp;
- private UserHandle mMainProfileUser;
- private UserHandle mWorkProfileUser;
@Before
public void setUp() {
@@ -90,17 +85,15 @@
final UserManager userManager = RuntimeEnvironment.application.getSystemService(
UserManager.class);
mUserManager = Shadow.extract(userManager);
- // sign in to primary user
- mMainProfileUser = mUserManager.addUser(SYSTEM_USER, "me", FLAG_SYSTEM);
// sign in to work profile
- mWorkProfileUser = mUserManager.addUser(WORK_PROFILE_ID, "work", FLAG_PROFILE);
+ mUserManager.addUser(WORK_PROFILE_ID, "work", ShadowUserManager.FLAG_MANAGED_PROFILE);
}
private void setupBackupManager() {
mBackupManager = new BackupManager(RuntimeEnvironment.application);
final LShadowBackupManager bm = Shadow.extract(mBackupManager);
- bm.addProfile(MY_OLD_PROFILE_ID, mMainProfileUser);
- bm.addProfile(OLD_WORK_PROFILE_ID, mWorkProfileUser);
+ bm.addProfile(MY_OLD_PROFILE_ID, Process.myUserHandle());
+ bm.addProfile(OLD_WORK_PROFILE_ID, UserHandle.of(WORK_PROFILE_ID));
}
@Test
@@ -134,7 +127,7 @@
{ APP_ICON, SHORTCUT, SHORTCUT, NO__ICON},
}}, 2, OLD_WORK_PROFILE_ID);
// simulates the creation of backup upon restore
- new GridBackupTable(RuntimeEnvironment.application, mDb, mIdp.numHotseatIcons,
+ new GridBackupTable(RuntimeEnvironment.application, mDb, mIdp.numDatabaseHotseatIcons,
mIdp.numColumns, mIdp.numRows).doBackup(
MY_OLD_PROFILE_ID, GridBackupTable.OPTION_REQUIRES_SANITIZATION);
// reset favorites table
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 5610b0e..9ac3fe7 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -53,10 +53,10 @@
mModelHelper = new LauncherModelHelper();
mModelHelper.initializeData("/cache_data_updated_task_data.txt");
- // Add dummy entries in the cache to simulate update
+ // Add placeholder entries in the cache to simulate update
Context context = RuntimeEnvironment.application;
IconCache iconCache = LauncherAppState.getInstance(context).getIconCache();
- CachingLogic<ItemInfo> dummyLogic = new CachingLogic<ItemInfo>() {
+ CachingLogic<ItemInfo> placeholderLogic = new CachingLogic<ItemInfo>() {
@Override
public ComponentName getComponent(ItemInfo info) {
return info.getTargetComponent();
@@ -81,7 +81,7 @@
UserManager um = context.getSystemService(UserManager.class);
for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
- iconCache.addIconToDBAndMemCache(info, dummyLogic, new PackageInfo(),
+ iconCache.addIconToDBAndMemCache(info, placeholderLogic, new PackageInfo(),
um.getSerialNumberForUser(info.user), true);
}
}
diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index bbbe21e..be03c7d 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -165,7 +165,7 @@
@Override
public void onOpen(SQLiteDatabase db) { }
};
- // Insert dummy data
+ // Insert mock data
for (int i = 0; i < 10; i++) {
ContentValues values = new ContentValues();
values.put(Favorites._ID, i);
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index 8f3a83e..655237d 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -109,7 +109,7 @@
public void testCustomProfileLoaded_with_widget() throws Exception {
String pendingAppPkg = "com.test.pending";
- // Add a dummy session info so that the widget exists
+ // Add a placeholder session info so that the widget exists
SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
params.setAppPackageName(pendingAppPkg);
@@ -120,7 +120,7 @@
setField(sessionInfo, "appIcon", BitmapInfo.LOW_RES_ICON);
writeLayoutAndLoad(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
- .putWidget(pendingAppPkg, "DummyWidget", 2, 2));
+ .putWidget(pendingAppPkg, "PlaceholderWidget", 2, 2));
// Verify widget
assertEquals(1, mModelHelper.getBgDataModel().appWidgets.size());
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 8e00dcb..d544a0b 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -64,7 +64,7 @@
mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0),
};
- mIdp.numHotseatIcons = 3;
+ mIdp.numDatabaseHotseatIcons = 3;
new GridSizeMigrationTask(mContext, mDb, mValidPackages, false, 5, 3)
.migrateHotseat();
// First item is dropped as it has the least weight.
@@ -81,7 +81,7 @@
mModelHelper.addItem(10, 4, HOTSEAT, 0, 0),
};
- mIdp.numHotseatIcons = 3;
+ mIdp.numDatabaseHotseatIcons = 3;
new GridSizeMigrationTask(mContext, mDb, mValidPackages, false, 5, 3)
.migrateHotseat();
// First item is dropped as it has the least weight.
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
index cca333c..8e49fae 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
@@ -120,7 +120,7 @@
};
mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage7);
- mIdp.numHotseatIcons = 4;
+ mIdp.numDatabaseHotseatIcons = 4;
mIdp.numColumns = 4;
mIdp.numRows = 4;
GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
@@ -128,16 +128,16 @@
srcHotseatItems.length);
GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages,
- mIdp.numHotseatIcons);
+ mIdp.numDatabaseHotseatIcons);
GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
- destReader, mIdp.numHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
+ destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
task.migrate();
// Check hotseat items
Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
"container=" + CONTAINER_HOTSEAT, null, null, null);
- assertEquals(c.getCount(), mIdp.numHotseatIcons);
+ assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons);
int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
c.moveToNext();
@@ -186,5 +186,101 @@
assertTrue(c.getString(intentIndex).contains(testPackage8));
assertEquals(c.getInt(cellXIndex), 0);
assertEquals(c.getInt(cellYIndex), 2);
+
+ c.close();
+ }
+
+ @Test
+ public void migrateToLargerHotseat() {
+ int[] srcHotseatItems = {
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 2, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+ };
+
+ int numSrcDatabaseHotseatIcons = srcHotseatItems.length;
+ mIdp.numDatabaseHotseatIcons = 6;
+ mIdp.numColumns = 4;
+ mIdp.numRows = 4;
+ GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages,
+ numSrcDatabaseHotseatIcons);
+ GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages,
+ mIdp.numDatabaseHotseatIcons);
+ GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+ destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
+ task.migrate();
+
+ // Check hotseat items
+ Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_HOTSEAT, null, null, null);
+ assertEquals(c.getCount(), numSrcDatabaseHotseatIcons);
+ int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+ int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 0);
+ assertTrue(c.getString(intentIndex).contains(testPackage1));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 1);
+ assertTrue(c.getString(intentIndex).contains(testPackage2));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 2);
+ assertTrue(c.getString(intentIndex).contains(testPackage3));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 3);
+ assertTrue(c.getString(intentIndex).contains(testPackage4));
+
+ c.close();
+ }
+
+ @Test
+ public void migrateFromLargerHotseat() {
+ int[] srcHotseatItems = {
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+ -1,
+ mModelHelper.addItem(SHORTCUT, 2, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 5, HOTSEAT, 0, 0, testPackage5, 5, TMP_CONTENT_URI),
+ };
+
+ int numSrcDatabaseHotseatIcons = srcHotseatItems.length;
+ mIdp.numDatabaseHotseatIcons = 4;
+ mIdp.numColumns = 4;
+ mIdp.numRows = 4;
+ GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages,
+ numSrcDatabaseHotseatIcons);
+ GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages,
+ mIdp.numDatabaseHotseatIcons);
+ GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+ destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
+ task.migrate();
+
+ // Check hotseat items
+ Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_HOTSEAT, null, null, null);
+ assertEquals(c.getCount(), mIdp.numDatabaseHotseatIcons);
+ int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+ int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 0);
+ assertTrue(c.getString(intentIndex).contains(testPackage1));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 1);
+ assertTrue(c.getString(intentIndex).contains(testPackage2));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 2);
+ assertTrue(c.getString(intentIndex).contains(testPackage3));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 3);
+ assertTrue(c.getString(intentIndex).contains(testPackage4));
+
+ c.close();
}
}
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 3a252dc..800311a 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -51,7 +51,7 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.Executors;
@@ -92,9 +92,9 @@
SCREEN, CELLX, CELLY, RESTORED, INTENT
});
- mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp,
- new UserManagerState());
- mLoaderCursor.allUsers.put(0, Process.myUserHandle());
+ UserManagerState ums = new UserManagerState();
+ mLoaderCursor = new LoaderCursor(mCursor, Favorites.CONTENT_URI, mApp, ums);
+ ums.allUsers.put(0, Process.myUserHandle());
}
private void initCursor(int itemType, String title) {
@@ -110,7 +110,7 @@
public void getAppShortcutInfo_dontAllowMissing_invalidComponent() {
initCursor(ITEM_TYPE_APPLICATION, "");
assertTrue(mLoaderCursor.moveToNext());
- ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
+ ComponentName cn = new ComponentName(mContext.getPackageName(), "placeholder-do");
assertNull(mLoaderCursor.getAppShortcutInfo(
new Intent().setComponent(cn), false /* allowMissingTarget */, true));
}
@@ -136,7 +136,7 @@
initCursor(ITEM_TYPE_APPLICATION, "");
assertTrue(mLoaderCursor.moveToNext());
- ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
+ ComponentName cn = new ComponentName(mContext.getPackageName(), "placeholder-do");
WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
mLoaderCursor.getAppShortcutInfo(
new Intent().setComponent(cn), true /* allowMissingTarget */, true))
@@ -160,7 +160,7 @@
public void checkItemPlacement_outsideBounds() {
mIDP.numRows = 4;
mIDP.numColumns = 4;
- mIDP.numHotseatIcons = 3;
+ mIDP.numDatabaseHotseatIcons = 3;
// Item outside screen bounds are not placed
assertFalse(mLoaderCursor.checkItemPlacement(
@@ -171,7 +171,7 @@
public void checkItemPlacement_overlappingItems() {
mIDP.numRows = 4;
mIDP.numColumns = 4;
- mIDP.numHotseatIcons = 3;
+ mIDP.numDatabaseHotseatIcons = 3;
// Overlapping mItems are not placed
assertTrue(mLoaderCursor.checkItemPlacement(
@@ -197,7 +197,7 @@
public void checkItemPlacement_hotseat() {
mIDP.numRows = 4;
mIDP.numColumns = 4;
- mIDP.numHotseatIcons = 3;
+ mIDP.numDatabaseHotseatIcons = 3;
// Hotseat mItems are only placed based on screenId
assertTrue(mLoaderCursor.checkItemPlacement(
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index 87fe3c0..a2abfd5 100644
--- a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -15,7 +15,7 @@
*/
package com.android.launcher3.model;
-import static com.android.launcher3.util.Executors.createAndStartNewForegroundLooper;
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
import static org.junit.Assert.assertEquals;
@@ -30,6 +30,7 @@
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.shadows.ShadowLooperExecutor;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.LauncherLayoutBuilder;
import com.android.launcher3.util.LauncherModelHelper;
@@ -43,8 +44,8 @@
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowPackageManager;
-import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import java.util.Arrays;
@@ -73,8 +74,9 @@
// Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
// so that we can wait appropriately for the loader to complete.
- mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain"));
- ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor);
+ mTempMainExecutor = new LooperExecutor(createAndStartNewLooper("tempMain"));
+ ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR);
+ sle.setHandler(mTempMainExecutor.getHandler());
}
@Test
diff --git a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index e43df21..412ace0 100644
--- a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -67,7 +67,7 @@
for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
if (info instanceof WorkspaceItemInfo) {
assertEquals(updates.contains(info.id) ? progress: 0,
- ((WorkspaceItemInfo) info).getInstallProgress());
+ ((WorkspaceItemInfo) info).getProgressLevel());
} else {
assertEquals(updates.contains(info.id) ? progress: -1,
((LauncherAppWidgetInfo) info).installProgress);
diff --git a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index ee73b82..4184d33 100644
--- a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -44,7 +44,7 @@
@Test
public void testMigrateProfileId() throws Exception {
SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
- // Add some dummy data
+ // Add some mock data
for (int i = 0; i < 5; i++) {
ContentValues values = new ContentValues();
values.put(Favorites._ID, i);
@@ -64,7 +64,7 @@
@Test
public void testChangeDefaultColumn() throws Exception {
SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
- // Add some dummy data
+ // Add some mock data
for (int i = 0; i < 5; i++) {
ContentValues values = new ContentValues();
values.put(Favorites._ID, i);
diff --git a/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java
index 0ca5ce6..e3694ae 100644
--- a/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java
+++ b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java
@@ -21,7 +21,6 @@
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
import android.content.Context;
import android.os.UserManager;
@@ -30,8 +29,6 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.allapps.AllAppsPagedView;
import com.android.launcher3.allapps.AllAppsRecyclerView;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.shadows.ShadowOverrides;
import com.android.launcher3.util.LauncherLayoutBuilder;
import com.android.launcher3.util.LauncherModelHelper;
@@ -62,20 +59,15 @@
private InvariantDeviceProfile mIdp;
private LauncherModelHelper mModelHelper;
- private LauncherLayoutBuilder mLayoutBuilder;
-
@Before
public void setup() throws Exception {
mModelHelper = new LauncherModelHelper();
mTargetContext = RuntimeEnvironment.application;
mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
- ShadowOverrides.setProvider(UserEventDispatcher.class,
- c -> mock(UserEventDispatcher.class));
Settings.Global.putFloat(mTargetContext.getContentResolver(),
Settings.Global.WINDOW_ANIMATION_SCALE, 0);
mModelHelper.installApp(TEST_PACKAGE);
- mLayoutBuilder = new LauncherLayoutBuilder();
}
@Test
@@ -91,7 +83,7 @@
public void testAllAppsList_workProfile() throws Exception {
ShadowUserManager sum = Shadow.extract(mTargetContext.getSystemService(UserManager.class));
sum.addUser(SYSTEM_USER, "me", FLAG_SYSTEM);
- sum.addUser(WORK_PROFILE_ID, "work", FLAG_PROFILE);
+ sum.addProfile(SYSTEM_USER, WORK_PROFILE_ID, "work", FLAG_PROFILE);
SecondaryDisplayLauncher launcher = loadLauncher();
launcher.showAppDrawer(true);
diff --git a/robolectric_tests/src/com/android/launcher3/settings/SettingsActivityTest.java b/robolectric_tests/src/com/android/launcher3/settings/SettingsActivityTest.java
new file mode 100644
index 0000000..3271812
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/settings/SettingsActivityTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.settings;
+
+import static androidx.preference.PreferenceFragmentCompat.ARG_PREFERENCE_ROOT;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem;
+import static androidx.test.espresso.intent.Intents.intended;
+import static androidx.test.espresso.intent.matcher.BundleMatchers.hasEntry;
+import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent;
+import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT;
+import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARGS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.intent.Intents;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.launcher3.R;
+import com.android.systemui.shared.plugins.PluginPrefs;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SettingsActivityTest {
+
+ private Context mApplicationContext;
+
+ @Before
+ public void setUp() {
+ mApplicationContext = ApplicationProvider.getApplicationContext();
+ Intents.init();
+ }
+
+ @After
+ public void tearDown() {
+ Intents.release();
+ }
+
+ @Test
+ public void testSettings_aboutTap_launchesActivity() {
+ ActivityScenario.launch(SettingsActivity.class);
+ onView(withId(R.id.recycler_view)).perform(
+ actionOnItem(hasDescendant(withText("About")), click()));
+
+ intended(allOf(
+ hasComponent(SettingsActivity.class.getName()),
+ hasExtra(
+ equalTo(EXTRA_FRAGMENT_ARGS),
+ hasEntry(ARG_PREFERENCE_ROOT, "about_screen"))));
+ }
+
+ @Test
+ public void testSettings_developerOptionsTap_launchesActivityWithFragment() {
+ PluginPrefs.setHasPlugins(mApplicationContext);
+ ActivityScenario.launch(SettingsActivity.class);
+ onView(withId(R.id.recycler_view)).perform(
+ actionOnItem(hasDescendant(withText("Developer Options")), click()));
+
+ intended(allOf(
+ hasComponent(SettingsActivity.class.getName()),
+ hasExtra(EXTRA_FRAGMENT, DeveloperOptionsFragment.class.getName())));
+ }
+
+ @Test
+ public void testSettings_aboutScreenIntent() {
+ Bundle fragmentArgs = new Bundle();
+ fragmentArgs.putString(ARG_PREFERENCE_ROOT, "about_screen");
+
+ Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
+ .putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs);
+ ActivityScenario.launch(intent);
+
+ onView(withText("About")).check(matches(isDisplayed()));
+ onView(withText("Version")).check(matches(isDisplayed()));
+ onView(withContentDescription("Navigate up")).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testSettings_developerOptionsFragmentIntent() {
+ Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
+ .putExtra(EXTRA_FRAGMENT, DeveloperOptionsFragment.class.getName());
+ ActivityScenario.launch(intent);
+
+ onView(withText("Developer Options")).check(matches(isDisplayed()));
+ onView(withId(R.id.filter_box)).check(matches(isDisplayed()));
+ onView(withContentDescription("Navigate up")).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testSettings_intentWithUnknownFragment() {
+ String fragmentClass = PreferenceFragmentCompat.class.getName();
+ Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
+ .putExtra(EXTRA_FRAGMENT, fragmentClass);
+
+ try {
+ ActivityScenario.launch(intent);
+ Assert.fail("Should have thrown an IllegalArgumentException.");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains(fragmentClass);
+ }
+ }
+
+ @Test
+ public void testSettings_backButtonFinishesActivity() {
+ Bundle fragmentArgs = new Bundle();
+ fragmentArgs.putString(ARG_PREFERENCE_ROOT, "about_screen");
+ Intent intent = new Intent(mApplicationContext, SettingsActivity.class)
+ .putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs);
+ ActivityScenario<SettingsActivity> scenario = ActivityScenario.launch(intent);
+
+ onView(withContentDescription("Navigate up")).perform(click());
+ scenario.onActivity(activity -> assertThat(activity.isFinishing()).isTrue());
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java
deleted file mode 100644
index da8b919..0000000
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shadows;
-
-import android.os.Process;
-import android.os.UserHandle;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.shadows.ShadowApplicationPackageManager;
-
-/**
- * Shadow for {@link ShadowApplicationPackageManager} which create mock predictors
- */
-@Implements(className = "android.app.ApplicationPackageManager")
-public class LShadowApplicationPackageManager extends ShadowApplicationPackageManager {
-
- @Implementation
- public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
- return Process.myUserHandle().equals(user) ? label : "Work " + label;
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
deleted file mode 100644
index abd90bb..0000000
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shadows;
-
-import android.graphics.Bitmap;
-import android.graphics.Paint;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.shadows.ShadowBitmap;
-
-/**
- * Extension of {@link ShadowBitmap} with missing shadow methods
- */
-@Implements(value = Bitmap.class)
-public class LShadowBitmap extends ShadowBitmap {
-
- @Implementation
- protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
- return extractAlpha();
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowTypeface.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowTypeface.java
deleted file mode 100644
index 0e7c1de..0000000
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowTypeface.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.shadows;
-
-import android.graphics.Typeface;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.shadows.ShadowTypeface;
-
-/**
- * Extension of {@link ShadowTypeface} with missing shadow methods
- */
-@Implements(Typeface.class)
-public class LShadowTypeface extends ShadowTypeface {
-
- @Implementation
- public static Typeface create(Typeface family, int weight, boolean italic) {
- int style = italic ? Typeface.ITALIC : Typeface.NORMAL;
- if (weight >= 400) {
- style |= Typeface.BOLD;
- }
- return create(family, style);
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
deleted file mode 100644
index edf8edb..0000000
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shadows;
-
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.SparseBooleanArray;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.shadows.ShadowUserManager;
-
-/**
- * Extension of {@link ShadowUserManager} with missing shadow methods
- */
-@Implements(value = UserManager.class)
-public class LShadowUserManager extends ShadowUserManager {
-
- private final SparseBooleanArray mQuietUsers = new SparseBooleanArray();
- private final SparseBooleanArray mLockedUsers = new SparseBooleanArray();
-
- @Implementation
- protected boolean isQuietModeEnabled(UserHandle userHandle) {
- return mQuietUsers.get(userHandle.hashCode());
- }
-
- public void setQuietModeEnabled(UserHandle userHandle, boolean enabled) {
- mQuietUsers.put(userHandle.hashCode(), enabled);
- }
-
- @Implementation
- protected boolean isUserUnlocked(UserHandle userHandle) {
- return !mLockedUsers.get(userHandle.hashCode());
- }
-
- public void setUserLocked(UserHandle userHandle, boolean enabled) {
- mLockedUsers.put(userHandle.hashCode(), enabled);
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowWallpaperManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowWallpaperManager.java
deleted file mode 100644
index d60251c..0000000
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowWallpaperManager.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shadows;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-
-import android.app.WallpaperManager;
-import android.content.Context;
-
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.shadows.ShadowApplication;
-import org.robolectric.shadows.ShadowUserManager;
-import org.robolectric.shadows.ShadowWallpaperManager;
-
-/**
- * Extension of {@link ShadowUserManager} with missing shadow methods
- */
-@Implements(WallpaperManager.class)
-public class LShadowWallpaperManager extends ShadowWallpaperManager {
-
- @Implementation
- protected static WallpaperManager getInstance(Context context) {
- return context.getSystemService(WallpaperManager.class);
- }
-
- /**
- * Remove this once the fix for
- * https://github.com/robolectric/robolectric/issues/5285
- * is available
- */
- public static void initializeMock() {
- WallpaperManager wm = mock(WallpaperManager.class);
- ShadowApplication shadowApplication = Shadows.shadowOf(RuntimeEnvironment.application);
- shadowApplication.setSystemService(Context.WALLPAPER_SERVICE, wm);
- doReturn(0).when(wm).getDesiredMinimumWidth();
- doReturn(0).when(wm).getDesiredMinimumHeight();
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
index 344f532..b58e4b7 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
@@ -18,11 +18,15 @@
import android.content.Context;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.uioverrides.DeviceFlag;
import com.android.launcher3.util.LooperExecutor;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.Shadow;
/**
* Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
@@ -30,6 +34,9 @@
@Implements(value = DeviceFlag.class, isInAndroidSdk = false)
public class ShadowDeviceFlag {
+ @RealObject private DeviceFlag mRealObject;
+ @Nullable private Boolean mValue;
+
/**
* Mock change listener as it uses internal system classes not available to robolectric
*/
@@ -40,4 +47,16 @@
protected static boolean getDeviceValue(String key, boolean defaultValue) {
return defaultValue;
}
+
+ @Implementation
+ public boolean get() {
+ if (mValue != null) {
+ return mValue;
+ }
+ return Shadow.directlyOn(mRealObject, DeviceFlag.class, "get");
+ }
+
+ public void setValue(boolean value) {
+ mValue = new Boolean(value);
+ }
}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
index a3b7dc7..57eda7e 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
@@ -23,6 +23,8 @@
import android.os.Handler;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.util.LooperExecutor;
import org.robolectric.annotation.Implementation;
@@ -37,8 +39,13 @@
@RealObject private LooperExecutor mRealExecutor;
+ private Handler mOverriddenHandler;
+
@Implementation
protected Handler getHandler() {
+ if (mOverriddenHandler != null) {
+ return mOverriddenHandler;
+ }
Handler handler = directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
Thread thread = handler.getLooper().getThread();
if (!thread.isAlive()) {
@@ -49,4 +56,8 @@
}
return directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
}
+
+ public void setHandler(@Nullable Handler handler) {
+ mOverriddenHandler = handler;
+ }
}
diff --git a/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java
new file mode 100644
index 0000000..17d0ac1
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.testing;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
+
+/** An empty activity for {@link android.app.Fragment}s, {@link android.view.View}s testing. */
+public class TestActivity extends BaseActivity implements ActivityContext {
+
+ private DeviceProfile mDeviceProfile;
+
+ @Override
+ public BaseDragLayer getDragLayer() {
+ return new BaseDragLayer(this, /* attrs= */ null, /* alphaChannelCount= */ 1) {
+ @Override
+ public void recreateControllers() {
+ // Do nothing.
+ }
+ };
+ }
+
+ @Override
+ public DeviceProfile getDeviceProfile() {
+ return mDeviceProfile;
+ }
+
+ public void setDeviceProfile(DeviceProfile deviceProfile) {
+ mDeviceProfile = deviceProfile;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
index d330d10..ea75548 100644
--- a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
+++ b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
@@ -20,7 +20,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
-import static org.mockito.Mockito.mock;
import android.content.Context;
import android.os.SystemClock;
@@ -36,12 +35,10 @@
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.folder.FolderPagedView;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.shadows.ShadowOverrides;
import com.android.launcher3.util.LauncherLayoutBuilder;
import com.android.launcher3.util.LauncherLayoutBuilder.FolderBuilder;
import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
import org.junit.Before;
import org.junit.Test;
@@ -57,6 +54,7 @@
*/
@RunWith(RobolectricTestRunner.class)
@LooperMode(Mode.PAUSED)
+@org.junit.Ignore
public class LauncherUIScrollTest {
private Context mTargetContext;
@@ -70,8 +68,6 @@
mModelHelper = new LauncherModelHelper();
mTargetContext = RuntimeEnvironment.application;
mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
- ShadowOverrides.setProvider(UserEventDispatcher.class,
- c -> mock(UserEventDispatcher.class));
Settings.Global.putFloat(mTargetContext.getContentResolver(),
Settings.Global.WINDOW_ANIMATION_SCALE, 0);
@@ -165,7 +161,7 @@
private static MotionEvent createScrollEvent(int scroll) {
DeviceProfile dp = InvariantDeviceProfile.INSTANCE
- .get(RuntimeEnvironment.application).portraitProfile;
+ .get(RuntimeEnvironment.application).supportedProfiles.get(0);
final PointerProperties[] pointerProperties = new PointerProperties[1];
pointerProperties[0] = new PointerProperties();
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 0388087..846e201 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -48,10 +48,12 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.shadows.ShadowLooperExecutor;
import org.mockito.ArgumentCaptor;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowContentResolver;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;
@@ -81,7 +83,7 @@
public static final int NO__ICON = -1;
public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
- // Authority for providing a dummy default-workspace-layout data.
+ // Authority for providing a test default-workspace-layout data.
private static final String TEST_PROVIDER_AUTHORITY =
LauncherModelHelper.class.getName().toLowerCase();
private static final int DEFAULT_BITMAP_SIZE = 10;
@@ -252,7 +254,7 @@
}
/**
- * Adds a dummy item in the DB.
+ * Adds a mock item in the DB.
* @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
* folder (where the type represents the number of items in the folder).
*/
@@ -310,7 +312,7 @@
}
/**
- * Initializes the DB with dummy elements to represent the provided grid structure.
+ * Initializes the DB with mock elements to represent the provided grid structure.
* @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
* type definitions. The first dimension represents the screens and the next
* two represent the workspace grid.
@@ -347,20 +349,20 @@
}
/**
- * Sets up a dummy provider to load the provided layout by default, next time the layout loads
+ * Sets up a mock provider to load the provided layout by default, next time the layout loads
*/
public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
throws Exception {
Context context = RuntimeEnvironment.application;
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
- idp.numRows = idp.numColumns = idp.numHotseatIcons = DEFAULT_GRID_SIZE;
+ idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
Settings.Secure.putString(context.getContentResolver(),
"launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
shadowOf(context.getPackageManager())
- .addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+ .addProviderIfNotPresent(new ComponentName("com.test", "Mock")).authority =
TEST_PROVIDER_AUTHORITY;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -403,14 +405,16 @@
public void loadModelSync() throws ExecutionException, InterruptedException {
// Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
// so that we can wait appropriately for the loader to complete.
- ReflectionHelpers.setField(getModel(), "mMainExecutor", Executors.UI_HELPER_EXECUTOR);
+ ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR);
+ sle.setHandler(Executors.UI_HELPER_EXECUTOR.getHandler());
Callbacks mockCb = mock(Callbacks.class);
getModel().addCallbacksAndLoad(mockCb);
Executors.MODEL_EXECUTOR.submit(() -> { }).get();
Executors.UI_HELPER_EXECUTOR.submit(() -> { }).get();
- ReflectionHelpers.setField(getModel(), "mMainExecutor", Executors.MAIN_EXECUTOR);
+
+ sle.setHandler(null);
getModel().removeCallbacks(mockCb);
}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherTestApplication.java b/robolectric_tests/src/com/android/launcher3/util/LauncherTestApplication.java
index 6dd4df8..efac150 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherTestApplication.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherTestApplication.java
@@ -19,7 +19,6 @@
import android.app.Application;
-import com.android.launcher3.shadows.LShadowWallpaperManager;
import com.android.launcher3.shadows.ShadowMainThreadInitializedObject;
import com.android.launcher3.shadows.ShadowOverrides;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
@@ -37,9 +36,6 @@
// Disable plugins
PluginManagerWrapper.INSTANCE.initializeForTesting(mock(PluginManagerWrapper.class));
-
- // Initialize mock wallpaper manager
- LShadowWallpaperManager.initializeMock();
}
@Override
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
index f019a20..fdddab4 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
@@ -18,6 +18,8 @@
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
+import static com.android.launcher3.Utilities.createHomeIntent;
+
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -46,10 +48,7 @@
*/
public static String getLauncherClassName() {
Context context = RuntimeEnvironment.application;
- Intent homeIntent = new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setPackage(context.getPackageName())
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Intent homeIntent = createHomeIntent().setPackage(context.getPackageName());
List<ResolveInfo> launchers = context.getPackageManager()
.queryIntentActivities(homeIntent, 0);
diff --git a/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java
new file mode 100644
index 0000000..1090d1e
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.os.UserHandle;
+import android.util.Size;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class CachingWidgetPreviewLoaderTest {
+ private final Size SIZE_10_10 = new Size(10, 10);
+ private final Size SIZE_20_20 = new Size(20, 20);
+ private static final String TEST_PACKAGE = "com.example.test";
+ private final ComponentName TEST_PROVIDER =
+ new ComponentName(TEST_PACKAGE, ".WidgetProvider");
+ private final ComponentName TEST_PROVIDER2 =
+ new ComponentName(TEST_PACKAGE, ".WidgetProvider2");
+ private final Bitmap BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
+ private final Bitmap BITMAP2 = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888);
+
+
+ @Mock private CancellationSignal mCancellationSignal;
+ @Mock private WidgetPreviewLoader mDelegate;
+ @Mock private IconCache mIconCache;
+ @Mock private DeviceProfile mDeviceProfile;
+ @Mock private LauncherAppWidgetProviderInfo mProviderInfo;
+ @Mock private LauncherAppWidgetProviderInfo mProviderInfo2;
+ @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback;
+ @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback2;
+ @Captor private ArgumentCaptor<WidgetPreviewLoadedCallback> mCallbackCaptor;
+
+ private TestActivity mTestActivity;
+ private CachingWidgetPreviewLoader mLoader;
+ private WidgetItem mWidgetItem;
+ private WidgetItem mWidgetItem2;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mLoader = new CachingWidgetPreviewLoader(mDelegate);
+
+ mTestActivity = Robolectric.buildActivity(TestActivity.class).setup().get();
+ mTestActivity.setDeviceProfile(mDeviceProfile);
+
+ when(mDelegate.loadPreview(any(), any(), any(), any())).thenReturn(mCancellationSignal);
+
+ mProviderInfo.provider = TEST_PROVIDER;
+ when(mProviderInfo.getProfile()).thenReturn(new UserHandle(0));
+
+ mProviderInfo2.provider = TEST_PROVIDER2;
+ when(mProviderInfo2.getProfile()).thenReturn(new UserHandle(0));
+
+ InvariantDeviceProfile testProfile = new InvariantDeviceProfile();
+ testProfile.numRows = 5;
+ testProfile.numColumns = 5;
+
+ mWidgetItem = new WidgetItem(mProviderInfo, testProfile, mIconCache);
+ mWidgetItem2 = new WidgetItem(mProviderInfo2, testProfile, mIconCache);
+ }
+
+ @Test
+ public void getPreview_notInCache_shouldReturnNull() {
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void getPreview_notInCache_shouldNotCallDelegate() {
+ mLoader.getPreview(mWidgetItem, SIZE_10_10);
+
+ verifyZeroInteractions(mDelegate);
+ }
+
+ @Test
+ public void getPreview_inCache_shouldReturnCachedBitmap() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void getPreview_otherSizeInCache_shouldReturnNull() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isNull();
+ }
+
+ @Test
+ public void getPreview_otherItemInCache_shouldReturnNull() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem2, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void getPreview_shouldStoreMultipleSizesPerItem() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP2);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isEqualTo(BITMAP2);
+ }
+
+ @Test
+ public void loadPreview_notInCache_shouldStartLoading() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ verify(mDelegate).loadPreview(eq(mTestActivity), eq(mWidgetItem), eq(SIZE_10_10), any());
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ }
+
+ @Test
+ public void loadPreview_thenLoaded_shouldCallBack() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_shouldCancelDelegateRequest() {
+ CancellationSignal cancellationSignal =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ cancellationSignal.cancel();
+
+ verify(mCancellationSignal).cancel();
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_otherCallListening_shouldNotCancelDelegateRequest() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ cancellationSignal1.cancel();
+
+ verifyZeroInteractions(mCancellationSignal);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_otherCallListening_loaded_shouldCallBackToNonCancelled() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ cancellationSignal1.cancel();
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_bothCallsCancelled_shouldCancelDelegateRequest() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ CancellationSignal cancellationSignal2 =
+ mLoader.loadPreview(
+ mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ cancellationSignal1.cancel();
+ cancellationSignal2.cancel();
+
+ verify(mCancellationSignal).cancel();
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ verifyZeroInteractions(mPreviewLoadedCallback2);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void loadPreview_multipleCallbacks_shouldOnlyCallDelegateOnce() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ verify(mDelegate).loadPreview(any(), any(), any(), any());
+ }
+
+ @Test
+ public void loadPreview_multipleCallbacks_shouldForwardResultToEachCallback() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_inCache_shouldCallBackImmediately() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ reset(mDelegate);
+
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ verifyZeroInteractions(mDelegate);
+ }
+
+ @Test
+ public void loadPreview_thenLoaded_thenCancelled_shouldNotRemovePreviewFromCache() {
+ CancellationSignal cancellationSignal =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ cancellationSignal.cancel();
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void isPreviewLoaded_notLoaded_shouldReturnFalse() {
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_otherSizeLoaded_shouldReturnFalse() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_otherItemLoaded_shouldReturnFalse() {
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_loaded_shouldReturnTrue() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isTrue();
+ }
+
+ @Test
+ public void clearPreviews_notInCache_shouldBeNoOp() {
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_shouldRemovePreview() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_multipleSizes_shouldRemoveAllSizes() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_otherItems_shouldOnlyRemoveSpecifiedItems() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isTrue();
+ }
+
+ @Test
+ public void clearPreviews_inCache_otherItems_shouldRemoveAllSpecifiedItems() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Arrays.asList(mWidgetItem, mWidgetItem2));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_loading_shouldCancelLoad() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ verify(mCancellationSignal).cancel();
+ }
+
+ @Test
+ public void clearAll_cacheEmpty_shouldBeNoOp() {
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_shouldRemovePreview() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_multipleSizes_shouldRemoveAllSizes() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_multipleItems_shouldRemoveAll() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_20_20, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearAll_loading_shouldCancelLoad() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ mLoader.clearAll();
+
+ verify(mCancellationSignal).cancel();
+ }
+
+ private void loadPreviewIntoCache(WidgetItem widgetItem, Size size, Bitmap bitmap) {
+ reset(mDelegate);
+ mLoader.loadPreview(mTestActivity, widgetItem, size, ignored -> {});
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(bitmap);
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
new file mode 100644
index 0000000..a6f892c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class LauncherAppWidgetProviderInfoTest {
+
+ private static final int CELL_SIZE = 50;
+ private static final int NUM_OF_COLS = 4;
+ private static final int NUM_OF_ROWS = 5;
+
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ }
+
+ @Test
+ public void initSpans_minWidthSmallerThanCellWidth_shouldInitializeSpansToOne() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 20;
+ info.minHeight = 20;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.spanX).isEqualTo(1);
+ assertThat(info.spanY).isEqualTo(1);
+ }
+
+ @Test
+ public void initSpans_minWidthLargerThanCellWidth_shouldInitializeSpans() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 80;
+ info.minHeight = 80;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.spanX).isEqualTo(2);
+ assertThat(info.spanY).isEqualTo(2);
+ }
+
+ @Test
+ public void
+ initSpans_minWidthLargerThanGridColumns_shouldInitializeSpansToAtMostTheGridColumns() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = CELL_SIZE * (NUM_OF_COLS + 1);
+ info.minHeight = 20;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.spanX).isEqualTo(NUM_OF_COLS);
+ assertThat(info.spanY).isEqualTo(1);
+ }
+
+ @Test
+ public void initSpans_minHeightLargerThanGridRows_shouldInitializeSpansToAtMostTheGridRows() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 20;
+ info.minHeight = 50 * (NUM_OF_ROWS + 1);
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.spanX).isEqualTo(1);
+ assertThat(info.spanY).isEqualTo(NUM_OF_ROWS);
+ }
+
+ @Test
+ public void initSpans_minResizeWidthUnspecified_shouldInitializeMinSpansToOne() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.minSpanX).isEqualTo(1);
+ assertThat(info.minSpanY).isEqualTo(1);
+ }
+
+ @Test
+ public void initSpans_minResizeWidthSmallerThanCellWidth_shouldInitializeMinSpansToOne() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 100;
+ info.minHeight = 100;
+ info.minResizeWidth = 20;
+ info.minResizeHeight = 20;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.minSpanX).isEqualTo(1);
+ assertThat(info.minSpanY).isEqualTo(1);
+ }
+
+ @Test
+ public void initSpans_minResizeWidthLargerThanCellWidth_shouldInitializeMinSpans() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 100;
+ info.minHeight = 100;
+ info.minResizeWidth = 80;
+ info.minResizeHeight = 80;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.minSpanX).isEqualTo(2);
+ assertThat(info.minSpanY).isEqualTo(2);
+ }
+
+ @Test
+ public void initSpans_minResizeWidthWithCellSpacingAndWidgetInset_shouldInitializeMinSpans() {
+ InvariantDeviceProfile idp = createIDP();
+ DeviceProfile dp = idp.supportedProfiles.get(0);
+ Rect padding = new Rect();
+ AppWidgetHostView.getDefaultPaddingForWidget(mContext, null, padding);
+ int maxPadding = Math.max(Math.max(padding.left, padding.right),
+ Math.max(padding.top, padding.bottom));
+ dp.cellLayoutBorderSpacingPx = maxPadding + 1;
+ Mockito.when(dp.shouldInsetWidgets()).thenReturn(true);
+
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = CELL_SIZE * 3;
+ info.minHeight = CELL_SIZE * 3;
+ info.minResizeWidth = CELL_SIZE * 2 + maxPadding;
+ info.minResizeHeight = CELL_SIZE * 2 + maxPadding;
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.minSpanX).isEqualTo(2);
+ assertThat(info.minSpanY).isEqualTo(2);
+ }
+
+ @Test
+ public void initSpans_minResizeWidthWithCellSpacingAndNoWidgetInset_shouldInitializeMinSpans() {
+ InvariantDeviceProfile idp = createIDP();
+ DeviceProfile dp = idp.supportedProfiles.get(0);
+ Rect padding = new Rect();
+ AppWidgetHostView.getDefaultPaddingForWidget(mContext, null, padding);
+ int maxPadding = Math.max(Math.max(padding.left, padding.right),
+ Math.max(padding.top, padding.bottom));
+ dp.cellLayoutBorderSpacingPx = maxPadding - 1;
+ Mockito.when(dp.shouldInsetWidgets()).thenReturn(false);
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = CELL_SIZE * 3;
+ info.minHeight = CELL_SIZE * 3;
+ info.minResizeWidth = CELL_SIZE * 2 + maxPadding;
+ info.minResizeHeight = CELL_SIZE * 2 + maxPadding;
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.minSpanX).isEqualTo(3);
+ assertThat(info.minSpanY).isEqualTo(3);
+ }
+
+ @Test
+ public void
+ initSpans_minResizeWidthHeightLargerThanMinWidth_shouldUseMinWidthHeightAsMinSpans() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 20;
+ info.minHeight = 20;
+ info.minResizeWidth = 80;
+ info.minResizeHeight = 80;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.minSpanX).isEqualTo(1);
+ assertThat(info.minSpanY).isEqualTo(1);
+ }
+
+ @Test
+ public void isMinSizeFulfilled_minWidthAndHeightWithinGridSize_shouldReturnTrue() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 80;
+ info.minHeight = 80;
+ info.minResizeWidth = 50;
+ info.minResizeHeight = 50;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.isMinSizeFulfilled()).isTrue();
+ }
+
+ @Test
+ public void
+ isMinSizeFulfilled_minWidthAndMinResizeWidthExceededGridColumns_shouldReturnFalse() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = CELL_SIZE * (NUM_OF_COLS + 2);
+ info.minHeight = 80;
+ info.minResizeWidth = CELL_SIZE * (NUM_OF_COLS + 1);
+ info.minResizeHeight = 50;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.isMinSizeFulfilled()).isFalse();
+ }
+
+ @Test
+ public void isMinSizeFulfilled_minHeightAndMinResizeHeightExceededGridRows_shouldReturnFalse() {
+ LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+ info.minWidth = 80;
+ info.minHeight = CELL_SIZE * (NUM_OF_ROWS + 2);
+ info.minResizeWidth = 50;
+ info.minResizeHeight = CELL_SIZE * (NUM_OF_ROWS + 1);
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.isMinSizeFulfilled()).isFalse();
+ }
+
+ private InvariantDeviceProfile createIDP() {
+ DeviceProfile profile = Mockito.mock(DeviceProfile.class);
+ doAnswer(i -> {
+ ((Point) i.getArgument(0)).set(CELL_SIZE, CELL_SIZE);
+ return null;
+ }).when(profile).getCellSize(any(Point.class));
+ Mockito.when(profile.getCellSize()).thenReturn(new Point(CELL_SIZE, CELL_SIZE));
+
+ InvariantDeviceProfile idp = new InvariantDeviceProfile();
+ List<DeviceProfile> supportedProfiles = new ArrayList<>(idp.supportedProfiles);
+ supportedProfiles.add(profile);
+ idp.supportedProfiles = Collections.unmodifiableList(supportedProfiles);
+ idp.numColumns = NUM_OF_COLS;
+ idp.numRows = NUM_OF_ROWS;
+ return idp;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
deleted file mode 100644
index 84c65b1..0000000
--- a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.robolectric.Shadows.shadowOf;
-
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.view.LayoutInflater;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowPackageManager;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-@RunWith(RobolectricTestRunner.class)
-public class WidgetsListAdapterTest {
-
- @Mock private LayoutInflater mMockLayoutInflater;
- @Mock private WidgetPreviewLoader mMockWidgetCache;
- @Mock private RecyclerView.AdapterDataObserver mListener;
- @Mock private IconCache mIconCache;
-
- private WidgetsListAdapter mAdapter;
- private InvariantDeviceProfile mTestProfile;
- private Context mContext;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- mContext = RuntimeEnvironment.application;
- mTestProfile = new InvariantDeviceProfile();
- mTestProfile.numRows = 5;
- mTestProfile.numColumns = 5;
- mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
- mIconCache, null, null);
- mAdapter.registerAdapterDataObserver(mListener);
- }
-
- @Test
- public void test_notifyDataSetChanged() throws Exception {
- mAdapter.setWidgets(generateSampleMap(1));
- verify(mListener, times(1)).onChanged();
- }
-
- @Test
- public void test_notifyItemInserted() throws Exception {
- mAdapter.setWidgets(generateSampleMap(1));
- mAdapter.setWidgets(generateSampleMap(2));
- verify(mListener, times(1)).onChanged();
- verify(mListener, times(1)).onItemRangeInserted(eq(1), eq(1));
- }
-
- @Test
- public void test_notifyItemRemoved() throws Exception {
- mAdapter.setWidgets(generateSampleMap(2));
- mAdapter.setWidgets(generateSampleMap(1));
- verify(mListener, times(1)).onChanged();
- verify(mListener, times(1)).onItemRangeRemoved(eq(1), eq(1));
- }
-
- @Test
- public void testNotifyItemChanged_PackageIconDiff() throws Exception {
- mAdapter.setWidgets(generateSampleMap(1));
- mAdapter.setWidgets(generateSampleMap(1));
- verify(mListener, times(1)).onChanged();
- verify(mListener, times(1)).onItemRangeChanged(eq(0), eq(1), isNull());
- }
-
- @Test
- public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception {
- // TODO: same package name but item number changed
- }
-
- @Test
- public void testNotifyItemInsertedRemoved_hodgepodge() throws Exception {
- // TODO: insert and remove combined. curMap
- // newMap [A, C, D] [A, B, E]
- // B - C < 0, removed B from index 1 [A, E]
- // E - C > 0, C inserted to index 1 [A, C, E]
- // E - D > 0, D inserted to index 2 [A, C, D, E]
- // E - null = -1, E deleted from index 3 [A, C, D]
- }
-
- /**
- * Helper method to generate the sample widget model map that can be used for the tests
- * @param num the number of WidgetItem the map should contain
- */
- private ArrayList<WidgetListRowEntry> generateSampleMap(int num) {
- ArrayList<WidgetListRowEntry> result = new ArrayList<>();
- if (num <= 0) return result;
- ShadowPackageManager spm = shadowOf(mContext.getPackageManager());
-
- for (int i = 0; i < num; i++) {
- ComponentName cn = new ComponentName("com.dummy.apk" + i, "DummyWidet");
-
- AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
- widgetInfo.provider = cn;
- ReflectionHelpers.setField(widgetInfo, "providerInfo", spm.addReceiverIfNotPresent(cn));
-
- WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
- .fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
-
- PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
- pInfo.title = pInfo.packageName;
- pInfo.user = wi.user;
- pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
-
- result.add(new WidgetListRowEntry(pInfo, new ArrayList<>(Collections.singleton(wi))));
- }
-
- return result;
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
new file mode 100644
index 0000000..b9f183c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.UserHandle;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsDiffReporterTest {
+ private static final String TEST_PACKAGE_PREFIX = "com.android.test";
+ private static final WidgetListBaseRowEntryComparator COMPARATOR =
+ new WidgetListBaseRowEntryComparator();
+
+ @Mock private IconCache mIconCache;
+ @Mock private RecyclerView.Adapter mAdapter;
+
+ private InvariantDeviceProfile mTestProfile;
+ private WidgetsDiffReporter mWidgetsDiffReporter;
+ private Context mContext;
+ private WidgetsListHeaderEntry mHeaderA;
+ private WidgetsListHeaderEntry mHeaderB;
+ private WidgetsListHeaderEntry mHeaderC;
+ private WidgetsListHeaderEntry mHeaderD;
+ private WidgetsListHeaderEntry mHeaderE;
+ private WidgetsListContentEntry mContentC;
+ private WidgetsListContentEntry mContentE;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+
+ doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+ .getComponent().getPackageName())
+ .when(mIconCache).getTitleNoCache(any());
+
+ mContext = RuntimeEnvironment.application;
+ mWidgetsDiffReporter = new WidgetsDiffReporter(mIconCache, mAdapter);
+ mHeaderA = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A",
+ /* appName= */ "A", /* numOfWidgets= */ 3);
+ mHeaderB = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B",
+ /* appName= */ "B", /* numOfWidgets= */ 3);
+ mHeaderC = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C",
+ /* appName= */ "C", /* numOfWidgets= */ 3);
+ mContentC = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "C",
+ /* appName= */ "C", /* numOfWidgets= */ 3);
+ mHeaderD = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "D",
+ /* appName= */ "D", /* numOfWidgets= */ 3);
+ mHeaderE = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "E",
+ /* appName= */ "E", /* numOfWidgets= */ 3);
+ mContentE = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "E",
+ /* appName= */ "E", /* numOfWidgets= */ 3);
+ }
+
+ @Test
+ public void listNotChanged_shouldNotInvokeAnyCallbacks() {
+ // GIVEN the current list has app headers [A, B, C].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mHeaderC));
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, currentList, COMPARATOR);
+
+ // THEN there is no adaptor callback.
+ verifyZeroInteractions(mAdapter);
+ // THEN the current list contains the same entries.
+ assertThat(currentList).containsExactly(mHeaderA, mHeaderB, mHeaderC);
+ }
+
+ @Test
+ public void headersOnly_emptyListToNonEmpty_shouldInvokeNotifyDataSetChanged() {
+ // GIVEN the current list has app headers [A, B, C].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>();
+
+ List<WidgetsListBaseEntry> newList = List.of(
+ createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A", "A", 3),
+ createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B", "B", 3),
+ createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C", "C", 3));
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN notifyDataSetChanged is called
+ verify(mAdapter).notifyDataSetChanged();
+ // THEN the current list contains all elements from the new list.
+ assertThat(currentList).containsExactlyElementsIn(newList);
+ }
+
+ @Test
+ public void headersOnly_nonEmptyToEmptyList_shouldInvokeNotifyDataSetChanged() {
+ // GIVEN the current list has app headers [A, B, C].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mHeaderC));
+ // GIVEN the new list is empty.
+ List<WidgetsListBaseEntry> newList = List.of();
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN notifyDataSetChanged is called.
+ verify(mAdapter).notifyDataSetChanged();
+ // THEN the current list isEmpty.
+ assertThat(currentList).isEmpty();
+ }
+
+ @Test
+ public void headersOnly_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() {
+ // GIVEN the current list has app headers [A, B, D].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mHeaderD));
+ // GIVEN the new list has app headers [A, C, E].
+ List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderC, mHeaderE);
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN "B" is removed from position 1.
+ verify(mAdapter).notifyItemRemoved(/* position= */ 1);
+ // THEN "D" is removed from position 2.
+ verify(mAdapter).notifyItemRemoved(/* position= */ 2);
+ // THEN "C" is inserted at position 1.
+ verify(mAdapter).notifyItemInserted(/* position= */ 1);
+ // THEN "E" is inserted at position 2.
+ verify(mAdapter).notifyItemInserted(/* position= */ 2);
+ // THEN the current list contains all elements from the new list.
+ assertThat(currentList).containsExactlyElementsIn(newList);
+ }
+
+ @Test
+ public void headersContentsMix_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() {
+ // GIVEN the current list has app headers [A, B, E content].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mContentE));
+ // GIVEN the new list has app headers [A, C content, D].
+ List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mContentC, mHeaderD);
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN "B" is removed from position 1.
+ verify(mAdapter).notifyItemRemoved(/* position= */ 1);
+ // THEN "C content" is inserted at position 1.
+ verify(mAdapter).notifyItemInserted(/* position= */ 1);
+ // THEN "D" is inserted at position 2.
+ verify(mAdapter).notifyItemInserted(/* position= */ 2);
+ // THEN "E content" is removed from position 3.
+ verify(mAdapter).notifyItemRemoved(/* position= */ 3);
+ // THEN the current list contains all elements from the new list.
+ assertThat(currentList).containsExactlyElementsIn(newList);
+ }
+
+ @Test
+ public void headersContentsMix_userInteractWithHeader_shouldInvokeCorrectCallbacks() {
+ // GIVEN the current list has app headers [A, B, E content].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mContentE));
+ // GIVEN the new list has app headers [A, B, E content] and the user has interacted with B.
+ List<WidgetsListBaseEntry> newList =
+ List.of(mHeaderA, mHeaderB.withWidgetListShown(), mContentE);
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN notify "B" has been changed.
+ verify(mAdapter).notifyItemChanged(/* position= */ 1);
+ // THEN the current list contains all elements from the new list.
+ assertThat(currentList).containsExactlyElementsIn(newList);
+ }
+
+ @Test
+ public void headersContentsMix_headerWidgetsModified_shouldInvokeCorrectCallbacks() {
+ // GIVEN the current list has app headers [A, B, E content].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mContentE));
+ // GIVEN the new list has one of the headers widgets list modified.
+ List<WidgetsListBaseEntry> newList = List.of(
+ new WidgetsListHeaderEntry(
+ mHeaderA.mPkgItem, mHeaderA.mTitleSectionName,
+ mHeaderA.mWidgets.subList(0, 1)),
+ mHeaderB, mContentE);
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN notify "A" has been changed.
+ verify(mAdapter).notifyItemChanged(/* position= */ 0);
+ // THEN the current list contains all elements from the new list.
+ assertThat(currentList).containsExactlyElementsIn(newList);
+ }
+
+ @Test
+ public void headersContentsMix_contentMaxSpanSizeModified_shouldInvokeCorrectCallbacks() {
+ // GIVEN the current list has app headers [A, B, E content].
+ ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+ List.of(mHeaderA, mHeaderB, mContentE));
+ // GIVEN the new list has max span size in "E content" modified.
+ List<WidgetsListBaseEntry> newList = List.of(
+ mHeaderA,
+ mHeaderB,
+ new WidgetsListContentEntry(
+ mContentE.mPkgItem,
+ mContentE.mTitleSectionName,
+ mContentE.mWidgets,
+ mContentE.getMaxSpanSizeInCells() + 1));
+
+ // WHEN computing the list difference.
+ mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+ // THEN notify "E content" has been changed.
+ verify(mAdapter).notifyItemChanged(/* position= */ 2);
+ // THEN the current list contains all elements from the new list.
+ assertThat(currentList).containsExactlyElementsIn(newList);
+ }
+
+
+ private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
+ int numOfWidgets) {
+ List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+ PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+ widgetItems.get(0).user);
+
+ return new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+ }
+
+ private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName,
+ int numOfWidgets) {
+ List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+ PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+ widgetItems.get(0).user);
+
+ return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+ }
+
+ private PackageItemInfo createPackageItemInfo(String packageName, String appName,
+ UserHandle userHandle) {
+ PackageItemInfo pInfo = new PackageItemInfo(packageName);
+ pInfo.title = appName;
+ pInfo.user = userHandle;
+ pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+ return pInfo;
+ }
+
+ private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ for (int i = 0; i < numOfWidgets; i++) {
+ ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(cn));
+
+ WidgetItem widgetItem = new WidgetItem(
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+ mTestProfile, mIconCache);
+ widgetItems.add(widgetItem);
+ }
+ return widgetItems;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
new file mode 100644
index 0000000..c730fc0
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -0,0 +1,283 @@
+/*
+ * 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.widget.picker;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Process;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListAdapterTest {
+ private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test";
+
+ @Mock private LayoutInflater mMockLayoutInflater;
+ @Mock private DatabaseWidgetPreviewLoader mMockWidgetCache;
+ @Mock private RecyclerView.AdapterDataObserver mListener;
+ @Mock private IconCache mIconCache;
+
+ private WidgetsListAdapter mAdapter;
+ private InvariantDeviceProfile mTestProfile;
+ private UserHandle mUserHandle;
+ private Context mContext;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+ mUserHandle = Process.myUserHandle();
+ mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
+ mIconCache, null, null);
+ mAdapter.registerAdapterDataObserver(mListener);
+
+ doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+ .getComponent().getPackageName())
+ .when(mIconCache).getTitleNoCache(any());
+ }
+
+ @Test
+ public void setWidgets_shouldNotifyDataSetChanged() {
+ mAdapter.setWidgets(generateSampleMap(1));
+
+ verify(mListener).onChanged();
+ }
+
+ @Test
+ public void setWidgets_withItemInserted_shouldNotifyItemInserted() {
+ mAdapter.setWidgets(generateSampleMap(1));
+ mAdapter.setWidgets(generateSampleMap(2));
+
+ verify(mListener).onItemRangeInserted(eq(1), eq(1));
+ }
+
+ @Test
+ public void setWidgets_withItemRemoved_shouldNotifyItemRemoved() {
+ mAdapter.setWidgets(generateSampleMap(2));
+ mAdapter.setWidgets(generateSampleMap(1));
+
+ verify(mListener).onItemRangeRemoved(eq(1), eq(1));
+ }
+
+ @Test
+ public void setWidgets_appIconChanged_shouldNotifyItemChanged() {
+ mAdapter.setWidgets(generateSampleMap(1));
+ mAdapter.setWidgets(generateSampleMap(1));
+
+ verify(mListener).onItemRangeChanged(eq(0), eq(1), isNull());
+ }
+
+ @Test
+ public void headerClick_expanded_shouldNotifyItemChange() {
+ // GIVEN a list of widgets entries:
+ // [com.google.test0, com.google.test0 content,
+ // com.google.test1, com.google.test1 content,
+ // com.google.test2, com.google.test2 content]
+ // The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2].
+ mAdapter.setWidgets(generateSampleMap(3));
+
+ // WHEN com.google.test.1 header is expanded.
+ mAdapter.onHeaderClicked(/* showWidgets= */ true,
+ new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
+
+ // THEN the visible entries list becomes:
+ // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
+ // com.google.test.1 content is inserted into position 2.
+ verify(mListener).onItemRangeInserted(eq(2), eq(1));
+ }
+
+ @Test
+ public void setWidgets_expandedApp_moreWidgets_shouldNotifyItemChangedWithWidgetItemInfoDiff() {
+ // GIVEN the adapter was first populated with com.google.test0 & com.google.test1. Each app
+ // has one widget.
+ ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(2);
+ mAdapter.setWidgets(allEntries);
+ // GIVEN test com.google.test1 is expanded.
+ // Visible entries in the adapter are:
+ // [com.google.test0, com.google.test1, com.google.test1 content]
+ mAdapter.onHeaderClicked(/* showWidgets= */ true,
+ new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
+ Mockito.reset(mListener);
+
+ // WHEN the adapter is updated with the same list of apps but com.google.test1 has 2 widgets
+ // now.
+ WidgetsListContentEntry testPackage1ContentEntry =
+ (WidgetsListContentEntry) allEntries.get(3);
+ WidgetItem widgetItem = testPackage1ContentEntry.mWidgets.get(0);
+ WidgetsListContentEntry newTestPackage1ContentEntry = new WidgetsListContentEntry(
+ testPackage1ContentEntry.mPkgItem,
+ testPackage1ContentEntry.mTitleSectionName, List.of(widgetItem, widgetItem));
+ allEntries.set(3, newTestPackage1ContentEntry);
+ mAdapter.setWidgets(allEntries);
+
+ // THEN the onItemRangeChanged is invoked for "com.google.test1 content" at index 2.
+ verify(mListener).onItemRangeChanged(eq(2), eq(1), isNull());
+ }
+
+ @Test
+ public void setWidgets_hodgepodge_shouldInvokeExpectedDataObserverCallbacks() {
+ // GIVEN a widgets entry list:
+ // Index: 0| 1 | 2| 3 | 4| 5 | 6| 7 | 8| 9 |
+ // [A, A content, B, B content, C, C content, D, D content, E, E content]
+ List<WidgetsListBaseEntry> allAppsWithWidgets = generateSampleMap(5);
+ // GIVEN the current widgets list consist of [A, A content, B, B content, E, E content].
+ // GIVEN the visible widgets list consist of [A, B, E]
+ List<WidgetsListBaseEntry> currentList = List.of(
+ // A & A content
+ allAppsWithWidgets.get(0), allAppsWithWidgets.get(1),
+ // B & B content
+ allAppsWithWidgets.get(2), allAppsWithWidgets.get(3),
+ // E & E content
+ allAppsWithWidgets.get(8), allAppsWithWidgets.get(9));
+ mAdapter.setWidgets(currentList);
+
+ // WHEN the widgets list is updated to [A, A content, C, C content, D, D content].
+ // WHEN the visible widgets list is updated to [A, C, D].
+ List<WidgetsListBaseEntry> newList = List.of(
+ // A & A content
+ allAppsWithWidgets.get(0), allAppsWithWidgets.get(1),
+ // C & C content
+ allAppsWithWidgets.get(4), allAppsWithWidgets.get(5),
+ // D & D content
+ allAppsWithWidgets.get(6), allAppsWithWidgets.get(7));
+ mAdapter.setWidgets(newList);
+
+ // Computation logic | [Intermediate list during computation]
+ // THEN B <> C < 0, removed B from index 1 | [A, E]
+ verify(mListener).onItemRangeRemoved(/* positionStart= */ 1, /* itemCount= */ 1);
+ // THEN E <> C > 0, C inserted to index 1 | [A, C, E]
+ verify(mListener).onItemRangeInserted(/* positionStart= */ 1, /* itemCount= */ 1);
+ // THEN E <> D > 0, D inserted to index 2 | [A, C, D, E]
+ verify(mListener).onItemRangeInserted(/* positionStart= */ 2, /* itemCount= */ 1);
+ // THEN E <> null = -1, E deleted from index 3 | [A, C, D]
+ verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1);
+ }
+
+ @Test
+ public void setWidgetsOnSearch_expandedApp_shouldResetExpandedApp() {
+ // GIVEN a list of widgets entries:
+ // [com.google.test0, com.google.test0 content,
+ // com.google.test1, com.google.test1 content,
+ // com.google.test2, com.google.test2 content]
+ // The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2].
+ ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(2);
+ mAdapter.setWidgetsOnSearch(allEntries);
+ // GIVEN com.google.test.1 header is expanded. The visible entries list becomes:
+ // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
+ mAdapter.onHeaderClicked(/* showWidgets= */ true,
+ new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
+ Mockito.reset(mListener);
+
+ // WHEN same widget entries are set again.
+ mAdapter.setWidgetsOnSearch(allEntries);
+
+ // THEN expanded app is reset and the visible entries list becomes:
+ // [com.google.test0, com.google.test1, com.google.test2]
+ verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull());
+ verify(mListener).onItemRangeRemoved(/* positionStart= */ 2, /* itemCount= */ 1);
+ }
+
+ /**
+ * Generates a list of sample widget entries.
+ *
+ * <p>Each sample app has 1 widget only. An app is represented by 2 entries,
+ * {@link WidgetsListHeaderEntry} & {@link WidgetsListContentEntry}. Only
+ * {@link WidgetsListHeaderEntry} is always visible in the {@link WidgetsListAdapter}.
+ * {@link WidgetsListContentEntry} is only shown upon clicking the corresponding app's
+ * {@link WidgetsListHeaderEntry}. Only at most one {@link WidgetsListContentEntry} is shown at
+ * a time.
+ *
+ * @param num the number of apps that have widgets.
+ */
+ private ArrayList<WidgetsListBaseEntry> generateSampleMap(int num) {
+ ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
+ if (num <= 0) return result;
+
+ for (int i = 0; i < num; i++) {
+ String packageName = TEST_PACKAGE_PLACEHOLDER + i;
+
+ List<WidgetItem> widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1);
+
+ PackageItemInfo pInfo = new PackageItemInfo(packageName);
+ pInfo.title = pInfo.packageName;
+ pInfo.user = widgetItems.get(0).user;
+ pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+ result.add(new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems));
+ result.add(new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems));
+ }
+
+ return result;
+ }
+
+ private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ for (int i = 0; i < numOfWidgets; i++) {
+ ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(cn));
+
+ widgetItems.add(new WidgetItem(
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+ mTestProfile, mIconCache));
+ }
+ return widgetItems;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
new file mode 100644
index 0000000..81b0c5f
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListHeaderViewHolderBinderTest {
+ private static final String TEST_PACKAGE = "com.google.test";
+ private static final String APP_NAME = "Test app";
+
+ private Context mContext;
+ private WidgetsListHeaderViewHolderBinder mViewHolderBinder;
+ private InvariantDeviceProfile mTestProfile;
+ // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+ // testing.
+ private ActivityController<TestActivity> mActivityController;
+ private TestActivity mTestActivity;
+
+ @Mock
+ private IconCache mIconCache;
+ @Mock
+ private DeviceProfile mDeviceProfile;
+ @Mock
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
+ @Mock
+ private OnHeaderClickListener mOnHeaderClickListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+
+ mActivityController = Robolectric.buildActivity(TestActivity.class);
+ mTestActivity = mActivityController.setup().get();
+ mTestActivity.setDeviceProfile(mDeviceProfile);
+
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+ return componentWithLabel.getComponent().getShortClassName();
+ }).when(mIconCache).getTitleNoCache(any());
+
+ WidgetsListAdapter widgetsListAdapter = new WidgetsListAdapter(mContext,
+ LayoutInflater.from(mTestActivity),
+ mWidgetPreviewLoader,
+ mIconCache,
+ /* iconClickListener= */ view -> {},
+ /* iconLongClickListener= */ view -> false);
+ mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
+ LayoutInflater.from(mTestActivity),
+ mOnHeaderClickListener,
+ new WidgetsListDrawableFactory(mTestActivity),
+ widgetsListAdapter);
+ }
+
+ @After
+ public void tearDown() {
+ mActivityController.destroy();
+ }
+
+ @Test
+ public void bindViewHolder_appWith3Widgets_shouldShowTheCorrectAppNameAndSubtitle() {
+ WidgetsListHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ WidgetsListHeaderEntry entry = generateSampleAppHeader(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+ mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
+
+ TextView appTitle = widgetsListHeader.findViewById(R.id.app_title);
+ TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle);
+ assertThat(appTitle.getText()).isEqualTo(APP_NAME);
+ assertThat(appSubtitle.getText()).isEqualTo("3 widgets");
+ }
+
+ @Test
+ public void bindViewHolder_shouldAttachOnHeaderClickListener() {
+ WidgetsListHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ WidgetsListHeaderEntry entry = generateSampleAppHeader(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+
+ mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
+ widgetsListHeader.callOnClick();
+
+ verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
+ eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
+ }
+
+ private WidgetsListHeaderEntry generateSampleAppHeader(String appName, String packageName,
+ int numOfWidgets) {
+ PackageItemInfo appInfo = new PackageItemInfo(packageName);
+ appInfo.title = appName;
+ appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+ return new WidgetsListHeaderEntry(appInfo,
+ /* titleSectionName= */ "",
+ generateWidgetItems(packageName, numOfWidgets));
+ }
+
+ private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ for (int i = 0; i < numOfWidgets; i++) {
+ ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(cn));
+
+ widgetItems.add(new WidgetItem(
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+ mTestProfile, mIconCache));
+ }
+ return widgetItems;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
new file mode 100644
index 0000000..a0ba7c3
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListSearchHeaderViewHolderBinderTest {
+ private static final String TEST_PACKAGE = "com.google.test";
+ private static final String APP_NAME = "Test app";
+
+ private Context mContext;
+ private WidgetsListSearchHeaderViewHolderBinder mViewHolderBinder;
+ private InvariantDeviceProfile mTestProfile;
+ // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+ // testing.
+ private ActivityController<TestActivity> mActivityController;
+ private TestActivity mTestActivity;
+
+ @Mock
+ private IconCache mIconCache;
+ @Mock
+ private DeviceProfile mDeviceProfile;
+ @Mock
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
+ @Mock
+ private OnHeaderClickListener mOnHeaderClickListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+
+ mActivityController = Robolectric.buildActivity(TestActivity.class);
+ mTestActivity = mActivityController.setup().get();
+ mTestActivity.setDeviceProfile(mDeviceProfile);
+
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+ return componentWithLabel.getComponent().getShortClassName();
+ }).when(mIconCache).getTitleNoCache(any());
+
+ WidgetsListAdapter widgetsListAdapter = new WidgetsListAdapter(mContext,
+ LayoutInflater.from(mTestActivity),
+ mWidgetPreviewLoader,
+ mIconCache,
+ /* iconClickListener= */ view -> {},
+ /* iconLongClickListener= */ view -> false);
+ mViewHolderBinder = new WidgetsListSearchHeaderViewHolderBinder(
+ LayoutInflater.from(mTestActivity),
+ mOnHeaderClickListener,
+ new WidgetsListDrawableFactory(mTestActivity),
+ widgetsListAdapter);
+ }
+
+ @After
+ public void tearDown() {
+ mActivityController.destroy();
+ }
+
+ @Test
+ public void bindViewHolder_appWith3Widgets_shouldShowTheCorrectAppNameAndSubtitle() {
+ WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+ mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
+
+ TextView appTitle = widgetsListHeader.findViewById(R.id.app_title);
+ TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle);
+ assertThat(appTitle.getText()).isEqualTo(APP_NAME);
+ assertThat(appSubtitle.getText())
+ .isEqualTo(".SampleWidget0, .SampleWidget1, .SampleWidget2");
+ }
+
+ @Test
+ public void bindViewHolder_shouldAttachOnHeaderClickListener() {
+ WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+
+ mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
+ widgetsListHeader.callOnClick();
+
+ verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
+ eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
+ }
+
+ private WidgetsListSearchHeaderEntry generateSampleSearchHeader(String appName,
+ String packageName, int numOfWidgets) {
+ PackageItemInfo appInfo = new PackageItemInfo(packageName);
+ appInfo.title = appName;
+ appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+ return new WidgetsListSearchHeaderEntry(appInfo,
+ /* titleSectionName= */ "",
+ generateWidgetItems(packageName, numOfWidgets));
+ }
+
+ private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ for (int i = 0; i < numOfWidgets; i++) {
+ ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(cn));
+
+ widgetItems.add(new WidgetItem(
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+ mTestProfile, mIconCache));
+ }
+ return widgetItems;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
new file mode 100644
index 0000000..8f9d132
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static android.os.Looper.getMainLooper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.FrameLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListTableViewHolderBinderTest {
+ private static final String TEST_PACKAGE = "com.google.test";
+ private static final String APP_NAME = "Test app";
+
+ private Context mContext;
+ private WidgetsListTableViewHolderBinder mViewHolderBinder;
+ private InvariantDeviceProfile mTestProfile;
+ // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+ // testing.
+ private ActivityController<TestActivity> mActivityController;
+ private TestActivity mTestActivity;
+
+ @Mock
+ private OnLongClickListener mOnLongClickListener;
+ @Mock
+ private OnClickListener mOnIconClickListener;
+ @Mock
+ private IconCache mIconCache;
+ @Mock
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
+ @Mock
+ private DeviceProfile mDeviceProfile;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+
+ mActivityController = Robolectric.buildActivity(TestActivity.class);
+ mTestActivity = mActivityController.setup().get();
+ mTestActivity.setDeviceProfile(mDeviceProfile);
+
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+ return componentWithLabel.getComponent().getShortClassName();
+ }).when(mIconCache).getTitleNoCache(any());
+
+ WidgetsListAdapter widgetsListAdapter = new WidgetsListAdapter(mContext,
+ LayoutInflater.from(mTestActivity),
+ mWidgetPreviewLoader,
+ mIconCache,
+ /* iconClickListener= */ view -> {},
+ /* iconLongClickListener= */ view -> false);
+ mViewHolderBinder = new WidgetsListTableViewHolderBinder(
+ LayoutInflater.from(mTestActivity),
+ mOnIconClickListener,
+ mOnLongClickListener,
+ new CachingWidgetPreviewLoader(mWidgetPreviewLoader),
+ new WidgetsListDrawableFactory(mTestActivity),
+ widgetsListAdapter);
+ }
+
+ @After
+ public void tearDown() {
+ mActivityController.destroy();
+ }
+
+ @Test
+ public void bindViewHolder_appWith3Widgets_shouldHave3Widgets() {
+ WidgetsRowViewHolder viewHolder = mViewHolderBinder.newViewHolder(
+ new FrameLayout(mTestActivity));
+ WidgetsListContentEntry entry = generateSampleAppWithWidgets(
+ APP_NAME,
+ TEST_PACKAGE,
+ /* numOfWidgets= */ 3);
+ mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
+ shadowOf(getMainLooper()).idle();
+
+ // THEN the table container has one row, which contains 3 widgets.
+ // View: .SampleWidget0 | .SampleWidget1 | .SampleWidget2
+ assertThat(viewHolder.mTableContainer.getChildCount()).isEqualTo(1);
+ TableRow row = (TableRow) viewHolder.mTableContainer.getChildAt(0);
+ assertThat(row.getChildCount()).isEqualTo(3);
+ // Widget 0 label is .SampleWidget0.
+ assertWidgetCellWithLabel(row.getChildAt(0), ".SampleWidget0");
+ // Widget 1 label is .SampleWidget1.
+ assertWidgetCellWithLabel(row.getChildAt(1), ".SampleWidget1");
+ // Widget 2 label is .SampleWidget2.
+ assertWidgetCellWithLabel(row.getChildAt(2), ".SampleWidget2");
+ }
+
+ private WidgetsListContentEntry generateSampleAppWithWidgets(String appName, String packageName,
+ int numOfWidgets) {
+ PackageItemInfo appInfo = new PackageItemInfo(packageName);
+ appInfo.title = appName;
+ appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+ return new WidgetsListContentEntry(appInfo,
+ /* titleSectionName= */ "",
+ generateWidgetItems(packageName, numOfWidgets));
+ }
+
+ private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ for (int i = 0; i < numOfWidgets; i++) {
+ ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(cn));
+
+ widgetItems.add(new WidgetItem(
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+ mTestProfile, mIconCache));
+ }
+ return widgetItems;
+ }
+
+ private void assertWidgetCellWithLabel(View view, String label) {
+ assertThat(view).isInstanceOf(WidgetCell.class);
+ TextView widgetLabel = (TextView) view.findViewById(R.id.widget_name);
+ assertThat(widgetLabel.getText()).isEqualTo(label);
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
new file mode 100644
index 0000000..106cac0
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListContentEntryTest {
+ private static final String PACKAGE_NAME = "com.android.test";
+ private static final String PACKAGE_NAME_2 = "com.android.test2";
+ private final PackageItemInfo mPackageItemInfo1 = new PackageItemInfo(PACKAGE_NAME);
+ private final PackageItemInfo mPackageItemInfo2 = new PackageItemInfo(PACKAGE_NAME_2);
+ private final ComponentName mWidget1 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget1");
+ private final ComponentName mWidget2 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget2");
+ private final ComponentName mWidget3 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget3");
+ private final Map<ComponentName, String> mWidgetsToLabels = new HashMap();
+
+ @Mock private IconCache mIconCache;
+
+ private Context mContext;
+ private InvariantDeviceProfile mTestProfile;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mWidgetsToLabels.put(mWidget1, "Cat");
+ mWidgetsToLabels.put(mWidget2, "Dog");
+ mWidgetsToLabels.put(mWidget3, "Bird");
+
+ mContext = RuntimeEnvironment.application;
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+ return mWidgetsToLabels.get(componentWithLabel.getComponent());
+ }).when(mIconCache).getTitleNoCache(any());
+ }
+
+ @Test
+ public void unsortedWidgets_diffLabels_shouldSortWidgetItems() {
+ // GIVEN a list of widgets in unsorted order.
+ // Cat 2x3
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3);
+ // Dog 2x3
+ WidgetItem widgetItem2 = createWidgetItem(mWidget2, /* spanX= */ 2, /* spanY= */ 3);
+ // Bird 2x3
+ WidgetItem widgetItem3 = createWidgetItem(mWidget3, /* spanX= */ 2, /* spanY= */ 3);
+
+ // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+ WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1, widgetItem2, widgetItem3));
+
+ // THEN the widgets list is sorted by their labels alphabetically: [Bird, Cat, Dog].
+ assertThat(widgetsListRowEntry.mWidgets)
+ .containsExactly(widgetItem3, widgetItem1, widgetItem2)
+ .inOrder();
+ assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+ assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo1);
+ }
+
+ @Test
+ public void unsortedWidgets_sameLabels_differentSize_shouldSortWidgetItems() {
+ // GIVEN a list of widgets in unsorted order.
+ // Cat 3x3
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 3, /* spanY= */ 3);
+ // Cat 1x2
+ WidgetItem widgetItem2 = createWidgetItem(mWidget1, /* spanX= */ 1, /* spanY= */ 2);
+ // Cat 2x2
+ WidgetItem widgetItem3 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 2);
+
+ // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+ WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1, widgetItem2, widgetItem3));
+
+ // THEN the widgets list is sorted by their gird sizes in an ascending order:
+ // [1x2, 2x2, 3x3].
+ assertThat(widgetsListRowEntry.mWidgets)
+ .containsExactly(widgetItem2, widgetItem3, widgetItem1)
+ .inOrder();
+ assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+ assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo1);
+ }
+
+ @Test
+ public void unsortedWidgets_hodgepodge_shouldSortWidgetItems() {
+ // GIVEN a list of widgets in unsorted order.
+ // Cat 3x3
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 3, /* spanY= */ 3);
+ // Cat 1x2
+ WidgetItem widgetItem2 = createWidgetItem(mWidget1, /* spanX= */ 1, /* spanY= */ 2);
+ // Dog 2x2
+ WidgetItem widgetItem3 = createWidgetItem(mWidget2, /* spanX= */ 2, /* spanY= */ 2);
+ // Bird 2x2
+ WidgetItem widgetItem4 = createWidgetItem(mWidget3, /* spanX= */ 2, /* spanY= */ 2);
+
+ // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+ WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1, widgetItem2, widgetItem3, widgetItem4));
+
+ // THEN the widgets list is first sorted by labels alphabetically. Then, for widgets with
+ // same labels, they are sorted by their gird sizes in an ascending order:
+ // [Bird 2x2, Cat 1x2, Cat 3x3, Dog 2x2]
+ assertThat(widgetsListRowEntry.mWidgets)
+ .containsExactly(widgetItem4, widgetItem2, widgetItem1, widgetItem3)
+ .inOrder();
+ assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+ assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo1);
+ }
+
+ @Test
+ public void equals_entriesWithDifferentPackageItemInfo_returnFalse() {
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry(
+ mPackageItemInfo2,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+
+ assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse();
+ }
+
+ @Test
+ public void equals_entriesWithDifferentTitleSectionName_returnFalse() {
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "S",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+
+ assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse();
+ }
+
+ @Test
+ public void equals_entriesWithDifferentWidgetsList_returnFalse() {
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3);
+ WidgetItem widgetItem2 = createWidgetItem(mWidget2, /* spanX= */ 2, /* spanY= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem2),
+ /* maxSpanSizeInCells= */ 3);
+
+ assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse();
+ }
+
+ @Test
+ public void equals_entriesWithDifferentMaxSpanSize_returnFalse() {
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 2);
+
+ assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isFalse();
+ }
+
+ @Test
+ public void equals_entriesWithSameContents_returnTrue() {
+ WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry1 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+ WidgetsListContentEntry widgetsListRowEntry2 = new WidgetsListContentEntry(
+ mPackageItemInfo1,
+ /* titleSectionName= */ "T",
+ List.of(widgetItem1),
+ /* maxSpanSizeInCells= */ 3);
+
+ assertThat(widgetsListRowEntry1.equals(widgetsListRowEntry2)).isTrue();
+ }
+
+
+ private WidgetItem createWidgetItem(ComponentName componentName, int spanX, int spanY) {
+ String label = mWidgetsToLabels.get(componentName);
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = componentName;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(componentName));
+
+ LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo);
+ launcherAppWidgetProviderInfo.spanX = spanX;
+ launcherAppWidgetProviderInfo.spanY = spanY;
+ launcherAppWidgetProviderInfo.label = label;
+
+ return new WidgetItem(launcherAppWidgetProviderInfo, mTestProfile, mIconCache);
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
new file mode 100644
index 0000000..36b6f01
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import static android.os.Looper.getMainLooper;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.UserHandle;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class SimpleWidgetsSearchAlgorithmTest {
+
+ @Mock private IconCache mIconCache;
+
+ private InvariantDeviceProfile mTestProfile;
+ private WidgetsListHeaderEntry mCalendarHeaderEntry;
+ private WidgetsListContentEntry mCalendarContentEntry;
+ private WidgetsListHeaderEntry mCameraHeaderEntry;
+ private WidgetsListContentEntry mCameraContentEntry;
+ private WidgetsListHeaderEntry mClockHeaderEntry;
+ private WidgetsListContentEntry mClockContentEntry;
+ private Context mContext;
+
+ private SimpleWidgetsSearchAlgorithm mSimpleWidgetsSearchAlgorithm;
+ @Mock
+ private PopupDataProvider mDataProvider;
+ @Mock
+ private SearchCallback<WidgetsListBaseEntry> mSearchCallback;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ doAnswer(invocation -> {
+ ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+ return componentWithLabel.getComponent().getShortClassName();
+ }).when(mIconCache).getTitleNoCache(any());
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+ mContext = RuntimeEnvironment.application;
+
+ mCalendarHeaderEntry =
+ createWidgetsHeaderEntry("com.example.android.Calendar", "Calendar", 2);
+ mCalendarContentEntry =
+ createWidgetsContentEntry("com.example.android.Calendar", "Calendar", 2);
+ mCameraHeaderEntry = createWidgetsHeaderEntry("com.example.android.Camera", "Camera", 11);
+ mCameraContentEntry = createWidgetsContentEntry("com.example.android.Camera", "Camera", 11);
+ mClockHeaderEntry = createWidgetsHeaderEntry("com.example.android.Clock", "Clock", 3);
+ mClockContentEntry = createWidgetsContentEntry("com.example.android.Clock", "Clock", 3);
+
+
+ mSimpleWidgetsSearchAlgorithm = new SimpleWidgetsSearchAlgorithm(mDataProvider);
+ doReturn(Collections.EMPTY_LIST).when(mDataProvider).getAllWidgets();
+ }
+
+ @Test
+ public void filter_shouldMatchOnAppName() {
+ doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
+ mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
+ .when(mDataProvider)
+ .getAllWidgets();
+
+ assertEquals(List.of(
+ new WidgetsListSearchHeaderEntry(
+ mCalendarHeaderEntry.mPkgItem,
+ mCalendarHeaderEntry.mTitleSectionName,
+ mCalendarHeaderEntry.mWidgets),
+ mCalendarContentEntry,
+ new WidgetsListSearchHeaderEntry(
+ mCameraHeaderEntry.mPkgItem,
+ mCameraHeaderEntry.mTitleSectionName,
+ mCameraHeaderEntry.mWidgets),
+ mCameraContentEntry),
+ SimpleWidgetsSearchAlgorithm.getFilteredWidgets(mDataProvider, "Ca"));
+ }
+
+ @Test
+ public void filter_shouldMatchOnWidgetLabel() {
+ doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
+ mCameraContentEntry))
+ .when(mDataProvider)
+ .getAllWidgets();
+
+ assertEquals(List.of(
+ new WidgetsListSearchHeaderEntry(
+ mCalendarHeaderEntry.mPkgItem,
+ mCalendarHeaderEntry.mTitleSectionName,
+ mCalendarHeaderEntry.mWidgets.subList(1, 2)),
+ new WidgetsListContentEntry(
+ mCalendarHeaderEntry.mPkgItem,
+ mCalendarHeaderEntry.mTitleSectionName,
+ mCalendarHeaderEntry.mWidgets.subList(1, 2)),
+ new WidgetsListSearchHeaderEntry(
+ mCameraHeaderEntry.mPkgItem,
+ mCameraHeaderEntry.mTitleSectionName,
+ mCameraHeaderEntry.mWidgets.subList(1, 3)),
+ new WidgetsListContentEntry(
+ mCameraHeaderEntry.mPkgItem,
+ mCameraHeaderEntry.mTitleSectionName,
+ mCameraHeaderEntry.mWidgets.subList(1, 3))),
+ SimpleWidgetsSearchAlgorithm.getFilteredWidgets(mDataProvider, "Widget1"));
+ }
+
+ @Test
+ public void doSearch_shouldInformCallback() {
+ doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
+ mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
+ .when(mDataProvider)
+ .getAllWidgets();
+ mSimpleWidgetsSearchAlgorithm.doSearch("Ca", mSearchCallback);
+ shadowOf(getMainLooper()).idle();
+ verify(mSearchCallback).onSearchResult(
+ matches("Ca"), argThat(a -> a != null && !a.isEmpty()));
+ }
+
+ private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
+ int numOfWidgets) {
+ List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+ PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+ widgetItems.get(0).user);
+
+ return new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+ }
+
+ private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName,
+ int numOfWidgets) {
+ List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+ PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+ widgetItems.get(0).user);
+
+ return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+ }
+
+ private PackageItemInfo createPackageItemInfo(String packageName, String appName,
+ UserHandle userHandle) {
+ PackageItemInfo pInfo = new PackageItemInfo(packageName);
+ pInfo.title = appName;
+ pInfo.user = userHandle;
+ pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+ return pInfo;
+ }
+
+ private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ for (int i = 0; i < numOfWidgets; i++) {
+ ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(cn));
+
+ WidgetItem widgetItem = new WidgetItem(
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+ mTestProfile, mIconCache);
+ widgetItems.add(widgetItem);
+ }
+ return widgetItems;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java
new file mode 100644
index 0000000..7ac879a
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+
+import java.util.ArrayList;
+
+@RunWith(RobolectricTestRunner.class)
+public class WidgetsSearchBarControllerTest {
+
+ private WidgetsSearchBarController mController;
+ // TODO: Replace ActivityController with ActivityScenario, which is the recommended way for
+ // activity testing.
+ private ActivityController<TestActivity> mActivityController;
+ private ExtendedEditText mEditText;
+ private ImageButton mCancelButton;
+ @Mock
+ private SearchModeListener mSearchModeListener;
+ @Mock
+ private SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mActivityController = Robolectric.buildActivity(TestActivity.class);
+ TestActivity testActivity = mActivityController.setup().get();
+
+ mEditText = new ExtendedEditText(testActivity);
+ mCancelButton = new ImageButton(testActivity);
+ mController = new WidgetsSearchBarController(
+ mSearchAlgorithm, mEditText, mCancelButton, mSearchModeListener);
+ }
+
+ @After
+ public void tearDown() {
+ mActivityController.destroy();
+ }
+
+ @Test
+ public void onSearchResult_shouldInformSearchModeListener() {
+ ArrayList<WidgetsListBaseEntry> entries = new ArrayList<>();
+ mController.onSearchResult("abc", entries);
+
+ verify(mSearchModeListener).onSearchResults(entries);
+ }
+
+ @Test
+ public void afterTextChanged_shouldInformSearchModeListenerToEnterSearch() {
+ mEditText.setText("abc");
+
+ verify(mSearchModeListener).enterSearchMode();
+ verifyNoMoreInteractions(mSearchModeListener);
+ }
+
+ @Test
+ public void afterTextChanged_shouldDoSearch() {
+ mEditText.setText("abc");
+
+ verify(mSearchAlgorithm).doSearch(eq("abc"), any());
+ }
+
+ @Test
+ public void afterTextChanged_shouldShowCancelButton() {
+ mEditText.setText("abc");
+
+ assertEquals(mCancelButton.getVisibility(), View.VISIBLE);
+ }
+
+ @Test
+ public void afterTextChanged_empty_shouldInformSearchModeListenerToExitSearch() {
+ mEditText.setText("");
+
+ verify(mSearchModeListener).exitSearchMode();
+ verifyNoMoreInteractions(mSearchModeListener);
+ }
+
+ @Test
+ public void afterTextChanged_empty_shouldCancelSearch() {
+ mEditText.setText("");
+
+ verify(mSearchAlgorithm).cancel(true);
+ verifyNoMoreInteractions(mSearchAlgorithm);
+ }
+
+ @Test
+ public void afterTextChanged_empty_shouldHideCancelButton() {
+ mEditText.setText("");
+
+ assertEquals(mCancelButton.getVisibility(), View.GONE);
+ }
+
+ @Test
+ public void cancelSearch_shouldInformSearchModeListenerToClearResultsAndExitSearch() {
+ mCancelButton.performClick();
+
+ verify(mSearchModeListener).exitSearchMode();
+ }
+
+ @Test
+ public void cancelSearch_shouldCancelSearch() {
+ mCancelButton.performClick();
+
+ verify(mSearchAlgorithm).cancel(true);
+ verifyNoMoreInteractions(mSearchAlgorithm);
+ }
+
+ @Test
+ public void cancelSearch_shouldClearSearchBar() {
+ mCancelButton.performClick();
+
+ assertEquals(mEditText.getText().toString(), "");
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
new file mode 100644
index 0000000..56d7d68
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsTableUtilsTest {
+ private static final String TEST_PACKAGE = "com.google.test";
+
+ @Mock
+ private IconCache mIconCache;
+
+ private Context mContext;
+ private InvariantDeviceProfile mTestProfile;
+ private WidgetItem mWidget1x1;
+ private WidgetItem mWidget2x2;
+ private WidgetItem mWidget2x3;
+ private WidgetItem mWidget2x4;
+ private WidgetItem mWidget4x4;
+
+ private WidgetItem mShortcut1;
+ private WidgetItem mShortcut2;
+ private WidgetItem mShortcut3;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = RuntimeEnvironment.application;
+
+ mTestProfile = new InvariantDeviceProfile();
+ mTestProfile.numRows = 5;
+ mTestProfile.numColumns = 5;
+
+ initTestWidgets();
+ initTestShortcuts();
+
+ doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+ .getComponent().getPackageName())
+ .when(mIconCache).getTitleNoCache(any());
+ }
+
+
+ @Test
+ public void groupWidgetItemsIntoTable_widgetsOnly_maxSpansPerRow5_shouldGroupWidgetsInTable() {
+ List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
+ mWidget2x2);
+
+ List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
+ widgetItems, /* maxSpansPerRow= */ 5);
+
+ // Row 0: 1x1, 2x2
+ // Row 1: 2x3, 2x4
+ // Row 2: 4x4
+ assertThat(widgetItemInTable).hasSize(3);
+ assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
+ assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4);
+ assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
+ }
+
+ @Test
+ public void groupWidgetItemsIntoTable_widgetsOnly_maxSpansPerRow4_shouldGroupWidgetsInTable() {
+ List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
+ mWidget2x2);
+
+ List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
+ widgetItems, /* maxSpansPerRow= */ 4);
+
+ // Row 0: 1x1, 2x2
+ // Row 1: 2x3,
+ // Row 2: 2x4,
+ // Row 3: 4x4
+ assertThat(widgetItemInTable).hasSize(4);
+ assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
+ assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3);
+ assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x4);
+ assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4);
+ }
+
+ @Test
+ public void groupWidgetItemsIntoTable_mixItems_maxSpansPerRow4_shouldGroupWidgetsInTable() {
+ List<WidgetItem> widgetItems = List.of(mWidget4x4, mShortcut3, mWidget2x3, mShortcut1,
+ mWidget1x1, mShortcut2, mWidget2x4, mWidget2x2);
+
+ List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
+ widgetItems, /* maxSpansPerRow= */ 4);
+
+ // Row 0: 1x1, 2x2
+ // Row 1: 2x3,
+ // Row 2: 2x4,
+ // Row 3: 4x4
+ // Row 4: shortcut3, shortcut1, shortcut2
+ assertThat(widgetItemInTable).hasSize(5);
+ assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
+ assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3);
+ assertThat(widgetItemInTable.get(2)).containsExactly(mWidget2x4);
+ assertThat(widgetItemInTable.get(3)).containsExactly(mWidget4x4);
+ assertThat(widgetItemInTable.get(4)).containsExactly(mShortcut3, mShortcut2, mShortcut1);
+ }
+
+ private void initTestWidgets() {
+ List<Point> widgetSizes = List.of(new Point(1, 1), new Point(2, 2), new Point(2, 3),
+ new Point(2, 4), new Point(4, 4));
+
+ ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+ widgetSizes.stream().forEach(
+ widgetSize -> {
+ ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+ AppWidgetProviderInfo info = new AppWidgetProviderInfo();
+ info.provider = ComponentName.createRelative(TEST_PACKAGE,
+ ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y);
+ LauncherAppWidgetProviderInfo widgetInfo =
+ LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+ widgetInfo.spanX = widgetSize.x;
+ widgetInfo.spanY = widgetSize.y;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo",
+ packageManager.addReceiverIfNotPresent(widgetInfo.provider));
+ widgetItems.add(new WidgetItem(widgetInfo, mTestProfile, mIconCache));
+ }
+ );
+ mWidget1x1 = widgetItems.get(0);
+ mWidget2x2 = widgetItems.get(1);
+ mWidget2x3 = widgetItems.get(2);
+ mWidget2x4 = widgetItems.get(3);
+ mWidget4x4 = widgetItems.get(4);
+ }
+
+ private void initTestShortcuts() {
+ PackageManager packageManager = mContext.getPackageManager();
+ mShortcut1 = new WidgetItem(new TestShortcutConfigActivityInfo(
+ ComponentName.createRelative(TEST_PACKAGE, ".shortcut1"), UserHandle.CURRENT),
+ mIconCache, packageManager);
+ mShortcut2 = new WidgetItem(new TestShortcutConfigActivityInfo(
+ ComponentName.createRelative(TEST_PACKAGE, ".shortcut2"), UserHandle.CURRENT),
+ mIconCache, packageManager);
+ mShortcut3 = new WidgetItem(new TestShortcutConfigActivityInfo(
+ ComponentName.createRelative(TEST_PACKAGE, ".shortcut3"), UserHandle.CURRENT),
+ mIconCache, packageManager);
+
+ }
+
+ private final class TestShortcutConfigActivityInfo extends ShortcutConfigActivityInfo {
+
+ TestShortcutConfigActivityInfo(ComponentName componentName, UserHandle user) {
+ super(componentName, user);
+ }
+
+ @Override
+ public Drawable getFullResIcon(IconCache cache) {
+ return null;
+ }
+
+ @Override
+ public CharSequence getLabel(PackageManager pm) {
+ return null;
+ }
+ }
+}
diff --git a/robolectric_tests/unstaged/SettingsCacheTest.java b/robolectric_tests/unstaged/SettingsCacheTest.java
new file mode 100644
index 0000000..fbf4c63
--- /dev/null
+++ b/robolectric_tests/unstaged/SettingsCacheTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class SettingsCacheTest {
+
+ public static final Uri KEY_SYSTEM_URI_TEST1 = Uri.parse("content://settings/system/test1");
+ public static final Uri KEY_SYSTEM_URI_TEST2 = Uri.parse("content://settings/system/test2");;
+
+ private SettingsCache.OnChangeListener mChangeListener;
+ private SettingsCache mSettingsCache;
+
+ @Before
+ public void setup() {
+ mChangeListener = mock(SettingsCache.OnChangeListener.class);
+ Context targetContext = RuntimeEnvironment.application;
+ mSettingsCache = SettingsCache.INSTANCE.get(targetContext);
+ mSettingsCache.register(KEY_SYSTEM_URI_TEST1, mChangeListener);
+ }
+
+ @Test
+ public void listenerCalledOnChange() {
+ mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+ verify(mChangeListener, times(1)).onSettingsChanged(true);
+ }
+
+ @Test
+ public void getValueRespectsDefaultValue() {
+ // Case of key not found
+ boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+ assertFalse(val);
+ }
+
+ @Test
+ public void getValueHitsCache() {
+ mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true));
+ boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+ assertTrue(val);
+ }
+
+ @Test
+ public void getValueUpdatedCache() {
+ // First ensure there's nothing in cache
+ boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+ assertFalse(val);
+
+ mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true));
+ val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+ assertTrue(val);
+ }
+
+ @Test
+ public void multipleListenersSingleKey() {
+ SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+ mSettingsCache.register(KEY_SYSTEM_URI_TEST1, secondListener);
+
+ mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+ verify(mChangeListener, times(1)).onSettingsChanged(true);
+ verify(secondListener, times(1)).onSettingsChanged(true);
+ }
+
+ @Test
+ public void singleListenerMultipleKeys() {
+ SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+ mSettingsCache.register(KEY_SYSTEM_URI_TEST2, secondListener);
+
+ mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+ mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2);
+ verify(mChangeListener, times(1)).onSettingsChanged(true);
+ verify(secondListener, times(1)).onSettingsChanged(true);
+ }
+
+ @Test
+ public void sameListenerMultipleKeys() {
+ SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+ mSettingsCache.register(KEY_SYSTEM_URI_TEST2, mChangeListener);
+
+ mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+ mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2);
+ verify(mChangeListener, times(2)).onSettingsChanged(true);
+ verify(secondListener, times(0)).onSettingsChanged(true);
+ }
+}
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index cd27a2d..4979b40 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -16,6 +16,7 @@
package com.android.launcher3;
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
@@ -35,8 +36,6 @@
import androidx.annotation.IntDef;
import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
@@ -60,9 +59,12 @@
TYPE_SNACKBAR,
TYPE_LISTENER,
TYPE_ALL_APPS_EDU,
-
+ TYPE_DRAG_DROP_POPUP,
TYPE_TASK_MENU,
- TYPE_OPTIONS_POPUP
+ TYPE_OPTIONS_POPUP,
+ TYPE_ICON_SURFACE,
+ TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP,
+ TYPE_WIDGETS_EDUCATION_DIALOG
})
@Retention(RetentionPolicy.SOURCE)
public @interface FloatingViewType {}
@@ -76,20 +78,27 @@
public static final int TYPE_SNACKBAR = 1 << 7;
public static final int TYPE_LISTENER = 1 << 8;
public static final int TYPE_ALL_APPS_EDU = 1 << 9;
+ public static final int TYPE_DRAG_DROP_POPUP = 1 << 10;
// Popups related to quickstep UI
- public static final int TYPE_TASK_MENU = 1 << 10;
- public static final int TYPE_OPTIONS_POPUP = 1 << 11;
+ public static final int TYPE_TASK_MENU = 1 << 11;
+ public static final int TYPE_OPTIONS_POPUP = 1 << 12;
+ public static final int TYPE_ICON_SURFACE = 1 << 13;
+
+ public static final int TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP = 1 << 14;
+ public static final int TYPE_WIDGETS_EDUCATION_DIALOG = 1 << 15;
public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
| TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
| TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU
- | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU;
+ | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU
+ | TYPE_ICON_SURFACE | TYPE_DRAG_DROP_POPUP | TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP
+ | TYPE_WIDGETS_EDUCATION_DIALOG;
// Type of popups which should be kept open during launcher rebind
public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
| TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
- | TYPE_ALL_APPS_EDU;
+ | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE | TYPE_WIDGETS_EDUCATION_DIALOG;
// Usually we show the back button when a floating view is open. Instead, hide for these types.
public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
@@ -101,7 +110,7 @@
// These view all have particular operation associated with swipe down interaction.
public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET |
TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_ON_BOARD_POPUP |
- TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU ;
+ TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_DRAG_DROP_POPUP;
protected boolean mIsOpen;
@@ -123,10 +132,9 @@
}
public final void close(boolean animate) {
- animate &= Utilities.areAnimationsEnabled(getContext());
+ animate &= areAnimatorsEnabled();
if (mIsOpen) {
- BaseActivity.fromContext(getContext()).getUserEventDispatcher()
- .resetElapsedContainerMillis("container closed");
+ // Add to WW logging
}
handleClose(animate);
mIsOpen = false;
@@ -141,12 +149,6 @@
public void addHintCloseAnim(
float distanceToMove, Interpolator interpolator, PendingAnimation target) { }
- public abstract void logActionCommand(int command);
-
- public int getLogContainerType() {
- return ContainerType.DEFAULT_CONTAINERTYPE;
- }
-
public final boolean isOpen() {
return mIsOpen;
}
@@ -155,7 +157,6 @@
/** @return Whether the back is consumed. If false, Launcher will handle the back as well. */
public boolean onBackPressed() {
- logActionCommand(Action.Command.BACK);
close(true);
return true;
}
diff --git a/src/com/android/launcher3/AppFilter.java b/src/com/android/launcher3/AppFilter.java
index 9b6166f..3db456c 100644
--- a/src/com/android/launcher3/AppFilter.java
+++ b/src/com/android/launcher3/AppFilter.java
@@ -3,15 +3,25 @@
import android.content.ComponentName;
import android.content.Context;
-import com.android.launcher3.util.ResourceBasedOverride;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
-public class AppFilter implements ResourceBasedOverride {
+/**
+ * Utility class to filter out components from various lists
+ */
+public class AppFilter {
- public static AppFilter newInstance(Context context) {
- return Overrides.getObject(AppFilter.class, context, R.string.app_filter_class);
+ private final Set<ComponentName> mFilteredComponents;
+
+ public AppFilter(Context context) {
+ mFilteredComponents = Arrays.stream(
+ context.getResources().getStringArray(R.array.filtered_components))
+ .map(ComponentName::unflattenFromString)
+ .collect(Collectors.toSet());
}
public boolean shouldShowApp(ComponentName app) {
- return true;
+ return !mFilteredComponents.contains(app);
}
}
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 6f7b684..ee71146 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -1,29 +1,42 @@
package com.android.launcher3;
+import static android.appwidget.AppWidgetHostView.getDefaultPaddingForWidget;
+
import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT;
import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_COMPLETED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_STARTED;
import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X;
import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
-import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
-import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
import com.android.launcher3.accessibility.DragViewStateAnnouncer;
import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.util.FocusLogic;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.PendingRequestArgs;
+import com.android.launcher3.views.ArrowTipView;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
import java.util.List;
@@ -33,16 +46,10 @@
private static final float DIMMED_HANDLE_ALPHA = 0f;
private static final float RESIZE_THRESHOLD = 0.66f;
+ private static final String KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
+ "launcher.reconfigurable_widget_education_tip_seen";
private static final Rect sTmpRect = new Rect();
- // Represents the cell size on the grid in the two orientations.
- private static final MainThreadInitializedObject<Point[]> CELL_SIZE =
- new MainThreadInitializedObject<>(c -> {
- InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
- return new Point[] {inv.landscapeProfile.getCellSize(),
- inv.portraitProfile.getCellSize()};
- });
-
private static final int HANDLE_COUNT = 4;
private static final int INDEX_LEFT = 0;
private static final int INDEX_TOP = 1;
@@ -55,10 +62,27 @@
private final View[] mDragHandles = new View[HANDLE_COUNT];
private final List<Rect> mSystemGestureExclusionRects = new ArrayList<>(HANDLE_COUNT);
+ private final OnAttachStateChangeListener mWidgetViewAttachStateChangeListener =
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ // Do nothing
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ // When the app widget view is detached, we should close the resize frame.
+ // An example is when the dragging starts, the widget view is detached from
+ // CellLayout and then reattached to DragLayout.
+ close(false);
+ }
+ };
+
private LauncherAppWidgetHostView mWidgetView;
private CellLayout mCellLayout;
private DragLayer mDragLayer;
+ private ImageButton mReconfigureButton;
private Rect mWidgetPadding;
@@ -77,17 +101,22 @@
private final IntRange mDeltaYRange = new IntRange();
private final IntRange mBaselineY = new IntRange();
+ private final InstanceId logInstanceId = new InstanceIdSequence().newInstanceId();
+
private boolean mLeftBorderActive;
private boolean mRightBorderActive;
private boolean mTopBorderActive;
private boolean mBottomBorderActive;
- private int mResizeMode;
+ private boolean mHorizontalResizeActive;
+ private boolean mVerticalResizeActive;
private int mRunningHInc;
private int mRunningVInc;
private int mMinHSpan;
private int mMinVSpan;
+ private int mMaxHSpan;
+ private int mMaxVSpan;
private int mDeltaX;
private int mDeltaY;
private int mDeltaXAddOn;
@@ -126,10 +155,10 @@
protected void onFinishInflate() {
super.onFinishInflate();
- ViewGroup content = (ViewGroup) getChildAt(0);
- for (int i = 0; i < HANDLE_COUNT; i ++) {
- mDragHandles[i] = content.getChildAt(i);
- }
+ mDragHandles[INDEX_LEFT] = findViewById(R.id.widget_resize_left_handle);
+ mDragHandles[INDEX_TOP] = findViewById(R.id.widget_resize_top_handle);
+ mDragHandles[INDEX_RIGHT] = findViewById(R.id.widget_resize_right_handle);
+ mDragHandles[INDEX_BOTTOM] = findViewById(R.id.widget_resize_bottom_handle);
}
@Override
@@ -152,6 +181,15 @@
DragLayer dl = launcher.getDragLayer();
AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater()
.inflate(R.layout.app_widget_resize_frame, dl, false);
+ if (widget.hasEnforcedCornerRadius()) {
+ float enforcedCornerRadius = widget.getEnforcedCornerRadius();
+ ImageView imageView = frame.findViewById(R.id.widget_resize_frame);
+ Drawable d = imageView.getDrawable();
+ if (d instanceof GradientDrawable) {
+ GradientDrawable gd = (GradientDrawable) d.mutate();
+ gd.setCornerRadius(enforcedCornerRadius);
+ }
+ }
frame.setupForWidget(widget, cellLayout, dl);
((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true;
@@ -163,43 +201,96 @@
private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
DragLayer dragLayer) {
mCellLayout = cellLayout;
+ if (mWidgetView != null) {
+ mWidgetView.removeOnAttachStateChangeListener(mWidgetViewAttachStateChangeListener);
+ }
mWidgetView = widgetView;
+ mWidgetView.addOnAttachStateChangeListener(mWidgetViewAttachStateChangeListener);
LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
widgetView.getAppWidgetInfo();
- mResizeMode = info.resizeMode;
mDragLayer = dragLayer;
mMinHSpan = info.minSpanX;
mMinVSpan = info.minSpanY;
+ mMaxHSpan = info.maxSpanX;
+ mMaxVSpan = info.maxSpanY;
- mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(getContext(),
+ mWidgetPadding = getDefaultPaddingForWidget(getContext(),
widgetView.getAppWidgetInfo().provider, null);
- if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
+ // Only show resize handles for the directions in which resizing is possible.
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(cellLayout.getContext());
+ mVerticalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0
+ && mMinVSpan < idp.numRows && mMaxVSpan > 1;
+ if (!mVerticalResizeActive) {
mDragHandles[INDEX_TOP].setVisibility(GONE);
mDragHandles[INDEX_BOTTOM].setVisibility(GONE);
- } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
+ }
+ mHorizontalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0
+ && mMinHSpan < idp.numColumns && mMaxHSpan > 1;
+ if (!mHorizontalResizeActive) {
mDragHandles[INDEX_LEFT].setVisibility(GONE);
mDragHandles[INDEX_RIGHT].setVisibility(GONE);
}
+ mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button);
+ if (info.isReconfigurable()) {
+ mReconfigureButton.setVisibility(VISIBLE);
+ mReconfigureButton.setOnClickListener(view -> {
+ mLauncher.setWaitingForResult(
+ PendingRequestArgs.forWidgetInfo(
+ mWidgetView.getAppWidgetId(),
+ // Widget add handler is null since we're reconfiguring an existing
+ // widget.
+ /* widgetHandler= */ null,
+ (ItemInfo) mWidgetView.getTag()));
+ mLauncher
+ .getAppWidgetHost()
+ .startConfigActivity(
+ mLauncher,
+ mWidgetView.getAppWidgetId(),
+ Launcher.REQUEST_RECONFIGURE_APPWIDGET);
+ });
+ if (!hasSeenReconfigurableWidgetEducationTip()) {
+ post(() -> {
+ if (showReconfigurableWidgetEducationTip() != null) {
+ mLauncher.getSharedPrefs().edit()
+ .putBoolean(KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN,
+ true).apply();
+ }
+ });
+ }
+ }
+
+ CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
+ ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag();
+ lp.cellX = lp.tmpCellX = widgetInfo.cellX;
+ lp.cellY = lp.tmpCellY = widgetInfo.cellY;
+ lp.cellHSpan = widgetInfo.spanX;
+ lp.cellVSpan = widgetInfo.spanY;
+ lp.isLockedToGrid = true;
+
// When we create the resize frame, we first mark all cells as unoccupied. The appropriate
// cells (same if not resized, or different) will be marked as occupied when the resize
// frame is dismissed.
mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
+ mLauncher.getStatsLogManager()
+ .logger()
+ .withInstanceId(logInstanceId)
+ .withItemInfo(widgetInfo)
+ .log(LAUNCHER_WIDGET_RESIZE_STARTED);
+
setOnKeyListener(this);
}
public boolean beginResizeIfPointInRegion(int x, int y) {
- boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
- boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
-
- mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive;
- mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive;
- mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive;
+ mLeftBorderActive = (x < mTouchTargetWidth) && mHorizontalResizeActive;
+ mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && mHorizontalResizeActive;
+ mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment)
+ && mVerticalResizeActive;
mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
- && verticalActive;
+ && mVerticalResizeActive;
boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
|| mTopBorderActive || mBottomBorderActive;
@@ -279,8 +370,9 @@
* Based on the current deltas, we determine if and how to resize the widget.
*/
private void resizeWidgetIfNeeded(boolean onDismiss) {
- float xThreshold = mCellLayout.getCellWidth();
- float yThreshold = mCellLayout.getCellHeight();
+ DeviceProfile dp = mLauncher.getDeviceProfile();
+ float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacingPx;
+ float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacingPx;
int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
@@ -301,7 +393,7 @@
// expandability.
mTempRange1.set(cellX, spanX + cellX);
int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive,
- hSpanInc, mMinHSpan, mCellLayout.getCountX(), mTempRange2);
+ hSpanInc, mMinHSpan, mMaxHSpan, mCellLayout.getCountX(), mTempRange2);
cellX = mTempRange2.start;
spanX = mTempRange2.size();
if (hSpanDelta != 0) {
@@ -310,7 +402,7 @@
mTempRange1.set(cellY, spanY + cellY);
int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive,
- vSpanInc, mMinVSpan, mCellLayout.getCountY(), mTempRange2);
+ vSpanInc, mMinVSpan, mMaxVSpan, mCellLayout.getCountY(), mTempRange2);
cellY = mTempRange2.start;
spanY = mTempRange2.size();
if (vSpanDelta != 0) {
@@ -344,48 +436,29 @@
mRunningHInc += hSpanDelta;
if (!onDismiss) {
- updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
+ WidgetSizes.updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
}
}
mWidgetView.requestLayout();
}
- public static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
- int spanX, int spanY) {
- getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
- widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
- sTmpRect.right, sTmpRect.bottom);
- }
-
- public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
- if (rect == null) {
- rect = new Rect();
- }
- final float density = context.getResources().getDisplayMetrics().density;
- final Point[] cellSize = CELL_SIZE.get(context);
-
- // Compute landscape size
- int landWidth = (int) ((spanX * cellSize[0].x) / density);
- int landHeight = (int) ((spanY * cellSize[0].y) / density);
-
- // Compute portrait size
- int portWidth = (int) ((spanX * cellSize[1].x) / density);
- int portHeight = (int) ((spanY * cellSize[1].y) / density);
- rect.set(portWidth, landHeight, landWidth, portHeight);
- return rect;
- }
-
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// We are done with resizing the widget. Save the widget size & position to LauncherModel
resizeWidgetIfNeeded(true);
+ mLauncher.getStatsLogManager()
+ .logger()
+ .withInstanceId(logInstanceId)
+ .withItemInfo((ItemInfo) mWidgetView.getTag())
+ .log(LAUNCHER_WIDGET_RESIZE_COMPLETED);
}
private void onTouchUp() {
- int xThreshold = mCellLayout.getCellWidth();
- int yThreshold = mCellLayout.getCellHeight();
+ DeviceProfile dp = mLauncher.getDeviceProfile();
+ int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacingPx;
+ int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacingPx;
mDeltaXAddOn = mRunningHInc * xThreshold;
mDeltaYAddOn = mRunningVInc * yThreshold;
@@ -476,7 +549,7 @@
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// Clear the frame and give focus to the widget host view when a directional key is pressed.
- if (FocusLogic.shouldConsume(keyCode)) {
+ if (shouldConsume(keyCode)) {
close(false);
mWidgetView.requestFocus();
return true;
@@ -500,6 +573,13 @@
return false;
}
+ private boolean isTouchOnReconfigureButton(MotionEvent ev) {
+ int xFrame = (int) ev.getX() - getLeft();
+ int yFrame = (int) ev.getY() - getTop();
+ mReconfigureButton.getHitRect(sTmpRect);
+ return sTmpRect.contains(xFrame, yFrame);
+ }
+
@Override
public boolean onControllerTouchEvent(MotionEvent ev) {
int action = ev.getAction();
@@ -527,6 +607,11 @@
if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) {
return true;
}
+ // Keep the resize frame open but let a click on the reconfigure button fall through to the
+ // button's OnClickListener.
+ if (isTouchOnReconfigureButton(ev)) {
+ return false;
+ }
close(false);
return false;
}
@@ -534,11 +619,9 @@
@Override
protected void handleClose(boolean animate) {
mDragLayer.removeView(this);
- }
-
- @Override
- public void logActionCommand(int command) {
- // TODO: Log this case.
+ if (mWidgetView != null) {
+ mWidgetView.removeOnAttachStateChangeListener(mWidgetViewAttachStateChangeListener);
+ }
}
@Override
@@ -581,12 +664,15 @@
* @param minSize minimum size after with the moving edge should not be shifted any further.
* For eg, if delta = -3 when moving the endEdge brings the size to less than
* minSize, only delta = -2 will applied
+ * @param maxSize maximum size after with the moving edge should not be shifted any further.
+ * For eg, if delta = -3 when moving the endEdge brings the size to greater
+ * than maxSize, only delta = -2 will applied
* @param maxEnd The maximum value to the end edge (start edge is always restricted to 0)
* @return the amount of increase when endEdge was moves and the amount of decrease when
* the start edge was moved.
*/
public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta,
- int minSize, int maxEnd, IntRange out) {
+ int minSize, int maxSize, int maxEnd, IntRange out) {
applyDelta(moveStart, moveEnd, delta, out);
if (out.start < 0) {
out.start = 0;
@@ -601,7 +687,45 @@
out.end = out.start + minSize;
}
}
+ if (out.size() > maxSize) {
+ if (moveStart) {
+ out.start = out.end - maxSize;
+ } else if (moveEnd) {
+ out.end = out.start + maxSize;
+ }
+ }
return moveEnd ? out.size() - size() : size() - out.size();
}
}
+
+ /**
+ * Returns true only if this utility class handles the key code.
+ */
+ public static boolean shouldConsume(int keyCode) {
+ return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+ || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
+ || keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END
+ || keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN);
+ }
+
+ @Nullable private ArrowTipView showReconfigurableWidgetEducationTip() {
+ Rect rect = new Rect();
+ if (!mReconfigureButton.getGlobalVisibleRect(rect)) {
+ return null;
+ }
+ @Px int tipMargin = mLauncher.getResources()
+ .getDimensionPixelSize(R.dimen.widget_reconfigure_tip_top_margin);
+ return new ArrowTipView(mLauncher, /* isPointingUp= */ true)
+ .showAroundRect(
+ getContext().getString(R.string.reconfigurable_widget_education_tip),
+ /* arrowXCoord= */ rect.left + mReconfigureButton.getWidth() / 2,
+ /* rect= */ rect,
+ /* margin= */ tipMargin);
+ }
+
+ private boolean hasSeenReconfigurableWidgetEducationTip() {
+ return mLauncher.getSharedPrefs()
+ .getBoolean(KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, false)
+ || Utilities.IS_RUNNING_IN_TEST_HARNESS;
+ }
}
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index a55c90d..75e89b2 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -21,6 +21,7 @@
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.provider.RestoreDbTask;
import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
@@ -86,13 +87,12 @@
long mainProfileId = UserCache.INSTANCE.get(context)
.getSerialNumberForUser(myUserHandle());
String oldWidgetId = Integer.toString(oldWidgetIds[i]);
- int result = new ContentWriter(context, new ContentWriter.CommitParams(
- "appWidgetId=? and (restored & 1) = 1 and profileId=?",
- new String[] { oldWidgetId, Long.toString(mainProfileId) }))
+ final String where = "appWidgetId=? and (restored & 1) = 1 and profileId=?";
+ final String[] args = new String[] { oldWidgetId, Long.toString(mainProfileId) };
+ int result = new ContentWriter(context, new ContentWriter.CommitParams(where, args))
.put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i])
.put(LauncherSettings.Favorites.RESTORED, state)
.commit();
-
if (result == 0) {
Cursor cursor = cr.query(Favorites.CONTENT_URI,
new String[] {Favorites.APPWIDGET_ID},
@@ -106,6 +106,12 @@
cursor.close();
}
}
+ // attempt to update widget id in backup table as well
+ new ContentWriter(context, ContentWriter.CommitParams.backupCommitParams(
+ "appWidgetId=? and profileId=?", args))
+ .put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i])
+ .put(LauncherSettings.Favorites.RESTORED, state)
+ .commit();
}
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 432073e..5b655a4 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -89,7 +89,7 @@
// Try with grid size and hotseat count
String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
- grid.numColumns, grid.numRows, grid.numHotseatIcons);
+ grid.numColumns, grid.numRows, grid.numDatabaseHotseatIcons);
int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
// Try with only grid size
@@ -658,7 +658,7 @@
}
}
- protected static void beginDocument(XmlPullParser parser, String firstElementName)
+ public static void beginDocument(XmlPullParser parser, String firstElementName)
throws XmlPullParserException, IOException {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 310c306..9778b61 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -17,7 +17,7 @@
package com.android.launcher3;
import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
-import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -31,17 +31,15 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.util.Log;
-import android.view.ContextThemeWrapper;
import androidx.annotation.IntDef;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.ViewCache;
import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.ScrimView;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -82,7 +80,6 @@
new ArrayList<>();
protected DeviceProfile mDeviceProfile;
- protected UserEventDispatcher mUserEventDispatcher;
protected StatsLogManager mStatsLogManager;
protected SystemUiController mSystemUiController;
@@ -135,6 +132,7 @@
private final ViewCache mViewCache = new ViewCache();
+ @Override
public ViewCache getViewCache() {
return mViewCache;
}
@@ -144,22 +142,16 @@
return mDeviceProfile;
}
- public void modifyUserEvent(LauncherLogProto.LauncherEvent event) {}
-
- public final StatsLogManager getStatsLogManager() {
+ /**
+ * Returns {@link StatsLogManager} for user event logging.
+ */
+ public StatsLogManager getStatsLogManager() {
if (mStatsLogManager == null) {
mStatsLogManager = StatsLogManager.newInstance(this);
}
return mStatsLogManager;
}
- public final UserEventDispatcher getUserEventDispatcher() {
- if (mUserEventDispatcher == null) {
- mUserEventDispatcher = UserEventDispatcher.newInstance(this);
- }
- return mUserEventDispatcher;
- }
-
public SystemUiController getSystemUiController() {
if (mSystemUiController == null) {
mSystemUiController = new SystemUiController(getWindow());
@@ -167,6 +159,10 @@
return mSystemUiController;
}
+ public ScrimView getScrimView() {
+ return null;
+ }
+
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -207,7 +203,7 @@
// Reset the overridden sysui flags used for the task-swipe launch animation, this is a
// catch all for if we do not get resumed (and therefore not paused below)
- getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
+ getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
}
@Override
@@ -219,7 +215,7 @@
// here instead of at the end of the animation because the start of the new activity does
// not happen immediately, which would cause us to reset to launcher's sysui flags and then
// back to the new app (causing a flash)
- getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
+ getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
}
@Override
@@ -289,7 +285,7 @@
/**
* Used to set the override visibility state, used only to handle the transition home with the
* recents animation.
- * @see QuickstepAppTransitionManagerImpl#createWallpaperOpenRunner
+ * @see QuickstepTransitionManager#createWallpaperOpenRunner
*/
public void addForceInvisibleFlag(@InvisibilityFlags int flag) {
mForceInvisible |= flag;
@@ -342,7 +338,7 @@
public static <T extends BaseActivity> T fromContext(Context context) {
if (context instanceof BaseActivity) {
return (T) context;
- } else if (context instanceof ContextThemeWrapper) {
+ } else if (context instanceof ContextWrapper) {
return fromContext(((ContextWrapper) context).getBaseContext());
} else {
throw new IllegalArgumentException("Cannot find BaseActivity in parent tree");
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 9cb8cf2..2c76e52 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -17,16 +17,22 @@
package com.android.launcher3;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
-import static com.android.launcher3.util.DefaultDisplay.CHANGE_ROTATION;
+import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.app.ActivityOptions;
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
+import android.app.WallpaperManager.OnColorsChangedListener;
import android.content.ActivityNotFoundException;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Process;
import android.os.StrictMode;
@@ -40,20 +46,24 @@
import android.view.WindowMetrics;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider;
+import com.android.launcher3.allapps.search.SearchAdapterProvider;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.InstanceIdSequence;
-import com.android.launcher3.model.AppLaunchTracker;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.uioverrides.WallpaperColorInfo;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.launcher3.util.DefaultDisplay.DisplayInfoChangeListener;
-import com.android.launcher3.util.DefaultDisplay.Info;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
+import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.util.WindowBounds;
@@ -61,8 +71,9 @@
/**
* Extension of BaseActivity allowing support for drag-n-drop
*/
+@SuppressWarnings("NewApi")
public abstract class BaseDraggingActivity extends BaseActivity
- implements WallpaperColorInfo.OnChangeListener, DisplayInfoChangeListener {
+ implements OnColorsChangedListener, DisplayInfoChangeListener {
private static final String TAG = "BaseDraggingActivity";
@@ -74,6 +85,7 @@
protected boolean mIsSafeModeEnabled;
private Runnable mOnStartCallback;
+ private RunnableList mOnResumeCallbacks = new RunnableList();
private int mThemeRes = R.style.AppTheme;
@@ -81,13 +93,15 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
- mIsSafeModeEnabled = TraceHelper.whitelistIpcs("isSafeMode",
+ mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> getPackageManager().isSafeMode());
- DefaultDisplay.INSTANCE.get(this).addChangeListener(this);
+ DisplayController.INSTANCE.get(this).addChangeListener(this);
// Update theme
- WallpaperColorInfo.INSTANCE.get(this).addOnChangeListener(this);
+ if (Utilities.ATLEAST_P) {
+ getSystemService(WallpaperManager.class)
+ .addOnColorsChangedListener(this, MAIN_EXECUTOR.getHandler());
+ }
int themeRes = Themes.getActivityThemeRes(this);
if (themeRes != mThemeRes) {
mThemeRes = themeRes;
@@ -96,7 +110,17 @@
}
@Override
- public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
+ protected void onResume() {
+ super.onResume();
+ mOnResumeCallbacks.executeAllAndClear();
+ }
+
+ public void addOnResumeCallback(Runnable callback) {
+ mOnResumeCallbacks.add(callback);
+ }
+
+ @Override
+ public void onColorsChanged(WallpaperColors wallpaperColors, int which) {
updateTheme();
}
@@ -147,21 +171,35 @@
return new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight());
}
- public final Bundle getActivityLaunchOptionsAsBundle(View v) {
- ActivityOptions activityOptions = getActivityLaunchOptions(v);
- return activityOptions == null ? null : activityOptions.toBundle();
+ @NonNull
+ public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
+ int left = 0, top = 0;
+ int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
+ if (v instanceof BubbleTextView) {
+ // Launch from center of icon, not entire view
+ Drawable icon = ((BubbleTextView) v).getIcon();
+ if (icon != null) {
+ Rect bounds = icon.getBounds();
+ left = (width - bounds.width()) / 2;
+ top = v.getPaddingTop();
+ width = bounds.width();
+ height = bounds.height();
+ }
+ }
+ ActivityOptions options =
+ ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
+ RunnableList callback = new RunnableList();
+ addOnResumeCallback(callback::executeAllAndDestroy);
+ return new ActivityOptionsWrapper(options, callback);
}
- public abstract ActivityOptions getActivityLaunchOptions(View v);
-
- public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item,
- @Nullable String sourceContainer) {
+ public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item) {
if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) {
Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
return false;
}
- Bundle optsBundle = (v != null) ? getActivityLaunchOptionsAsBundle(v) : null;
+ Bundle optsBundle = (v != null) ? getActivityLaunchOptions(v, item).toBundle() : null;
UserHandle user = item == null ? null : item.user;
// Prepare intent
@@ -176,25 +214,20 @@
&& !((WorkspaceItemInfo) item).isPromise();
if (isShortcut) {
// Shortcuts need some special checks due to legacy reasons.
- startShortcutIntentSafely(intent, optsBundle, item, sourceContainer);
+ startShortcutIntentSafely(intent, optsBundle, item);
} else if (user == null || user.equals(Process.myUserHandle())) {
// Could be launching some bookkeeping activity
startActivity(intent, optsBundle);
- AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(),
- Process.myUserHandle(), sourceContainer);
} else {
getSystemService(LauncherApps.class).startMainActivity(
intent.getComponent(), user, intent.getSourceBounds(), optsBundle);
- AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(), user,
- sourceContainer);
}
- getUserEventDispatcher().logAppLaunch(v, intent, user);
if (item != null) {
InstanceId instanceId = new InstanceIdSequence().newInstanceId();
logAppLaunch(item, instanceId);
}
return true;
- } catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
+ } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e);
}
@@ -206,8 +239,7 @@
.log(LAUNCHER_APP_LAUNCH_TAP);
}
- private void startShortcutIntentSafely(Intent intent, Bundle optsBundle, ItemInfo info,
- @Nullable String sourceContainer) {
+ private void startShortcutIntentSafely(Intent intent, Bundle optsBundle, ItemInfo info) {
try {
StrictMode.VmPolicy oldPolicy = StrictMode.getVmPolicy();
try {
@@ -221,8 +253,6 @@
String id = ((WorkspaceItemInfo) info).getDeepShortcutId();
String packageName = intent.getPackage();
startShortcut(packageName, id, intent.getSourceBounds(), optsBundle, info.user);
- AppLaunchTracker.INSTANCE.get(this).onStartShortcut(packageName, id, info.user,
- sourceContainer);
} else {
// Could be launching some bookkeeping activity
startActivity(intent, optsBundle);
@@ -254,8 +284,10 @@
@Override
protected void onDestroy() {
super.onDestroy();
- WallpaperColorInfo.INSTANCE.get(this).removeOnChangeListener(this);
- DefaultDisplay.INSTANCE.get(this).removeChangeListener(this);
+ if (Utilities.ATLEAST_P) {
+ getSystemService(WallpaperManager.class).removeOnColorsChangedListener(this);
+ }
+ DisplayController.INSTANCE.get(this).removeChangeListener(this);
}
public void runOnceOnStart(Runnable action) {
@@ -273,7 +305,7 @@
}
@Override
- public void onDisplayInfoChanged(Info info, int flags) {
+ public void onDisplayInfoChanged(Context context, Info info, int flags) {
if ((flags & CHANGE_ROTATION) != 0 && mDeviceProfile.updateIsSeascape(this)) {
reapplyUi();
}
@@ -300,4 +332,12 @@
display.getSize(mwSize);
return new WindowBounds(new Rect(0, 0, mwSize.x, mwSize.y), new Rect());
}
+
+ /**
+ * Creates and returns {@link SearchAdapterProvider} for build variant specific search result
+ * views
+ */
+ public SearchAdapterProvider createSearchAdapterProvider(AllAppsContainerView allapps) {
+ return new DefaultSearchAdapterProvider(this, allapps);
+ }
}
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 8eceec0..9369bdc 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -16,21 +16,17 @@
package com.android.launcher3;
-import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
-
import android.content.Context;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
+import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -194,13 +190,20 @@
if (isLayoutSuppressed()) info.setScrollable(false);
}
- @Override
- public void setLayoutFrozen(boolean frozen) {
- final boolean changing = frozen != isLayoutSuppressed();
- super.setLayoutFrozen(frozen);
- if (changing) {
- ActivityContext.lookupContext(getContext()).getDragLayer()
- .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
+ /**
+ * Scrolls this recycler view to the top.
+ */
+ public void scrollToTop() {
+ if (mScrollbar != null) {
+ mScrollbar.reattachThumbToScroll();
}
+ if (getLayoutManager() instanceof LinearLayoutManager) {
+ LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+ if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
+ // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
+ return;
+ }
+ }
+ scrollToPosition(0);
}
}
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 60b6da6..ddbd425 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,7 +16,6 @@
package com.android.launcher3;
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -43,7 +42,9 @@
import android.view.ViewDebug;
import android.widget.TextView;
-import com.android.launcher3.Launcher.OnResumeCallback;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DraggableView;
@@ -52,13 +53,15 @@
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.icons.IconCache.IconLoadRequest;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.icons.PlaceHolderIconDrawable;
+import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
+import com.android.launcher3.model.data.SearchActionItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.views.ActivityContext;
@@ -71,14 +74,18 @@
* because we want to make the bubble taller than the text and TextView's clip is
* too aggressive.
*/
-public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
+public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
IconLabelDotView, DraggableView, Reorderable {
private static final int DISPLAY_WORKSPACE = 0;
private static final int DISPLAY_ALL_APPS = 1;
private static final int DISPLAY_FOLDER = 2;
+ protected static final int DISPLAY_TASKBAR = 5;
+ private static final int DISPLAY_SEARCH_RESULT = 6;
+ private static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
- private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed};
+ private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
+ private static final float HIGHLIGHT_SCALE = 1.16f;
private final PointF mTranslationForReorderBounce = new PointF(0, 0);
private final PointF mTranslationForReorderPreview = new PointF(0, 0);
@@ -113,10 +120,10 @@
};
private final ActivityContext mActivity;
- private Drawable mIcon;
+ private FastBitmapDrawable mIcon;
private boolean mCenterVertically;
- private final int mDisplay;
+ protected final int mDisplay;
private final CheckLongPressHelper mLongPressHelper;
@@ -134,7 +141,7 @@
private DotInfo mDotInfo;
private DotRenderer mDotRenderer;
@ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
- private DotRenderer.DrawParams mDotParams;
+ protected DotRenderer.DrawParams mDotParams;
private Animator mDotScaleAnim;
private boolean mForceHideDot;
@@ -145,7 +152,9 @@
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mDisableRelayout = false;
- private IconLoadRequest mIconLoadRequest;
+ private HandlerRunnable mIconLoadRequest;
+
+ private boolean mEnableIconUpdateAnimation = false;
public BubbleTextView(Context context) {
this(context, null, 0);
@@ -170,6 +179,7 @@
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
defaultIconSize = grid.iconSizePx;
+ setCenterVertically(grid.isScalableGrid);
} else if (mDisplay == DISPLAY_ALL_APPS) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
@@ -178,6 +188,13 @@
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx);
setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx);
defaultIconSize = grid.folderChildIconSizePx;
+ } else if (mDisplay == DISPLAY_SEARCH_RESULT) {
+ defaultIconSize = getResources().getDimensionPixelSize(R.dimen.search_row_icon_size);
+ } else if (mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
+ defaultIconSize = getResources().getDimensionPixelSize(
+ R.dimen.search_row_small_icon_size);
+ } else if (mDisplay == DISPLAY_TASKBAR) {
+ defaultIconSize = grid.iconSizePx;
} else {
// widget_selection or shortcut_popup
defaultIconSize = grid.iconSizePx;
@@ -235,6 +252,7 @@
mDotScaleAnim.start();
}
+ @UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
applyFromWorkspaceItem(info, false);
}
@@ -251,16 +269,16 @@
}
}
+ @UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
applyIconAndLabel(info);
setTag(info);
- if (promiseStateChanged || (info.hasPromiseIconUi())) {
- applyPromiseState(promiseStateChanged);
- }
-
+ applyLoadingState(promiseStateChanged);
applyDotState(info, false /* animate */);
+ setDownloadStateContentDescription(info, info.getProgressLevel());
}
+ @UiThread
public void applyFromApplicationInfo(AppInfo info) {
applyIconAndLabel(info);
@@ -270,27 +288,49 @@
// Verify high res immediately
verifyHighRes();
- if (info instanceof PromiseAppInfo) {
- PromiseAppInfo promiseAppInfo = (PromiseAppInfo) info;
- applyProgressLevel(promiseAppInfo.level);
+ if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
+ applyProgressLevel();
}
applyDotState(info, false /* animate */);
+ setDownloadStateContentDescription(info, info.getProgressLevel());
}
- public void applyFromPackageItemInfo(PackageItemInfo info) {
+ /**
+ * Apply label and tag using a generic {@link ItemInfoWithIcon}
+ */
+ @UiThread
+ public void applyFromItemInfoWithIcon(ItemInfoWithIcon info) {
applyIconAndLabel(info);
// We don't need to check the info since it's not a WorkspaceItemInfo
super.setTag(info);
// Verify high res immediately
verifyHighRes();
+
+ setDownloadStateContentDescription(info, info.getProgressLevel());
}
- private void applyIconAndLabel(ItemInfoWithIcon info) {
- FastBitmapDrawable iconDrawable = newIcon(getContext(), info);
- mDotParams.color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
+ /**
+ * Apply label and tag using a {@link SearchActionItemInfo}
+ */
+ @UiThread
+ public void applyFromSearchActionItemInfo(SearchActionItemInfo searchActionItemInfo) {
+ applyIconAndLabel(searchActionItemInfo);
+ setTag(searchActionItemInfo);
+ }
+
+ @UiThread
+ protected void applyIconAndLabel(ItemInfoWithIcon info) {
+ boolean useTheme = mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER;
+ FastBitmapDrawable iconDrawable = info.newIcon(getContext(), useTheme);
+ mDotParams.color = IconPalette.getMutedColor(iconDrawable.getIconColor(), 0.54f);
setIcon(iconDrawable);
+ applyLabel(info);
+ }
+
+ @UiThread
+ private void applyLabel(ItemInfoWithIcon info) {
setText(info.title);
if (info.contentDescription != null) {
setContentDescription(info.isDisabled()
@@ -323,7 +363,7 @@
}
/** Returns the icon for this view. */
- public Drawable getIcon() {
+ public FastBitmapDrawable getIcon() {
return mIcon;
}
@@ -331,10 +371,7 @@
public boolean onTouchEvent(MotionEvent event) {
// ignore events if they happen in padding area
if (event.getAction() == MotionEvent.ACTION_DOWN
- && (event.getY() < getPaddingTop()
- || event.getX() < getPaddingLeft()
- || event.getY() > getHeight() - getPaddingBottom()
- || event.getX() > getWidth() - getPaddingRight())) {
+ && shouldIgnoreTouchDown(event.getX(), event.getY())) {
return false;
}
if (isLongClickable()) {
@@ -347,6 +384,16 @@
}
}
+ /**
+ * Returns true if the touch down at the provided position be ignored
+ */
+ protected boolean shouldIgnoreTouchDown(float x, float y) {
+ return y < getPaddingTop()
+ || x < getPaddingLeft()
+ || y > getHeight() - getPaddingBottom()
+ || x > getWidth() - getPaddingRight();
+ }
+
void setStayPressed(boolean stayPressed) {
mStayPressed = stayPressed;
refreshDrawableState();
@@ -360,13 +407,6 @@
}
}
- @Override
- public void onLauncherResume() {
- // Reset the pressed state of icon that was locked in the press state while activity
- // was launching
- setStayPressed(false);
- }
-
void clearPressedBackground() {
setPressed(false);
setStayPressed(false);
@@ -397,12 +437,14 @@
/**
* Draws the notification dot in the top right corner of the icon bounds.
+ *
* @param canvas The canvas to draw to.
*/
protected void drawDotIfNecessary(Canvas canvas) {
if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
getIconBounds(mDotParams.iconBounds);
- Utilities.scaleRectAboutCenter(mDotParams.iconBounds, IconShape.getNormalizationScale());
+ Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
+ IconShape.getNormalizationScale());
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
@@ -441,6 +483,14 @@
outBounds.set(left, top, right, bottom);
}
+
+ /**
+ * Sets whether to vertically center the content.
+ */
+ public void setCenterVertically(boolean centerVertically) {
+ mCenterVertically = centerVertically;
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mCenterVertically) {
@@ -497,6 +547,7 @@
/**
* Creates an animator to fade the text in or out.
+ *
* @param fadeIn Whether the text should fade in or fade out.
*/
public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) {
@@ -510,51 +561,86 @@
mLongPressHelper.cancelLongPress();
}
- public void applyPromiseState(boolean promiseStateChanged) {
- if (getTag() instanceof WorkspaceItemInfo) {
+ /**
+ * Applies the loading progress value to the progress bar.
+ *
+ * If this app is installing, the progress bar will be updated with the installation progress.
+ * If this app is installed and downloading incrementally, the progress bar will be updated
+ * with the total download progress.
+ */
+ public void applyLoadingState(boolean promiseStateChanged) {
+ if (getTag() instanceof ItemInfoWithIcon) {
WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
- final boolean isPromise = info.hasPromiseIconUi();
- final int progressLevel = isPromise ?
- ((info.hasStatusFlag(WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE) ?
- info.getInstallProgress() : 0)) : 100;
-
- PreloadIconDrawable preloadDrawable = applyProgressLevel(progressLevel);
- if (preloadDrawable != null && promiseStateChanged) {
- preloadDrawable.maybePerformFinishedAnimation();
+ if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE)
+ != 0) {
+ updateProgressBarUi(info.getProgressLevel() == 100);
+ } else if (info.hasPromiseIconUi() || (info.runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+ updateProgressBarUi(promiseStateChanged);
}
}
}
- public PreloadIconDrawable applyProgressLevel(int progressLevel) {
- if (getTag() instanceof ItemInfoWithIcon) {
- ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
- if (progressLevel >= 100) {
- setContentDescription(info.contentDescription != null
- ? info.contentDescription : "");
- } else if (progressLevel > 0) {
- setContentDescription(getContext()
- .getString(R.string.app_downloading_title, info.title,
- NumberFormat.getPercentInstance().format(progressLevel * 0.01)));
+ private void updateProgressBarUi(boolean maybePerformFinishedAnimation) {
+ PreloadIconDrawable preloadDrawable = applyProgressLevel();
+ if (preloadDrawable != null && maybePerformFinishedAnimation) {
+ preloadDrawable.maybePerformFinishedAnimation();
+ }
+ }
+
+ /** Applies the given progress level to the this icon's progress bar. */
+ @Nullable
+ public PreloadIconDrawable applyProgressLevel() {
+ if (!(getTag() instanceof ItemInfoWithIcon)) {
+ return null;
+ }
+
+ ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
+ int progressLevel = info.getProgressLevel();
+ if (progressLevel >= 100) {
+ setContentDescription(info.contentDescription != null
+ ? info.contentDescription : "");
+ } else if (progressLevel > 0) {
+ setDownloadStateContentDescription(info, progressLevel);
+ } else {
+ setContentDescription(getContext()
+ .getString(R.string.app_waiting_download_title, info.title));
+ }
+ if (mIcon != null) {
+ PreloadIconDrawable preloadIconDrawable;
+ if (mIcon instanceof PreloadIconDrawable) {
+ preloadIconDrawable = (PreloadIconDrawable) mIcon;
+ preloadIconDrawable.setLevel(progressLevel);
+ preloadIconDrawable.setIsDisabled(!info.isAppStartable());
} else {
- setContentDescription(getContext()
- .getString(R.string.app_waiting_download_title, info.title));
+ preloadIconDrawable = makePreloadIcon();
+ setIcon(preloadIconDrawable);
}
- if (mIcon != null) {
- final PreloadIconDrawable preloadDrawable;
- if (mIcon instanceof PreloadIconDrawable) {
- preloadDrawable = (PreloadIconDrawable) mIcon;
- preloadDrawable.setLevel(progressLevel);
- } else {
- preloadDrawable = newPendingIcon(getContext(), info);
- preloadDrawable.setLevel(progressLevel);
- setIcon(preloadDrawable);
- }
- return preloadDrawable;
- }
+ return preloadIconDrawable;
}
return null;
}
+ /**
+ * Creates a PreloadIconDrawable with the appropriate progress level without mutating this
+ * object.
+ */
+ @Nullable
+ public PreloadIconDrawable makePreloadIcon() {
+ if (!(getTag() instanceof ItemInfoWithIcon)) {
+ return null;
+ }
+
+ ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
+ int progressLevel = info.getProgressLevel();
+ final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info);
+
+ preloadDrawable.setLevel(progressLevel);
+ preloadDrawable.setIsDisabled(!info.isAppStartable());
+
+ return preloadDrawable;
+ }
+
public void applyDotState(ItemInfo itemInfo, boolean animate) {
if (mIcon instanceof FastBitmapDrawable) {
boolean wasDotted = mDotInfo != null;
@@ -591,10 +677,28 @@
}
}
+ private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
+ if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
+ != 0) {
+ String percentageString = NumberFormat.getPercentInstance()
+ .format(progressLevel * 0.01);
+ if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+ setContentDescription(getContext()
+ .getString(
+ R.string.app_installing_title, info.title, percentageString));
+ } else if ((info.runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
+ setContentDescription(getContext()
+ .getString(
+ R.string.app_downloading_title, info.title, percentageString));
+ }
+ }
+ }
+
/**
* Sets the icon for this view based on the layout direction.
*/
- private void setIcon(Drawable icon) {
+ protected void setIcon(FastBitmapDrawable icon) {
if (mIsIconVisible) {
applyCompoundDrawables(icon);
}
@@ -607,21 +711,33 @@
@Override
public void setIconVisible(boolean visible) {
mIsIconVisible = visible;
+ if (!mIsIconVisible) {
+ resetIconScale();
+ }
Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
applyCompoundDrawables(icon);
}
+ protected boolean iconUpdateAnimationEnabled() {
+ return mEnableIconUpdateAnimation;
+ }
+
protected void applyCompoundDrawables(Drawable icon) {
// If we had already set an icon before, disable relayout as the icon size is the
// same as before.
mDisableRelayout = mIcon != null;
icon.setBounds(0, 0, mIconSize, mIconSize);
- if (mLayoutHorizontal) {
- setCompoundDrawablesRelative(icon, null, null, null);
- } else {
- setCompoundDrawables(null, icon, null, null);
+
+ updateIcon(icon);
+
+ // If the current icon is a placeholder color, animate its update.
+ if (mIcon != null
+ && mIcon instanceof PlaceHolderIconDrawable
+ && iconUpdateAnimationEnabled()) {
+ ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
}
+
mDisableRelayout = false;
}
@@ -640,6 +756,7 @@
if (getTag() == info) {
mIconLoadRequest = null;
mDisableRelayout = true;
+ mEnableIconUpdateAnimation = true;
// Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
info.bitmap.icon.prepareToDraw();
@@ -650,10 +767,13 @@
applyFromWorkspaceItem((WorkspaceItemInfo) info);
mActivity.invalidateParent(info);
} else if (info instanceof PackageItemInfo) {
- applyFromPackageItemInfo((PackageItemInfo) info);
+ applyFromItemInfoWithIcon((PackageItemInfo) info);
+ } else if (info instanceof SearchActionItemInfo) {
+ applyFromSearchActionItemInfo((SearchActionItemInfo) info);
}
mDisableRelayout = false;
+ mEnableIconUpdateAnimation = false;
}
}
@@ -746,11 +866,22 @@
@Override
public SafeCloseable prepareDrawDragView() {
- if (getIcon() instanceof FastBitmapDrawable) {
- FastBitmapDrawable icon = (FastBitmapDrawable) getIcon();
- icon.setScale(1f);
- }
+ resetIconScale();
setForceHideDot(true);
return () -> { };
}
+
+ private void resetIconScale() {
+ if (mIcon instanceof FastBitmapDrawable) {
+ ((FastBitmapDrawable) mIcon).resetScale();
+ }
+ }
+
+ private void updateIcon(Drawable newIcon) {
+ if (mLayoutHorizontal) {
+ setCompoundDrawablesRelative(newIcon, null, null, null);
+ } else {
+ setCompoundDrawables(null, newIcon, null, null);
+ }
+ }
}
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 09827d6..61b5564 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -20,20 +20,12 @@
import static com.android.launcher3.LauncherState.NORMAL;
-import android.animation.AnimatorSet;
-import android.animation.FloatArrayEvaluator;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
import android.content.Context;
-import android.content.res.ColorStateList;
import android.content.res.Resources;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
-import android.util.Property;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -47,9 +39,6 @@
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.Thunk;
/**
* Implements a DropTarget.
@@ -57,22 +46,9 @@
public abstract class ButtonDropTarget extends TextView
implements DropTarget, DragController.DragListener, OnClickListener {
- private static final Property<ButtonDropTarget, Integer> TEXT_COLOR =
- new Property<ButtonDropTarget, Integer>(Integer.TYPE, "textColor") {
-
- @Override
- public Integer get(ButtonDropTarget target) {
- return target.getTextColor();
- }
-
- @Override
- public void set(ButtonDropTarget target, Integer value) {
- target.setTextColor(value);
- }
- };
-
private static final int[] sTempCords = new int[2];
private static final int DRAG_VIEW_DROP_DURATION = 285;
+ private static final float DRAG_VIEW_HOVER_OVER_OPACITY = 0.65f;
public static final int TOOLTIP_DEFAULT = 0;
public static final int TOOLTIP_LEFT = 1;
@@ -80,7 +56,6 @@
protected final Launcher mLauncher;
- private int mBottomDragPadding;
protected DropTargetBar mDropTargetBar;
/** Whether this drop target is active for the current drag */
@@ -89,21 +64,16 @@
private boolean mAccessibleDrag;
/** An item must be dragged at least this many pixels before this drop target is enabled. */
private final int mDragDistanceThreshold;
-
- /** The paint applied to the drag view on hover */
- protected int mHoverColor = 0;
+ /** The size of the drawable shown in the drop target. */
+ private final int mDrawableSize;
protected CharSequence mText;
- protected ColorStateList mOriginalTextColor;
protected Drawable mDrawable;
private boolean mTextVisible = true;
private PopupWindow mToolTip;
private int mToolTipLocation;
- private AnimatorSet mCurrentColorAnim;
- @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter;
-
public ButtonDropTarget(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@@ -113,15 +83,14 @@
mLauncher = Launcher.getLauncher(context);
Resources resources = getResources();
- mBottomDragPadding = resources.getDimensionPixelSize(R.dimen.drop_target_drag_padding);
mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold);
+ mDrawableSize = resources.getDimensionPixelSize(R.dimen.drop_target_text_size);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mText = getText();
- mOriginalTextColor = getTextColors();
setContentDescription(mText);
}
@@ -134,13 +103,10 @@
protected void setDrawable(int resId) {
// We do not set the drawable in the xml as that inflates two drawables corresponding to
// drawableLeft and drawableStart.
- if (mTextVisible) {
- setCompoundDrawablesRelativeWithIntrinsicBounds(resId, 0, 0, 0);
- mDrawable = getCompoundDrawablesRelative()[0];
- } else {
- setCompoundDrawablesRelativeWithIntrinsicBounds(0, resId, 0, 0);
- mDrawable = getCompoundDrawablesRelative()[1];
- }
+ mDrawable = getContext().getDrawable(resId).mutate();
+ mDrawable.setBounds(0, 0, mDrawableSize, mDrawableSize);
+ mDrawable.setTintList(getTextColors());
+ setCompoundDrawablesRelative(mDrawable, null, null, null);
}
public void setDropTargetBar(DropTargetBar dropTargetBar) {
@@ -178,8 +144,8 @@
mToolTip.showAsDropDown(this, x, y);
}
- d.dragView.setColor(mHoverColor);
- animateTextColor(mHoverColor);
+ d.dragView.setAlpha(DRAG_VIEW_HOVER_OVER_OPACITY);
+ setSelected(true);
if (d.stateAnnouncer != null) {
d.stateAnnouncer.cancel();
}
@@ -191,63 +157,21 @@
// Do nothing
}
- protected void resetHoverColor() {
- animateTextColor(mOriginalTextColor.getDefaultColor());
- }
-
- private void animateTextColor(int targetColor) {
- if (mCurrentColorAnim != null) {
- mCurrentColorAnim.cancel();
- }
-
- mCurrentColorAnim = new AnimatorSet();
- mCurrentColorAnim.setDuration(DragView.COLOR_CHANGE_DURATION);
-
- if (mSrcFilter == null) {
- mSrcFilter = new ColorMatrix();
- mDstFilter = new ColorMatrix();
- mCurrentFilter = new ColorMatrix();
- }
-
- int defaultTextColor = mOriginalTextColor.getDefaultColor();
- Themes.setColorChangeOnMatrix(defaultTextColor, getTextColor(), mSrcFilter);
- Themes.setColorChangeOnMatrix(defaultTextColor, targetColor, mDstFilter);
-
- ValueAnimator anim1 = ValueAnimator.ofObject(
- new FloatArrayEvaluator(mCurrentFilter.getArray()),
- mSrcFilter.getArray(), mDstFilter.getArray());
- anim1.addUpdateListener((anim) -> {
- mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter));
- invalidate();
- });
-
- mCurrentColorAnim.play(anim1);
- mCurrentColorAnim.play(ObjectAnimator.ofArgb(this, TEXT_COLOR, targetColor));
- mCurrentColorAnim.start();
- }
-
@Override
public final void onDragExit(DragObject d) {
hideTooltip();
if (!d.dragComplete) {
- d.dragView.setColor(0);
- resetHoverColor();
+ d.dragView.setAlpha(1f);
+ setSelected(false);
} else {
- // Restore the hover color
- d.dragView.setColor(mHoverColor);
+ d.dragView.setAlpha(DRAG_VIEW_HOVER_OVER_OPACITY);
}
}
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
- mActive = supportsDrop(dragObject.dragInfo);
- mDrawable.setColorFilter(null);
- if (mCurrentColorAnim != null) {
- mCurrentColorAnim.cancel();
- mCurrentColorAnim = null;
- }
- setTextColor(mOriginalTextColor);
+ mActive = !options.isKeyboardDrag && supportsDrop(dragObject.dragInfo);
setVisibility(mActive ? View.VISIBLE : View.GONE);
mAccessibleDrag = options.isAccessibleDrag;
@@ -273,6 +197,7 @@
public void onDragEnd() {
mActive = false;
setOnClickListener(null);
+ setSelected(false);
}
/**
@@ -285,17 +210,23 @@
return;
}
final DragLayer dragLayer = mLauncher.getDragLayer();
+ final DragView dragView = d.dragView;
final Rect from = new Rect();
dragLayer.getViewRectRelativeToSelf(d.dragView, from);
final Rect to = getIconRect(d);
final float scale = (float) to.width() / from.width();
+ dragView.disableColorExtraction();
+ dragView.detachContentView(/* reattachToPreviousParent= */ true);
mDropTargetBar.deferOnDragEnd();
Runnable onAnimationEndRunnable = () -> {
completeDrop(d);
mDropTargetBar.onDragEnd();
mLauncher.getStateManager().goToState(NORMAL);
+ // Only re-enable updates once the workspace is back to normal, which will be after the
+ // current frame.
+ post(dragView::resumeColorExtraction);
};
dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
@@ -316,7 +247,7 @@
@Override
public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) {
super.getHitRect(outRect);
- outRect.bottom += mBottomDragPadding;
+ outRect.bottom += mLauncher.getDeviceProfile().dropTargetDragPaddingPx;
sTempCords[0] = sTempCords[1] = 0;
mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords);
@@ -366,20 +297,12 @@
mLauncher.getAccessibilityDelegate().handleAccessibleDrop(this, null, null);
}
- public int getTextColor() {
- return getTextColors().getDefaultColor();
- }
-
public void setTextVisible(boolean isVisible) {
CharSequence newText = isVisible ? mText : "";
if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) {
mTextVisible = isVisible;
setText(newText);
- if (mTextVisible) {
- setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null);
- } else {
- setCompoundDrawablesRelativeWithIntrinsicBounds(null, mDrawable, null, null);
- }
+ setCompoundDrawablesRelative(mDrawable, null, null, null);
}
}
@@ -395,6 +318,4 @@
TextUtils.TruncateAt.END);
return !mText.equals(displayedText);
}
-
- public abstract Target getDropTargetForLogging();
}
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 89d768c..a6adfc4 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -16,6 +16,9 @@
package com.android.launcher3;
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
+import static com.android.launcher3.Utilities.getBoundsForViewInDragLayer;
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
import android.animation.Animator;
@@ -28,18 +31,19 @@
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
-import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Parcelable;
import android.util.ArrayMap;
import android.util.AttributeSet;
+import android.util.FloatProperty;
import android.util.Log;
import android.util.Property;
import android.util.SparseArray;
@@ -50,15 +54,15 @@
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
import androidx.core.view.ViewCompat;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.PreviewBackground;
-import com.android.launcher3.graphics.DragPreviewProvider;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.CellAndSpan;
import com.android.launcher3.util.GridOccupancy;
@@ -66,6 +70,7 @@
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -86,6 +91,8 @@
@Thunk int mCellHeight;
private int mFixedCellWidth;
private int mFixedCellHeight;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private final int mBorderSpacing;
@ViewDebug.ExportedProperty(category = "launcher")
private int mCountX;
@@ -100,11 +107,6 @@
@Thunk final int[] mTempLocation = new int[2];
final PointF mTmpPointF = new PointF();
- // Used to visualize / debug the Grid of the CellLayout
- private static final boolean VISUALIZE_GRID = false;
- private Rect mVisualizeGridRect = new Rect();
- private Paint mVisualizeGridPaint = new Paint();
-
private GridOccupancy mOccupied;
private GridOccupancy mTmpOccupied;
@@ -126,7 +128,7 @@
// These arrays are used to implement the drag visualization on x-large screens.
// They are used as circular arrays, indexed by mDragOutlineCurrent.
- @Thunk final Rect[] mDragOutlines = new Rect[4];
+ @Thunk final CellLayout.LayoutParams[] mDragOutlines = new CellLayout.LayoutParams[4];
@Thunk final float[] mDragOutlineAlphas = new float[mDragOutlines.length];
private final InterruptibleInOutAnimator[] mDragOutlineAnims =
new InterruptibleInOutAnimator[mDragOutlines.length];
@@ -140,8 +142,21 @@
private boolean mItemPlacementDirty = false;
+ // Used to visualize the grid and drop locations
+ private boolean mVisualizeCells = false;
+ private boolean mVisualizeDropLocation = true;
+ private RectF mVisualizeGridRect = new RectF();
+ private Paint mVisualizeGridPaint = new Paint();
+ private int mGridVisualizationPadding;
+ private int mGridVisualizationRoundingRadius;
+ private float mGridAlpha = 0f;
+ private int mGridColor = 0;
+ private float mSpringLoadedProgress = 0f;
+ private float mScrollProgress = 0f;
+
// When a drag operation is in progress, holds the nearest cell to the touch point
private final int[] mDragCell = new int[2];
+ private final int[] mDragCellSpan = new int[2];
private boolean mDragging = false;
@@ -174,16 +189,33 @@
private final ArrayList<View> mIntersectingViews = new ArrayList<>();
private final Rect mOccupiedRect = new Rect();
private final int[] mDirectionVector = new int[2];
+
final int[] mPreviousReorderDirection = new int[2];
private static final int INVALID_DIRECTION = -100;
private final Rect mTempRect = new Rect();
+ private final RectF mTempRectF = new RectF();
+ private final float[] mTmpFloatArray = new float[4];
private static final Paint sPaint = new Paint();
// Related to accessible drag and drop
DragAndDropAccessibilityDelegate mTouchHelper;
+
+ public static final FloatProperty<CellLayout> SPRING_LOADED_PROGRESS =
+ new FloatProperty<CellLayout>("spring_loaded_progress") {
+ @Override
+ public Float get(CellLayout cl) {
+ return cl.getSpringLoadedProgress();
+ }
+
+ @Override
+ public void setValue(CellLayout cl, float progress) {
+ cl.setSpringLoadedProgress(progress);
+ }
+ };
+
public CellLayout(Context context) {
this(context, null);
}
@@ -203,14 +235,17 @@
setWillNotDraw(false);
setClipToPadding(false);
mActivity = ActivityContext.lookupContext(context);
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
- DeviceProfile grid = mActivity.getDeviceProfile();
+ mBorderSpacing = mContainerType == FOLDER
+ ? deviceProfile.folderCellLayoutBorderSpacingPx
+ : deviceProfile.cellLayoutBorderSpacingPx;
mCellWidth = mCellHeight = -1;
mFixedCellWidth = mFixedCellHeight = -1;
- mCountX = grid.inv.numColumns;
- mCountY = grid.inv.numRows;
+ mCountX = deviceProfile.inv.numColumns;
+ mCountY = deviceProfile.inv.numRows;
mOccupied = new GridOccupancy(mCountX, mCountY);
mTmpOccupied = new GridOccupancy(mCountX, mCountY);
@@ -221,19 +256,26 @@
mFolderLeaveBehind.mDelegateCellY = -1;
setAlwaysDrawnWithCacheEnabled(false);
- final Resources res = getResources();
- mBackground = res.getDrawable(R.drawable.bg_celllayout);
+ Resources res = getResources();
+
+ mBackground = getContext().getDrawable(R.drawable.bg_celllayout);
mBackground.setCallback(this);
mBackground.setAlpha(0);
- mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * grid.iconSizePx);
+ mGridColor = Themes.getAttrColor(getContext(), R.attr.workspaceAccentColor);
+ mGridVisualizationPadding =
+ res.getDimensionPixelSize(R.dimen.grid_visualization_cell_spacing);
+ mGridVisualizationRoundingRadius =
+ res.getDimensionPixelSize(R.dimen.grid_visualization_rounding_radius);
+ mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * deviceProfile.iconSizePx);
// Initialize the data structures used for the drag visualization.
mEaseOutInterpolator = Interpolators.DEACCEL_2_5; // Quint ease out
mDragCell[0] = mDragCell[1] = -1;
+ mDragCellSpan[0] = mDragCellSpan[1] = -1;
for (int i = 0; i < mDragOutlines.length; i++) {
- mDragOutlines[i] = new Rect(-1, -1, -1, -1);
+ mDragOutlines[i] = new CellLayout.LayoutParams(0, 0, 0, 0);
}
mDragOutlinePaint.setColor(Themes.getAttrColor(context, R.attr.workspaceTextColor));
@@ -254,39 +296,20 @@
final int thisIndex = i;
anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
- final Bitmap outline = (Bitmap)anim.getTag();
-
// If an animation is started and then stopped very quickly, we can still
// get spurious updates we've cleared the tag. Guard against this.
- if (outline == null) {
- if (LOGD) {
- Object val = animation.getAnimatedValue();
- Log.d(TAG, "anim " + thisIndex + " update: " + val +
- ", isStopped " + anim.isStopped());
- }
- // Try to prevent it from continuing to run
- animation.cancel();
- } else {
- mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue();
- CellLayout.this.invalidate(mDragOutlines[thisIndex]);
- }
+ mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue();
+ CellLayout.this.invalidate();
}
});
// The animation holds a reference to the drag outline bitmap as long is it's
// running. This way the bitmap can be GCed when the animations are complete.
- anim.getAnimator().addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- if ((Float) ((ValueAnimator) animation).getAnimatedValue() == 0f) {
- anim.setTag(null);
- }
- }
- });
mDragOutlineAnims[i] = anim;
}
mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType);
- mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+ mBorderSpacing);
addView(mShortcutsAndWidgets);
}
@@ -303,6 +326,8 @@
setImportantForAccessibility(accessibilityFlag);
getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
+ // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared.
+ setFocusable(delegate != null);
// Invalidate the accessibility hierarchy
if (getParent() != null) {
getParent().notifySubtreeAccessibilityStateChanged(
@@ -310,6 +335,13 @@
}
}
+ /**
+ * Returns the currently set accessibility delegate
+ */
+ public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() {
+ return mTouchHelper;
+ }
+
@Override
public boolean dispatchHoverEvent(MotionEvent event) {
// Always attempt to dispatch hover events to accessibility first.
@@ -321,11 +353,8 @@
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (mTouchHelper != null
- || (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) {
- return true;
- }
- return false;
+ return mTouchHelper != null
+ || (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev));
}
public void enableHardwareLayer(boolean hasLayer) {
@@ -339,7 +368,8 @@
public void setCellDimensions(int width, int height) {
mFixedCellWidth = mCellWidth = width;
mFixedCellHeight = mCellHeight = height;
- mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+ mBorderSpacing);
}
public void setGridSize(int x, int y) {
@@ -348,7 +378,8 @@
mOccupied = new GridOccupancy(mCountX, mCountY);
mTmpOccupied = new GridOccupancy(mCountX, mCountY);
mTempRectStack.clear();
- mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+ mBorderSpacing);
requestLayout();
}
@@ -411,16 +442,6 @@
mBackground.draw(canvas);
}
- final Paint paint = mDragOutlinePaint;
- for (int i = 0; i < mDragOutlines.length; i++) {
- final float alpha = mDragOutlineAlphas[i];
- if (alpha > 0) {
- final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag();
- paint.setAlpha((int)(alpha + .5f));
- canvas.drawBitmap(b, null, mDragOutlines[i], paint);
- }
- }
-
if (DEBUG_VISUALIZE_OCCUPIED) {
int[] pt = new int[2];
ColorDrawable cd = new ColorDrawable(Color.RED);
@@ -456,34 +477,113 @@
canvas.restore();
}
- if (VISUALIZE_GRID) {
+ if (mVisualizeCells || mVisualizeDropLocation) {
visualizeGrid(canvas);
}
}
+ /**
+ * Indicates the progress of the Workspace entering the SpringLoaded state; allows the
+ * CellLayout to update various visuals for this state.
+ *
+ * @param progress
+ */
+ public void setSpringLoadedProgress(float progress) {
+ if (Float.compare(progress, mSpringLoadedProgress) != 0) {
+ mSpringLoadedProgress = progress;
+ updateBgAlpha();
+ setGridAlpha(progress);
+ }
+ }
+
+ /**
+ * See setSpringLoadedProgress
+ * @return progress
+ */
+ public float getSpringLoadedProgress() {
+ return mSpringLoadedProgress;
+ }
+
+ private void updateBgAlpha() {
+ mBackground.setAlpha((int) (mSpringLoadedProgress * 255));
+ }
+
+ /**
+ * Set the progress of this page's scroll
+ *
+ * @param progress 0 if the screen is centered, +/-1 if it is to the right / left respectively
+ */
+ public void setScrollProgress(float progress) {
+ if (Float.compare(Math.abs(progress), mScrollProgress) != 0) {
+ mScrollProgress = Math.abs(progress);
+ updateBgAlpha();
+ }
+ }
+
+ private void setGridAlpha(float gridAlpha) {
+ if (Float.compare(gridAlpha, mGridAlpha) != 0) {
+ mGridAlpha = gridAlpha;
+ invalidate();
+ }
+ }
+
protected void visualizeGrid(Canvas canvas) {
- mVisualizeGridRect.set(0, 0, mCellWidth, mCellHeight);
- mVisualizeGridPaint.setStrokeWidth(4);
+ DeviceProfile dp = mActivity.getDeviceProfile();
+ int paddingX = (int) Math.min((mCellWidth - dp.iconSizePx) / 2, mGridVisualizationPadding);
+ int paddingY = (int) Math.min((mCellHeight - dp.iconSizePx) / 2, mGridVisualizationPadding);
+ mVisualizeGridRect.set(paddingX, paddingY,
+ mCellWidth - paddingX,
+ mCellHeight - paddingY);
- for (int i = 0; i < mCountX; i++) {
- for (int j = 0; j < mCountY; j++) {
- canvas.save();
+ mVisualizeGridPaint.setStrokeWidth(8);
+ int paintAlpha = (int) (120 * mGridAlpha);
+ mVisualizeGridPaint.setColor(ColorUtils.setAlphaComponent(mGridColor, paintAlpha));
- int transX = i * mCellWidth;
- int transY = j * mCellHeight;
+ if (mVisualizeCells) {
+ for (int i = 0; i < mCountX; i++) {
+ for (int j = 0; j < mCountY; j++) {
+ int transX = i * mCellWidth + (i * mBorderSpacing) + getPaddingLeft()
+ + paddingX;
+ int transY = j * mCellHeight + (j * mBorderSpacing) + getPaddingTop()
+ + paddingY;
- canvas.translate(getPaddingLeft() + transX, getPaddingTop() + transY);
+ mVisualizeGridRect.offsetTo(transX, transY);
+ mVisualizeGridPaint.setStyle(Paint.Style.FILL);
+ canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius,
+ mGridVisualizationRoundingRadius, mVisualizeGridPaint);
+ }
+ }
+ }
- mVisualizeGridPaint.setStyle(Paint.Style.FILL);
- mVisualizeGridPaint.setColor(Color.argb(80, 255, 100, 100));
+ if (mVisualizeDropLocation) {
+ for (int i = 0; i < mDragOutlines.length; i++) {
+ final float alpha = mDragOutlineAlphas[i];
+ if (alpha <= 0) continue;
- canvas.drawRect(mVisualizeGridRect, mVisualizeGridPaint);
+ mVisualizeGridPaint.setAlpha(255);
+ int x = mDragOutlines[i].cellX;
+ int y = mDragOutlines[i].cellY;
+ int spanX = mDragOutlines[i].cellHSpan;
+ int spanY = mDragOutlines[i].cellVSpan;
+
+ // TODO b/194414754 clean this up, reconcile with cellToRect
+ mVisualizeGridRect.set(paddingX, paddingY,
+ mCellWidth * spanX + mBorderSpacing * (spanX - 1) - paddingX,
+ mCellHeight * spanY + mBorderSpacing * (spanY - 1) - paddingY);
+
+ int transX = x * mCellWidth + (x * mBorderSpacing)
+ + getPaddingLeft() + paddingX;
+ int transY = y * mCellHeight + (y * mBorderSpacing)
+ + getPaddingTop() + paddingY;
+
+ mVisualizeGridRect.offsetTo(transX, transY);
mVisualizeGridPaint.setStyle(Paint.Style.STROKE);
- mVisualizeGridPaint.setColor(Color.argb(255, 255, 100, 100));
+ mVisualizeGridPaint.setColor(Color.argb((int) (alpha),
+ Color.red(mGridColor), Color.green(mGridColor), Color.blue(mGridColor)));
- canvas.drawRect(mVisualizeGridRect, mVisualizeGridPaint);
- canvas.restore();
+ canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius,
+ mGridVisualizationRoundingRadius, mVisualizeGridPaint);
}
}
}
@@ -700,11 +800,9 @@
* @param result Array of 2 ints to hold the x and y coordinate of the point
*/
void cellToPoint(int cellX, int cellY, int[] result) {
- final int hStartPadding = getPaddingLeft();
- final int vStartPadding = getPaddingTop();
-
- result[0] = hStartPadding + cellX * mCellWidth;
- result[1] = vStartPadding + cellY * mCellHeight;
+ cellToRect(cellX, cellY, 1, 1, mTempRect);
+ result[0] = mTempRect.left;
+ result[1] = mTempRect.top;
}
/**
@@ -728,25 +826,9 @@
* @param result Array of 2 ints to hold the x and y coordinate of the point
*/
void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
- final int hStartPadding = getPaddingLeft();
- final int vStartPadding = getPaddingTop();
- result[0] = hStartPadding + cellX * mCellWidth + (spanX * mCellWidth) / 2;
- result[1] = vStartPadding + cellY * mCellHeight + (spanY * mCellHeight) / 2;
- }
-
- /**
- * Given a cell coordinate and span fills out a corresponding pixel rect
- *
- * @param cellX X coordinate of the cell
- * @param cellY Y coordinate of the cell
- * @param result Rect in which to write the result
- */
- void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) {
- final int hStartPadding = getPaddingLeft();
- final int vStartPadding = getPaddingTop();
- final int left = hStartPadding + cellX * mCellWidth;
- final int top = vStartPadding + cellY * mCellHeight;
- result.set(left, top, left + (spanX * mCellWidth), top + (spanY * mCellHeight));
+ cellToRect(cellX, cellY, spanX, spanY, mTempRect);
+ result[0] = mTempRect.centerX();
+ result[1] = mTempRect.centerY();
}
public float getDistanceFromCell(float x, float y, int[] cell) {
@@ -777,12 +859,15 @@
int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
if (mFixedCellWidth < 0 || mFixedCellHeight < 0) {
- int cw = DeviceProfile.calculateCellWidth(childWidthSize, mCountX);
- int ch = DeviceProfile.calculateCellHeight(childHeightSize, mCountY);
+ int cw = DeviceProfile.calculateCellWidth(childWidthSize, mBorderSpacing,
+ mCountX);
+ int ch = DeviceProfile.calculateCellHeight(childHeightSize, mBorderSpacing,
+ mCountY);
if (cw != mCellWidth || ch != mCellHeight) {
mCellWidth = cw;
mCellHeight = ch;
- mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+ mBorderSpacing);
}
}
@@ -832,14 +917,11 @@
/**
* Returns the amount of space left over after subtracting padding and cells. This space will be
* very small, a few pixels at most, and is a result of rounding down when calculating the cell
- * width in {@link DeviceProfile#calculateCellWidth(int, int)}.
+ * width in {@link DeviceProfile#calculateCellWidth(int, int, int)}.
*/
public int getUnusedHorizontalSpace() {
- return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth);
- }
-
- public Drawable getScrimBackground() {
- return mBackground;
+ return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth)
+ - ((mCountX - 1) * mBorderSpacing);
}
@Override
@@ -851,8 +933,8 @@
return mShortcutsAndWidgets;
}
- public View getChildAt(int x, int y) {
- return mShortcutsAndWidgets.getChildAt(x, y);
+ public View getChildAt(int cellX, int cellY) {
+ return mShortcutsAndWidgets.getChildAt(cellX, cellY);
}
public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
@@ -951,58 +1033,56 @@
return false;
}
- void visualizeDropLocation(DraggableView v, DragPreviewProvider outlineProvider, int cellX, int
- cellY, int spanX, int spanY, boolean resize, DropTarget.DragObject dragObject) {
- final int oldDragCellX = mDragCell[0];
- final int oldDragCellY = mDragCell[1];
-
- if (outlineProvider == null || outlineProvider.generatedDragOutline == null) {
- return;
- }
-
- Bitmap dragOutline = outlineProvider.generatedDragOutline;
- if (cellX != oldDragCellX || cellY != oldDragCellY) {
+ void visualizeDropLocation(int cellX, int cellY, int spanX, int spanY,
+ DropTarget.DragObject dragObject) {
+ if (mDragCell[0] != cellX || mDragCell[1] != cellY || mDragCellSpan[0] != spanX
+ || mDragCellSpan[1] != spanY) {
mDragCell[0] = cellX;
mDragCell[1] = cellY;
+ mDragCellSpan[0] = spanX;
+ mDragCellSpan[1] = spanY;
+
+ // Apply color extraction on a widget when dragging.
+ applyColorExtractionOnWidget(dragObject, mDragCell, spanX, spanY);
final int oldIndex = mDragOutlineCurrent;
mDragOutlineAnims[oldIndex].animateOut();
mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
- Rect r = mDragOutlines[mDragOutlineCurrent];
- cellToRect(cellX, cellY, spanX, spanY, r);
- int left = r.left;
- int top = r.top;
+ LayoutParams cell = mDragOutlines[mDragOutlineCurrent];
+ cell.cellX = cellX;
+ cell.cellY = cellY;
+ cell.cellHSpan = spanX;
+ cell.cellVSpan = spanY;
- int width = dragOutline.getWidth();
- int height = dragOutline.getHeight();
-
- if (resize) {
- width = r.width();
- height = r.height();
- }
-
- // Center horizontaly
- left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
-
- if (v != null && v.getViewType() == DraggableView.DRAGGABLE_WIDGET) {
- // Center vertically
- top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
- } else if (v != null && v.getViewType() == DraggableView.DRAGGABLE_ICON) {
- int cHeight = getShortcutsAndWidgets().getCellContentHeight();
- int cellPaddingY = (int) Math.max(0, ((mCellHeight - cHeight) / 2f));
- top += cellPaddingY;
- }
-
- r.set(left, top, left + width, top + height);
-
- Utilities.scaleRectAboutCenter(r, mChildScale);
- mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline);
mDragOutlineAnims[mDragOutlineCurrent].animateIn();
+ invalidate();
if (dragObject.stateAnnouncer != null) {
dragObject.stateAnnouncer.announce(getItemMoveDescription(cellX, cellY));
}
+
+ }
+ }
+
+ /** Applies the local color extraction to a dragging widget object. */
+ private void applyColorExtractionOnWidget(DropTarget.DragObject dragObject, int[] targetCell,
+ int spanX, int spanY) {
+ // Apply local extracted color if the DragView is an AppWidgetHostViewDrawable.
+ View view = dragObject.dragView.getContentView();
+ if (view instanceof LauncherAppWidgetHostView) {
+ Launcher launcher = Launcher.getLauncher(dragObject.dragView.getContext());
+ Workspace workspace = launcher.getWorkspace();
+ int screenId = workspace.getIdForScreen(this);
+ int pageId = workspace.getPageIndexForScreenId(screenId);
+ cellToRect(targetCell[0], targetCell[1], spanX, spanY, mTempRect);
+
+ // Now get the rect in drag layer coordinates.
+ getBoundsForViewInDragLayer(launcher.getDragLayer(), this, mTempRect, true,
+ mTmpFloatArray, mTempRectF);
+ Utilities.setRect(mTempRectF, mTempRect);
+
+ ((LauncherAppWidgetHostView) view).handleDrag(mTempRect, pageId);
}
}
@@ -2009,7 +2089,7 @@
// Animations are disabled in power save mode, causing the repeated animation to jump
// spastically between beginning and end states. Since this looks bad, we don't repeat
// the animation in power save mode.
- if (Utilities.areAnimationsEnabled(getContext())) {
+ if (areAnimatorsEnabled()) {
va.setRepeatMode(ValueAnimator.REVERSE);
va.setRepeatCount(ValueAnimator.INFINITE);
}
@@ -2147,7 +2227,7 @@
findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
Rect dragRect = new Rect();
- regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
+ cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
Rect dropRegionRect = new Rect();
@@ -2157,7 +2237,7 @@
int dropRegionSpanX = dropRegionRect.width();
int dropRegionSpanY = dropRegionRect.height();
- regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
+ cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
dropRegionRect.height(), dropRegionRect);
int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
@@ -2477,6 +2557,7 @@
// Invalidate the drag data
mDragCell[0] = mDragCell[1] = -1;
+ mDragCellSpan[0] = mDragCellSpan[1] = -1;
mDragOutlineAnims[mDragOutlineCurrent].animateOut();
mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length;
revertTempState();
@@ -2512,13 +2593,16 @@
final int cellWidth = mCellWidth;
final int cellHeight = mCellHeight;
- final int hStartPadding = getPaddingLeft();
+ // We observe a shift of 1 pixel on the x coordinate compared to the actual cell coordinates
+ final int hStartPadding = getPaddingLeft()
+ + (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
final int vStartPadding = getPaddingTop();
- int width = cellHSpan * cellWidth;
- int height = cellVSpan * cellHeight;
- int x = hStartPadding + cellX * cellWidth;
- int y = vStartPadding + cellY * cellHeight;
+ int x = hStartPadding + (cellX * mBorderSpacing) + (cellX * cellWidth);
+ int y = vStartPadding + (cellY * mBorderSpacing) + (cellY * cellHeight);
+
+ int width = cellHSpan * cellWidth + ((cellHSpan - 1) * mBorderSpacing);
+ int height = cellVSpan * cellHeight + ((cellVSpan - 1) * mBorderSpacing);
resultRect.set(x, y, x + width, y + height);
}
@@ -2536,11 +2620,13 @@
}
public int getDesiredWidth() {
- return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth);
+ return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth)
+ + ((mCountX - 1) * mBorderSpacing);
}
public int getDesiredHeight() {
- return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight);
+ return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight)
+ + ((mCountY - 1) * mBorderSpacing);
}
public boolean isOccupied(int x, int y) {
@@ -2655,19 +2741,22 @@
this.cellVSpan = cellVSpan;
}
- public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount) {
- setup(cellWidth, cellHeight, invertHorizontally, colCount, 1.0f, 1.0f);
+ public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
+ int rowCount, int borderSpacing, @Nullable Rect inset) {
+ setup(cellWidth, cellHeight, invertHorizontally, colCount, rowCount, 1.0f, 1.0f,
+ borderSpacing, inset);
}
/**
- * Use this method, as opposed to {@link #setup(int, int, boolean, int)}, if the view needs
- * to be scaled.
+ * Use this method, as opposed to {@link #setup(int, int, boolean, int, int, int, Rect)},
+ * if the view needs to be scaled.
*
* ie. In multi-window mode, we setup widgets so that they are measured and laid out
* using their full/invariant device profile sizes.
*/
public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
- float cellScaleX, float cellScaleY) {
+ int rowCount, float cellScaleX, float cellScaleY, int borderSpacing,
+ @Nullable Rect inset) {
if (isLockedToGrid) {
final int myCellHSpan = cellHSpan;
final int myCellVSpan = cellVSpan;
@@ -2678,17 +2767,30 @@
myCellX = colCount - myCellX - cellHSpan;
}
- width = (int) (myCellHSpan * cellWidth / cellScaleX - leftMargin - rightMargin);
- height = (int) (myCellVSpan * cellHeight / cellScaleY - topMargin - bottomMargin);
- x = (myCellX * cellWidth + leftMargin);
- y = (myCellY * cellHeight + topMargin);
+ int hBorderSpacing = (myCellHSpan - 1) * borderSpacing;
+ int vBorderSpacing = (myCellVSpan - 1) * borderSpacing;
+
+ float myCellWidth = ((myCellHSpan * cellWidth) + hBorderSpacing) / cellScaleX;
+ float myCellHeight = ((myCellVSpan * cellHeight) + vBorderSpacing) / cellScaleY;
+
+ width = Math.round(myCellWidth) - leftMargin - rightMargin;
+ height = Math.round(myCellHeight) - topMargin - bottomMargin;
+ x = leftMargin + (myCellX * cellWidth) + (myCellX * borderSpacing);
+ y = topMargin + (myCellY * cellHeight) + (myCellY * borderSpacing);
+
+ if (inset != null) {
+ x -= inset.left;
+ y -= inset.top;
+ width += inset.left + inset.right;
+ height += inset.top + inset.bottom;
+ }
}
}
/**
* Sets the position to the provided point
*/
- public void setXY(Point point) {
+ public void setCellXY(Point point) {
cellX = point.x;
cellY = point.y;
}
diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java
index ff405ec..c707df0 100644
--- a/src/com/android/launcher3/CheckLongPressHelper.java
+++ b/src/com/android/launcher3/CheckLongPressHelper.java
@@ -115,7 +115,7 @@
private void triggerLongPress() {
if ((mView.getParent() != null)
&& mView.hasWindowFocus()
- && (!mView.isPressed() || mListener == null)
+ && (!mView.isPressed() || mListener != null)
&& !mHasPerformedLongPress) {
boolean handled;
if (mListener != null) {
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 2857497..80ec192 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -18,8 +18,7 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_CANCEL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_REMOVE;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.UNDO;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNDO;
import android.content.Context;
import android.text.TextUtils;
@@ -28,22 +27,19 @@
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.logging.LoggerUtils;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.ModelWriter;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.views.Snackbar;
public class DeleteDropTarget extends ButtonDropTarget {
private final StatsLogManager mStatsLogManager;
- private int mControlType = ControlType.DEFAULT_CONTROLTYPE;
+ private StatsLogManager.LauncherEvent mLauncherEvent;
public DeleteDropTarget(Context context, AttributeSet attrs) {
this(context, attrs, 0);
@@ -57,10 +53,7 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- // Get the hover color
- mHoverColor = getResources().getColor(R.color.delete_target_hover_tint);
-
- setDrawable(R.drawable.ic_remove_shadow);
+ setDrawable(R.drawable.ic_remove_no_shadow);
}
@Override
@@ -115,8 +108,8 @@
* Set mControlType depending on the drag item.
*/
private void setControlTypeBasedOnDragSource(ItemInfo item) {
- mControlType = item.id != ItemInfo.NO_ID ? ControlType.REMOVE_TARGET
- : ControlType.CANCEL_TARGET;
+ mLauncherEvent = item.id != ItemInfo.NO_ID ? LAUNCHER_ITEM_DROPPED_ON_REMOVE
+ : LAUNCHER_ITEM_DROPPED_ON_CANCEL;
}
@Override
@@ -127,8 +120,7 @@
}
super.onDrop(d, options);
mStatsLogManager.logger().withInstanceId(d.logInstanceId)
- .log(mControlType == ControlType.REMOVE_TARGET ? LAUNCHER_ITEM_DROPPED_ON_REMOVE
- : LAUNCHER_ITEM_DROPPED_ON_CANCEL);
+ .log(mLauncherEvent);
}
@Override
@@ -141,7 +133,7 @@
Runnable onUndoClicked = () -> {
mLauncher.setPageToBindSynchronously(itemPage);
modelWriter.abortDelete();
- mLauncher.getUserEventDispatcher().logActionOnControl(TAP, UNDO);
+ mLauncher.getStatsLogManager().logger().log(LAUNCHER_UNDO);
};
Snackbar.show(mLauncher, R.string.item_removed, R.string.undo,
modelWriter::commitDelete, onUndoClicked);
@@ -161,11 +153,4 @@
mLauncher.getDragLayer()
.announceForAccessibility(getContext().getString(R.string.item_removed));
}
-
- @Override
- public Target getDropTargetForLogging() {
- Target t = LoggerUtils.newTarget(Target.Type.CONTROL);
- t.controlType = mControlType;
- return t;
- }
}
diff --git a/src/com/android/launcher3/DevicePaddings.java b/src/com/android/launcher3/DevicePaddings.java
new file mode 100644
index 0000000..7c387b1
--- /dev/null
+++ b/src/com/android/launcher3/DevicePaddings.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Workspace items have a fixed height, so we need a way to distribute any unused workspace height.
+ *
+ * The unused or "extra" height is allocated to three different variable heights:
+ * - The space above the workspace
+ * - The space between the workspace and hotseat
+ * - The espace below the hotseat
+ */
+public class DevicePaddings {
+
+ private static final String DEVICE_PADDING = "device-paddings";
+ private static final String DEVICE_PADDINGS = "device-padding";
+
+ private static final String WORKSPACE_TOP_PADDING = "workspaceTopPadding";
+ private static final String WORKSPACE_BOTTOM_PADDING = "workspaceBottomPadding";
+ private static final String HOTSEAT_BOTTOM_PADDING = "hotseatBottomPadding";
+
+ private static final String TAG = DevicePaddings.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ ArrayList<DevicePadding> mDevicePaddings = new ArrayList<>();
+
+ public DevicePaddings(Context context, int devicePaddingId) {
+ try (XmlResourceParser parser = context.getResources().getXml(devicePaddingId)) {
+ final int depth = parser.getDepth();
+ int type;
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+ if ((type == XmlPullParser.START_TAG) && DEVICE_PADDING.equals(parser.getName())) {
+ final int displayDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > displayDepth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ if ((type == XmlPullParser.START_TAG)
+ && DEVICE_PADDINGS.equals(parser.getName())) {
+ TypedArray a = context.obtainStyledAttributes(
+ Xml.asAttributeSet(parser), R.styleable.DevicePadding);
+ int maxWidthPx = a.getDimensionPixelSize(
+ R.styleable.DevicePadding_maxEmptySpace, 0);
+ a.recycle();
+
+ PaddingFormula workspaceTopPadding = null;
+ PaddingFormula workspaceBottomPadding = null;
+ PaddingFormula hotseatBottomPadding = null;
+
+ final int limitDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > limitDepth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ AttributeSet attr = Xml.asAttributeSet(parser);
+ if ((type == XmlPullParser.START_TAG)) {
+ if (WORKSPACE_TOP_PADDING.equals(parser.getName())) {
+ workspaceTopPadding = new PaddingFormula(context, attr);
+ } else if (WORKSPACE_BOTTOM_PADDING.equals(parser.getName())) {
+ workspaceBottomPadding = new PaddingFormula(context, attr);
+ } else if (HOTSEAT_BOTTOM_PADDING.equals(parser.getName())) {
+ hotseatBottomPadding = new PaddingFormula(context, attr);
+ }
+ }
+ }
+
+ if (workspaceTopPadding == null
+ || workspaceBottomPadding == null
+ || hotseatBottomPadding == null) {
+ if (Utilities.IS_DEBUG_DEVICE) {
+ throw new RuntimeException("DevicePadding missing padding.");
+ }
+ }
+
+ DevicePadding dp = new DevicePadding(maxWidthPx, workspaceTopPadding,
+ workspaceBottomPadding, hotseatBottomPadding);
+ if (dp.isValid()) {
+ mDevicePaddings.add(dp);
+ } else {
+ Log.e(TAG, "Invalid device padding found.");
+ if (Utilities.IS_DEBUG_DEVICE) {
+ throw new RuntimeException("DevicePadding is invalid");
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ Log.e(TAG, "Failure parsing device padding layout.", e);
+ throw new RuntimeException(e);
+ }
+
+ // Sort ascending by maxEmptySpacePx
+ mDevicePaddings.sort((sl1, sl2) -> Integer.compare(sl1.maxEmptySpacePx,
+ sl2.maxEmptySpacePx));
+ }
+
+ public DevicePadding getDevicePadding(int extraSpacePx) {
+ for (DevicePadding limit : mDevicePaddings) {
+ if (extraSpacePx <= limit.maxEmptySpacePx) {
+ return limit;
+ }
+ }
+
+ return mDevicePaddings.get(mDevicePaddings.size() - 1);
+ }
+
+ /**
+ * Holds all the formulas to calculate the padding for a particular device based on the
+ * amount of extra space.
+ */
+ public static final class DevicePadding {
+
+ // One for each padding since they can each be off by 1 due to rounding errors.
+ private static final int ROUNDING_THRESHOLD_PX = 3;
+
+ private final int maxEmptySpacePx;
+ private final PaddingFormula workspaceTopPadding;
+ private final PaddingFormula workspaceBottomPadding;
+ private final PaddingFormula hotseatBottomPadding;
+
+ public DevicePadding(int maxEmptySpacePx,
+ PaddingFormula workspaceTopPadding,
+ PaddingFormula workspaceBottomPadding,
+ PaddingFormula hotseatBottomPadding) {
+ this.maxEmptySpacePx = maxEmptySpacePx;
+ this.workspaceTopPadding = workspaceTopPadding;
+ this.workspaceBottomPadding = workspaceBottomPadding;
+ this.hotseatBottomPadding = hotseatBottomPadding;
+ }
+
+ public int getMaxEmptySpacePx() {
+ return maxEmptySpacePx;
+ }
+
+ public int getWorkspaceTopPadding(int extraSpacePx) {
+ return workspaceTopPadding.calculate(extraSpacePx);
+ }
+
+ public int getWorkspaceBottomPadding(int extraSpacePx) {
+ return workspaceBottomPadding.calculate(extraSpacePx);
+ }
+
+ public int getHotseatBottomPadding(int extraSpacePx) {
+ return hotseatBottomPadding.calculate(extraSpacePx);
+ }
+
+ public boolean isValid() {
+ int workspaceTopPadding = getWorkspaceTopPadding(maxEmptySpacePx);
+ int workspaceBottomPadding = getWorkspaceBottomPadding(maxEmptySpacePx);
+ int hotseatBottomPadding = getHotseatBottomPadding(maxEmptySpacePx);
+ int sum = workspaceTopPadding + workspaceBottomPadding + hotseatBottomPadding;
+ int diff = Math.abs(sum - maxEmptySpacePx);
+ if (DEBUG) {
+ Log.d(TAG, "isValid: workspaceTopPadding=" + workspaceTopPadding
+ + ", workspaceBottomPadding=" + workspaceBottomPadding
+ + ", hotseatBottomPadding=" + hotseatBottomPadding
+ + ", sum=" + sum
+ + ", diff=" + diff);
+ }
+ return diff <= ROUNDING_THRESHOLD_PX;
+ }
+ }
+
+ /**
+ * Used to calculate a padding based on three variables: a, b, and c.
+ *
+ * Calculation: a * (extraSpace - c) + b
+ */
+ private static final class PaddingFormula {
+
+ private final float a;
+ private final float b;
+ private final float c;
+
+ public PaddingFormula(Context context, AttributeSet attrs) {
+ TypedArray t = context.obtainStyledAttributes(attrs,
+ R.styleable.DevicePaddingFormula);
+
+ a = getValue(t, R.styleable.DevicePaddingFormula_a);
+ b = getValue(t, R.styleable.DevicePaddingFormula_b);
+ c = getValue(t, R.styleable.DevicePaddingFormula_c);
+
+ t.recycle();
+ }
+
+ public int calculate(int extraSpacePx) {
+ if (DEBUG) {
+ Log.d(TAG, "a=" + a + " * (" + extraSpacePx + " - " + c + ") + b=" + b);
+ }
+ return Math.round(a * (extraSpacePx - c) + b);
+ }
+
+ private static float getValue(TypedArray a, int index) {
+ if (a.getType(index) == TypedValue.TYPE_DIMENSION) {
+ return a.getDimensionPixelSize(index, 0);
+ } else if (a.getType(index) == TypedValue.TYPE_FLOAT) {
+ return a.getFloat(index, 0);
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "a=" + a + ", b=" + b + ", c=" + c;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 49caa93..81eda10 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -16,35 +16,56 @@
package com.android.launcher3;
+import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import static com.android.launcher3.ResourceUtils.pxFromDp;
+import static com.android.launcher3.Utilities.dpiFromPx;
+import static com.android.launcher3.Utilities.pxFromSp;
+import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
+import static com.android.launcher3.util.WindowManagerCompat.MIN_TABLET_WIDTH;
+
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.util.DisplayMetrics;
import android.view.Surface;
+import android.view.WindowInsets;
+import android.view.WindowManager;
import com.android.launcher3.CellLayout.ContainerType;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.DevicePaddings.DevicePadding;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.DotRenderer;
+import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.WindowBounds;
+import java.io.PrintWriter;
+
+@SuppressLint("NewApi")
public class DeviceProfile {
- private static final float TABLET_MIN_DPS = 600;
- private static final float LARGE_TABLET_MIN_DPS = 720;
-
+ private static final int DEFAULT_DOT_SIZE = 100;
public final InvariantDeviceProfile inv;
- private final DefaultDisplay.Info mInfo;
+ private final Info mInfo;
+ private final DisplayMetrics mMetrics;
// Device properties
public final boolean isTablet;
- public final boolean isLargeTablet;
public final boolean isPhone;
public final boolean transposeLayoutWithOrientation;
+ public final boolean isTwoPanels;
+ public final boolean allowRotation;
// Device properties in current orientation
public final boolean isLandscape;
@@ -59,6 +80,8 @@
public final float aspectRatio;
+ public final boolean isScalableGrid;
+
/**
* The maximum amount of left/right workspace padding as a percentage of the screen width.
* To be clear, this means that up to 7% of the screen width can be used as left padding, and
@@ -67,36 +90,58 @@
private static final float MAX_HORIZONTAL_PADDING_PERCENT = 0.14f;
private static final float TALL_DEVICE_ASPECT_RATIO_THRESHOLD = 2.0f;
+ private static final float TALLER_DEVICE_ASPECT_RATIO_THRESHOLD = 2.15f;
+ private static final float TALL_DEVICE_EXTRA_SPACE_THRESHOLD_DP = 252;
+ private static final float TALL_DEVICE_MORE_EXTRA_SPACE_THRESHOLD_DP = 268;
// To evenly space the icons, increase the left/right margins for tablets in portrait mode.
private static final int PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER = 4;
// Workspace
- public final int desiredWorkspaceLeftRightMarginPx;
+ public final int desiredWorkspaceLeftRightOriginalPx;
+ public int desiredWorkspaceLeftRightMarginPx;
+ public final int cellLayoutBorderSpacingOriginalPx;
+ public int cellLayoutBorderSpacingPx;
public final int cellLayoutPaddingLeftRightPx;
public final int cellLayoutBottomPaddingPx;
public final int edgeMarginPx;
public float workspaceSpringLoadShrinkFactor;
public final int workspaceSpringLoadedBottomSpace;
+ private final int extraSpace;
+ public int workspaceTopPadding;
+ public int workspaceBottomPadding;
+ public int extraHotseatBottomPadding;
+
// Workspace page indicator
public final int workspacePageIndicatorHeight;
private final int mWorkspacePageIndicatorOverlapWorkspace;
// Workspace icons
+ public float iconScale;
public int iconSizePx;
public int iconTextSizePx;
public int iconDrawablePaddingPx;
public int iconDrawablePaddingOriginalPx;
+ public float cellScaleToFit;
public int cellWidthPx;
public int cellHeightPx;
public int workspaceCellPaddingXPx;
+ public int cellYPaddingPx;
+
// Folder
+ public float folderLabelTextScale;
+ public int folderLabelTextSizePx;
public int folderIconSizePx;
public int folderIconOffsetYPx;
+ // Folder content
+ public int folderCellLayoutBorderSpacingPx;
+ public int folderContentPaddingLeftRight;
+ public int folderContentPaddingTop;
+
// Folder cell
public int folderCellWidthPx;
public int folderCellHeightPx;
@@ -107,27 +152,44 @@
public int folderChildDrawablePaddingPx;
// Hotseat
+ public int hotseatBarSizeExtraSpacePx;
+ public final int numShownHotseatIcons;
public int hotseatCellHeightPx;
+ private final int hotseatExtraVerticalSize;
// In portrait: size = height, in landscape: size = width
public int hotseatBarSizePx;
- public final int hotseatBarTopPaddingPx;
- public int hotseatBarBottomPaddingPx;
+ public int hotseatBarTopPaddingPx;
+ public final int hotseatBarBottomPaddingPx;
// Start is the side next to the nav bar, end is the side next to the workspace
public final int hotseatBarSidePaddingStartPx;
public final int hotseatBarSidePaddingEndPx;
+ public final float qsbBottomMarginOriginalPx;
+ public int qsbBottomMarginPx;
+
// All apps
+ public int allAppsOpenVerticalTranslate;
public int allAppsCellHeightPx;
public int allAppsCellWidthPx;
public int allAppsIconSizePx;
public int allAppsIconDrawablePaddingPx;
+ public final int numShownAllAppsColumns;
public float allAppsIconTextSizePx;
+ // Overview
+ public int overviewTaskMarginPx;
+ public int overviewTaskIconSizePx;
+ public int overviewTaskThumbnailTopMarginPx;
+ public final int overviewActionsMarginThreeButtonPx;
+ public final int overviewActionsMarginGesturePx;
+
// Widgets
public final PointF appWidgetScale = new PointF(1.0f, 1.0f);
// Drop Target
public int dropTargetBarSizePx;
+ public int dropTargetDragPaddingPx;
+ public int dropTargetTextSizePx;
// Insets
private final Rect mInsets = new Rect();
@@ -140,54 +202,103 @@
public DotRenderer mDotRendererWorkSpace;
public DotRenderer mDotRendererAllApps;
- DeviceProfile(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info,
- Point minSize, Point maxSize, int width, int height, boolean isLandscape,
+ // Taskbar
+ public boolean isTaskbarPresent;
+ public int taskbarSize;
+ // How much of the bottom inset is due to Taskbar rather than other system elements.
+ public int nonOverlappingTaskbarInset;
+
+ // DragController
+ public int flingToDeleteThresholdVelocity;
+
+ DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds,
boolean isMultiWindowMode, boolean transposeLayoutWithOrientation,
- Point windowPosition) {
+ boolean useTwoPanels) {
this.inv = inv;
- this.isLandscape = isLandscape;
+ this.isLandscape = windowBounds.isLandscape();
this.isMultiWindowMode = isMultiWindowMode;
- windowX = windowPosition.x;
- windowY = windowPosition.y;
+ this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
+ windowX = windowBounds.bounds.left;
+ windowY = windowBounds.bounds.top;
+
+ isScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
// Determine sizes.
- widthPx = width;
- heightPx = height;
- if (isLandscape) {
- availableWidthPx = maxSize.x;
- availableHeightPx = minSize.y;
- } else {
- availableWidthPx = minSize.x;
- availableHeightPx = maxSize.y;
- }
+ widthPx = windowBounds.bounds.width();
+ heightPx = windowBounds.bounds.height();
+ availableWidthPx = windowBounds.availableSize.x;
+ int nonFinalAvailableHeightPx = windowBounds.availableSize.y;
mInfo = info;
+ // If the device's pixel density was scaled (usually via settings for A11y), use the
+ // original dimensions to determine if rotation is allowed of not.
+ float originalSmallestWidth = dpiFromPx(Math.min(widthPx, heightPx), DENSITY_DEVICE_STABLE);
+ allowRotation = originalSmallestWidth >= MIN_TABLET_WIDTH;
+ // Tablet UI does not support emulated landscape.
+ isTablet = allowRotation && info.isTablet(windowBounds);
+ isPhone = !isTablet;
+ isTwoPanels = isTablet && useTwoPanels;
- // Constants from resources
- float swDPs = Utilities.dpiFromPx(
- Math.min(info.smallestSize.x, info.smallestSize.y), info.metrics);
- isTablet = swDPs >= TABLET_MIN_DPS;
- isLargeTablet = swDPs >= LARGE_TABLET_MIN_DPS;
- isPhone = !isTablet && !isLargeTablet;
aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
// Some more constants
- this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
-
context = getContext(context, info, isVerticalBarLayout()
? Configuration.ORIENTATION_LANDSCAPE
: Configuration.ORIENTATION_PORTRAIT);
+ mMetrics = context.getResources().getDisplayMetrics();
final Resources res = context.getResources();
+ isTaskbarPresent = isTablet && FeatureFlags.ENABLE_TASKBAR.get();
+ if (isTaskbarPresent) {
+ // Taskbar will be added later, but provides bottom insets that we should subtract
+ // from availableHeightPx.
+ taskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_size);
+ WindowInsets windowInsets =
+ context.createWindowContext(
+ context.getSystemService(DisplayManager.class).getDisplay(mInfo.id),
+ TYPE_APPLICATION, null)
+ .getSystemService(WindowManager.class)
+ .getCurrentWindowMetrics().getWindowInsets();
+ nonOverlappingTaskbarInset = taskbarSize - windowInsets.getSystemWindowInsetBottom();
+ if (nonOverlappingTaskbarInset > 0) {
+ nonFinalAvailableHeightPx -= nonOverlappingTaskbarInset;
+ }
+ }
+ availableHeightPx = nonFinalAvailableHeightPx;
+
edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
- desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : edgeMarginPx;
+
+ desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : isScalableGrid
+ ? res.getDimensionPixelSize(R.dimen.scalable_grid_left_right_margin)
+ : res.getDimensionPixelSize(R.dimen.dynamic_grid_left_right_margin);
+ desiredWorkspaceLeftRightOriginalPx = desiredWorkspaceLeftRightMarginPx;
+
+
+ allAppsOpenVerticalTranslate = res.getDimensionPixelSize(
+ R.dimen.all_apps_open_vertical_translate);
+
+ folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
+ folderContentPaddingLeftRight =
+ res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
+ folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_content_padding_top);
+
+ setCellLayoutBorderSpacing(pxFromDp(inv.borderSpacing, mMetrics, 1f));
+ cellLayoutBorderSpacingOriginalPx = cellLayoutBorderSpacingPx;
+ folderCellLayoutBorderSpacingPx = cellLayoutBorderSpacingPx;
int cellLayoutPaddingLeftRightMultiplier = !isVerticalBarLayout() && isTablet
? PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER : 1;
- int cellLayoutPadding = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
- if (isLandscape) {
+ int cellLayoutPadding = isScalableGrid
+ ? 0
+ : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
+
+ if (isTwoPanels) {
+ cellLayoutPaddingLeftRightPx =
+ res.getDimensionPixelSize(R.dimen.two_panel_home_side_padding);
+ cellLayoutBottomPaddingPx = 0;
+ } else if (isLandscape) {
cellLayoutPaddingLeftRightPx = 0;
cellLayoutBottomPaddingPx = cellLayoutPadding;
} else {
@@ -202,12 +313,21 @@
iconDrawablePaddingOriginalPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);
+
dropTargetBarSizePx = res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size);
+ dropTargetDragPaddingPx = res.getDimensionPixelSize(R.dimen.drop_target_drag_padding);
+ dropTargetTextSizePx = res.getDimensionPixelSize(R.dimen.drop_target_text_size);
+
workspaceSpringLoadedBottomSpace =
res.getDimensionPixelSize(R.dimen.dynamic_grid_min_spring_loaded_space);
workspaceCellPaddingXPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x);
+ numShownHotseatIcons =
+ isTwoPanels ? inv.numDatabaseHotseatIcons : inv.numShownHotseatIcons;
+ numShownAllAppsColumns =
+ isTwoPanels ? inv.numDatabaseAllAppsColumns : inv.numAllAppsColumns;
+ hotseatBarSizeExtraSpacePx = 0;
hotseatBarTopPaddingPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_top_padding);
hotseatBarBottomPaddingPx = (isTallDevice ? 0
@@ -217,45 +337,129 @@
res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
// Add a bit of space between nav bar and hotseat in vertical bar layout.
hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
- hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
- + (isVerticalBarLayout()
- ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
- : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
- + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
+ hotseatExtraVerticalSize =
+ res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size);
+ updateHotseatIconSize(pxFromDp(inv.iconSize, mMetrics, 1f));
+
+ qsbBottomMarginOriginalPx = isScalableGrid
+ ? res.getDimensionPixelSize(R.dimen.scalable_grid_qsb_bottom_margin)
+ : 0;
+
+ overviewTaskMarginPx = res.getDimensionPixelSize(R.dimen.overview_task_margin);
+ overviewTaskIconSizePx =
+ isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get() ? res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_size_grid) : res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_size);
+ overviewTaskThumbnailTopMarginPx = overviewTaskIconSizePx + overviewTaskMarginPx * 2;
+ overviewActionsMarginGesturePx = res.getDimensionPixelSize(
+ R.dimen.overview_actions_bottom_margin_gesture);
+ overviewActionsMarginThreeButtonPx = res.getDimensionPixelSize(
+ R.dimen.overview_actions_bottom_margin_three_button);
// Calculate all of the remaining variables.
- updateAvailableDimensions(res);
+ extraSpace = updateAvailableDimensions(res);
// Now that we have all of the variables calculated, we can tune certain sizes.
- if (!isVerticalBarLayout() && isPhone && isTallDevice) {
+ if (isScalableGrid && inv.devicePaddings != null) {
+ // Paddings were created assuming no scaling, so we first unscale the extra space.
+ int unscaledExtraSpace = (int) (extraSpace / cellScaleToFit);
+ DevicePadding padding = inv.devicePaddings.getDevicePadding(unscaledExtraSpace);
+
+ int paddingWorkspaceTop = padding.getWorkspaceTopPadding(unscaledExtraSpace);
+ int paddingWorkspaceBottom = padding.getWorkspaceBottomPadding(unscaledExtraSpace);
+ int paddingHotseatBottom = padding.getHotseatBottomPadding(unscaledExtraSpace);
+
+ workspaceTopPadding = Math.round(paddingWorkspaceTop * cellScaleToFit);
+ workspaceBottomPadding = Math.round(paddingWorkspaceBottom * cellScaleToFit);
+ extraHotseatBottomPadding = Math.round(paddingHotseatBottom * cellScaleToFit);
+
+ hotseatBarSizePx += extraHotseatBottomPadding;
+
+ qsbBottomMarginPx = Math.round(qsbBottomMarginOriginalPx * cellScaleToFit);
+ } else if (!isVerticalBarLayout() && isPhone && isTallDevice) {
// We increase the hotseat size when there is extra space.
- // ie. For a display with a large aspect ratio, we can keep the icons on the workspace
- // in portrait mode closer together by adding more height to the hotseat.
- // Note: This calculation was created after noticing a pattern in the design spec.
- int extraSpace = getCellSize().y - iconSizePx - iconDrawablePaddingPx * 2
- - workspacePageIndicatorHeight;
- hotseatBarSizePx += extraSpace;
- hotseatBarBottomPaddingPx += extraSpace;
+
+ if (Float.compare(aspectRatio, TALLER_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0
+ && extraSpace >= Utilities.dpToPx(TALL_DEVICE_EXTRA_SPACE_THRESHOLD_DP)) {
+ // For taller devices, we will take a piece of the extra space from each row,
+ // and add it to the space above and below the hotseat.
+
+ // For devices with more extra space, we take a larger piece from each cell.
+ int piece = extraSpace < Utilities.dpToPx(TALL_DEVICE_MORE_EXTRA_SPACE_THRESHOLD_DP)
+ ? 7 : 5;
+
+ int extraSpace = ((getCellSize().y - iconSizePx - iconDrawablePaddingPx * 2)
+ * inv.numRows) / piece;
+
+ workspaceTopPadding = extraSpace / 8;
+ int halfLeftOver = (extraSpace - workspaceTopPadding) / 2;
+ hotseatBarTopPaddingPx += halfLeftOver;
+ hotseatBarSizeExtraSpacePx = halfLeftOver;
+ } else {
+ // ie. For a display with a large aspect ratio, we can keep the icons on the
+ // workspace in portrait mode closer together by adding more height to the hotseat.
+ // Note: This calculation was created after noticing a pattern in the design spec.
+ hotseatBarSizeExtraSpacePx = getCellSize().y - iconSizePx
+ - iconDrawablePaddingPx * 2 - workspacePageIndicatorHeight;
+ }
+
+ updateHotseatIconSize(iconSizePx);
// Recalculate the available dimensions using the new hotseat size.
updateAvailableDimensions(res);
}
updateWorkspacePadding();
+ flingToDeleteThresholdVelocity = res.getDimensionPixelSize(
+ R.dimen.drag_flingToDeleteMinVelocity);
+
// This is done last, after iconSizePx is calculated above.
- mDotRendererWorkSpace = new DotRenderer(iconSizePx, IconShape.getShapePath(),
- IconShape.DEFAULT_PATH_SIZE);
+ Path dotPath = GraphicsUtils.getShapePath(DEFAULT_DOT_SIZE);
+ mDotRendererWorkSpace = new DotRenderer(iconSizePx, dotPath, DEFAULT_DOT_SIZE);
mDotRendererAllApps = iconSizePx == allAppsIconSizePx ? mDotRendererWorkSpace :
- new DotRenderer(allAppsIconSizePx, IconShape.getShapePath(),
- IconShape.DEFAULT_PATH_SIZE);
+ new DotRenderer(allAppsIconSizePx, dotPath, DEFAULT_DOT_SIZE);
+ }
+
+ private void updateHotseatIconSize(int hotseatIconSizePx) {
+ // Ensure there is enough space for folder icons, which have a slightly larger radius.
+ hotseatCellHeightPx = (int) Math.ceil(hotseatIconSizePx * ICON_OVERLAP_FACTOR);
+ if (isVerticalBarLayout()) {
+ hotseatBarSizePx = hotseatIconSizePx + hotseatBarSidePaddingStartPx
+ + hotseatBarSidePaddingEndPx;
+ } else {
+ hotseatBarSizePx = hotseatIconSizePx + hotseatBarTopPaddingPx
+ + hotseatBarBottomPaddingPx + (isScalableGrid ? 0 : hotseatExtraVerticalSize)
+ + hotseatBarSizeExtraSpacePx;
+ }
+ }
+
+ private void setCellLayoutBorderSpacing(int borderSpacing) {
+ cellLayoutBorderSpacingPx = isScalableGrid ? borderSpacing : 0;
+ }
+
+ /**
+ * We inset the widget padding added by the system and instead rely on the border spacing
+ * between cells to create reliable consistency between widgets
+ */
+ public boolean shouldInsetWidgets() {
+ Rect widgetPadding = inv.defaultWidgetPadding;
+
+ // Check all sides to ensure that the widget won't overlap into another cell, or into
+ // status bar.
+ return workspaceTopPadding > widgetPadding.top
+ && cellLayoutBorderSpacingPx > widgetPadding.left
+ && cellLayoutBorderSpacingPx > widgetPadding.top
+ && cellLayoutBorderSpacingPx > widgetPadding.right
+ && cellLayoutBorderSpacingPx > widgetPadding.bottom;
}
public Builder toBuilder(Context context) {
- Point size = new Point(availableWidthPx, availableHeightPx);
+ WindowBounds bounds =
+ new WindowBounds(widthPx, heightPx, availableWidthPx, availableHeightPx);
+ bounds.bounds.offsetTo(windowX, windowY);
return new Builder(context, inv, mInfo)
- .setSizeRange(size, size)
- .setSize(widthPx, heightPx)
- .setWindowPosition(windowX, windowY)
+ .setWindowBounds(bounds)
+ .setUseTwoPanels(isTwoPanels)
.setMultiWindowMode(isMultiWindowMode);
}
@@ -267,24 +471,12 @@
* TODO: Move this to the builder as part of setMultiWindowMode
*/
public DeviceProfile getMultiWindowProfile(Context context, WindowBounds windowBounds) {
- // We take the minimum sizes of this profile and it's multi-window variant to ensure that
- // the system decor is always excluded.
- Point mwSize = new Point(Math.min(availableWidthPx, windowBounds.availableSize.x),
- Math.min(availableHeightPx, windowBounds.availableSize.y));
-
DeviceProfile profile = toBuilder(context)
- .setSizeRange(mwSize, mwSize)
- .setSize(windowBounds.bounds.width(), windowBounds.bounds.height())
- .setWindowPosition(windowBounds.bounds.left, windowBounds.bounds.top)
+ .setWindowBounds(windowBounds)
.setMultiWindowMode(true)
.build();
- // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
- float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
- - iconDrawablePaddingPx - profile.iconTextSizePx;
- if (workspaceCellPaddingY < profile.iconDrawablePaddingPx * 2) {
- profile.adjustToHideWorkspaceLabels();
- }
+ profile.hideWorkspaceLabelsIfNotEnoughSpace();
// We use these scales to measure and layout the widgets using their full invariant profile
// sizes and then draw them scaled and centered to fit in their multi-window mode cellspans.
@@ -297,45 +489,74 @@
}
/**
- * Inverse of {@link #getMultiWindowProfile(Context, WindowBounds)}
- * @return device profile corresponding to the current orientation in non multi-window mode.
- */
- public DeviceProfile getFullScreenProfile() {
- return isLandscape ? inv.landscapeProfile : inv.portraitProfile;
- }
-
- /**
- * Adjusts the profile so that the labels on the Workspace are hidden.
+ * Checks if there is enough space for labels on the workspace.
+ * If there is not, labels on the Workspace are hidden.
* It is important to call this method after the All Apps variables have been set.
*/
- private void adjustToHideWorkspaceLabels() {
- iconTextSizePx = 0;
- iconDrawablePaddingPx = 0;
- cellHeightPx = iconSizePx;
- autoResizeAllAppsCells();
+ private void hideWorkspaceLabelsIfNotEnoughSpace() {
+ float iconTextHeight = Utilities.calculateTextHeight(iconTextSizePx);
+ float workspaceCellPaddingY = getCellSize().y - iconSizePx - iconDrawablePaddingPx
+ - iconTextHeight;
+
+ // We want enough space so that the text is closer to its corresponding icon.
+ if (workspaceCellPaddingY < iconTextHeight) {
+ iconTextSizePx = 0;
+ iconDrawablePaddingPx = 0;
+ cellHeightPx = (int) Math.ceil(iconSizePx * ICON_OVERLAP_FACTOR);
+ autoResizeAllAppsCells();
+ }
}
/**
* Re-computes the all-apps cell size to be independent of workspace
*/
public void autoResizeAllAppsCells() {
- int topBottomPadding = allAppsIconDrawablePaddingPx * (isVerticalBarLayout() ? 2 : 1);
+ int textHeight = Utilities.calculateTextHeight(allAppsIconTextSizePx);
+ int topBottomPadding = textHeight;
allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx
- + Utilities.calculateTextHeight(allAppsIconTextSizePx)
- + topBottomPadding * 2;
+ + textHeight + (topBottomPadding * 2);
}
- private void updateAvailableDimensions(Resources res) {
+ /**
+ * Returns the amount of extra (or unused) vertical space.
+ */
+ private int updateAvailableDimensions(Resources res) {
updateIconSize(1f, res);
- // Check to see if the icons fit within the available height. If not, then scale down.
- float usedHeight = (cellHeightPx * inv.numRows);
- int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
- if (usedHeight > maxHeight) {
- float scale = maxHeight / usedHeight;
- updateIconSize(scale, res);
+ Point workspacePadding = getTotalWorkspacePadding();
+
+ // Check to see if the icons fit within the available height.
+ float usedHeight = getCellLayoutHeight();
+ final int maxHeight = availableHeightPx - workspacePadding.y;
+ float extraHeight = Math.max(0, maxHeight - usedHeight);
+ float scaleY = maxHeight / usedHeight;
+ boolean shouldScale = scaleY < 1f;
+
+ float scaleX = 1f;
+ if (isScalableGrid) {
+ // We scale to fit the cellWidth and cellHeight in the available space.
+ // The benefit of scalable grids is that we can get consistent aspect ratios between
+ // devices.
+ float usedWidth = (cellWidthPx * inv.numColumns)
+ + (cellLayoutBorderSpacingPx * (inv.numColumns - 1))
+ + (desiredWorkspaceLeftRightMarginPx * 2);
+ // We do not subtract padding here, as we also scale the workspace padding if needed.
+ scaleX = availableWidthPx / usedWidth;
+ shouldScale = true;
}
+
+ if (shouldScale) {
+ float scale = Math.min(scaleX, scaleY);
+ updateIconSize(scale, res);
+ extraHeight = Math.max(0, maxHeight - getCellLayoutHeight());
+ }
+
updateAvailableFolderCellDimensions(res);
+ return Math.round(extraHeight);
+ }
+
+ private int getCellLayoutHeight() {
+ return (cellHeightPx * inv.numRows) + (cellLayoutBorderSpacingPx * (inv.numRows - 1));
}
/**
@@ -343,36 +564,51 @@
* iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
* hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
*/
- private void updateIconSize(float scale, Resources res) {
+ public void updateIconSize(float scale, Resources res) {
+ // Icon scale should never exceed 1, otherwise pixellation may occur.
+ iconScale = Math.min(1f, scale);
+ cellScaleToFit = scale;
+
+
// Workspace
final boolean isVerticalLayout = isVerticalBarLayout();
- float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
- iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
- * scale));
- iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
- iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
+ float invIconSizeDp = isLandscape ? inv.landscapeIconSize : inv.iconSize;
+ iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, iconScale));
+ float invIconTextSizeSp = isLandscape ? inv.landscapeIconTextSize : inv.iconTextSize;
+ iconTextSizePx = (int) (pxFromSp(invIconTextSizeSp, mMetrics) * iconScale);
+ iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * iconScale);
- cellHeightPx = iconSizePx + iconDrawablePaddingPx
- + Utilities.calculateTextHeight(iconTextSizePx);
- int cellYPadding = (getCellSize().y - cellHeightPx) / 2;
- if (iconDrawablePaddingPx > cellYPadding && !isVerticalLayout
- && !isMultiWindowMode) {
- // Ensures that the label is closer to its corresponding icon. This is not an issue
- // with vertical bar layout or multi-window mode since the issue is handled separately
- // with their calls to {@link #adjustToHideWorkspaceLabels}.
- cellHeightPx -= (iconDrawablePaddingPx - cellYPadding);
- iconDrawablePaddingPx = cellYPadding;
+ setCellLayoutBorderSpacing((int) (cellLayoutBorderSpacingOriginalPx * scale));
+
+ if (isScalableGrid) {
+ cellWidthPx = pxFromDp(inv.minCellWidth, mMetrics, scale);
+ cellHeightPx = pxFromDp(inv.minCellHeight, mMetrics, scale);
+ int cellContentHeight = iconSizePx + iconDrawablePaddingPx
+ + Utilities.calculateTextHeight(iconTextSizePx);
+ cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+ desiredWorkspaceLeftRightMarginPx = (int) (desiredWorkspaceLeftRightOriginalPx * scale);
+ } else {
+ cellWidthPx = iconSizePx + iconDrawablePaddingPx;
+ cellHeightPx = (int) Math.ceil(iconSizePx * ICON_OVERLAP_FACTOR)
+ + iconDrawablePaddingPx
+ + Utilities.calculateTextHeight(iconTextSizePx);
+ int cellPaddingY = (getCellSize().y - cellHeightPx) / 2;
+ if (iconDrawablePaddingPx > cellPaddingY && !isVerticalLayout
+ && !isMultiWindowMode) {
+ // Ensures that the label is closer to its corresponding icon. This is not an issue
+ // with vertical bar layout or multi-window mode since the issue is handled
+ // separately with their calls to {@link #adjustToHideWorkspaceLabels}.
+ cellHeightPx -= (iconDrawablePaddingPx - cellPaddingY);
+ iconDrawablePaddingPx = cellPaddingY;
+ }
}
- cellWidthPx = iconSizePx + iconDrawablePaddingPx;
// All apps
- if (allAppsHasDifferentNumColumns()) {
- allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
- allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
+ if (numShownAllAppsColumns != inv.numColumns) {
+ allAppsIconSizePx = pxFromDp(inv.allAppsIconSize, mMetrics);
+ allAppsIconTextSizePx = pxFromSp(inv.allAppsIconTextSize, mMetrics);
allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
- // We use 4 below to ensure labels are closer to their corresponding icon.
- allAppsCellHeightPx = Math.round(allAppsIconSizePx + allAppsIconTextSizePx
- + (4 * allAppsIconDrawablePaddingPx));
+ autoResizeAllAppsCells();
} else {
allAppsIconSizePx = iconSizePx;
allAppsIconTextSizePx = iconTextSizePx;
@@ -381,17 +617,12 @@
}
allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
- if (isVerticalBarLayout()) {
- // Always hide the Workspace text with vertical bar layout.
- adjustToHideWorkspaceLabels();
+ if (isVerticalLayout) {
+ hideWorkspaceLabelsIfNotEnoughSpace();
}
// Hotseat
- if (isVerticalLayout) {
- hotseatBarSizePx = iconSizePx + hotseatBarSidePaddingStartPx
- + hotseatBarSidePaddingEndPx;
- }
- hotseatCellHeightPx = iconSizePx;
+ updateHotseatIconSize(iconSizePx);
if (!isVerticalLayout) {
int expectedWorkspaceHeight = availableHeightPx - hotseatBarSizePx
@@ -411,25 +642,26 @@
}
private void updateAvailableFolderCellDimensions(Resources res) {
- int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top)
- + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom)
- + Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size));
-
updateFolderCellSize(1f, res);
+ final int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_height);
+
// Don't let the folder get too close to the edges of the screen.
int folderMargin = edgeMarginPx * 2;
Point totalWorkspacePadding = getTotalWorkspacePadding();
// Check if the icons fit within the available height.
- float contentUsedHeight = folderCellHeightPx * inv.numFolderRows;
+ float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
+ + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacingPx);
int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
- - folderMargin;
+ - folderMargin - folderContentPaddingTop;
float scaleY = contentMaxHeight / contentUsedHeight;
// Check if the icons fit within the available width.
- float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns;
- int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
+ float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
+ + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacingPx);
+ int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin
+ - folderContentPaddingLeftRight * 2;
float scaleX = contentMaxWidth / contentUsedWidth;
float scale = Math.min(scaleX, scaleY);
@@ -439,16 +671,34 @@
}
private void updateFolderCellSize(float scale, Resources res) {
- folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics) * scale);
- folderChildTextSizePx =
- (int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale);
+ float invIconSizeDp = isVerticalBarLayout() ? inv.landscapeIconSize : inv.iconSize;
+ folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, scale));
+ folderChildTextSizePx = pxFromSp(inv.iconTextSize, mMetrics, scale);
+ folderLabelTextSizePx = (int) (folderChildTextSizePx * folderLabelTextScale);
int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
- int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding) * scale);
- int cellPaddingY = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_y_padding) * scale);
- folderCellWidthPx = folderChildIconSizePx + 2 * cellPaddingX;
- folderCellHeightPx = folderChildIconSizePx + 2 * cellPaddingY + textHeight;
+ if (isScalableGrid) {
+ int minWidth = folderChildIconSizePx + iconDrawablePaddingPx * 2;
+ int minHeight = folderChildIconSizePx + iconDrawablePaddingPx * 2 + textHeight;
+
+ folderCellWidthPx = (int) Math.max(minWidth, cellWidthPx * scale);
+ folderCellHeightPx = (int) Math.max(minHeight, cellHeightPx * scale);
+
+ int borderSpacing = (int) (cellLayoutBorderSpacingOriginalPx * scale);
+ folderCellLayoutBorderSpacingPx = borderSpacing;
+ folderContentPaddingLeftRight = borderSpacing;
+ folderContentPaddingTop = borderSpacing;
+ } else {
+ int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding)
+ * scale);
+ int cellPaddingY = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_y_padding)
+ * scale);
+
+ folderCellWidthPx = folderChildIconSizePx + 2 * cellPaddingX;
+ folderCellHeightPx = folderChildIconSizePx + 2 * cellPaddingY + textHeight;
+ }
+
folderChildDrawablePaddingPx = Math.max(0,
(folderCellHeightPx - folderChildIconSizePx - textHeight) / 3);
}
@@ -467,18 +717,20 @@
}
public Point getCellSize() {
- return getCellSize(inv.numColumns, inv.numRows);
+ return getCellSize(null);
}
- private Point getCellSize(int numColumns, int numRows) {
- Point result = new Point();
+ public Point getCellSize(Point result) {
+ if (result == null) {
+ result = new Point();
+ }
// Since we are only concerned with the overall padding, layout direction does
// not matter.
Point padding = getTotalWorkspacePadding();
result.x = calculateCellWidth(availableWidthPx - padding.x
- - cellLayoutPaddingLeftRightPx * 2, numColumns);
+ - cellLayoutPaddingLeftRightPx * 2, cellLayoutBorderSpacingPx, inv.numColumns);
result.y = calculateCellHeight(availableHeightPx - padding.y
- - cellLayoutBottomPaddingPx, numRows);
+ - cellLayoutBottomPaddingPx, cellLayoutBorderSpacingPx, inv.numRows);
return result;
}
@@ -505,8 +757,9 @@
padding.right = hotseatBarSizePx;
}
} else {
- int paddingBottom = hotseatBarSizePx + workspacePageIndicatorHeight
- - mWorkspacePageIndicatorOverlapWorkspace;
+ int hotseatTop = isTaskbarPresent ? taskbarSize : hotseatBarSizePx;
+ int paddingBottom = hotseatTop + workspacePageIndicatorHeight
+ + workspaceBottomPadding - mWorkspacePageIndicatorOverlapWorkspace;
if (isTablet) {
// Pad the left and right of the workspace to ensure consistent spacing
// between all icons
@@ -515,15 +768,20 @@
((inv.numColumns - 1) * cellWidthPx)));
availablePaddingX = (int) Math.min(availablePaddingX,
widthPx * MAX_HORIZONTAL_PADDING_PERCENT);
+ int hotseatVerticalPadding = isTaskbarPresent ? 0
+ : hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx;
int availablePaddingY = Math.max(0, heightPx - edgeMarginPx - paddingBottom
- - (2 * inv.numRows * cellHeightPx) - hotseatBarTopPaddingPx
- - hotseatBarBottomPaddingPx);
+ - (2 * inv.numRows * cellHeightPx) - hotseatVerticalPadding);
padding.set(availablePaddingX / 2, edgeMarginPx + availablePaddingY / 2,
availablePaddingX / 2, paddingBottom + availablePaddingY / 2);
+
+ if (isTwoPanels) {
+ padding.set(0, padding.top, 0, padding.bottom);
+ }
} else {
// Pad the top and bottom of the workspace with search/hotseat bar sizes
padding.set(desiredWorkspaceLeftRightMarginPx,
- edgeMarginPx,
+ workspaceTopPadding + (isScalableGrid ? 0 : edgeMarginPx),
desiredWorkspaceLeftRightMarginPx,
paddingBottom);
}
@@ -545,7 +803,7 @@
// for this, we pad the left and right of the hotseat with half of the difference of a
// workspace cell vs a hotseat cell.
float workspaceCellWidth = (float) widthPx / inv.numColumns;
- float hotseatCellWidth = (float) widthPx / inv.numHotseatIcons;
+ float hotseatCellWidth = (float) widthPx / numShownHotseatIcons;
int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2);
mHotseatPadding.set(
hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx
@@ -553,7 +811,8 @@
hotseatBarTopPaddingPx,
hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx
+ mInsets.right,
- hotseatBarBottomPaddingPx + mInsets.bottom + cellLayoutBottomPaddingPx);
+ hotseatBarSizePx - hotseatCellHeightPx - hotseatBarTopPaddingPx
+ + cellLayoutBottomPaddingPx + mInsets.bottom);
}
return mHotseatPadding;
}
@@ -570,19 +829,20 @@
mInsets.top + availableHeightPx);
} else {
// Folders should only appear below the drop target bar and above the hotseat
+ int hotseatTop = isTaskbarPresent ? taskbarSize : hotseatBarSizePx;
return new Rect(mInsets.left + edgeMarginPx,
mInsets.top + dropTargetBarSizePx + edgeMarginPx,
mInsets.left + availableWidthPx - edgeMarginPx,
- mInsets.top + availableHeightPx - hotseatBarSizePx
+ mInsets.top + availableHeightPx - hotseatTop
- workspacePageIndicatorHeight - edgeMarginPx);
}
}
- public static int calculateCellWidth(int width, int countX) {
- return width / countX;
+ public static int calculateCellWidth(int width, int borderSpacing, int countX) {
+ return (width - ((countX - 1) * borderSpacing)) / countX;
}
- public static int calculateCellHeight(int height, int countY) {
- return height / countY;
+ public static int calculateCellHeight(int height, int borderSpacing, int countY) {
+ return (height - ((countY - 1) * borderSpacing)) / countY;
}
/**
@@ -595,19 +855,12 @@
}
/**
- * Returns true when the number of workspace columns and all apps columns differs.
- */
- private boolean allAppsHasDifferentNumColumns() {
- return inv.numAllAppsColumns != inv.numColumns;
- }
-
- /**
* Updates orientation information and returns true if it has changed from the previous value.
*/
public boolean updateIsSeascape(Context context) {
if (isVerticalBarLayout()) {
- boolean isSeascape = DefaultDisplay.INSTANCE.get(context).getInfo().rotation
- == Surface.ROTATION_270;
+ boolean isSeascape = DisplayController.INSTANCE.get(context)
+ .getInfo().rotation == Surface.ROTATION_270;
if (mIsSeascape != isSeascape) {
mIsSeascape = isSeascape;
return true;
@@ -621,27 +874,132 @@
}
public boolean shouldFadeAdjacentWorkspaceScreens() {
- return isVerticalBarLayout() || isLargeTablet;
+ return isVerticalBarLayout();
}
- public int getCellHeight(@ContainerType int containerType) {
+ public int getCellContentHeight(@ContainerType int containerType) {
switch (containerType) {
case CellLayout.WORKSPACE:
return cellHeightPx;
case CellLayout.FOLDER:
return folderCellHeightPx;
case CellLayout.HOTSEAT:
- return hotseatCellHeightPx;
+ // The hotseat is the only container where the cell height is going to be
+ // different from the content within that cell.
+ return iconSizePx;
default:
// ??
return 0;
}
}
- private static Context getContext(Context c, DefaultDisplay.Info info, int orientation) {
+ private String pxToDpStr(String name, float value) {
+ return "\t" + name + ": " + value + "px (" + dpiFromPx(value, mMetrics.densityDpi) + "dp)";
+ }
+
+ public void dump(String prefix, PrintWriter writer) {
+ writer.println(prefix + "DeviceProfile:");
+ writer.println(prefix + "\t1 dp = " + mMetrics.density + " px");
+
+ writer.println(prefix + "\tallowRotation:" + allowRotation);
+ writer.println(prefix + "\tisTablet:" + isTablet);
+ writer.println(prefix + "\tisPhone:" + isPhone);
+ writer.println(prefix + "\ttransposeLayoutWithOrientation:"
+ + transposeLayoutWithOrientation);
+
+ writer.println(prefix + "\tisLandscape:" + isLandscape);
+ writer.println(prefix + "\tisMultiWindowMode:" + isMultiWindowMode);
+ writer.println(prefix + "\tisTwoPanels:" + isTwoPanels);
+
+ writer.println(prefix + pxToDpStr("windowX", windowX));
+ writer.println(prefix + pxToDpStr("windowY", windowY));
+ writer.println(prefix + pxToDpStr("widthPx", widthPx));
+ writer.println(prefix + pxToDpStr("heightPx", heightPx));
+
+ writer.println(prefix + pxToDpStr("availableWidthPx", availableWidthPx));
+ writer.println(prefix + pxToDpStr("availableHeightPx", availableHeightPx));
+
+ writer.println(prefix + "\taspectRatio:" + aspectRatio);
+
+ writer.println(prefix + "\tisScalableGrid:" + isScalableGrid);
+
+ writer.println(prefix + "\tinv.minCellWidth:" + inv.minCellWidth + "dp");
+ writer.println(prefix + "\tinv.minCellHeight:" + inv.minCellHeight + "dp");
+
+ writer.println(prefix + "\tinv.numColumns:" + inv.numColumns);
+ writer.println(prefix + "\tinv.numRows:" + inv.numRows);
+
+ writer.println(prefix + pxToDpStr("cellWidthPx", cellWidthPx));
+ writer.println(prefix + pxToDpStr("cellHeightPx", cellHeightPx));
+
+ writer.println(prefix + pxToDpStr("getCellSize().x", getCellSize().x));
+ writer.println(prefix + pxToDpStr("getCellSize().y", getCellSize().y));
+
+ writer.println(prefix + "\tinv.iconSize:" + inv.iconSize + "dp");
+ writer.println(prefix + pxToDpStr("iconSizePx", iconSizePx));
+ writer.println(prefix + pxToDpStr("iconTextSizePx", iconTextSizePx));
+ writer.println(prefix + pxToDpStr("iconDrawablePaddingPx", iconDrawablePaddingPx));
+
+ writer.println(prefix + pxToDpStr("folderCellWidthPx", folderCellWidthPx));
+ writer.println(prefix + pxToDpStr("folderCellHeightPx", folderCellHeightPx));
+ writer.println(prefix + pxToDpStr("folderChildIconSizePx", folderChildIconSizePx));
+ writer.println(prefix + pxToDpStr("folderChildTextSizePx", folderChildTextSizePx));
+ writer.println(prefix + pxToDpStr("folderChildDrawablePaddingPx",
+ folderChildDrawablePaddingPx));
+ writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacingPx",
+ folderCellLayoutBorderSpacingPx));
+
+ writer.println(prefix + pxToDpStr("cellLayoutBorderSpacingPx",
+ cellLayoutBorderSpacingPx));
+ writer.println(prefix + pxToDpStr("desiredWorkspaceLeftRightMarginPx",
+ desiredWorkspaceLeftRightMarginPx));
+
+ writer.println(prefix + pxToDpStr("allAppsIconSizePx", allAppsIconSizePx));
+ writer.println(prefix + pxToDpStr("allAppsIconTextSizePx", allAppsIconTextSizePx));
+ writer.println(prefix + pxToDpStr("allAppsIconDrawablePaddingPx",
+ allAppsIconDrawablePaddingPx));
+ writer.println(prefix + pxToDpStr("allAppsCellHeightPx", allAppsCellHeightPx));
+ writer.println(prefix + "\tnumShownAllAppsColumns: " + numShownAllAppsColumns);
+
+ writer.println(prefix + pxToDpStr("hotseatBarSizePx", hotseatBarSizePx));
+ writer.println(prefix + pxToDpStr("hotseatCellHeightPx", hotseatCellHeightPx));
+ writer.println(prefix + pxToDpStr("hotseatBarTopPaddingPx", hotseatBarTopPaddingPx));
+ writer.println(prefix + pxToDpStr("hotseatBarBottomPaddingPx", hotseatBarBottomPaddingPx));
+ writer.println(prefix + pxToDpStr("hotseatBarSidePaddingStartPx",
+ hotseatBarSidePaddingStartPx));
+ writer.println(prefix + pxToDpStr("hotseatBarSidePaddingEndPx",
+ hotseatBarSidePaddingEndPx));
+ writer.println(prefix + "\tnumShownHotseatIcons: " + numShownHotseatIcons);
+
+ writer.println(prefix + "\tisTaskbarPresent:" + isTaskbarPresent);
+
+ writer.println(prefix + pxToDpStr("taskbarSize", taskbarSize));
+ writer.println(prefix + pxToDpStr("nonOverlappingTaskbarInset",
+ nonOverlappingTaskbarInset));
+
+ writer.println(prefix + pxToDpStr("workspacePadding.left", workspacePadding.left));
+ writer.println(prefix + pxToDpStr("workspacePadding.top", workspacePadding.top));
+ writer.println(prefix + pxToDpStr("workspacePadding.right", workspacePadding.right));
+ writer.println(prefix + pxToDpStr("workspacePadding.bottom", workspacePadding.bottom));
+
+ writer.println(prefix + pxToDpStr("iconScale", iconScale));
+ writer.println(prefix + pxToDpStr("cellScaleToFit ", cellScaleToFit));
+ writer.println(prefix + pxToDpStr("extraSpace", extraSpace));
+
+ if (inv.devicePaddings != null) {
+ int unscaledExtraSpace = (int) (extraSpace / iconScale);
+ writer.println(prefix + pxToDpStr("maxEmptySpace",
+ inv.devicePaddings.getDevicePadding(unscaledExtraSpace).getMaxEmptySpacePx()));
+ }
+ writer.println(prefix + pxToDpStr("workspaceTopPadding", workspaceTopPadding));
+ writer.println(prefix + pxToDpStr("workspaceBottomPadding", workspaceBottomPadding));
+ writer.println(prefix + pxToDpStr("extraHotseatBottomPadding", extraHotseatBottomPadding));
+ }
+
+ private static Context getContext(Context c, Info info, int orientation) {
Configuration config = new Configuration(c.getResources().getConfiguration());
config.orientation = orientation;
- config.densityDpi = info.metrics.densityDpi;
+ config.densityDpi = info.densityDpi;
return c.createConfigurationContext(config);
}
@@ -662,35 +1020,18 @@
public static class Builder {
private Context mContext;
private InvariantDeviceProfile mInv;
- private DefaultDisplay.Info mInfo;
+ private Info mInfo;
- private final Point mWindowPosition = new Point();
- private Point mMinSize, mMaxSize;
- private int mWidth, mHeight;
+ private WindowBounds mWindowBounds;
+ private boolean mUseTwoPanels;
- private boolean mIsLandscape;
private boolean mIsMultiWindowMode = false;
- private boolean mTransposeLayoutWithOrientation;
+ private Boolean mTransposeLayoutWithOrientation;
- public Builder(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info) {
+ public Builder(Context context, InvariantDeviceProfile inv, Info info) {
mContext = context;
mInv = inv;
mInfo = info;
- mTransposeLayoutWithOrientation = context.getResources()
- .getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
- }
-
- public Builder setSizeRange(Point minSize, Point maxSize) {
- mMinSize = minSize;
- mMaxSize = maxSize;
- return this;
- }
-
- public Builder setSize(int width, int height) {
- mWidth = width;
- mHeight = height;
- mIsLandscape = mWidth > mHeight;
- return this;
}
public Builder setMultiWindowMode(boolean isMultiWindowMode) {
@@ -698,11 +1039,14 @@
return this;
}
- /**
- * Sets the window position if not full-screen
- */
- public Builder setWindowPosition(int x, int y) {
- mWindowPosition.set(x, y);
+ public Builder setUseTwoPanels(boolean useTwoPanels) {
+ mUseTwoPanels = useTwoPanels;
+ return this;
+ }
+
+
+ public Builder setWindowBounds(WindowBounds bounds) {
+ mWindowBounds = bounds;
return this;
}
@@ -712,9 +1056,14 @@
}
public DeviceProfile build() {
- return new DeviceProfile(mContext, mInv, mInfo, mMinSize, mMaxSize,
- mWidth, mHeight, mIsLandscape, mIsMultiWindowMode,
- mTransposeLayoutWithOrientation, mWindowPosition);
+ if (mWindowBounds == null) {
+ throw new IllegalArgumentException("Window bounds not set");
+ }
+ if (mTransposeLayoutWithOrientation == null) {
+ mTransposeLayoutWithOrientation = !mInfo.isTablet(mWindowBounds);
+ }
+ return new DeviceProfile(mContext, mInv, mInfo, mWindowBounds,
+ mIsMultiWindowMode, mTransposeLayoutWithOrientation, mUseTwoPanels);
}
}
diff --git a/src/com/android/launcher3/DragSource.java b/src/com/android/launcher3/DragSource.java
index d4d7b99..ba227d4 100644
--- a/src/com/android/launcher3/DragSource.java
+++ b/src/com/android/launcher3/DragSource.java
@@ -19,12 +19,11 @@
import android.view.View;
import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
/**
* Interface defining an object that can originate a drag.
*/
-public interface DragSource extends LogContainerProvider {
+public interface DragSource {
/**
* A callback made back to the source after an item from this source has been dropped on a
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index b27abc4..70d8476 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -79,9 +79,7 @@
public DraggableView originalView = null;
/** Used for matching DROP event with its corresponding DRAG event on the server side. */
- public final InstanceId logInstanceId =
- new InstanceIdSequence(1 << 20 /*InstanceId.INSTANCE_ID_MAX*/)
- .newInstanceId();
+ public final InstanceId logInstanceId = new InstanceIdSequence().newInstanceId();
public DragObject(Context context) {
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
@@ -113,18 +111,6 @@
return res;
}
-
-
- /**
- * This is used to determine if an object is dropped at a different location than it was
- * dragged from
- */
- public boolean isMoved() {
- return dragInfo.cellX != originalDragInfo.cellX
- || dragInfo.cellY != originalDragInfo.cellY
- || dragInfo.screenId != originalDragInfo.screenId
- || dragInfo.container != originalDragInfo.container;
- }
}
/**
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index ca001a3..88f6c49 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
+import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
@@ -104,7 +105,8 @@
/ (2 * (grid.inv.numColumns + 1)))
+ grid.edgeMarginPx;
} else {
- gap = grid.desiredWorkspaceLeftRightMarginPx - grid.inv.defaultWidgetPadding.right;
+ gap = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.drop_target_bar_margin_horizontal);
}
lp.width = grid.availableWidthPx - 2 * gap;
@@ -114,6 +116,7 @@
}
setLayoutParams(lp);
for (ButtonDropTarget button : mDropTargets) {
+ button.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.dropTargetTextSizePx);
button.setToolTipLocation(tooltipLocation);
}
}
@@ -131,7 +134,10 @@
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
- if (mIsVertical) {
+ int visibleCount = getVisibleButtonsCount();
+ if (visibleCount == 0) {
+ // do nothing
+ } else if (mIsVertical) {
int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
@@ -142,7 +148,6 @@
}
}
} else {
- int visibleCount = getVisibleButtonsCount();
int availableWidth = width / visibleCount;
boolean textVisible = true;
for (ButtonDropTarget buttons : mDropTargets) {
@@ -165,7 +170,10 @@
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- if (mIsVertical) {
+ int visibleCount = getVisibleButtonsCount();
+ if (visibleCount == 0) {
+ // do nothing
+ } else if (mIsVertical) {
int gap = getResources().getDimensionPixelSize(R.dimen.drop_target_vertical_gap);
int start = gap;
int end;
@@ -178,7 +186,6 @@
}
}
} else {
- int visibleCount = getVisibleButtonsCount();
int frameSize = (right - left) / visibleCount;
int start = frameSize / 2;
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index d64967b..a4e1af6 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3;
+import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync;
+
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -24,7 +26,8 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.views.ActivityContext;
/**
@@ -67,6 +70,9 @@
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
// If this is a back key, propagate the key back to the listener
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ if (TextUtils.isEmpty(getText())) {
+ hideKeyboard();
+ }
if (mBackKeyListener != null) {
return mBackKeyListener.onBackKey();
}
@@ -93,12 +99,15 @@
}
}
+ // inherited class can override to change the appearance of the edit text.
+ public void show() {}
+
public void showKeyboard() {
mShowImeAfterFirstLayout = !showSoftInput();
}
public void hideKeyboard() {
- UiThreadHelper.hideKeyboardAsync(getContext(), getWindowToken());
+ hideKeyboardAsync(ActivityContext.lookupContext(getContext()), getWindowToken());
}
private boolean showSoftInput() {
@@ -131,6 +140,9 @@
if (!TextUtils.isEmpty(getText())) {
setText("");
}
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ return;
+ }
if (isFocused()) {
View nextFocus = focusSearch(View.FOCUS_DOWN);
if (nextFocus != null) {
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
deleted file mode 100644
index d3b86de..0000000
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.Property;
-
-import com.android.launcher3.graphics.PlaceHolderIconDrawable;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.util.Themes;
-
-
-public class FastBitmapDrawable extends Drawable {
-
- private static final float PRESSED_SCALE = 1.1f;
-
- private static final float DISABLED_DESATURATION = 1f;
- private static final float DISABLED_BRIGHTNESS = 0.5f;
-
- public static final int CLICK_FEEDBACK_DURATION = 200;
-
- private static ColorFilter sDisabledFColorFilter;
-
- protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
- protected Bitmap mBitmap;
- protected final int mIconColor;
-
- private boolean mIsPressed;
- private boolean mIsDisabled;
- private float mDisabledAlpha = 1f;
-
- // Animator and properties for the fast bitmap drawable's scale
- private static final Property<FastBitmapDrawable, Float> SCALE
- = new Property<FastBitmapDrawable, Float>(Float.TYPE, "scale") {
- @Override
- public Float get(FastBitmapDrawable fastBitmapDrawable) {
- return fastBitmapDrawable.mScale;
- }
-
- @Override
- public void set(FastBitmapDrawable fastBitmapDrawable, Float value) {
- fastBitmapDrawable.mScale = value;
- fastBitmapDrawable.invalidateSelf();
- }
- };
- private ObjectAnimator mScaleAnimation;
- private float mScale = 1;
-
- private int mAlpha = 255;
-
- public FastBitmapDrawable(Bitmap b) {
- this(b, Color.TRANSPARENT);
- }
-
- public FastBitmapDrawable(BitmapInfo info) {
- this(info.icon, info.color);
- }
-
- protected FastBitmapDrawable(Bitmap b, int iconColor) {
- this(b, iconColor, false);
- }
-
- protected FastBitmapDrawable(Bitmap b, int iconColor, boolean isDisabled) {
- mBitmap = b;
- mIconColor = iconColor;
- setFilterBitmap(true);
- setIsDisabled(isDisabled);
- }
-
- @Override
- public final void draw(Canvas canvas) {
- if (mScale != 1f) {
- int count = canvas.save();
- Rect bounds = getBounds();
- canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY());
- drawInternal(canvas, bounds);
- canvas.restoreToCount(count);
- } else {
- drawInternal(canvas, getBounds());
- }
- }
-
- protected void drawInternal(Canvas canvas, Rect bounds) {
- canvas.drawBitmap(mBitmap, null, bounds, mPaint);
- }
-
- @Override
- public void setColorFilter(ColorFilter cf) {
- // No op
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- @Override
- public void setAlpha(int alpha) {
- if (mAlpha != alpha) {
- mAlpha = alpha;
- mPaint.setAlpha(alpha);
- invalidateSelf();
- }
- }
-
- @Override
- public void setFilterBitmap(boolean filterBitmap) {
- mPaint.setFilterBitmap(filterBitmap);
- mPaint.setAntiAlias(filterBitmap);
- }
-
- public int getAlpha() {
- return mAlpha;
- }
-
- public void setScale(float scale) {
- if (mScaleAnimation != null) {
- mScaleAnimation.cancel();
- mScaleAnimation = null;
- }
- mScale = scale;
- invalidateSelf();
- }
-
- public float getAnimatedScale() {
- return mScaleAnimation == null ? 1 : mScale;
- }
-
- public float getScale() {
- return mScale;
- }
-
- @Override
- public int getIntrinsicWidth() {
- return mBitmap.getWidth();
- }
-
- @Override
- public int getIntrinsicHeight() {
- return mBitmap.getHeight();
- }
-
- @Override
- public int getMinimumWidth() {
- return getBounds().width();
- }
-
- @Override
- public int getMinimumHeight() {
- return getBounds().height();
- }
-
- @Override
- public boolean isStateful() {
- return true;
- }
-
- @Override
- public ColorFilter getColorFilter() {
- return mPaint.getColorFilter();
- }
-
- @Override
- protected boolean onStateChange(int[] state) {
- boolean isPressed = false;
- for (int s : state) {
- if (s == android.R.attr.state_pressed) {
- isPressed = true;
- break;
- }
- }
- if (mIsPressed != isPressed) {
- mIsPressed = isPressed;
-
- if (mScaleAnimation != null) {
- mScaleAnimation.cancel();
- mScaleAnimation = null;
- }
-
- if (mIsPressed) {
- // Animate when going to pressed state
- mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE);
- mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
- mScaleAnimation.setInterpolator(ACCEL);
- mScaleAnimation.start();
- } else {
- if (isVisible()) {
- mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, 1f);
- mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
- mScaleAnimation.setInterpolator(DEACCEL);
- mScaleAnimation.start();
- } else {
- mScale = 1f;
- invalidateSelf();
- }
- }
- return true;
- }
- return false;
- }
-
- public void setIsDisabled(boolean isDisabled) {
- if (mIsDisabled != isDisabled) {
- mIsDisabled = isDisabled;
- updateFilter();
- }
- }
-
- protected boolean isDisabled() {
- return mIsDisabled;
- }
-
- private ColorFilter getDisabledColorFilter() {
- if (sDisabledFColorFilter == null) {
- ColorMatrix tempBrightnessMatrix = new ColorMatrix();
- ColorMatrix tempFilterMatrix = new ColorMatrix();
-
- tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION);
- float scale = 1 - DISABLED_BRIGHTNESS;
- int brightnessI = (int) (255 * DISABLED_BRIGHTNESS);
- float[] mat = tempBrightnessMatrix.getArray();
- mat[0] = scale;
- mat[6] = scale;
- mat[12] = scale;
- mat[4] = brightnessI;
- mat[9] = brightnessI;
- mat[14] = brightnessI;
- mat[18] = mDisabledAlpha;
- tempFilterMatrix.preConcat(tempBrightnessMatrix);
- sDisabledFColorFilter = new ColorMatrixColorFilter(tempFilterMatrix);
- }
- return sDisabledFColorFilter;
- }
-
- /**
- * Updates the paint to reflect the current brightness and saturation.
- */
- protected void updateFilter() {
- mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : null);
- invalidateSelf();
- }
-
- @Override
- public ConstantState getConstantState() {
- return new MyConstantState(mBitmap, mIconColor, mIsDisabled);
- }
-
- protected static class MyConstantState extends ConstantState {
- protected final Bitmap mBitmap;
- protected final int mIconColor;
- protected final boolean mIsDisabled;
-
- public MyConstantState(Bitmap bitmap, int color, boolean isDisabled) {
- mBitmap = bitmap;
- mIconColor = color;
- mIsDisabled = isDisabled;
- }
-
- @Override
- public FastBitmapDrawable newDrawable() {
- return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled);
- }
-
- @Override
- public int getChangingConfigurations() {
- return 0;
- }
- }
-
- /**
- * Interface to be implemented by custom {@link BitmapInfo} to handle drawable construction
- */
- public interface Factory {
-
- /**
- * Called to create a new drawable
- */
- FastBitmapDrawable newDrawable();
- }
-
- /**
- * Returns a FastBitmapDrawable with the icon.
- */
- public static FastBitmapDrawable newIcon(Context context, ItemInfoWithIcon info) {
- FastBitmapDrawable drawable = newIcon(context, info.bitmap);
- drawable.setIsDisabled(info.isDisabled());
- return drawable;
- }
-
- /**
- * Creates a drawable for the provided BitmapInfo
- */
- public static FastBitmapDrawable newIcon(Context context, BitmapInfo info) {
- final FastBitmapDrawable drawable;
- if (info instanceof Factory) {
- drawable = ((Factory) info).newDrawable();
- } else if (info.isLowRes()) {
- drawable = new PlaceHolderIconDrawable(info, context);
- } else {
- drawable = new FastBitmapDrawable(info);
- }
- drawable.mDisabledAlpha = Themes.getFloat(context, R.attr.disabledIconAlpha, 1f);
- return drawable;
- }
-}
diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
index 6c5bc40..a199a57 100644
--- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java
+++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
@@ -15,7 +15,7 @@
*/
package com.android.launcher3;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
deleted file mode 100644
index e5aecf7..0000000
--- a/src/com/android/launcher3/FocusHelper.java
+++ /dev/null
@@ -1,567 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.util.Log;
-import android.view.KeyEvent;
-import android.view.SoundEffectConstants;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderPagedView;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.util.FocusLogic;
-import com.android.launcher3.util.Thunk;
-
-/**
- * A keyboard listener we set on all the workspace icons.
- */
-class IconKeyEventListener implements View.OnKeyListener {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- return FocusHelper.handleIconKeyEvent(v, keyCode, event);
- }
-}
-
-/**
- * A keyboard listener we set on all the hotseat buttons.
- */
-class HotseatIconKeyEventListener implements View.OnKeyListener {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event);
- }
-}
-
-/**
- * A keyboard listener we set on full screen pages (e.g. custom content).
- */
-class FullscreenKeyEventListener implements View.OnKeyListener {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
- || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || keyCode == KeyEvent.KEYCODE_PAGE_UP) {
- // Handle the key event just like a workspace icon would in these cases. In this case,
- // it will basically act as if there is a single icon in the top left (so you could
- // think of the fullscreen page as a focusable fullscreen widget).
- return FocusHelper.handleIconKeyEvent(v, keyCode, event);
- }
- return false;
- }
-}
-
-/**
- * TODO: Reevaluate if this is still required
- */
-public class FocusHelper {
-
- private static final String TAG = "FocusHelper";
- private static final boolean DEBUG = false;
-
- /**
- * Handles key events in paged folder.
- */
- public static class PagedFolderKeyEventListener implements View.OnKeyListener {
-
- private final Folder mFolder;
-
- public PagedFolderKeyEventListener(Folder folder) {
- mFolder = folder;
- }
-
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent e) {
- boolean consume = FocusLogic.shouldConsume(keyCode);
- if (e.getAction() == KeyEvent.ACTION_UP) {
- return consume;
- }
- if (DEBUG) {
- Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].",
- KeyEvent.keyCodeToString(keyCode)));
- }
-
- if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
- if (FeatureFlags.IS_STUDIO_BUILD) {
- throw new IllegalStateException("Parent of the focused item is not supported.");
- } else {
- return false;
- }
- }
-
- // Initialize variables.
- final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent();
- final CellLayout cellLayout = (CellLayout) itemContainer.getParent();
-
- final int iconIndex = itemContainer.indexOfChild(v);
- final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent();
-
- final int pageIndex = pagedView.indexOfChild(cellLayout);
- final int pageCount = pagedView.getPageCount();
- final boolean isLayoutRtl = Utilities.isRtl(v.getResources());
-
- int[][] matrix = FocusLogic.createSparseMatrix(cellLayout);
- // Process focus.
- int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
- pageCount, isLayoutRtl);
- if (newIconIndex == FocusLogic.NOOP) {
- handleNoopKey(keyCode, v);
- return consume;
- }
- ShortcutAndWidgetContainer newParent = null;
- View child = null;
-
- switch (newIconIndex) {
- case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
- case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
- newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
- if (newParent != null) {
- int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
- pagedView.snapToPage(pageIndex - 1);
- child = newParent.getChildAt(
- ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN)
- ^ newParent.invertLayoutHorizontally()) ? 0 : matrix.length - 1,
- row);
- }
- break;
- case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
- newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
- if (newParent != null) {
- pagedView.snapToPage(pageIndex - 1);
- child = newParent.getChildAt(0, 0);
- }
- break;
- case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
- newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
- if (newParent != null) {
- pagedView.snapToPage(pageIndex - 1);
- child = newParent.getChildAt(matrix.length - 1, matrix[0].length - 1);
- }
- break;
- case FocusLogic.NEXT_PAGE_FIRST_ITEM:
- newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
- if (newParent != null) {
- pagedView.snapToPage(pageIndex + 1);
- child = newParent.getChildAt(0, 0);
- }
- break;
- case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
- case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
- newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
- if (newParent != null) {
- pagedView.snapToPage(pageIndex + 1);
- child = FocusLogic.getAdjacentChildInNextFolderPage(
- newParent, v, newIconIndex);
- }
- break;
- case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
- child = cellLayout.getChildAt(0, 0);
- break;
- case FocusLogic.CURRENT_PAGE_LAST_ITEM:
- child = pagedView.getLastItem();
- break;
- default: // Go to some item on the current page.
- child = itemContainer.getChildAt(newIconIndex);
- break;
- }
- if (child != null) {
- child.requestFocus();
- playSoundEffect(keyCode, v);
- } else {
- handleNoopKey(keyCode, v);
- }
- return consume;
- }
-
- public void handleNoopKey(int keyCode, View v) {
- if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
- mFolder.mFolderName.requestFocus();
- playSoundEffect(keyCode, v);
- }
- }
- }
-
- /**
- * Handles key events in the workspace hotseat (bottom of the screen).
- * <p>Currently we don't special case for the phone UI in different orientations, even though
- * the hotseat is on the side in landscape mode. This is to ensure that accessibility
- * consistency is maintained across rotations.
- */
- static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
- boolean consume = FocusLogic.shouldConsume(keyCode);
- if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
- return consume;
- }
-
- final Launcher launcher = Launcher.getLauncher(v.getContext());
- final DeviceProfile profile = launcher.getDeviceProfile();
-
- if (DEBUG) {
- Log.v(TAG, String.format(
- "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
- KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
- }
-
- // Initialize the variables.
- final Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
- final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
- final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
-
- final ItemInfo itemInfo = (ItemInfo) v.getTag();
- int pageIndex = workspace.getNextPage();
- int pageCount = workspace.getChildCount();
- int iconIndex = hotseatParent.indexOfChild(v);
- int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
- .getChildAt(iconIndex).getLayoutParams()).cellX;
-
- final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
- if (iconLayout == null) {
- // This check is to guard against cases where key strokes rushes in when workspace
- // child creation/deletion is still in flux. (e.g., during drop or fling
- // animation.)
- return consume;
- }
- final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
-
- ViewGroup parent = null;
- int[][] matrix = null;
-
- if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
- !profile.isVerticalBarLayout()) {
- matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
- iconIndex += iconParent.getChildCount();
- parent = iconParent;
- } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
- profile.isVerticalBarLayout()) {
- matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
- iconIndex += iconParent.getChildCount();
- parent = iconParent;
- } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
- profile.isVerticalBarLayout()) {
- keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
- } else {
- // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
- // matrix extended with hotseat.
- matrix = FocusLogic.createSparseMatrix(hotseatLayout);
- parent = hotseatParent;
- }
-
- // Process the focus.
- int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
- pageCount, Utilities.isRtl(v.getResources()));
-
- View newIcon = null;
- switch (newIconIndex) {
- case FocusLogic.NEXT_PAGE_FIRST_ITEM:
- parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
- newIcon = parent.getChildAt(0);
- // TODO(hyunyoungs): handle cases where the child is not an icon but
- // a folder or a widget.
- workspace.snapToPage(pageIndex + 1);
- break;
- case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
- parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
- newIcon = parent.getChildAt(0);
- // TODO(hyunyoungs): handle cases where the child is not an icon but
- // a folder or a widget.
- workspace.snapToPage(pageIndex - 1);
- break;
- case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
- parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
- newIcon = parent.getChildAt(parent.getChildCount() - 1);
- // TODO(hyunyoungs): handle cases where the child is not an icon but
- // a folder or a widget.
- workspace.snapToPage(pageIndex - 1);
- break;
- case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
- case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
- // Go to the previous page but keep the focus on the same hotseat icon.
- workspace.snapToPage(pageIndex - 1);
- break;
- case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
- case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
- // Go to the next page but keep the focus on the same hotseat icon.
- workspace.snapToPage(pageIndex + 1);
- break;
- }
- if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
- newIconIndex -= iconParent.getChildCount();
- }
- if (parent != null) {
- if (newIcon == null && newIconIndex >= 0) {
- newIcon = parent.getChildAt(newIconIndex);
- }
- if (newIcon != null) {
- newIcon.requestFocus();
- playSoundEffect(keyCode, v);
- }
- }
- return consume;
- }
-
- /**
- * Handles key events in a workspace containing icons.
- */
- static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
- boolean consume = FocusLogic.shouldConsume(keyCode);
- if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
- return consume;
- }
-
- Launcher launcher = Launcher.getLauncher(v.getContext());
- DeviceProfile profile = launcher.getDeviceProfile();
-
- if (DEBUG) {
- Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
- KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
- }
-
- // Initialize the variables.
- ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
- CellLayout iconLayout = (CellLayout) parent.getParent();
- final Workspace workspace = (Workspace) iconLayout.getParent();
- final ViewGroup dragLayer = (ViewGroup) workspace.getParent();
- final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.drop_target_bar);
- final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
-
- final ItemInfo itemInfo = (ItemInfo) v.getTag();
- final int iconIndex = parent.indexOfChild(v);
- final int pageIndex = workspace.indexOfChild(iconLayout);
- final int pageCount = workspace.getChildCount();
-
- CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
- ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
- int[][] matrix;
-
- // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
- // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
- // with the hotseat.
- if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
- matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
- } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
- profile.isVerticalBarLayout()) {
- matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
- } else {
- matrix = FocusLogic.createSparseMatrix(iconLayout);
- }
-
- // Process the focus.
- int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
- pageCount, Utilities.isRtl(v.getResources()));
- boolean isRtl = Utilities.isRtl(v.getResources());
- View newIcon = null;
- CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex);
- switch (newIconIndex) {
- case FocusLogic.NOOP:
- if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
- newIcon = tabs;
- }
- break;
- case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
- case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
- int newPageIndex = pageIndex - 1;
- if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) {
- newPageIndex = pageIndex + 1;
- }
- int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
- parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
- if (parent != null) {
- iconLayout = (CellLayout) parent.getParent();
- matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout,
- iconLayout.getCountX(), row);
- newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
- newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
- if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
- newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
- isRtl);
- } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
- newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
- isRtl);
- } else {
- newIcon = parent.getChildAt(newIconIndex);
- }
- }
- break;
- case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
- workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
- newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
- if (newIcon == null) {
- // Check the hotseat if no focusable item was found on the workspace.
- newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
- workspace.snapToPage(pageIndex - 1);
- }
- break;
- case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
- newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl);
- break;
- case FocusLogic.NEXT_PAGE_FIRST_ITEM:
- newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl);
- break;
- case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
- case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
- newPageIndex = pageIndex + 1;
- if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) {
- newPageIndex = pageIndex - 1;
- }
- row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
- parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
- if (parent != null) {
- iconLayout = (CellLayout) parent.getParent();
- matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, -1, row);
- newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
- newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
- if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
- newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
- isRtl);
- } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
- newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
- isRtl);
- } else {
- newIcon = parent.getChildAt(newIconIndex);
- }
- }
- break;
- case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
- newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
- if (newIcon == null) {
- // Check the hotseat if no focusable item was found on the workspace.
- newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
- }
- break;
- case FocusLogic.CURRENT_PAGE_LAST_ITEM:
- newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
- if (newIcon == null) {
- // Check the hotseat if no focusable item was found on the workspace.
- newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout, isRtl);
- }
- break;
- default:
- // current page, some item.
- if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
- newIcon = parent.getChildAt(newIconIndex);
- } else if (parent.getChildCount() <= newIconIndex &&
- newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
- newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
- }
- break;
- }
- if (newIcon != null) {
- newIcon.requestFocus();
- playSoundEffect(keyCode, v);
- }
- return consume;
- }
-
- //
- // Helper methods.
- //
-
- /**
- * Private helper method to get the CellLayoutChildren given a CellLayout index.
- */
- @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
- ViewGroup container, int i) {
- CellLayout parent = (CellLayout) container.getChildAt(i);
- return parent.getShortcutsAndWidgets();
- }
-
- /**
- * Helper method to be used for playing sound effects.
- */
- @Thunk static void playSoundEffect(int keyCode, View v) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_LEFT:
- v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
- break;
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_PAGE_DOWN:
- case KeyEvent.KEYCODE_MOVE_END:
- v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
- break;
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_PAGE_UP:
- case KeyEvent.KEYCODE_MOVE_HOME:
- v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
- break;
- default:
- break;
- }
- }
-
- private static View handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout,
- int pageIndex, boolean isRtl) {
- if (pageIndex - 1 < 0) {
- return null;
- }
- CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
- View newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
- if (newIcon == null) {
- // Check the hotseat if no focusable item was found on the workspace.
- newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout,isRtl);
- workspace.snapToPage(pageIndex - 1);
- }
- return newIcon;
- }
-
- private static View handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout,
- int pageIndex, boolean isRtl) {
- if (pageIndex + 1 >= workspace.getPageCount()) {
- return null;
- }
- CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex + 1);
- View newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
- if (newIcon == null) {
- // Check the hotseat if no focusable item was found on the workspace.
- newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
- workspace.snapToPage(pageIndex + 1);
- }
- return newIcon;
- }
-
- private static View getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl) {
- View icon;
- int countX = cellLayout.getCountX();
- for (int y = 0; y < cellLayout.getCountY(); y++) {
- int increment = isRtl ? -1 : 1;
- for (int x = isRtl ? countX - 1 : 0; 0 <= x && x < countX; x += increment) {
- if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
- return icon;
- }
- }
- }
- return null;
- }
-
- private static View getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout,
- boolean isRtl) {
- View icon;
- int countX = cellLayout.getCountX();
- for (int y = cellLayout.getCountY() - 1; y >= 0; y--) {
- int increment = isRtl ? 1 : -1;
- for (int x = isRtl ? 0 : countX - 1; 0 <= x && x < countX; x += increment) {
- if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
- return icon;
- }
- }
- }
- return null;
- }
-}
diff --git a/src/com/android/launcher3/GestureNavContract.java b/src/com/android/launcher3/GestureNavContract.java
new file mode 100644
index 0000000..2a7e629
--- /dev/null
+++ b/src/com/android/launcher3/GestureNavContract.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.SurfaceControl;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Class to encapsulate the handshake protocol between Launcher and gestureNav.
+ */
+public class GestureNavContract {
+
+ private static final String TAG = "GestureNavContract";
+
+ public static final String EXTRA_GESTURE_CONTRACT = "gesture_nav_contract_v1";
+ public static final String EXTRA_ICON_POSITION = "gesture_nav_contract_icon_position";
+ public static final String EXTRA_ICON_SURFACE = "gesture_nav_contract_surface_control";
+ public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK";
+
+ public final ComponentName componentName;
+ public final UserHandle user;
+
+ private final Message mCallback;
+
+ public GestureNavContract(ComponentName componentName, UserHandle user, Message callback) {
+ this.componentName = componentName;
+ this.user = user;
+ this.mCallback = callback;
+ }
+
+ /**
+ * Sends the position information to the receiver
+ */
+ @TargetApi(Build.VERSION_CODES.R)
+ public void sendEndPosition(RectF position, @Nullable SurfaceControl surfaceControl) {
+ Bundle result = new Bundle();
+ result.putParcelable(EXTRA_ICON_POSITION, position);
+ result.putParcelable(EXTRA_ICON_SURFACE, surfaceControl);
+
+ Message callback = Message.obtain();
+ callback.copyFrom(mCallback);
+ callback.setData(result);
+
+ try {
+ callback.replyTo.send(callback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending icon position", e);
+ }
+ }
+
+ /**
+ * Clears and returns the GestureNavContract if it was present in the intent.
+ */
+ public static GestureNavContract fromIntent(Intent intent) {
+ if (!Utilities.ATLEAST_R) {
+ return null;
+ }
+ Bundle extras = intent.getBundleExtra(EXTRA_GESTURE_CONTRACT);
+ if (extras == null) {
+ return null;
+ }
+ intent.removeExtra(EXTRA_GESTURE_CONTRACT);
+
+ ComponentName componentName = extras.getParcelable(EXTRA_COMPONENT_NAME);
+ UserHandle userHandle = extras.getParcelable(EXTRA_USER);
+ Message callback = extras.getParcelable(EXTRA_REMOTE_CALLBACK);
+
+ if (componentName != null && userHandle != null && callback != null
+ && callback.replyTo != null) {
+ return new GestureNavContract(componentName, userHandle, callback);
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 51f3819..eaca162 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -16,30 +16,40 @@
package com.android.launcher3;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.Gravity;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.widget.FrameLayout;
-import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import androidx.annotation.Nullable;
-import java.util.ArrayList;
+import java.util.function.Consumer;
-public class Hotseat extends CellLayout implements LogContainerProvider, Insettable {
+/**
+ * View class that represents the bottom row of the home screen.
+ */
+public class Hotseat extends CellLayout implements Insettable {
+
+ // Ratio of empty space, qsb should take up to appear visually centered.
+ public static final float QSB_CENTER_FACTOR = .325f;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mHasVerticalHotseat;
private Workspace mWorkspace;
private boolean mSendTouchToWorkspace;
+ @Nullable
+ private Consumer<Boolean> mOnVisibilityAggregatedCallback;
+
+ private final View mQsb;
+ private final int mQsbHeight;
+
+ private final int mTaskbarViewHeight;
public Hotseat(Context context) {
this(context, null);
@@ -51,6 +61,12 @@
public Hotseat(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+
+ mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
+ mQsbHeight = mQsb.getLayoutParams().height;
+ addView(mQsb);
+
+ mTaskbarViewHeight = context.getResources().getDimensionPixelSize(R.dimen.taskbar_size);
}
/**
@@ -70,29 +86,21 @@
public void resetLayout(boolean hasVerticalHotseat) {
removeAllViewsInLayout();
mHasVerticalHotseat = hasVerticalHotseat;
- InvariantDeviceProfile idp = mActivity.getDeviceProfile().inv;
+ DeviceProfile dp = mActivity.getDeviceProfile();
if (hasVerticalHotseat) {
- setGridSize(1, idp.numHotseatIcons);
+ setGridSize(1, dp.numShownHotseatIcons);
} else {
- setGridSize(idp.numHotseatIcons, 1);
+ setGridSize(dp.numShownHotseatIcons, 1);
}
}
@Override
- public void fillInLogContainerData(ItemInfo childInfo, Target child,
- ArrayList<Target> parents) {
- child.rank = childInfo.rank;
- child.gridX = childInfo.cellX;
- child.gridY = childInfo.cellY;
- parents.add(newContainerTarget(LauncherLogProto.ContainerType.HOTSEAT));
- }
-
- @Override
public void setInsets(Rect insets) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
DeviceProfile grid = mActivity.getDeviceProfile();
if (grid.isVerticalBarLayout()) {
+ mQsb.setVisibility(View.GONE);
lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
if (grid.isSeascape()) {
lp.gravity = Gravity.LEFT;
@@ -102,12 +110,21 @@
lp.width = grid.hotseatBarSizePx + insets.right;
}
} else {
+ mQsb.setVisibility(View.VISIBLE);
lp.gravity = Gravity.BOTTOM;
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
- lp.height = grid.hotseatBarSizePx + insets.bottom;
+ lp.height = (grid.isTaskbarPresent
+ ? grid.workspacePadding.bottom
+ : grid.hotseatBarSizePx)
+ + (grid.isTaskbarPresent ? grid.taskbarSize : insets.bottom);
}
- Rect padding = grid.getHotseatLayoutPadding();
- setPadding(padding.left, padding.top, padding.right, padding.bottom);
+
+ if (!grid.isTaskbarPresent) {
+ // When taskbar is present, we set the padding separately to ensure a seamless visual
+ // handoff between taskbar and hotseat during drag and drop.
+ Rect padding = grid.getHotseatLayoutPadding();
+ setPadding(padding.left, padding.top, padding.right, padding.bottom);
+ }
setLayoutParams(lp);
InsettableFrameLayout.dispatchInsets(this, insets);
@@ -144,4 +161,80 @@
}
return event.getY() > getCellHeight();
}
+
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+
+ if (mOnVisibilityAggregatedCallback != null) {
+ mOnVisibilityAggregatedCallback.accept(isVisible);
+ }
+ }
+
+ /** Sets a callback to be called onVisibilityAggregated */
+ public void setOnVisibilityAggregatedCallback(@Nullable Consumer<Boolean> callback) {
+ mOnVisibilityAggregatedCallback = callback;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int width = getShortcutsAndWidgets().getMeasuredWidth();
+ mQsb.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mQsbHeight, MeasureSpec.EXACTLY));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ int qsbWidth = mQsb.getMeasuredWidth();
+ int left = (r - l - qsbWidth) / 2;
+ int right = left + qsbWidth;
+
+ int bottom = b - t - getQsbOffsetY();
+ int top = bottom - mQsbHeight;
+ mQsb.layout(left, top, right, bottom);
+ }
+
+ /**
+ * Returns the number of pixels the QSB is translated from the bottom of the screen.
+ */
+ private int getQsbOffsetY() {
+ DeviceProfile dp = mActivity.getDeviceProfile();
+ int freeSpace = dp.isTaskbarPresent
+ ? dp.workspacePadding.bottom
+ : dp.hotseatBarSizePx - dp.hotseatCellHeightPx - mQsbHeight;
+
+ if (dp.isScalableGrid && dp.qsbBottomMarginPx > dp.getInsets().bottom) {
+ return Math.min(dp.qsbBottomMarginPx, freeSpace);
+ } else {
+ return (int) (freeSpace * QSB_CENTER_FACTOR) + (dp.isTaskbarPresent
+ ? dp.taskbarSize
+ : dp.getInsets().bottom);
+ }
+ }
+
+ /**
+ * Returns the number of pixels the taskbar is translated from the bottom of the screen.
+ */
+ public int getTaskbarOffsetY() {
+ return (getQsbOffsetY() - mTaskbarViewHeight) / 2;
+ }
+
+ /**
+ * Sets the alpha value of just our ShortcutAndWidgetContainer.
+ */
+ public void setIconsAlpha(float alpha) {
+ getShortcutsAndWidgets().setAlpha(alpha);
+ }
+
+ /**
+ * Returns the QSB inside hotseat
+ */
+ public View getQsb() {
+ return mQsb;
+ }
+
}
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
deleted file mode 100644
index 62c9b4d..0000000
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ /dev/null
@@ -1,672 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.util.Executors.MODEL_EXECUTOR;
-
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ActivityInfo;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.Parcelable;
-import android.os.Process;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.GraphicsUtils;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.Thunk;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.json.JSONStringer;
-
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-
-public class InstallShortcutReceiver extends BroadcastReceiver {
-
- public static final int FLAG_ACTIVITY_PAUSED = 1;
- public static final int FLAG_LOADER_RUNNING = 2;
- public static final int FLAG_DRAG_AND_DROP = 4;
-
- // Determines whether to defer installing shortcuts immediately until
- // processAllPendingInstalls() is called.
- private static int sInstallQueueDisabledFlags = 0;
-
- private static final String TAG = "InstallShortcutReceiver";
- private static final boolean DBG = false;
-
- private static final String ACTION_INSTALL_SHORTCUT =
- "com.android.launcher.action.INSTALL_SHORTCUT";
-
- private static final String LAUNCH_INTENT_KEY = "intent.launch";
- private static final String NAME_KEY = "name";
- private static final String ICON_KEY = "icon";
- private static final String ICON_RESOURCE_NAME_KEY = "iconResource";
- private static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage";
-
- private static final String APP_SHORTCUT_TYPE_KEY = "isAppShortcut";
- private static final String DEEPSHORTCUT_TYPE_KEY = "isDeepShortcut";
- private static final String APP_WIDGET_TYPE_KEY = "isAppWidget";
- private static final String USER_HANDLE_KEY = "userHandle";
-
- // The set of shortcuts that are pending install
- private static final String APPS_PENDING_INSTALL = "apps_to_install";
-
- public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
- public static final int NEW_SHORTCUT_STAGGER_DELAY = 85;
-
- @WorkerThread
- private static void addToQueue(Context context, PendingInstallShortcutInfo info) {
- String encoded = info.encodeToString();
- SharedPreferences prefs = Utilities.getPrefs(context);
- Set<String> strings = prefs.getStringSet(APPS_PENDING_INSTALL, null);
- strings = (strings != null) ? new HashSet<>(strings) : new HashSet<>(1);
- strings.add(encoded);
- prefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).apply();
- }
-
- @WorkerThread
- private static void flushQueueInBackground(Context context) {
- if (Launcher.ACTIVITY_TRACKER.getCreatedActivity() == null) {
- // Launcher not loaded
- return;
- }
-
- ArrayList<Pair<ItemInfo, Object>> installQueue = new ArrayList<>();
- SharedPreferences prefs = Utilities.getPrefs(context);
- Set<String> strings = prefs.getStringSet(APPS_PENDING_INSTALL, null);
- if (DBG) Log.d(TAG, "Getting and clearing APPS_PENDING_INSTALL: " + strings);
- if (strings == null) {
- return;
- }
-
- LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
- for (String encoded : strings) {
- PendingInstallShortcutInfo info = decode(encoded, context);
- if (info == null) {
- continue;
- }
-
- String pkg = getIntentPackage(info.launchIntent);
- if (!TextUtils.isEmpty(pkg)
- && !launcherApps.isPackageEnabled(pkg, info.user)
- && !info.isActivity) {
- if (DBG) {
- Log.d(TAG, "Ignoring shortcut for absent package: " + info.launchIntent);
- }
- continue;
- }
-
- // Generate a shortcut info to add into the model
- installQueue.add(info.getItemInfo());
- }
- prefs.edit().remove(APPS_PENDING_INSTALL).apply();
- if (!installQueue.isEmpty()) {
- LauncherAppState.getInstance(context).getModel()
- .addAndBindAddedWorkspaceItems(installQueue);
- }
- }
-
- public static void removeFromInstallQueue(Context context, HashSet<String> packageNames,
- UserHandle user) {
- if (packageNames.isEmpty()) {
- return;
- }
- Preconditions.assertWorkerThread();
-
- SharedPreferences sp = Utilities.getPrefs(context);
- Set<String> strings = sp.getStringSet(APPS_PENDING_INSTALL, null);
- if (DBG) {
- Log.d(TAG, "APPS_PENDING_INSTALL: " + strings
- + ", removing packages: " + packageNames);
- }
- if (strings == null || ((Collection) strings).isEmpty()) {
- return;
- }
- Set<String> newStrings = new HashSet<>(strings);
- Iterator<String> newStringsIter = newStrings.iterator();
- while (newStringsIter.hasNext()) {
- String encoded = newStringsIter.next();
- try {
- Decoder decoder = new Decoder(encoded, context);
- if (packageNames.contains(getIntentPackage(decoder.launcherIntent)) &&
- user.equals(decoder.user)) {
- newStringsIter.remove();
- }
- } catch (JSONException | URISyntaxException e) {
- Log.d(TAG, "Exception reading shortcut to add: " + e);
- newStringsIter.remove();
- }
- }
- sp.edit().putStringSet(APPS_PENDING_INSTALL, newStrings).apply();
- }
-
- public void onReceive(Context context, Intent data) {
- if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) {
- return;
- }
- PendingInstallShortcutInfo info = createPendingInfo(context, data);
- if (info != null) {
- if (!info.isLauncherActivity()) {
- // Since its a custom shortcut, verify that it is safe to launch.
- if (!new PackageManagerHelper(context).hasPermissionForActivity(
- info.launchIntent, null)) {
- // Target cannot be launched, or requires some special permission to launch
- Log.e(TAG, "Ignoring malicious intent " + info.launchIntent.toUri(0));
- return;
- }
- }
- queuePendingShortcutInfo(info, context);
- }
- }
-
- /**
- * @return true is the extra is either null or is of type {@param type}
- */
- private static boolean isValidExtraType(Intent intent, String key, Class type) {
- Object extra = intent.getParcelableExtra(key);
- return extra == null || type.isInstance(extra);
- }
-
- /**
- * Verifies the intent and creates a {@link PendingInstallShortcutInfo}
- */
- private static PendingInstallShortcutInfo createPendingInfo(Context context, Intent data) {
- if (!isValidExtraType(data, Intent.EXTRA_SHORTCUT_INTENT, Intent.class) ||
- !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
- Intent.ShortcutIconResource.class)) ||
- !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON, Bitmap.class))) {
-
- if (DBG) Log.e(TAG, "Invalid install shortcut intent");
- return null;
- }
-
- PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(
- data, Process.myUserHandle(), context);
- if (info.launchIntent == null || info.label == null) {
- if (DBG) Log.e(TAG, "Invalid install shortcut intent");
- return null;
- }
-
- return convertToLauncherActivityIfPossible(info);
- }
-
- public static WorkspaceItemInfo fromShortcutIntent(Context context, Intent data) {
- PendingInstallShortcutInfo info = createPendingInfo(context, data);
- return info == null ? null : (WorkspaceItemInfo) info.getItemInfo().first;
- }
-
- public static void queueShortcut(ShortcutInfo info, Context context) {
- queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context);
- }
-
- public static void queueWidget(AppWidgetProviderInfo info, int widgetId, Context context) {
- queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, widgetId, context), context);
- }
-
- public static void queueApplication(Intent data, UserHandle user, Context context) {
- queuePendingShortcutInfo(new PendingInstallShortcutInfo(data, context, user),
- context);
- }
-
- public static HashSet<ShortcutKey> getPendingShortcuts(Context context) {
- HashSet<ShortcutKey> result = new HashSet<>();
-
- Set<String> strings = Utilities.getPrefs(context).getStringSet(APPS_PENDING_INSTALL, null);
- if (strings == null || ((Collection) strings).isEmpty()) {
- return result;
- }
-
- for (String encoded : strings) {
- try {
- Decoder decoder = new Decoder(encoded, context);
- if (decoder.optBoolean(DEEPSHORTCUT_TYPE_KEY)) {
- result.add(ShortcutKey.fromIntent(decoder.launcherIntent, decoder.user));
- }
- } catch (JSONException | URISyntaxException e) {
- Log.d(TAG, "Exception reading shortcut to add: " + e);
- }
- }
- return result;
- }
-
- private static void queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context) {
- // Queue the item up for adding if launcher has not loaded properly yet
- MODEL_EXECUTOR.post(() -> addToQueue(context, info));
- flushInstallQueue(context);
- }
-
- public static void enableInstallQueue(int flag) {
- sInstallQueueDisabledFlags |= flag;
- }
- public static void disableAndFlushInstallQueue(int flag, Context context) {
- sInstallQueueDisabledFlags &= ~flag;
- flushInstallQueue(context);
- }
-
- static void flushInstallQueue(Context context) {
- if (sInstallQueueDisabledFlags != 0) {
- return;
- }
- MODEL_EXECUTOR.post(() -> flushQueueInBackground(context));
- }
-
- /**
- * Ensures that we have a valid, non-null name. If the provided name is null, we will return
- * the application name instead.
- */
- @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) {
- if (name == null) {
- try {
- PackageManager pm = context.getPackageManager();
- ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0);
- name = info.loadLabel(pm);
- } catch (PackageManager.NameNotFoundException nnfe) {
- return "";
- }
- }
- return name;
- }
-
- private static class PendingInstallShortcutInfo {
-
- final boolean isActivity;
- @Nullable final ShortcutInfo shortcutInfo;
- @Nullable final AppWidgetProviderInfo providerInfo;
-
- @Nullable final Intent data;
- final Context mContext;
- final Intent launchIntent;
- final String label;
- final UserHandle user;
-
- /**
- * Initializes a PendingInstallShortcutInfo received from a different app.
- */
- public PendingInstallShortcutInfo(Intent data, UserHandle user, Context context) {
- isActivity = false;
- shortcutInfo = null;
- providerInfo = null;
-
- this.data = data;
- this.user = user;
- mContext = context;
-
- launchIntent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
- label = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
- }
-
- /**
- * Initializes a PendingInstallShortcutInfo to represent a launcher target.
- */
- public PendingInstallShortcutInfo(LauncherActivityInfo info, Context context) {
- isActivity = true;
- shortcutInfo = null;
- providerInfo = null;
-
- String packageName = info.getComponentName().getPackageName();
- data = new Intent();
- data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent().setComponent(
- new ComponentName(packageName, "")).setPackage(packageName));
- data.putExtra(Intent.EXTRA_SHORTCUT_NAME, info.getLabel());
-
- user = info.getUser();
- mContext = context;
-
- launchIntent = AppInfo.makeLaunchIntent(info);
- label = info.getLabel().toString();
- }
-
- /**
- * Initializes a PendingInstallShortcutInfo to represent a launcher target.
- */
- public PendingInstallShortcutInfo(Intent data, Context context, UserHandle user) {
- isActivity = true;
- shortcutInfo = null;
- providerInfo = null;
-
- this.data = data;
- this.user = user;
- mContext = context;
-
- launchIntent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
- label = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
- }
-
- /**
- * Initializes a PendingInstallShortcutInfo to represent a launcher target.
- */
- public PendingInstallShortcutInfo(ShortcutInfo info, Context context) {
- isActivity = false;
- shortcutInfo = info;
- providerInfo = null;
-
- data = null;
- mContext = context;
- user = info.getUserHandle();
-
- launchIntent = ShortcutKey.makeIntent(info);
- label = info.getShortLabel().toString();
- }
-
- /**
- * Initializes a PendingInstallShortcutInfo to represent a launcher target.
- */
- public PendingInstallShortcutInfo(
- AppWidgetProviderInfo info, int widgetId, Context context) {
- isActivity = false;
- shortcutInfo = null;
- providerInfo = info;
-
- data = null;
- mContext = context;
- user = info.getProfile();
-
- launchIntent = new Intent().setComponent(info.provider)
- .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
- label = info.label;
- }
-
- public String encodeToString() {
- try {
- if (shortcutInfo != null) {
- // If it a launcher target, we only need component name, and user to
- // recreate this.
- return new JSONStringer()
- .object()
- .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
- .key(DEEPSHORTCUT_TYPE_KEY).value(true)
- .key(USER_HANDLE_KEY).value(UserCache.INSTANCE.get(mContext)
- .getSerialNumberForUser(user))
- .endObject().toString();
- } else if (providerInfo != null) {
- // If it a launcher target, we only need component name, and user to
- // recreate this.
- return new JSONStringer()
- .object()
- .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
- .key(APP_WIDGET_TYPE_KEY).value(true)
- .key(USER_HANDLE_KEY).value(UserCache.INSTANCE.get(mContext)
- .getSerialNumberForUser(user))
- .endObject().toString();
- }
-
- if (launchIntent.getAction() == null) {
- launchIntent.setAction(Intent.ACTION_VIEW);
- } else if (launchIntent.getAction().equals(Intent.ACTION_MAIN) &&
- launchIntent.getCategories() != null &&
- launchIntent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
- launchIntent.addFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
- }
-
- // This name is only used for comparisons and notifications, so fall back to activity
- // name if not supplied
- String name = ensureValidName(mContext, launchIntent, label).toString();
- Bitmap icon = data == null ? null
- : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
- Intent.ShortcutIconResource iconResource = data == null ? null
- : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
-
- // Only encode the parameters which are supported by the API.
- JSONStringer json = new JSONStringer()
- .object()
- .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
- .key(NAME_KEY).value(name)
- .key(USER_HANDLE_KEY).value(
- UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user))
- .key(APP_SHORTCUT_TYPE_KEY).value(isActivity);
- if (icon != null) {
- byte[] iconByteArray = GraphicsUtils.flattenBitmap(icon);
- if (iconByteArray != null) {
- json = json.key(ICON_KEY).value(
- Base64.encodeToString(
- iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
- }
- }
- if (iconResource != null) {
- json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName);
- json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY)
- .value(iconResource.packageName);
- }
- return json.endObject().toString();
- } catch (JSONException e) {
- Log.d(TAG, "Exception when adding shortcut: " + e);
- return null;
- }
- }
-
- public Pair<ItemInfo, Object> getItemInfo() {
- if (isActivity) {
- WorkspaceItemInfo si = createWorkspaceItemInfo(data, user,
- LauncherAppState.getInstance(mContext));
- si.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
- si.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
- return Pair.create(si, null);
- } else if (shortcutInfo != null) {
- WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(shortcutInfo, mContext);
- LauncherAppState.getInstance(mContext).getIconCache().getShortcutIcon(
- itemInfo, shortcutInfo);
- return Pair.create(itemInfo, shortcutInfo);
- } else if (providerInfo != null) {
- LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo
- .fromProviderInfo(mContext, providerInfo);
- LauncherAppWidgetInfo widgetInfo = new LauncherAppWidgetInfo(
- launchIntent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0),
- info.provider);
- InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
- widgetInfo.minSpanX = info.minSpanX;
- widgetInfo.minSpanY = info.minSpanY;
- widgetInfo.spanX = Math.min(info.spanX, idp.numColumns);
- widgetInfo.spanY = Math.min(info.spanY, idp.numRows);
- return Pair.create(widgetInfo, providerInfo);
- } else {
- WorkspaceItemInfo itemInfo =
- createWorkspaceItemInfo(data, user, LauncherAppState.getInstance(mContext));
- return Pair.create(itemInfo, null);
- }
- }
-
- public boolean isLauncherActivity() {
- return isActivity;
- }
- }
-
- private static String getIntentPackage(Intent intent) {
- return intent.getComponent() == null
- ? intent.getPackage() : intent.getComponent().getPackageName();
- }
-
- private static PendingInstallShortcutInfo decode(String encoded, Context context) {
- try {
- Decoder decoder = new Decoder(encoded, context);
- if (decoder.optBoolean(APP_SHORTCUT_TYPE_KEY)) {
- LauncherActivityInfo info = context.getSystemService(LauncherApps.class)
- .resolveActivity(decoder.launcherIntent, decoder.user);
- if (info != null) {
- return new PendingInstallShortcutInfo(info, context);
- }
- } else if (decoder.optBoolean(DEEPSHORTCUT_TYPE_KEY)) {
- List<ShortcutInfo> si = ShortcutKey.fromIntent(decoder.launcherIntent, decoder.user)
- .buildRequest(context)
- .query(ShortcutRequest.ALL);
- if (si.isEmpty()) {
- return null;
- } else {
- return new PendingInstallShortcutInfo(si.get(0), context);
- }
- } else if (decoder.optBoolean(APP_WIDGET_TYPE_KEY)) {
- int widgetId = decoder.launcherIntent
- .getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0);
- AppWidgetProviderInfo info = AppWidgetManager.getInstance(context)
- .getAppWidgetInfo(widgetId);
- if (info == null || !info.provider.equals(decoder.launcherIntent.getComponent()) ||
- !info.getProfile().equals(decoder.user)) {
- return null;
- }
- return new PendingInstallShortcutInfo(info, widgetId, context);
- }
-
- Intent data = new Intent();
- data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, decoder.launcherIntent);
- data.putExtra(Intent.EXTRA_SHORTCUT_NAME, decoder.getString(NAME_KEY));
-
- String iconBase64 = decoder.optString(ICON_KEY);
- String iconResourceName = decoder.optString(ICON_RESOURCE_NAME_KEY);
- String iconResourcePackageName = decoder.optString(ICON_RESOURCE_PACKAGE_NAME_KEY);
- if (iconBase64 != null && !iconBase64.isEmpty()) {
- byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT);
- Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
- data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b);
- } else if (iconResourceName != null && !iconResourceName.isEmpty()) {
- Intent.ShortcutIconResource iconResource =
- new Intent.ShortcutIconResource();
- iconResource.resourceName = iconResourceName;
- iconResource.packageName = iconResourcePackageName;
- data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
- }
-
- if (decoder.optBoolean(APP_SHORTCUT_TYPE_KEY)) {
- return new PendingInstallShortcutInfo(data, context, decoder.user);
- } else {
- return new PendingInstallShortcutInfo(data, decoder.user, context);
- }
- } catch (JSONException | URISyntaxException e) {
- Log.d(TAG, "Exception reading shortcut to add: " + e);
- }
- return null;
- }
-
- private static class Decoder extends JSONObject {
- public final Intent launcherIntent;
- public final UserHandle user;
-
- private Decoder(String encoded, Context context) throws JSONException, URISyntaxException {
- super(encoded);
- launcherIntent = Intent.parseUri(getString(LAUNCH_INTENT_KEY), 0);
- user = has(USER_HANDLE_KEY) ? UserCache.INSTANCE.get(context)
- .getUserForSerialNumber(getLong(USER_HANDLE_KEY))
- : Process.myUserHandle();
- if (user == null) {
- throw new JSONException("Invalid user");
- }
- }
- }
-
- /**
- * Tries to create a new PendingInstallShortcutInfo which represents the same target,
- * but is an app target and not a shortcut.
- * @return the newly created info or the original one.
- */
- private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible(
- PendingInstallShortcutInfo original) {
- if (original.isLauncherActivity()) {
- // Already an activity target
- return original;
- }
- if (!PackageManagerHelper.isLauncherAppTarget(original.launchIntent)) {
- return original;
- }
-
- LauncherActivityInfo info = original.mContext.getSystemService(LauncherApps.class)
- .resolveActivity(original.launchIntent, original.user);
- if (info == null) {
- return original;
- }
- // Ignore any conflicts in the label name, as that can change based on locale.
- return new PendingInstallShortcutInfo(info, original.mContext);
- }
-
- private static WorkspaceItemInfo createWorkspaceItemInfo(Intent data, UserHandle user,
- LauncherAppState app) {
- if (data == null) {
- Log.e(TAG, "Can't construct WorkspaceItemInfo with null data");
- return null;
- }
-
- Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
- String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
- Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
-
- if (intent == null) {
- // If the intent is null, return null as we can't construct a valid WorkspaceItemInfo
- Log.e(TAG, "Can't construct WorkspaceItemInfo with null intent");
- return null;
- }
-
- final WorkspaceItemInfo info = new WorkspaceItemInfo();
- info.user = user;
-
- BitmapInfo iconInfo = null;
- LauncherIcons li = LauncherIcons.obtain(app.getContext());
- if (bitmap instanceof Bitmap) {
- iconInfo = li.createIconBitmap((Bitmap) bitmap);
- } else {
- Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
- if (extra instanceof Intent.ShortcutIconResource) {
- info.iconResource = (Intent.ShortcutIconResource) extra;
- iconInfo = li.createIconBitmap(info.iconResource);
- }
- }
- li.recycle();
-
- if (iconInfo == null) {
- iconInfo = app.getIconCache().getDefaultIcon(info.user);
- }
- info.bitmap = iconInfo;
-
- info.title = Utilities.trim(name);
- info.contentDescription = app.getContext().getPackageManager()
- .getUserBadgedLabel(info.title, info.user);
- info.intent = intent;
- return info;
- }
-
-}
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index e39e89c..29a0223 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -16,18 +16,17 @@
package com.android.launcher3;
-import static com.android.launcher3.Utilities.getDevicePrefs;
+import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.Utilities.getPointString;
-import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME;
+import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
+import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
import android.annotation.TargetApi;
import android.appwidget.AppWidgetHostView;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
@@ -46,13 +45,13 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.util.ConfigMonitor;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.launcher3.util.DefaultDisplay.Info;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.WindowBounds;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -60,6 +59,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
public class InvariantDeviceProfile {
@@ -71,15 +71,13 @@
public static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
public static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
+ private static final int DEFAULT_TRUE = -1;
+ private static final int DEFAULT_SPLIT_DISPLAY = 2;
+
private static final String KEY_IDP_GRID_NAME = "idp_grid_name";
private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
- public static final int CHANGE_FLAG_GRID = 1 << 0;
- public static final int CHANGE_FLAG_ICON_PARAMS = 1 << 1;
-
- public static final String KEY_ICON_PATH_REF = "pref_icon_shape_path";
-
// Constants that affects the interpolation curve between statically defined device profile
// buckets.
private static final float KNEARESTNEIGHBOR = 3;
@@ -88,9 +86,6 @@
// used to offset float not being able to express extremely small weights in extreme cases.
private static final float WEIGHT_EFFICIENT = 100000f;
- private static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
- "config_icon_mask", "string", "android");
-
/**
* Number of icons per row and column in the workspace.
*/
@@ -103,39 +98,59 @@
public int numFolderRows;
public int numFolderColumns;
public float iconSize;
- public String iconShapePath;
public float landscapeIconSize;
+ public float landscapeIconTextSize;
public int iconBitmapSize;
public int fillResIconDpi;
public float iconTextSize;
public float allAppsIconSize;
public float allAppsIconTextSize;
+ public float minCellHeight;
+ public float minCellWidth;
+ public float borderSpacing;
+
private SparseArray<TypedValue> mExtraAttrs;
/**
* Number of icons inside the hotseat area.
*/
- public int numHotseatIcons;
+ protected int numShownHotseatIcons;
+
+ /**
+ * Number of icons inside the hotseat area that is stored in the database. This is greater than
+ * or equal to numnShownHotseatIcons, allowing for a seamless transition between two hotseat
+ * sizes that share the same DB.
+ */
+ public int numDatabaseHotseatIcons;
/**
* Number of columns in the all apps list.
*/
public int numAllAppsColumns;
+ public int numDatabaseAllAppsColumns;
+
+ /**
+ * Do not query directly. see {@link DeviceProfile#isScalableGrid}.
+ */
+ protected boolean isScalable;
+ public int devicePaddingId;
public String dbFile;
public int defaultLayoutId;
int demoModeLayoutId;
- public DeviceProfile landscapeProfile;
- public DeviceProfile portraitProfile;
+ /**
+ * An immutable list of supported profiles.
+ */
+ public List<DeviceProfile> supportedProfiles = Collections.EMPTY_LIST;
+
+ @Nullable public DevicePaddings devicePaddings;
public Point defaultWallpaperSize;
public Rect defaultWidgetPadding;
private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
- private ConfigMonitor mConfigMonitor;
- private OverlayMonitor mOverlayMonitor;
@VisibleForTesting
public InvariantDeviceProfile() {}
@@ -146,19 +161,26 @@
numFolderRows = p.numFolderRows;
numFolderColumns = p.numFolderColumns;
iconSize = p.iconSize;
- iconShapePath = p.iconShapePath;
landscapeIconSize = p.landscapeIconSize;
iconBitmapSize = p.iconBitmapSize;
iconTextSize = p.iconTextSize;
- numHotseatIcons = p.numHotseatIcons;
+ landscapeIconTextSize = p.landscapeIconTextSize;
+ numShownHotseatIcons = p.numShownHotseatIcons;
+ numDatabaseHotseatIcons = p.numDatabaseHotseatIcons;
numAllAppsColumns = p.numAllAppsColumns;
+ numDatabaseAllAppsColumns = p.numDatabaseAllAppsColumns;
+ isScalable = p.isScalable;
+ devicePaddingId = p.devicePaddingId;
+ minCellHeight = p.minCellHeight;
+ minCellWidth = p.minCellWidth;
+ borderSpacing = p.borderSpacing;
dbFile = p.dbFile;
allAppsIconSize = p.allAppsIconSize;
allAppsIconTextSize = p.allAppsIconTextSize;
defaultLayoutId = p.defaultLayoutId;
demoModeLayoutId = p.demoModeLayoutId;
mExtraAttrs = p.mExtraAttrs;
- mOverlayMonitor = p.mOverlayMonitor;
+ devicePaddings = p.devicePaddings;
}
@TargetApi(23)
@@ -169,13 +191,16 @@
Utilities.getPrefs(context).edit().putString(KEY_IDP_GRID_NAME, newGridName).apply();
}
Utilities.getPrefs(context).edit()
- .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, numHotseatIcons)
+ .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, numDatabaseHotseatIcons)
.putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(numColumns, numRows))
.apply();
- mConfigMonitor = new ConfigMonitor(context,
- APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
- mOverlayMonitor = new OverlayMonitor(context);
+ DisplayController.INSTANCE.get(context).addChangeListener(
+ (displayContext, info, flags) -> {
+ if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS)) != 0) {
+ onConfigChanged(displayContext);
+ }
+ });
}
/**
@@ -193,25 +218,32 @@
*/
public InvariantDeviceProfile(Context context, Display display) {
// Ensure that the main device profile is initialized
- InvariantDeviceProfile originalProfile = INSTANCE.get(context);
+ INSTANCE.get(context);
String gridName = getCurrentGridName(context);
// Get the display info based on default display and interpolate it to existing display
DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
- DefaultDisplay.INSTANCE.get(context).getInfo(),
- getPredefinedDeviceProfiles(context, gridName));
+ DisplayController.INSTANCE.get(context).getInfo(),
+ getPredefinedDeviceProfiles(context, gridName, false), false);
Info myInfo = new Info(context, display);
DisplayOption myDisplayOption = invDistWeightedInterpolate(
- myInfo, getPredefinedDeviceProfiles(context, gridName));
+ myInfo, getPredefinedDeviceProfiles(context, gridName, false), false);
DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
.add(myDisplayOption);
result.iconSize = defaultDisplayOption.iconSize;
result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
- result.allAppsIconSize = Math.min(
- defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
- initGrid(context, myInfo, result);
+ if (defaultDisplayOption.allAppsIconSize < myDisplayOption.allAppsIconSize) {
+ result.allAppsIconSize = defaultDisplayOption.allAppsIconSize;
+ } else {
+ result.allAppsIconSize = myDisplayOption.allAppsIconSize;
+ }
+ result.minCellHeight = defaultDisplayOption.minCellHeight;
+ result.minCellWidth = defaultDisplayOption.minCellWidth;
+ result.borderSpacing = defaultDisplayOption.borderSpacing;
+
+ initGrid(context, myInfo, result, false);
}
public static String getCurrentGridName(Context context) {
@@ -219,48 +251,64 @@
? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null;
}
- /**
- * Retrieve system defined or RRO overriden icon shape.
- */
- private static String getIconShapePath(Context context) {
- if (CONFIG_ICON_MASK_RES_ID == 0) {
- Log.e(TAG, "Icon mask res identifier failed to retrieve.");
- return "";
- }
- return context.getResources().getString(CONFIG_ICON_MASK_RES_ID);
- }
-
private String initGrid(Context context, String gridName) {
- DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(context).getInfo();
- ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
+ Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
+ // Determine if we have split display
- DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
- initGrid(context, displayInfo, displayOption);
+ boolean isTablet = false, isPhone = false;
+ for (WindowBounds bounds : displayInfo.supportedBounds) {
+ if (displayInfo.isTablet(bounds)) {
+ isTablet = true;
+ } else {
+ isPhone = true;
+ }
+ }
+ boolean isSplitDisplay = isPhone && isTablet && ENABLE_TWO_PANEL_HOME.get();
+
+ ArrayList<DisplayOption> allOptions =
+ getPredefinedDeviceProfiles(context, gridName, isSplitDisplay);
+ DisplayOption displayOption =
+ invDistWeightedInterpolate(displayInfo, allOptions, isSplitDisplay);
+ initGrid(context, displayInfo, displayOption, isSplitDisplay);
return displayOption.grid.name;
}
private void initGrid(
- Context context, DefaultDisplay.Info displayInfo, DisplayOption displayOption) {
+ Context context, Info displayInfo, DisplayOption displayOption,
+ boolean isSplitDisplay) {
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
GridOption closestProfile = displayOption.grid;
numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns;
- numHotseatIcons = closestProfile.numHotseatIcons;
dbFile = closestProfile.dbFile;
defaultLayoutId = closestProfile.defaultLayoutId;
demoModeLayoutId = closestProfile.demoModeLayoutId;
numFolderRows = closestProfile.numFolderRows;
numFolderColumns = closestProfile.numFolderColumns;
- numAllAppsColumns = closestProfile.numAllAppsColumns;
+ isScalable = closestProfile.isScalable;
+ devicePaddingId = closestProfile.devicePaddingId;
mExtraAttrs = closestProfile.extraAttrs;
iconSize = displayOption.iconSize;
- iconShapePath = getIconShapePath(context);
landscapeIconSize = displayOption.landscapeIconSize;
- iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics);
+ iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
iconTextSize = displayOption.iconTextSize;
+ landscapeIconTextSize = displayOption.landscapeIconTextSize;
fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
+ minCellHeight = displayOption.minCellHeight;
+ minCellWidth = displayOption.minCellWidth;
+ borderSpacing = displayOption.borderSpacing;
+
+ numShownHotseatIcons = closestProfile.numHotseatIcons;
+ numDatabaseHotseatIcons = isSplitDisplay
+ ? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons;
+
+ numAllAppsColumns = closestProfile.numAllAppsColumns;
+ numDatabaseAllAppsColumns = isSplitDisplay
+ ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns;
+
if (Utilities.isGridOptionsEnabled(context)) {
allAppsIconSize = displayOption.allAppsIconSize;
allAppsIconTextSize = displayOption.allAppsIconTextSize;
@@ -269,32 +317,36 @@
allAppsIconTextSize = iconTextSize;
}
+ if (devicePaddingId != 0) {
+ devicePaddings = new DevicePaddings(context, devicePaddingId);
+ }
+
// If the partner customization apk contains any grid overrides, apply them
// Supported overrides: numRows, numColumns, iconSize
- applyPartnerDeviceProfileOverrides(context, displayInfo.metrics);
+ applyPartnerDeviceProfileOverrides(context, metrics);
- Point realSize = new Point(displayInfo.realSize);
- // The real size never changes. smallSide and largeSide will remain the
- // same in any orientation.
- int smallSide = Math.min(realSize.x, realSize.y);
- int largeSide = Math.max(realSize.x, realSize.y);
+ final List<DeviceProfile> localSupportedProfiles = new ArrayList<>();
+ defaultWallpaperSize = new Point(displayInfo.currentSize);
+ for (WindowBounds bounds : displayInfo.supportedBounds) {
+ localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
+ .setUseTwoPanels(isSplitDisplay)
+ .setWindowBounds(bounds).build());
- DeviceProfile.Builder builder = new DeviceProfile.Builder(context, this, displayInfo)
- .setSizeRange(new Point(displayInfo.smallestSize),
- new Point(displayInfo.largestSize));
+ // Wallpaper size should be the maximum of the all possible sizes Launcher expects
+ int displayWidth = bounds.bounds.width();
+ int displayHeight = bounds.bounds.height();
+ defaultWallpaperSize.y = Math.max(defaultWallpaperSize.y, displayHeight);
- landscapeProfile = builder.setSize(largeSide, smallSide).build();
- portraitProfile = builder.setSize(smallSide, largeSide).build();
-
- // We need to ensure that there is enough extra space in the wallpaper
- // for the intended parallax effects
- if (context.getResources().getConfiguration().smallestScreenWidthDp >= 720) {
- defaultWallpaperSize = new Point(
- (int) (largeSide * wallpaperTravelToScreenWidthRatio(largeSide, smallSide)),
- largeSide);
- } else {
- defaultWallpaperSize = new Point(Math.max(smallSide * 2, largeSide), largeSide);
+ // We need to ensure that there is enough extra space in the wallpaper
+ // for the intended parallax effects
+ float parallaxFactor =
+ dpiFromPx(Math.min(displayWidth, displayHeight), displayInfo.densityDpi) < 720
+ ? 2
+ : wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight);
+ defaultWallpaperSize.x =
+ Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth));
}
+ supportedProfiles = Collections.unmodifiableList(localSupportedProfiles);
ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
@@ -313,23 +365,6 @@
mChangeListeners.remove(listener);
}
- private void killProcess(Context context) {
- Log.e("ConfigMonitor", "restarting launcher");
- android.os.Process.killProcess(android.os.Process.myPid());
- }
-
- public void verifyConfigChangedInBackground(final Context context) {
- String savedIconMaskPath = getDevicePrefs(context).getString(KEY_ICON_PATH_REF, "");
- // Good place to check if grid size changed in themepicker when launcher was dead.
- if (savedIconMaskPath.isEmpty()) {
- getDevicePrefs(context).edit().putString(KEY_ICON_PATH_REF, getIconShapePath(context))
- .apply();
- } else if (!savedIconMaskPath.equals(getIconShapePath(context))) {
- getDevicePrefs(context).edit().putString(KEY_ICON_PATH_REF, getIconShapePath(context))
- .apply();
- apply(context, CHANGE_FLAG_ICON_PARAMS);
- }
- }
public void setCurrentGrid(Context context, String gridName) {
Context appContext = context.getApplicationContext();
@@ -338,44 +373,17 @@
}
private void onConfigChanged(Context context) {
- // Config changes, what shall we do?
- InvariantDeviceProfile oldProfile = new InvariantDeviceProfile(this);
-
// Re-init grid
String gridName = getCurrentGridName(context);
initGrid(context, gridName);
- int changeFlags = 0;
- if (numRows != oldProfile.numRows ||
- numColumns != oldProfile.numColumns ||
- numFolderColumns != oldProfile.numFolderColumns ||
- numFolderRows != oldProfile.numFolderRows ||
- numHotseatIcons != oldProfile.numHotseatIcons) {
- changeFlags |= CHANGE_FLAG_GRID;
- }
-
- if (iconSize != oldProfile.iconSize || iconBitmapSize != oldProfile.iconBitmapSize ||
- !iconShapePath.equals(oldProfile.iconShapePath)) {
- changeFlags |= CHANGE_FLAG_ICON_PARAMS;
- }
- if (!iconShapePath.equals(oldProfile.iconShapePath)) {
- IconShape.init(context);
- }
-
- apply(context, changeFlags);
- }
-
- private void apply(Context context, int changeFlags) {
- // Create a new config monitor
- mConfigMonitor.unregister();
- mConfigMonitor = new ConfigMonitor(context, this::onConfigChanged);
-
for (OnIDPChangeListener listener : mChangeListeners) {
- listener.onIdpChanged(changeFlags, this);
+ listener.onIdpChanged(this);
}
}
- static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context, String gridName) {
+ private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(
+ Context context, String gridName, boolean isSplitDisplay) {
ArrayList<DisplayOption> profiles = new ArrayList<>();
try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
final int depth = parser.getDepth();
@@ -392,8 +400,9 @@
&& type != XmlPullParser.END_DOCUMENT) {
if ((type == XmlPullParser.START_TAG) && "display-option".equals(
parser.getName())) {
- profiles.add(new DisplayOption(
- gridOption, context, Xml.asAttributeSet(parser)));
+ profiles.add(new DisplayOption(gridOption, context,
+ Xml.asAttributeSet(parser),
+ isSplitDisplay ? DEFAULT_SPLIT_DISPLAY : DEFAULT_TRUE));
}
}
}
@@ -464,17 +473,29 @@
return (float) Math.hypot(x1 - x0, y1 - y0);
}
- @VisibleForTesting
- static DisplayOption invDistWeightedInterpolate(
- DefaultDisplay.Info displayInfo, ArrayList<DisplayOption> points) {
- Point smallestSize = new Point(displayInfo.smallestSize);
- Point largestSize = new Point(displayInfo.largestSize);
+ private static DisplayOption invDistWeightedInterpolate(
+ Info displayInfo, ArrayList<DisplayOption> points, boolean isSplitDisplay) {
+ int minWidthPx = Integer.MAX_VALUE;
+ int minHeightPx = Integer.MAX_VALUE;
+ for (WindowBounds bounds : displayInfo.supportedBounds) {
+ boolean isTablet = displayInfo.isTablet(bounds);
+ if (isTablet && isSplitDisplay) {
+ // For split displays, take half width per page
+ minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
+ minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
- // This guarantees that width < height
- float width = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
- displayInfo.metrics);
- float height = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
- displayInfo.metrics);
+ } else if (!isTablet && bounds.isLandscape()) {
+ // We will use transposed layout in this case
+ minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
+ minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
+ } else {
+ minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
+ minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+ }
+ }
+
+ float width = dpiFromPx(minWidthPx, displayInfo.densityDpi);
+ float height = dpiFromPx(minHeightPx, displayInfo.densityDpi);
// Sort the profiles based on the closeness to the device size
Collections.sort(points, (a, b) ->
@@ -499,29 +520,25 @@
return out.multiply(1.0f / weights);
}
- @VisibleForTesting
- static DisplayOption invDistWeightedInterpolate(float width, float height,
- ArrayList<DisplayOption> points) {
- float weights = 0;
-
- DisplayOption p = points.get(0);
- if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
- return p;
- }
-
- DisplayOption out = new DisplayOption();
- for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
- p = points.get(i);
- float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
- weights += w;
- out.add(new DisplayOption().add(p).multiply(w));
- }
- return out.multiply(1.0f / weights);
- }
-
public DeviceProfile getDeviceProfile(Context context) {
- return context.getResources().getConfiguration().orientation
- == Configuration.ORIENTATION_LANDSCAPE ? landscapeProfile : portraitProfile;
+ Resources res = context.getResources();
+ Configuration config = context.getResources().getConfiguration();
+
+ float availableWidth = config.screenWidthDp * res.getDisplayMetrics().density;
+ float availableHeight = config.screenHeightDp * res.getDisplayMetrics().density;
+
+ DeviceProfile bestMatch = supportedProfiles.get(0);
+ float minDiff = Float.MAX_VALUE;
+
+ for (DeviceProfile profile : supportedProfiles) {
+ float diff = Math.abs(profile.availableWidthPx - availableWidth)
+ + Math.abs(profile.availableHeightPx - availableHeight);
+ if (diff < minDiff) {
+ minDiff = diff;
+ bestMatch = profile;
+ }
+ }
+ return bestMatch;
}
private static float weight(float x0, float y0, float x1, float y1, float pow) {
@@ -563,7 +580,10 @@
public interface OnIDPChangeListener {
- void onIdpChanged(int changeFlags, InvariantDeviceProfile profile);
+ /**
+ * Called when the device provide changes
+ */
+ void onIdpChanged(InvariantDeviceProfile profile);
}
@@ -578,14 +598,19 @@
private final int numFolderRows;
private final int numFolderColumns;
+ private final int numAllAppsColumns;
+ private final int numDatabaseAllAppsColumns;
private final int numHotseatIcons;
+ private final int numDatabaseHotseatIcons;
private final String dbFile;
- private final int numAllAppsColumns;
private final int defaultLayoutId;
private final int demoModeLayoutId;
+ private final boolean isScalable;
+ private final int devicePaddingId;
+
private final SparseArray<TypedValue> extraAttrs;
public GridOption(Context context, AttributeSet attrs) {
@@ -600,36 +625,54 @@
R.styleable.GridDisplayOption_defaultLayoutId, 0);
demoModeLayoutId = a.getResourceId(
R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId);
+
+ numAllAppsColumns = a.getInt(
+ R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
+ numDatabaseAllAppsColumns = a.getInt(
+ R.styleable.GridDisplayOption_numExtendedAllAppsColumns, 2 * numAllAppsColumns);
+
numHotseatIcons = a.getInt(
R.styleable.GridDisplayOption_numHotseatIcons, numColumns);
+ numDatabaseHotseatIcons = a.getInt(
+ R.styleable.GridDisplayOption_numExtendedHotseatIcons, 2 * numHotseatIcons);
+
numFolderRows = a.getInt(
R.styleable.GridDisplayOption_numFolderRows, numRows);
numFolderColumns = a.getInt(
R.styleable.GridDisplayOption_numFolderColumns, numColumns);
- numAllAppsColumns = a.getInt(
- R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
+
+ isScalable = a.getBoolean(
+ R.styleable.GridDisplayOption_isScalable, false);
+ devicePaddingId = a.getResourceId(
+ R.styleable.GridDisplayOption_devicePaddingId, 0);
a.recycle();
-
extraAttrs = Themes.createValueMap(context, attrs,
IntArray.wrap(R.styleable.GridDisplayOption));
}
}
- private static final class DisplayOption {
- private final GridOption grid;
+ @VisibleForTesting
+ static final class DisplayOption {
+
+ public final GridOption grid;
private final float minWidthDps;
private final float minHeightDps;
private final boolean canBeDefault;
+ private float minCellHeight;
+ private float minCellWidth;
+ private float borderSpacing;
+
private float iconSize;
private float iconTextSize;
private float landscapeIconSize;
+ private float landscapeIconTextSize;
private float allAppsIconSize;
private float allAppsIconTextSize;
- DisplayOption(GridOption grid, Context context, AttributeSet attrs) {
+ DisplayOption(GridOption grid, Context context, AttributeSet attrs, int defaultFlagValue) {
this.grid = grid;
TypedArray a = context.obtainStyledAttributes(
@@ -637,13 +680,20 @@
minWidthDps = a.getFloat(R.styleable.ProfileDisplayOption_minWidthDps, 0);
minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0);
- canBeDefault = a.getBoolean(
- R.styleable.ProfileDisplayOption_canBeDefault, false);
+
+ canBeDefault = a.getInt(R.styleable.ProfileDisplayOption_canBeDefault, 0)
+ == defaultFlagValue;
+
+ minCellHeight = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightDps, 0);
+ minCellWidth = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthDps, 0);
+ borderSpacing = a.getFloat(R.styleable.ProfileDisplayOption_borderSpacingDps, 0);
iconSize = a.getFloat(R.styleable.ProfileDisplayOption_iconImageSize, 0);
landscapeIconSize = a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize,
iconSize);
iconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0);
+ landscapeIconTextSize = a.getFloat(
+ R.styleable.ProfileDisplayOption_landscapeIconTextSize, iconTextSize);
allAppsIconSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconSize,
iconSize);
@@ -661,6 +711,9 @@
minWidthDps = 0;
minHeightDps = 0;
canBeDefault = false;
+ minCellHeight = 0;
+ minCellWidth = 0;
+ borderSpacing = 0;
}
private DisplayOption multiply(float w) {
@@ -668,7 +721,11 @@
landscapeIconSize *= w;
allAppsIconSize *= w;
iconTextSize *= w;
+ landscapeIconTextSize *= w;
allAppsIconTextSize *= w;
+ minCellHeight *= w;
+ minCellWidth *= w;
+ borderSpacing *= w;
return this;
}
@@ -677,22 +734,12 @@
landscapeIconSize += p.landscapeIconSize;
allAppsIconSize += p.allAppsIconSize;
iconTextSize += p.iconTextSize;
+ landscapeIconTextSize += p.landscapeIconTextSize;
allAppsIconTextSize += p.allAppsIconTextSize;
+ minCellHeight += p.minCellHeight;
+ minCellWidth += p.minCellWidth;
+ borderSpacing += p.borderSpacing;
return this;
}
}
-
- private class OverlayMonitor extends BroadcastReceiver {
-
- private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
-
- OverlayMonitor(Context context) {
- context.registerReceiver(this, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- onConfigChanged(context);
- }
- }
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0970dae..099f256 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -18,12 +18,14 @@
import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
+import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO;
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
-import static com.android.launcher3.InstallShortcutReceiver.FLAG_DRAG_AND_DROP;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
@@ -32,16 +34,21 @@
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.NO_OFFSET;
import static com.android.launcher3.LauncherState.NO_SCALE;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP;
-import static com.android.launcher3.logging.StatsLogManager.containerTypeToAtomState;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED;
+import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED;
+import static com.android.launcher3.model.ItemInstallQueue.FLAG_DRAG_AND_DROP;
+import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING;
import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
import static com.android.launcher3.popup.SystemShortcut.INSTALL;
import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
@@ -54,7 +61,10 @@
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
-import android.app.ActivityOptions;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetManager;
import android.content.ActivityNotFoundException;
@@ -68,12 +78,15 @@
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Parcelable;
import android.os.Process;
import android.os.StrictMode;
+import android.os.SystemClock;
import android.text.TextUtils;
import android.text.method.TextKeyListener;
import android.util.Log;
@@ -86,8 +99,10 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.OvershootInterpolator;
+import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.CallSuper;
@@ -97,6 +112,7 @@
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.LauncherAction;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.allapps.AllAppsTransitionController;
@@ -107,25 +123,28 @@
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.folder.Folder;
+import com.android.launcher3.dragndrop.LauncherDragController;
import com.android.launcher3.folder.FolderGridOrganizer;
import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.icons.BitmapRenderer;
import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.keyboard.CustomActionsPopup;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.model.AppLaunchTracker;
import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.ItemInstallQueue;
+import com.android.launcher3.model.ModelUtils;
import com.android.launcher3.model.ModelWriter;
+import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.pm.PinRequestHelper;
@@ -136,7 +155,6 @@
import com.android.launcher3.qsb.QsbContainerView;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StateManager.StateHandler;
-import com.android.launcher3.statemanager.StateManager.StateListener;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.states.RotationHelper;
import com.android.launcher3.testing.TestLogging;
@@ -144,9 +162,6 @@
import com.android.launcher3.touch.AllAppsSwipeController;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.ActivityResultInfo;
import com.android.launcher3.util.ActivityTracker;
import com.android.launcher3.util.ComponentKey;
@@ -159,7 +174,6 @@
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.PendingRequestArgs;
import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.ShortcutUtil;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.Thunk;
@@ -168,18 +182,20 @@
import com.android.launcher3.util.UiThreadHelper;
import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.FloatingSurfaceView;
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.views.ScrimView;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.PendingAppWidgetHostView;
import com.android.launcher3.widget.WidgetAddFlowHandler;
-import com.android.launcher3.widget.WidgetHostViewLoader;
-import com.android.launcher3.widget.WidgetListRowEntry;
import com.android.launcher3.widget.WidgetManagerHelper;
-import com.android.launcher3.widget.WidgetsFullSheet;
import com.android.launcher3.widget.custom.CustomWidgetManager;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.systemui.plugins.OverlayPlugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.shared.LauncherExterns;
@@ -257,10 +273,8 @@
private static final int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5;
@Thunk @VisibleForTesting public static final int NEW_APPS_ANIMATION_DELAY = 500;
- private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 1;
- private static final int SCRIM_VIEW_ALPHA_CHANNEL_INDEX = 0;
+ private static final int THEME_CROSS_FADE_ANIMATION_DURATION = 375;
- private LauncherAppTransitionManager mAppTransitionManager;
private Configuration mOldConfig;
@Thunk
@@ -294,8 +308,6 @@
@Thunk
boolean mWorkspaceLoading = true;
- private ArrayList<OnResumeCallback> mOnResumeCallbacks = new ArrayList<>();
-
// Used to notify when an activity launch has been deferred because launcher is not yet resumed
// TODO: See if we can remove this later
private Runnable mOnDeferredActivityLaunchCallback;
@@ -331,18 +343,23 @@
private RotationHelper mRotationHelper;
- private float mCurrentAssistantVisibility = 0f;
-
protected LauncherOverlayManager mOverlayManager;
// If true, overlay callbacks are deferred
private boolean mDeferOverlayCallbacks;
private final Runnable mDeferredOverlayCallbacks = this::checkIfOverlayStillDeferred;
- private long mLastTouchUpTime = -1;
+ protected long mLastTouchUpTime = -1;
private boolean mTouchInProgress;
private SafeCloseable mUserChangedCallbackCloseable;
+ // New InstanceId is assigned to mAllAppsSessionLogId for each AllApps sessions.
+ // When Launcher is not in AllApps state mAllAppsSessionLogId will be null.
+ // User actions within AllApps state are logged with this InstanceId, to recreate AllApps
+ // session on the server side.
+ protected InstanceId mAllAppsSessionLogId;
+ private LauncherState mPrevLauncherState;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT,
@@ -362,6 +379,44 @@
.build());
}
+ if (Utilities.IS_DEBUG_DEVICE && FeatureFlags.NOTIFY_CRASHES.get()) {
+ final String notificationChannelId = "com.android.launcher3.Debug";
+ final String notificationChannelName = "Debug";
+ final String notificationTag = "Debug";
+ final int notificationId = 0;
+
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(new NotificationChannel(
+ notificationChannelId, notificationChannelName,
+ NotificationManager.IMPORTANCE_HIGH));
+
+ Thread.currentThread().setUncaughtExceptionHandler((thread, throwable) -> {
+ String stackTrace = Log.getStackTraceString(throwable);
+
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, stackTrace);
+ shareIntent = Intent.createChooser(shareIntent, null);
+ PendingIntent sharePendingIntent = PendingIntent.getActivity(
+ this, 0, shareIntent, PendingIntent.FLAG_UPDATE_CURRENT
+ );
+
+ Notification notification = new Notification.Builder(this, notificationChannelId)
+ .setSmallIcon(android.R.drawable.ic_menu_close_clear_cancel)
+ .setContentTitle("Launcher crash detected!")
+ .setStyle(new Notification.BigTextStyle().bigText(stackTrace))
+ .addAction(android.R.drawable.ic_menu_share, "Share", sharePendingIntent)
+ .build();
+ notificationManager.notify(notificationTag, notificationId, notification);
+
+ Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler =
+ Thread.getDefaultUncaughtExceptionHandler();
+ if (defaultUncaughtExceptionHandler != null) {
+ defaultUncaughtExceptionHandler.uncaughtException(thread, throwable);
+ }
+ });
+ }
+
super.onCreate(savedInstanceState);
LauncherAppState app = LauncherAppState.getInstance(this);
@@ -374,26 +429,23 @@
idp.addOnChangeListener(this);
mSharedPrefs = Utilities.getPrefs(this);
mIconCache = app.getIconCache();
- mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);
+ mAccessibilityDelegate = createAccessibilityDelegate();
- mDragController = new DragController(this);
+ mDragController = new LauncherDragController(this);
mAllAppsController = new AllAppsTransitionController(this);
mStateManager = new StateManager<>(this, NORMAL);
mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs);
mAppWidgetManager = new WidgetManagerHelper(this);
- mAppWidgetHost = new LauncherAppWidgetHost(this,
- appWidgetId -> getWorkspace().removeWidget(appWidgetId));
+ mAppWidgetHost = createAppWidgetHost();
mAppWidgetHost.startListening();
inflateRootView(R.layout.launcher);
setupViews();
+ crossFadeWithPreviousAppearance();
mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
- mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);
- mAppTransitionManager.registerRemoteAnimations();
-
boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
if (internalStateHandled) {
if (savedInstanceState != null) {
@@ -441,28 +493,14 @@
OverlayPlugin.class, false /* allowedMultiple */);
mRotationHelper.initialize();
-
- mStateManager.addStateListener(new StateListener<LauncherState>() {
-
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- float alpha = 1f - mCurrentAssistantVisibility;
- if (finalState == NORMAL) {
- mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
- } else if (finalState == OVERVIEW || finalState == OVERVIEW_PEEK) {
- mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
- mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
- } else {
- mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
- mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
- }
- }
- });
-
TraceHelper.INSTANCE.endSection(traceToken);
mUserChangedCallbackCloseable = UserCache.INSTANCE.get(this).addUserChangeListener(
() -> getStateManager().goToState(NORMAL));
+
+ if (Utilities.ATLEAST_R) {
+ getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
+ }
}
protected LauncherOverlayManager getDefaultOverlay() {
@@ -509,12 +547,12 @@
public void onEnterAnimationComplete() {
super.onEnterAnimationComplete();
mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
+ AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
int diff = newConfig.diff(mOldConfig);
-
if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) {
onIdpChanged(mDeviceProfile.inv);
}
@@ -524,13 +562,7 @@
}
@Override
- public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
- onIdpChanged(idp);
- }
-
- private void onIdpChanged(InvariantDeviceProfile idp) {
- mUserEventDispatcher = null;
-
+ public void onIdpChanged(InvariantDeviceProfile idp) {
initDeviceProfile(idp);
dispatchDeviceProfileChanged();
reapplyUi();
@@ -543,15 +575,15 @@
}
public void onAssistantVisibilityChanged(float visibility) {
- mCurrentAssistantVisibility = visibility;
- float alpha = 1f - visibility;
- LauncherState state = mStateManager.getState();
- if (state == NORMAL) {
- mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
- } else if (state == OVERVIEW || state == OVERVIEW_PEEK) {
- mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
- mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
- }
+ mHotseat.getQsb().setAlpha(1f - visibility);
+ }
+
+ /**
+ * Called when one handed mode activated and deactivated.
+ * @param activated true if one handed mode activated, false otherwise.
+ */
+ public void onOneHandedStateChanged(boolean activated) {
+ mDragLayer.onOneHandedModeStateChanged(activated);
}
private void initDeviceProfile(InvariantDeviceProfile idp) {
@@ -653,6 +685,7 @@
completeAddAppWidget(appWidgetId, info, null, null);
break;
case REQUEST_RECONFIGURE_APPWIDGET:
+ mStatsLogManager.logger().withItemInfo(info).log(LAUNCHER_WIDGET_RECONFIGURED);
completeRestoreAppWidget(appWidgetId, LauncherAppWidgetInfo.RESTORE_COMPLETED);
break;
case REQUEST_BIND_PENDING_APPWIDGET: {
@@ -814,7 +847,7 @@
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- startActivitySafely(v, intent, null, null);
+ startActivitySafely(v, intent, null);
} else {
// TODO: Show a snack bar with link to settings
Toast.makeText(this, getString(R.string.msg_no_phone_permission,
@@ -883,9 +916,9 @@
} else {
mOverlayManager.onActivityStopped(this);
}
-
- logStopAndResume(Action.Command.STOP);
- mAppWidgetHost.setListenIfResumed(false);
+ hideKeyboard();
+ logStopAndResume(false /* isResume */);
+ mAppWidgetHost.setActivityStarted(false);
NotificationListener.removeNotificationsChangedListener();
}
@@ -898,57 +931,51 @@
mOverlayManager.onActivityStarted(this);
}
- mAppWidgetHost.setListenIfResumed(true);
+ mAppWidgetHost.setActivityStarted(true);
TraceHelper.INSTANCE.endSection(traceToken);
}
@Override
@CallSuper
protected void onDeferredResumed() {
- logStopAndResume(Action.Command.RESUME);
- getUserEventDispatcher().startSession();
-
- AppLaunchTracker.INSTANCE.get(this).onReturnedToHome();
+ logStopAndResume(true /* isResume */);
// Process any items that were added while Launcher was away.
- InstallShortcutReceiver.disableAndFlushInstallQueue(
- InstallShortcutReceiver.FLAG_ACTIVITY_PAUSED, this);
+ ItemInstallQueue.INSTANCE.get(this)
+ .resumeModelPush(FLAG_ACTIVITY_PAUSED);
// Refresh shortcuts if the permission changed.
- mModel.refreshShortcutsIfRequired();
+ mModel.validateModelDataOnResume();
// Set the notification listener and fetch updated notifications when we resume
NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
DiscoveryBounce.showForHomeIfNeeded(this);
+ mAppWidgetHost.setActivityResumed(true);
}
-
- private void logStopAndResume(int command) {
+ private void logStopAndResume(boolean isResume) {
+ if (mPendingExecutor != null) return;
int pageIndex = mWorkspace.isOverlayShown() ? -1 : mWorkspace.getCurrentPage();
- int containerType = mStateManager.getState().containerType;
+ int statsLogOrdinal = mStateManager.getState().statsLogOrdinal;
StatsLogManager.EventEnum event;
StatsLogManager.StatsLogger logger = getStatsLogManager().logger();
- if (command == Action.Command.RESUME) {
+ if (isResume) {
logger.withSrcState(LAUNCHER_STATE_BACKGROUND)
- .withDstState(containerTypeToAtomState(mStateManager.getState().containerType));
+ .withDstState(mStateManager.getState().statsLogOrdinal);
event = LAUNCHER_ONRESUME;
} else { /* command == Action.Command.STOP */
- logger.withSrcState(containerTypeToAtomState(mStateManager.getState().containerType))
+ logger.withSrcState(mStateManager.getState().statsLogOrdinal)
.withDstState(LAUNCHER_STATE_BACKGROUND);
event = LAUNCHER_ONSTOP;
}
- if (containerType == ContainerType.WORKSPACE && mWorkspace != null) {
- getUserEventDispatcher().logActionCommand(command,
- containerType, -1, pageIndex);
+ if (statsLogOrdinal == LAUNCHER_STATE_HOME && mWorkspace != null) {
logger.withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
.setWorkspace(
LauncherAtom.WorkspaceContainer.newBuilder()
.setPageIndex(pageIndex)).build());
- } else {
- getUserEventDispatcher().logActionCommand(command, containerType, -1);
}
logger.log(event);
}
@@ -1005,7 +1032,7 @@
if (state == SPRING_LOADED) {
// Prevent any Un/InstallShortcutReceivers from updating the db while we are
// not on homescreen
- InstallShortcutReceiver.enableInstallQueue(FLAG_DRAG_AND_DROP);
+ ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_DRAG_AND_DROP);
getRotationHelper().setCurrentStateRequest(REQUEST_LOCK);
mWorkspace.showPageIndicatorAtCurrentScroll();
@@ -1013,12 +1040,25 @@
}
// When multiple pages are visible, show persistent page indicator
mWorkspace.getPageIndicator().setShouldAutoHide(!state.hasFlag(FLAG_MULTI_PAGE));
+
+ mPrevLauncherState = mStateManager.getCurrentStableState();
+ if (mPrevLauncherState != state && ALL_APPS.equals(state)
+ // Making sure mAllAppsSessionLogId is null to avoid double logging.
+ && mAllAppsSessionLogId == null) {
+ // creates new instance ID since new all apps session is started.
+ mAllAppsSessionLogId = new InstanceIdSequence().newInstanceId();
+ getStatsLogManager()
+ .logger()
+ .log(FeatureFlags.ENABLE_DEVICE_SEARCH.get()
+ ? LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH
+ : LAUNCHER_ALLAPPS_ENTRY);
+ }
}
@Override
public void onStateSetEnd(LauncherState state) {
- super.onStateSetStart(state);
- getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
+ super.onStateSetEnd(state);
+ getAppWidgetHost().setStateIsNormal(state == LauncherState.NORMAL);
getWorkspace().setClipChildren(!state.hasFlag(FLAG_MULTI_PAGE));
finishAutoCancelActionMode();
@@ -1030,11 +1070,26 @@
if (state == NORMAL) {
// Re-enable any Un/InstallShortcutReceiver and now process any queued items
- InstallShortcutReceiver.disableAndFlushInstallQueue(FLAG_DRAG_AND_DROP, this);
+ ItemInstallQueue.INSTANCE.get(this)
+ .resumeModelPush(FLAG_DRAG_AND_DROP);
// Clear any rotation locks when going to normal state
getRotationHelper().setCurrentStateRequest(REQUEST_NONE);
}
+
+ if (ALL_APPS.equals(mPrevLauncherState) && !ALL_APPS.equals(state)
+ // Making sure mAllAppsSessionLogId is not null to avoid double logging.
+ && mAllAppsSessionLogId != null) {
+ getAppsView().reset(false);
+ getStatsLogManager().logger()
+ .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+ .setWorkspace(
+ LauncherAtom.WorkspaceContainer.newBuilder()
+ .setPageIndex(getWorkspace().getCurrentPage()))
+ .build())
+ .log(LAUNCHER_ALLAPPS_EXIT);
+ mAllAppsSessionLogId = null;
+ }
}
@Override
@@ -1043,15 +1098,6 @@
TraceHelper.FLAG_UI_EVENT);
super.onResume();
- if (!mOnResumeCallbacks.isEmpty()) {
- final ArrayList<OnResumeCallback> resumeCallbacks = new ArrayList<>(mOnResumeCallbacks);
- mOnResumeCallbacks.clear();
- for (int i = resumeCallbacks.size() - 1; i >= 0; i--) {
- resumeCallbacks.get(i).onLauncherResume();
- }
- resumeCallbacks.clear();
- }
-
if (mDeferOverlayCallbacks) {
scheduleDeferredCheck();
} else {
@@ -1064,7 +1110,7 @@
@Override
protected void onPause() {
// Ensure that items added to Launcher are queued until Launcher returns
- InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_ACTIVITY_PAUSED);
+ ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_ACTIVITY_PAUSED);
super.onPause();
mDragController.cancelDrag();
@@ -1074,6 +1120,7 @@
if (!mDeferOverlayCallbacks) {
mOverlayManager.onActivityPaused(this);
}
+ mAppWidgetHost.setActivityResumed(false);
}
class LauncherOverlayCallbacksImpl implements LauncherOverlayCallbacks {
@@ -1098,7 +1145,11 @@
int stateOrdinal = savedState.getInt(RUNTIME_STATE, NORMAL.ordinal);
LauncherState[] stateValues = LauncherState.values();
LauncherState state = stateValues[stateOrdinal];
- if (!state.shouldDisableRestore()) {
+
+ NonConfigInstance lastInstance = (NonConfigInstance) getLastNonConfigurationInstance();
+ boolean forceRestore = lastInstance != null
+ && (lastInstance.config.diff(mOldConfig) & CONFIG_UI_MODE) != 0;
+ if (forceRestore || !state.shouldDisableRestore()) {
mStateManager.goToState(state, false /* animated */);
}
@@ -1151,8 +1202,7 @@
// Setup the drag controller (drop targets have to be added in reverse order in priority)
mDropTargetBar.setup(mDragController);
-
- mAllAppsController.setupViews(mAppsView, mScrimView);
+ mAllAppsController.setupViews(mScrimView, mAppsView);
}
/**
@@ -1195,16 +1245,13 @@
int[] cellXY = mTmpAddItemCellCoordinates;
CellLayout layout = getCellLayout(container, screenId);
- WorkspaceItemInfo info = null;
- if (Utilities.ATLEAST_OREO) {
- info = PinRequestHelper.createWorkspaceItemFromPinItemRequest(
+ WorkspaceItemInfo info = PinRequestHelper.createWorkspaceItemFromPinItemRequest(
this, PinRequestHelper.getPinItemRequest(data), 0);
- }
if (info == null) {
// Legacy shortcuts are only supported for primary profile.
info = Process.myUserHandle().equals(args.user)
- ? InstallShortcutReceiver.fromShortcutIntent(this, data) : null;
+ ? ModelUtils.fromLegacyShortcutIntent(this, data) : null;
if (info == null) {
Log.e(TAG, "Unable to parse a valid custom shortcut result");
@@ -1278,25 +1325,68 @@
appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId);
}
+ if (hostView == null) {
+ // Perform actual inflation because we're live
+ hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
+ }
+
LauncherAppWidgetInfo launcherInfo;
- launcherInfo = new LauncherAppWidgetInfo(appWidgetId, appWidgetInfo.provider);
+ launcherInfo =
+ new LauncherAppWidgetInfo(
+ appWidgetId, appWidgetInfo.provider, appWidgetInfo, hostView);
launcherInfo.spanX = itemInfo.spanX;
launcherInfo.spanY = itemInfo.spanY;
launcherInfo.minSpanX = itemInfo.minSpanX;
launcherInfo.minSpanY = itemInfo.minSpanY;
launcherInfo.user = appWidgetInfo.getProfile();
+ if (itemInfo instanceof PendingAddWidgetInfo) {
+ launcherInfo.sourceContainer = ((PendingAddWidgetInfo) itemInfo).sourceContainer;
+ } else if (itemInfo instanceof PendingRequestArgs) {
+ launcherInfo.sourceContainer =
+ ((PendingRequestArgs) itemInfo).getWidgetSourceContainer();
+ }
getModelWriter().addItemToDatabase(launcherInfo,
itemInfo.container, itemInfo.screenId, itemInfo.cellX, itemInfo.cellY);
- if (hostView == null) {
- // Perform actual inflation because we're live
- hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
- }
hostView.setVisibility(View.VISIBLE);
prepareAppWidget(hostView, launcherInfo);
mWorkspace.addInScreen(hostView, launcherInfo);
announceForAccessibility(R.string.item_added_to_workspace);
+
+ // Show the widget resize frame.
+ if (hostView instanceof LauncherAppWidgetHostView) {
+ final LauncherAppWidgetHostView launcherHostView = (LauncherAppWidgetHostView) hostView;
+ CellLayout cellLayout = getCellLayout(launcherInfo.container, launcherInfo.screenId);
+ if (mStateManager.getState() == NORMAL) {
+ // Show resize frame once the widget layout is drawn.
+ View.OnLayoutChangeListener onLayoutChangeListener =
+ new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View view, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight,
+ int oldBottom) {
+ AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+ launcherHostView.removeOnLayoutChangeListener(this);
+ }
+ };
+ launcherHostView.addOnLayoutChangeListener(onLayoutChangeListener);
+ // There is a small chance that the layout was already drawn before the layout
+ // change listener was registered, which means that the resize frame wouldn't be
+ // shown. Directly call requestLayout to force a layout change.
+ launcherHostView.requestLayout();
+ } else {
+ mStateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (mPrevLauncherState == SPRING_LOADED && finalState == NORMAL) {
+ AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+ mStateManager.removeStateListener(this);
+ }
+ }
+ });
+ }
+ }
}
private void prepareAppWidget(AppWidgetHostView hostView, LauncherAppWidgetInfo item) {
@@ -1309,14 +1399,7 @@
private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- // Reset AllApps to its initial state only if we are not in the middle of
- // processing a multi-step drop
- if (mPendingRequestArgs == null) {
- if (!isInState(NORMAL)) {
- onUiChangedWhileSleeping();
- }
- mStateManager.goToState(NORMAL);
- }
+ onScreenOff();
}
};
@@ -1338,6 +1421,23 @@
closeContextMenu();
}
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ NonConfigInstance instance = new NonConfigInstance();
+ instance.config = new Configuration(mOldConfig);
+
+ int width = mDragLayer.getWidth();
+ int height = mDragLayer.getHeight();
+
+ if (FeatureFlags.ENABLE_LAUNCHER_ACTIVITY_THEME_CROSSFADE.get()
+ && width > 0
+ && height > 0) {
+ instance.snapshot =
+ BitmapRenderer.createHardwareBitmap(width, height, mDragLayer::draw);
+ }
+ return instance;
+ }
+
public AllAppsTransitionController getAllAppsController() {
return mAllAppsController;
}
@@ -1367,6 +1467,7 @@
return mDropTargetBar;
}
+ @Override
public ScrimView getScrimView() {
return mScrimView;
}
@@ -1375,6 +1476,11 @@
return mAppWidgetHost;
}
+ protected LauncherAppWidgetHost createAppWidgetHost() {
+ return new LauncherAppWidgetHost(this,
+ appWidgetId -> getWorkspace().removeWidget(appWidgetId));
+ }
+
public LauncherModel getModel() {
return mModel;
}
@@ -1413,8 +1519,8 @@
boolean shouldMoveToDefaultScreen = alreadyOnHome && isInState(NORMAL)
&& AbstractFloatingView.getTopOpenView(this) == null;
boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction());
- boolean internalStateHandled = ACTIVITY_TRACKER.handleNewIntent(this, intent);
-
+ boolean internalStateHandled = ACTIVITY_TRACKER.handleNewIntent(this);
+ hideKeyboard();
if (isActionMain) {
if (!internalStateHandled) {
// In all these cases, only animate if we're already on home
@@ -1423,7 +1529,7 @@
if (!isInState(NORMAL)) {
// Only change state, if not already the same. This prevents cancelling any
// animations running as part of resume
- mStateManager.goToState(NORMAL);
+ mStateManager.goToState(NORMAL, mStateManager.shouldAnimateStateChange());
}
// Reset the apps view
@@ -1436,25 +1542,34 @@
}
}
- // Handle HOME_INTENT
- UserEventDispatcher ued = getUserEventDispatcher();
- Target target = newContainerTarget(mStateManager.getState().containerType);
- target.pageIndex = mWorkspace.getCurrentPage();
- ued.logActionCommand(Action.Command.HOME_INTENT, target,
- newContainerTarget(ContainerType.WORKSPACE));
- hideKeyboard();
-
if (mLauncherCallbacks != null) {
mLauncherCallbacks.onHomeIntent(internalStateHandled);
}
mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
+ handleGestureContract(intent);
} else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
- getStateManager().goToState(ALL_APPS, alreadyOnHome);
+ showAllAppsFromIntent(alreadyOnHome);
}
TraceHelper.INSTANCE.endSection(traceToken);
}
+ protected void showAllAppsFromIntent(boolean alreadyOnHome) {
+ AbstractFloatingView.closeAllOpenViews(this);
+ getStateManager().goToState(ALL_APPS, alreadyOnHome);
+ }
+
+ /**
+ * Handles gesture nav contract
+ */
+ protected void handleGestureContract(Intent intent) {
+ GestureNavContract gnc = GestureNavContract.fromIntent(intent);
+ if (gnc != null) {
+ AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
+ FloatingSurfaceView.show(this, gnc);
+ }
+ }
+
/**
* Hides the keyboard if visible
*/
@@ -1531,9 +1646,7 @@
LauncherAppState.getIDP(this).removeOnChangeListener(this);
mOverlayManager.onActivityDestroyed(this);
- mAppTransitionManager.unregisterRemoteAnimations();
mUserChangedCallbackCloseable.close();
- mAllAppsController.onActivityDestroyed();
}
public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1727,15 +1840,40 @@
return newFolder;
}
- /**
- * Called when a workspace item is converted into a folder
- */
- public void folderCreatedFromItem(Folder folder, WorkspaceItemInfo itemInfo){}
+ @Override
+ public Rect getFolderBoundingBox() {
+ // We need to bound the folder to the currently visible workspace area
+ return getWorkspace().getPageAreaRelativeToDragLayer();
+ }
- /**
- * Called when a folder is converted into a workspace item
- */
- public void folderConvertedToItem(Folder folder, WorkspaceItemInfo itemInfo) {}
+ @Override
+ public void updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height) {
+ int left = inOutPosition[0];
+ int top = inOutPosition[1];
+ DeviceProfile grid = getDeviceProfile();
+ int distFromEdgeOfScreen = getWorkspace().getPaddingLeft();
+ if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) {
+ // Center the folder if it is very close to being centered anyway, by virtue of
+ // filling the majority of the viewport. ie. remove it from the uncanny valley
+ // of centeredness.
+ left = (grid.availableWidthPx - width) / 2;
+ } else if (width >= bounds.width()) {
+ // If the folder doesn't fit within the bounds, center it about the desired bounds
+ left = bounds.left + (bounds.width() - width) / 2;
+ }
+ if (height >= bounds.height()) {
+ // Folder height is greater than page height, center on page
+ top = bounds.top + (bounds.height() - height) / 2;
+ } else {
+ // Folder height is less than page height, so bound it to the absolute open folder
+ // bounds if necessary
+ Rect folderBounds = grid.getAbsoluteOpenFolderBounds();
+ left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width));
+ top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height));
+ }
+ inOutPosition[0] = left;
+ inOutPosition[1] = top;
+ }
/**
* Unbinds the view for the specified item, and removes the item and all its children.
@@ -1790,7 +1928,7 @@
mTouchInProgress = true;
break;
case MotionEvent.ACTION_UP:
- mLastTouchUpTime = System.currentTimeMillis();
+ mLastTouchUpTime = SystemClock.uptimeMillis();
// Follow through
case MotionEvent.ACTION_CANCEL:
mTouchInProgress = false;
@@ -1828,14 +1966,15 @@
}
}
- @TargetApi(Build.VERSION_CODES.M)
- @Override
- public ActivityOptions getActivityLaunchOptions(View v) {
- return mAppTransitionManager.getActivityLaunchOptions(this, v);
- }
-
- public LauncherAppTransitionManager getAppTransitionManager() {
- return mAppTransitionManager;
+ protected void onScreenOff() {
+ // Reset AllApps to its initial state only if we are not in the middle of
+ // processing a multi-step drop
+ if (mPendingRequestArgs == null) {
+ if (!isInState(NORMAL)) {
+ onUiChangedWhileSleeping();
+ }
+ mStateManager.goToState(NORMAL);
+ }
}
@TargetApi(Build.VERSION_CODES.M)
@@ -1860,13 +1999,12 @@
}
@Override
- public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
- @Nullable String sourceContainer) {
+ public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
if (!hasBeenResumed()) {
// Workaround an issue where the WM launch animation is clobbered when finishing the
// recents animation into launcher. Defer launching the activity until Launcher is
// next resumed.
- addOnResumeCallback(() -> startActivitySafely(v, intent, item, sourceContainer));
+ addOnResumeCallback(() -> startActivitySafely(v, intent, item));
if (mOnDeferredActivityLaunchCallback != null) {
mOnDeferredActivityLaunchCallback.run();
mOnDeferredActivityLaunchCallback = null;
@@ -1874,7 +2012,7 @@
return true;
}
- boolean success = super.startActivitySafely(v, intent, item, sourceContainer);
+ boolean success = super.startActivitySafely(v, intent, item);
if (success && v instanceof BubbleTextView) {
// This is set to the view that launched the activity that navigated the user away
// from launcher. Since there is no callback for when the activity has finished
@@ -1882,7 +2020,7 @@
// state when we return to launcher.
BubbleTextView btv = (BubbleTextView) v;
btv.setStayPressed(true);
- addOnResumeCallback(btv);
+ addOnResumeCallback(() -> btv.setStayPressed(false));
}
return success;
}
@@ -1921,15 +2059,11 @@
// Populate event with a fake title based on the current state.
// TODO: When can workspace be null?
text.add(mWorkspace == null
- ? getString(R.string.all_apps_home_button_label)
+ ? getString(R.string.home_screen)
: mStateManager.getState().getDescription(this));
return result;
}
- public void addOnResumeCallback(OnResumeCallback callback) {
- mOnResumeCallbacks.add(callback);
- }
-
/**
* Persistant callback which notifies when an activity launch is deferred because the activity
* was not yet resumed.
@@ -2072,12 +2206,29 @@
*/
@Override
public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
+ bindItems(items, forceAnimateIcons, /* focusFirstItemForAccessibility= */ false);
+ }
+
+
+ /**
+ * Bind the items start-end from the list.
+ *
+ * Implementation of the method from LauncherModel.Callbacks.
+ *
+ * @param focusFirstItemForAccessibility true iff the first item to be added to the workspace
+ * should be focused for accessibility.
+ */
+ public void bindItems(
+ final List<ItemInfo> items,
+ final boolean forceAnimateIcons,
+ final boolean focusFirstItemForAccessibility) {
// Get the list of added items and intersect them with the set of items here
final Collection<Animator> bounceAnims = new ArrayList<>();
final boolean animateIcons = forceAnimateIcons && canRunNewAppsAnimation();
Workspace workspace = mWorkspace;
int newItemsScreenId = -1;
int end = items.size();
+ View newView = null;
for (int i = 0; i < end; i++) {
final ItemInfo item = items.get(i);
@@ -2142,12 +2293,25 @@
bounceAnims.add(createNewAppBounceAnimation(view, i));
newItemsScreenId = item.screenId;
}
+
+ if (newView == null) {
+ newView = view;
+ }
}
- // Animate to the correct page
+ View viewToFocus = newView;
+ // Animate to the correct pager
if (animateIcons && newItemsScreenId > -1) {
AnimatorSet anim = new AnimatorSet();
anim.playTogether(bounceAnims);
+ if (focusFirstItemForAccessibility && viewToFocus != null) {
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ viewToFocus.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }
+ });
+ }
int currentScreenId = mWorkspace.getScreenIdForPageIndex(mWorkspace.getNextPage());
final int newScreenIndex = mWorkspace.getPageIndexForScreenId(newItemsScreenId);
@@ -2170,13 +2334,12 @@
} else {
mWorkspace.postDelayed(startBounceAnimRunnable, NEW_APPS_ANIMATION_DELAY);
}
+ } else if (focusFirstItemForAccessibility && viewToFocus != null) {
+ viewToFocus.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
workspace.requestLayout();
}
- @Override
- public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
-
/**
* Add the views for a widget to the workspace.
*/
@@ -2207,24 +2370,45 @@
try {
final LauncherAppWidgetProviderInfo appWidgetInfo;
+ String removalReason = "";
if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
// If the provider is not ready, bind as a pending widget.
appWidgetInfo = null;
+ removalReason = "the provider isn't ready.";
} else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
// The widget id is not valid. Try to find the widget based on the provider info.
appWidgetInfo = mAppWidgetManager.findProvider(item.providerName, item.user);
+ if (appWidgetInfo == null) {
+ if (WidgetsModel.GO_DISABLE_WIDGETS) {
+ removalReason = "widgets are disabled on go device.";
+ } else {
+ removalReason =
+ "WidgetManagerHelper cannot find a provider from provider info.";
+ }
+ }
} else {
appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(item.appWidgetId);
+ if (appWidgetInfo == null) {
+ if (item.appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
+ removalReason =
+ "CustomWidgetManager cannot find provider from that widget id.";
+ } else {
+ removalReason = "AppWidgetManager cannot find provider for that widget id."
+ + " It could be because AppWidgetService is not available, or the"
+ + " appWidgetId has not been bound to a the provider yet, or you"
+ + " don't have access to that appWidgetId.";
+ }
+ }
}
// If the provider is ready, but the width is not yet restored, try to restore it.
if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
&& (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) {
if (appWidgetInfo == null) {
- Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId
- + " belongs to component " + item.providerName
- + ", as the provider is null");
+ FileLog.d(TAG, "Removing restored widget: id=" + item.appWidgetId
+ + " belongs to component " + item.providerName + " user " + item.user
+ + ", as the provider is null and " + removalReason);
getModelWriter().deleteItemFromDatabase(item);
return null;
}
@@ -2238,13 +2422,13 @@
// Also try to bind the widget. If the bind fails, the user will be shown
// a click to setup UI, which will ask for the bind permission.
- PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(appWidgetInfo);
+ PendingAddWidgetInfo pendingInfo =
+ new PendingAddWidgetInfo(appWidgetInfo, item.sourceContainer);
pendingInfo.spanX = item.spanX;
pendingInfo.spanY = item.spanY;
pendingInfo.minSpanX = item.minSpanX;
pendingInfo.minSpanY = item.minSpanY;
- Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this,
- pendingInfo);
+ Bundle options = pendingInfo.getDefaultSizeOptions(this);
boolean isDirectConfig =
item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
@@ -2405,8 +2589,8 @@
mPendingActivityResult = null;
}
- InstallShortcutReceiver.disableAndFlushInstallQueue(
- InstallShortcutReceiver.FLAG_LOADER_RUNNING, this);
+ ItemInstallQueue.INSTANCE.get(this)
+ .resumeModelPush(FLAG_LOADER_RUNNING);
// When undoing the removal of the last item on a page, return to that page.
// Since we are just resetting the current page without user interaction,
@@ -2426,15 +2610,15 @@
if (mDragController.isDragging()) {
return false;
} else {
- return (System.currentTimeMillis() - mLastTouchUpTime)
+ return (SystemClock.uptimeMillis() - mLastTouchUpTime)
> (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000);
}
}
private ValueAnimator createNewAppBounceAnimation(View v, int i) {
ValueAnimator bounceAnim = new PropertyListBuilder().alpha(1).scale(1).build(v)
- .setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION);
- bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY);
+ .setDuration(ItemInstallQueue.NEW_SHORTCUT_BOUNCE_DURATION);
+ bounceAnim.setStartDelay(i * ItemInstallQueue.NEW_SHORTCUT_STAGGER_DELAY);
bounceAnim.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
return bounceAnim;
}
@@ -2451,6 +2635,7 @@
@Override
public void bindAllApplications(AppInfo[] apps, int flags) {
mAppsView.getAppsStore().setApps(apps, flags);
+ PopupContainerWithArrow.dismissInvalidPopup(this);
}
/**
@@ -2463,8 +2648,8 @@
}
@Override
- public void bindPromiseAppProgressUpdated(PromiseAppInfo app) {
- mAppsView.getAppsStore().updatePromiseAppProgress(app);
+ public void bindIncrementalDownloadProgressUpdated(AppInfo app) {
+ mAppsView.getAppsStore().updateProgressBar(app);
}
@Override
@@ -2479,9 +2664,10 @@
* @param updated list of shortcuts which have changed.
*/
@Override
- public void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated) {
+ public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
if (!updated.isEmpty()) {
mWorkspace.updateShortcuts(updated);
+ PopupContainerWithArrow.dismissInvalidPopup(this);
}
}
@@ -2506,10 +2692,11 @@
public void bindWorkspaceComponentsRemoved(final ItemInfoMatcher matcher) {
mWorkspace.removeItemsByMatcher(matcher);
mDragController.onAppsRemoved(matcher);
+ PopupContainerWithArrow.dismissInvalidPopup(this);
}
@Override
- public void bindAllWidgets(final ArrayList<WidgetListRowEntry> allWidgets) {
+ public void bindAllWidgets(final List<WidgetsListBaseEntry> allWidgets) {
mPopupDataProvider.setAllWidgets(allWidgets);
}
@@ -2564,6 +2751,7 @@
mDragLayer.dump(prefix, writer);
mStateManager.dump(prefix, writer);
mPopupDataProvider.dump(prefix, writer);
+ mDeviceProfile.dump(prefix, writer);
try {
FileLog.flushAll(writer);
@@ -2590,19 +2778,9 @@
shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.widget_button_text),
KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON));
}
- final View currentFocus = getCurrentFocus();
- if (currentFocus != null) {
- if (new CustomActionsPopup(this, currentFocus).canShow()) {
- shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.custom_actions),
- KeyEvent.KEYCODE_O, KeyEvent.META_CTRL_ON));
- }
- if (currentFocus.getTag() instanceof ItemInfo
- && ShortcutUtil.supportsShortcuts((ItemInfo) currentFocus.getTag())) {
+ getSupportedActions(this, getCurrentFocus()).forEach(la ->
shortcutInfos.add(new KeyboardShortcutInfo(
- getString(R.string.shortcuts_menu_with_notifications_description),
- KeyEvent.KEYCODE_S, KeyEvent.META_CTRL_ON));
- }
- }
+ la.accessibilityAction.getLabel(), la.keyCode, KeyEvent.META_CTRL_ON)));
if (!shortcutInfos.isEmpty()) {
data.add(new KeyboardShortcutGroup(getString(R.string.home_screen), shortcutInfos));
}
@@ -2620,29 +2798,18 @@
return true;
}
break;
- case KeyEvent.KEYCODE_S: {
- View focusedView = getCurrentFocus();
- if (focusedView instanceof BubbleTextView
- && focusedView.getTag() instanceof ItemInfo
- && mAccessibilityDelegate.performAction(focusedView,
- (ItemInfo) focusedView.getTag(),
- LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
- PopupContainerWithArrow.getOpen(this).requestFocus();
- return true;
- }
- break;
- }
- case KeyEvent.KEYCODE_O:
- if (new CustomActionsPopup(this, getCurrentFocus()).show()) {
- return true;
- }
- break;
case KeyEvent.KEYCODE_W:
if (isInState(NORMAL)) {
OptionsPopupView.openWidgets(this);
return true;
}
break;
+ default:
+ for (LauncherAction la : getSupportedActions(this, getCurrentFocus())) {
+ if (la.keyCode == keyCode) {
+ return la.invokeFromKeyboard(getCurrentFocus());
+ }
+ }
}
}
return super.onKeyShortcut(keyCode, event);
@@ -2670,8 +2837,10 @@
return super.onKeyUp(keyCode, event);
}
- protected StateHandler<LauncherState>[] createStateHandlers() {
- return new StateHandler[] { getAllAppsController(), getWorkspace() };
+ @Override
+ protected void collectStateHandlers(List<StateHandler> out) {
+ out.add(getAllAppsController());
+ out.add(getWorkspace());
}
public TouchController[] createTouchControllers() {
@@ -2700,6 +2869,9 @@
return Stream.of(APP_INFO, WIDGETS, INSTALL);
}
+ protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
+ return new LauncherAccessibilityDelegate(this);
+ }
/**
* @see LauncherState#getOverviewScaleAndOffset(Launcher)
@@ -2708,6 +2880,13 @@
return new float[] {NO_SCALE, NO_OFFSET};
}
+ /**
+ * @see LauncherState#getTaskbarScale(Launcher)
+ */
+ public float getNormalTaskbarScale() {
+ return 1f;
+ }
+
public static Launcher getLauncher(Context context) {
return fromContext(context);
}
@@ -2719,12 +2898,56 @@
return (T) activityContext;
}
-
/**
- * Callback for listening for onResume
+ * Cross-fades the launcher's updated appearance with its previous appearance.
+ *
+ * This method is used to cross-fade UI updates on activity creation, specifically dark mode
+ * updates.
*/
- public interface OnResumeCallback {
+ private void crossFadeWithPreviousAppearance() {
+ NonConfigInstance lastInstance = (NonConfigInstance) getLastNonConfigurationInstance();
- void onLauncherResume();
+ if (lastInstance == null || lastInstance.snapshot == null) {
+ return;
+ }
+
+ ImageView crossFadeHelper = new ImageView(this);
+ crossFadeHelper.setImageBitmap(lastInstance.snapshot);
+ crossFadeHelper.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+
+ InsettableFrameLayout.LayoutParams layoutParams = new InsettableFrameLayout.LayoutParams(
+ InsettableFrameLayout.LayoutParams.MATCH_PARENT,
+ InsettableFrameLayout.LayoutParams.MATCH_PARENT);
+
+ layoutParams.ignoreInsets = true;
+
+ crossFadeHelper.setLayoutParams(layoutParams);
+
+ getRootView().addView(crossFadeHelper);
+
+ crossFadeHelper
+ .animate()
+ .setDuration(THEME_CROSS_FADE_ANIMATION_DURATION)
+ .alpha(0f)
+ .withEndAction(() -> getRootView().removeView(crossFadeHelper))
+ .start();
+ }
+
+ public boolean supportsAdaptiveIconAnimation(View clickedView) {
+ return false;
+ }
+
+ public DragOptions getDefaultWorkspaceDragOptions() {
+ return new DragOptions();
+ }
+
+ private static class NonConfigInstance {
+ public Configuration config;
+ public Bitmap snapshot;
+ }
+
+ @Override
+ public StatsLogManager getStatsLogManager() {
+ return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
}
}
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index d9cf7f1..b56c012 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -16,6 +16,11 @@
package com.android.launcher3;
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.util.IntProperty;
@@ -29,8 +34,8 @@
*/
public static final int SPRING_LOADED_EXIT_DELAY = 500;
- // The progress of an animation to all apps must be at least this far along to snap to all apps.
- public static final float MIN_PROGRESS_TO_ALL_APPS = 0.5f;
+ // Progress after which the transition is assumed to be a success
+ public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
public static final IntProperty<Drawable> DRAWABLE_ALPHA =
new IntProperty<Drawable>("drawableAlpha") {
@@ -131,4 +136,39 @@
return view.getAlpha();
}
};
+
+ public static final IntProperty<View> VIEW_BACKGROUND_COLOR =
+ new IntProperty<View>("backgroundColor") {
+ @Override
+ public void setValue(View view, int color) {
+ view.setBackgroundColor(color);
+ }
+
+ @Override
+ public Integer get(View view) {
+ if (!(view.getBackground() instanceof ColorDrawable)) {
+ return Color.TRANSPARENT;
+ }
+ return ((ColorDrawable) view.getBackground()).getColor();
+ }
+ };
+
+ /**
+ * Utility method to create an {@link AnimatorListener} which executes a callback on animation
+ * cancel.
+ */
+ public static AnimatorListener newCancelListener(Runnable callback) {
+ return new AnimatorListenerAdapter() {
+
+ boolean mDispatched = false;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (!mDispatched) {
+ mDispatched = true;
+ callback.run();
+ }
+ }
+ };
+ }
}
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 53e5274..3d6be69 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,38 +16,45 @@
package com.android.launcher3;
-import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+import static com.android.launcher3.Utilities.getDevicePrefs;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_THEMED_ICONS;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.LauncherApps;
-import android.os.Handler;
+import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.PredictionModel;
import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.InstallSessionTracker;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SimpleBroadcastReceiver;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.custom.CustomWidgetManager;
public class LauncherAppState {
public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
+ private static final String KEY_ICON_STATE = "pref_icon_shape_path";
// We do not need any synchronization for this variable as its only written on UI thread.
public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -55,16 +62,11 @@
private final Context mContext;
private final LauncherModel mModel;
+ private final IconProvider mIconProvider;
private final IconCache mIconCache;
- private final WidgetPreviewLoader mWidgetCache;
+ private final DatabaseWidgetPreviewLoader mWidgetCache;
private final InvariantDeviceProfile mInvariantDeviceProfile;
- private final PredictionModel mPredictionModel;
-
- private SecureSettingsObserver mNotificationDotsObserver;
- private InstallSessionTracker mInstallSessionTracker;
- private SimpleBroadcastReceiver mModelChangeReceiver;
- private SafeCloseable mCalendarChangeTracker;
- private SafeCloseable mUserChangeListener;
+ private final RunnableList mOnTerminateCallback = new RunnableList();
public static LauncherAppState getInstance(final Context context) {
return INSTANCE.get(context);
@@ -80,76 +82,80 @@
public LauncherAppState(Context context) {
this(context, LauncherFiles.APP_ICONS_DB);
+ Log.v(Launcher.TAG, "LauncherAppState initiated");
+ Preconditions.assertUIThread();
- mModelChangeReceiver = new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
+ mInvariantDeviceProfile.addOnChangeListener(idp -> refreshAndReloadLauncher());
mContext.getSystemService(LauncherApps.class).registerCallback(mModel);
- mModelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
+
+ SimpleBroadcastReceiver modelChangeReceiver =
+ new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
+ modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
if (FeatureFlags.IS_STUDIO_BUILD) {
- mModelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
+ modelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
}
+ mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver));
- mCalendarChangeTracker = IconProvider.registerIconChangeListener(mContext,
- mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
-
- // TODO: remove listener on terminate
- FeatureFlags.APP_SEARCH_IMPROVEMENTS.addChangeListener(context, mModel::forceReload);
CustomWidgetManager.INSTANCE.get(mContext)
.setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
- mUserChangeListener = UserCache.INSTANCE.get(mContext)
+ SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
.addUserChangeListener(mModel::forceReload);
+ mOnTerminateCallback.add(userChangeListener::close);
- mInvariantDeviceProfile.addOnChangeListener(this::onIdpChanged);
- new Handler().post( () -> mInvariantDeviceProfile.verifyConfigChangedInBackground(context));
-
- mInstallSessionTracker = InstallSessionHelper.INSTANCE.get(context)
- .registerInstallTracker(mModel, MODEL_EXECUTOR);
-
- if (!mContext.getResources().getBoolean(R.bool.notification_dots_enabled)) {
- mNotificationDotsObserver = null;
- } else {
- // Register an observer to rebind the notification listener when dots are re-enabled.
- mNotificationDotsObserver =
- newNotificationSettingsObserver(mContext, this::onNotificationSettingsChanged);
- mNotificationDotsObserver.register();
- mNotificationDotsObserver.dispatchOnChange();
+ IconObserver observer = new IconObserver();
+ SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
+ observer, MODEL_EXECUTOR.getHandler());
+ mOnTerminateCallback.add(iconChangeTracker::close);
+ MODEL_EXECUTOR.execute(observer::verifyIconChanged);
+ if (ENABLE_THEMED_ICONS.get()) {
+ SharedPreferences prefs = Utilities.getPrefs(mContext);
+ prefs.registerOnSharedPreferenceChangeListener(observer);
+ mOnTerminateCallback.add(
+ () -> prefs.unregisterOnSharedPreferenceChangeListener(observer));
}
+
+ InstallSessionTracker installSessionTracker =
+ InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(mModel);
+ mOnTerminateCallback.add(installSessionTracker::unregister);
+
+ // Register an observer to rebind the notification listener when dots are re-enabled.
+ SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
+ SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged;
+ settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister);
+ onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
+ mOnTerminateCallback.add(() ->
+ settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
- Log.v(Launcher.TAG, "LauncherAppState initiated");
- Preconditions.assertUIThread();
mContext = context;
mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
- mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
- mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
- mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
- mPredictionModel = PredictionModel.newInstance(mContext);
+ mIconProvider = new IconProvider(context, Themes.isThemedIconEnabled(context));
+ mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
+ iconCacheFileName, mIconProvider);
+ mWidgetCache = new DatabaseWidgetPreviewLoader(mContext, mIconCache);
+ mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext));
+ mOnTerminateCallback.add(mIconCache::close);
}
- protected void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
+ private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
if (areNotificationDotsEnabled) {
NotificationListener.requestRebind(new ComponentName(
mContext, NotificationListener.class));
}
}
- private void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
- if (changeFlags == 0) {
- return;
- }
-
- if ((changeFlags & CHANGE_FLAG_ICON_PARAMS) != 0) {
- LauncherIcons.clearPool();
- mIconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize);
- mWidgetCache.refresh();
- }
-
+ private void refreshAndReloadLauncher() {
+ LauncherIcons.clearPool();
+ mIconCache.updateIconParams(
+ mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
+ mWidgetCache.refresh();
mModel.forceReload();
}
@@ -157,24 +163,14 @@
* Call from Application.onTerminate(), which is not guaranteed to ever be called.
*/
public void onTerminate() {
- if (mModelChangeReceiver != null) {
- mContext.unregisterReceiver(mModelChangeReceiver);
- }
+ mModel.destroy();
mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel);
- if (mInstallSessionTracker != null) {
- mInstallSessionTracker.unregister();
- }
- if (mCalendarChangeTracker != null) {
- mCalendarChangeTracker.close();
- }
- if (mUserChangeListener != null) {
- mUserChangeListener.close();
- }
CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
+ mOnTerminateCallback.executeAllAndDestroy();
+ }
- if (mNotificationDotsObserver != null) {
- mNotificationDotsObserver.unregister();
- }
+ public IconProvider getIconProvider() {
+ return mIconProvider;
}
public IconCache getIconCache() {
@@ -185,11 +181,7 @@
return mModel;
}
- public PredictionModel getPredictionModel() {
- return mPredictionModel;
- }
-
- public WidgetPreviewLoader getWidgetCache() {
+ public DatabaseWidgetPreviewLoader getWidgetCache() {
return mWidgetCache;
}
@@ -203,4 +195,35 @@
public static InvariantDeviceProfile getIDP(Context context) {
return InvariantDeviceProfile.INSTANCE.get(context);
}
+
+ private class IconObserver
+ implements IconProvider.IconChangeListener, OnSharedPreferenceChangeListener {
+
+ @Override
+ public void onAppIconChanged(String packageName, UserHandle user) {
+ mModel.onAppIconChanged(packageName, user);
+ }
+
+ @Override
+ public void onSystemIconStateChanged(String iconState) {
+ IconShape.init(mContext);
+ refreshAndReloadLauncher();
+ getDevicePrefs(mContext).edit().putString(KEY_ICON_STATE, iconState).apply();
+ }
+
+ void verifyIconChanged() {
+ String iconState = mIconProvider.getSystemIconState();
+ if (!iconState.equals(getDevicePrefs(mContext).getString(KEY_ICON_STATE, ""))) {
+ onSystemIconStateChanged(iconState);
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ if (Themes.KEY_THEMED_ICONS.equals(key)) {
+ mIconProvider.setIconThemeSupported(Themes.isThemedIconEnabled(mContext));
+ verifyIconChanged();
+ }
+ }
+ }
}
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
deleted file mode 100644
index 24e0d14..0000000
--- a/src/com/android/launcher3/LauncherAppTransitionManager.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3;
-
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Manages the opening and closing app transitions from Launcher.
- */
-public class LauncherAppTransitionManager implements ResourceBasedOverride {
-
- public static LauncherAppTransitionManager newInstance(Context context) {
- return Overrides.getObject(LauncherAppTransitionManager.class,
- context, R.string.app_transition_manager_class);
- }
-
- public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
- int left = 0, top = 0;
- int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
- if (v instanceof BubbleTextView) {
- // Launch from center of icon, not entire view
- Drawable icon = ((BubbleTextView) v).getIcon();
- if (icon != null) {
- Rect bounds = icon.getBounds();
- left = (width - bounds.width()) / 2;
- top = v.getPaddingTop();
- width = bounds.width();
- height = bounds.height();
- }
- }
- return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
- }
-
- public boolean supportsAdaptiveIconAnimation() {
- return false;
- }
-
- /**
- * Registers remote animations for certain system transitions.
- */
- public void registerRemoteAnimations() {
- // Do nothing
- }
-
- /**
- * Unregisters all remote animations.
- */
- public void unregisterRemoteAnimations() {
- // Do nothing
- }
-}
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
deleted file mode 100644
index 7ea6851..0000000
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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 android.app.Activity.RESULT_CANCELED;
-
-import android.appwidget.AppWidgetHost;
-import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Handler;
-import android.util.SparseArray;
-import android.widget.Toast;
-
-import com.android.launcher3.model.WidgetsModel;
-import com.android.launcher3.testing.TestLogging;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.widget.DeferredAppWidgetHostView;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.launcher3.widget.PendingAppWidgetHostView;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
-
-import java.util.ArrayList;
-import java.util.function.IntConsumer;
-
-
-/**
- * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
- * which correctly captures all long-press events. This ensures that users can
- * always pick up and move widgets.
- */
-public class LauncherAppWidgetHost extends AppWidgetHost {
-
- private static final int FLAG_LISTENING = 1;
- private static final int FLAG_RESUMED = 1 << 1;
- private static final int FLAG_LISTEN_IF_RESUMED = 1 << 2;
-
- public static final int APPWIDGET_HOST_ID = 1024;
-
- private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>();
- private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
- private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
-
- private final Context mContext;
- private int mFlags = FLAG_RESUMED;
-
- private IntConsumer mAppWidgetRemovedCallback = null;
-
-
- public LauncherAppWidgetHost(Context context) {
- this(context, null);
- }
-
- public LauncherAppWidgetHost(Context context,
- IntConsumer appWidgetRemovedCallback) {
- super(context, APPWIDGET_HOST_ID);
- mContext = context;
- mAppWidgetRemovedCallback = appWidgetRemovedCallback;
- }
-
- @Override
- protected LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
- AppWidgetProviderInfo appWidget) {
- final LauncherAppWidgetHostView view;
- if (mPendingViews.get(appWidgetId) != null) {
- view = mPendingViews.get(appWidgetId);
- mPendingViews.remove(appWidgetId);
- } else {
- view = new LauncherAppWidgetHostView(context);
- }
- mViews.put(appWidgetId, view);
- return view;
- }
-
- @Override
- public void startListening() {
- if (WidgetsModel.GO_DISABLE_WIDGETS) {
- return;
- }
- mFlags |= FLAG_LISTENING;
- try {
- super.startListening();
- } catch (Exception e) {
- if (!Utilities.isBinderSizeError(e)) {
- throw new RuntimeException(e);
- }
- // We're willing to let this slide. The exception is being caused by the list of
- // RemoteViews which is being passed back. The startListening relationship will
- // have been established by this point, and we will end up populating the
- // widgets upon bind anyway. See issue 14255011 for more context.
- }
-
- // We go in reverse order and inflate any deferred widget
- for (int i = mViews.size() - 1; i >= 0; i--) {
- LauncherAppWidgetHostView view = mViews.valueAt(i);
- if (view instanceof DeferredAppWidgetHostView) {
- view.reInflate();
- }
- }
- }
-
- @Override
- public void stopListening() {
- if (WidgetsModel.GO_DISABLE_WIDGETS) {
- return;
- }
- mFlags &= ~FLAG_LISTENING;
- super.stopListening();
- }
-
- public boolean isListening() {
- return (mFlags & FLAG_LISTENING) != 0;
- }
-
- /**
- * Updates the resumed state of the host.
- * When a host is not resumed, it defers calls to startListening until host is resumed again.
- * But if the host was already listening, it will not call stopListening.
- *
- * @see #setListenIfResumed(boolean)
- */
- public void setResumed(boolean isResumed) {
- if (isResumed == ((mFlags & FLAG_RESUMED) != 0)) {
- return;
- }
- if (isResumed) {
- mFlags |= FLAG_RESUMED;
- // Start listening if we were supposed to start listening on resume
- if ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0 && (mFlags & FLAG_LISTENING) == 0) {
- startListening();
- }
- } else {
- mFlags &= ~FLAG_RESUMED;
- }
- }
-
- /**
- * Updates the listening state of the host. If the host is not resumed, startListening is
- * deferred until next resume.
- *
- * @see #setResumed(boolean)
- */
- public void setListenIfResumed(boolean listenIfResumed) {
- if (listenIfResumed == ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0)) {
- return;
- }
- if (listenIfResumed) {
- mFlags |= FLAG_LISTEN_IF_RESUMED;
- if ((mFlags & FLAG_RESUMED) != 0) {
- // If we are resumed, start listening immediately. Note we do not check for
- // duplicate calls before calling startListening as startListening is safe to call
- // multiple times.
- startListening();
- }
- } else {
- mFlags &= ~FLAG_LISTEN_IF_RESUMED;
- stopListening();
- }
- }
-
- @Override
- public int allocateAppWidgetId() {
- if (WidgetsModel.GO_DISABLE_WIDGETS) {
- return AppWidgetManager.INVALID_APPWIDGET_ID;
- }
-
- return super.allocateAppWidgetId();
- }
-
- public void addProviderChangeListener(ProviderChangedListener callback) {
- mProviderChangeListeners.add(callback);
- }
-
- public void removeProviderChangeListener(ProviderChangedListener callback) {
- mProviderChangeListeners.remove(callback);
- }
-
- protected void onProvidersChanged() {
- if (!mProviderChangeListeners.isEmpty()) {
- for (ProviderChangedListener callback : new ArrayList<>(mProviderChangeListeners)) {
- callback.notifyWidgetProvidersChanged();
- }
- }
- }
-
- void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
- mPendingViews.put(appWidgetId, view);
- }
-
- public AppWidgetHostView createView(Context context, int appWidgetId,
- LauncherAppWidgetProviderInfo appWidget) {
- if (appWidget.isCustomWidget()) {
- LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
- lahv.setAppWidget(0, appWidget);
- CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
- return lahv;
- } else if ((mFlags & FLAG_LISTENING) == 0) {
- DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
- view.setAppWidget(appWidgetId, appWidget);
- mViews.put(appWidgetId, view);
- return view;
- } else {
- try {
- return super.createView(context, appWidgetId, appWidget);
- } catch (Exception e) {
- if (!Utilities.isBinderSizeError(e)) {
- throw new RuntimeException(e);
- }
-
- // If the exception was thrown while fetching the remote views, let the view stay.
- // This will ensure that if the widget posts a valid update later, the view
- // will update.
- LauncherAppWidgetHostView view = mViews.get(appWidgetId);
- if (view == null) {
- view = onCreateView(mContext, appWidgetId, appWidget);
- }
- view.setAppWidget(appWidgetId, appWidget);
- view.switchToErrorView();
- return view;
- }
- }
- }
-
- /**
- * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
- */
- @Override
- protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) {
- LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
- mContext, appWidget);
- super.onProviderChanged(appWidgetId, info);
- // The super method updates the dimensions of the providerInfo. Update the
- // launcher spans accordingly.
- info.initSpans(mContext);
- }
-
- /**
- * Called on an appWidget is removed for a widgetId
- *
- * @param appWidgetId TODO: make this override when SDK is updated
- */
- public void onAppWidgetRemoved(int appWidgetId) {
- if (mAppWidgetRemovedCallback == null) {
- return;
- }
- mAppWidgetRemovedCallback.accept(appWidgetId);
- }
-
- @Override
- public void deleteAppWidgetId(int appWidgetId) {
- super.deleteAppWidgetId(appWidgetId);
- mViews.remove(appWidgetId);
- }
-
- @Override
- public void clearViews() {
- super.clearViews();
- mViews.clear();
- }
-
- public void startBindFlow(BaseActivity activity,
- int appWidgetId, AppWidgetProviderInfo info, int requestCode) {
-
- if (WidgetsModel.GO_DISABLE_WIDGETS) {
- sendActionCancelled(activity, requestCode);
- return;
- }
-
- Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
- .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
- .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
- .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile());
- // TODO: we need to make sure that this accounts for the options bundle.
- // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
- activity.startActivityForResult(intent, requestCode);
- }
-
-
- public void startConfigActivity(BaseActivity activity, int widgetId, int requestCode) {
- if (WidgetsModel.GO_DISABLE_WIDGETS) {
- sendActionCancelled(activity, requestCode);
- return;
- }
-
- try {
- TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity");
- startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode, null);
- } catch (ActivityNotFoundException | SecurityException e) {
- Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
- sendActionCancelled(activity, requestCode);
- }
- }
-
- private void sendActionCancelled(final BaseActivity activity, final int requestCode) {
- new Handler().post(() -> activity.onActivityResult(requestCode, RESULT_CANCELED, null));
- }
-
- /**
- * Listener for getting notifications on provider changes.
- */
- public interface ProviderChangedListener {
-
- void notifyWidgetProvidersChanged();
- }
-}
diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
deleted file mode 100644
index 618b5de..0000000
--- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.android.launcher3;
-
-import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Parcel;
-import android.os.UserHandle;
-
-import com.android.launcher3.icons.ComponentWithLabelAndIcon;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-
-/**
- * This class is a thin wrapper around the framework AppWidgetProviderInfo class. This class affords
- * a common object for describing both framework provided AppWidgets as well as custom widgets
- * (who's implementation is owned by the launcher). This object represents a widget type / class,
- * as opposed to a widget instance, and so should not be confused with {@link LauncherAppWidgetInfo}
- */
-public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo
- implements ComponentWithLabelAndIcon {
-
- public static final String CLS_CUSTOM_WIDGET_PREFIX = "#custom-widget-";
-
- public int spanX;
- public int spanY;
- public int minSpanX;
- public int minSpanY;
-
- public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context,
- AppWidgetProviderInfo info) {
- final LauncherAppWidgetProviderInfo launcherInfo;
- if (info instanceof LauncherAppWidgetProviderInfo) {
- launcherInfo = (LauncherAppWidgetProviderInfo) info;
- } else {
-
- // In lieu of a public super copy constructor, we first write the AppWidgetProviderInfo
- // into a parcel, and then construct a new LauncherAppWidgetProvider info from the
- // associated super parcel constructor. This allows us to copy non-public members without
- // using reflection.
- Parcel p = Parcel.obtain();
- info.writeToParcel(p, 0);
- p.setDataPosition(0);
- launcherInfo = new LauncherAppWidgetProviderInfo(p);
- p.recycle();
- }
- launcherInfo.initSpans(context);
- return launcherInfo;
- }
-
- protected LauncherAppWidgetProviderInfo() {}
-
- protected LauncherAppWidgetProviderInfo(Parcel in) {
- super(in);
- }
-
- public void initSpans(Context context) {
- InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-
- Point landCellSize = idp.landscapeProfile.getCellSize();
- Point portCellSize = idp.portraitProfile.getCellSize();
-
- // Always assume we're working with the smallest span to make sure we
- // reserve enough space in both orientations.
- float smallestCellWidth = Math.min(landCellSize.x, portCellSize.x);
- float smallestCellHeight = Math.min(landCellSize.y, portCellSize.y);
-
- // We want to account for the extra amount of padding that we are adding to the widget
- // to ensure that it gets the full amount of space that it has requested.
- Rect widgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(
- context, provider, null);
- spanX = Math.max(1, (int) Math.ceil(
- (minWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth));
- spanY = Math.max(1, (int) Math.ceil(
- (minHeight + widgetPadding.top + widgetPadding.bottom) / smallestCellHeight));
-
- minSpanX = Math.max(1, (int) Math.ceil(
- (minResizeWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth));
- minSpanY = Math.max(1, (int) Math.ceil(
- (minResizeHeight + widgetPadding.top + widgetPadding.bottom) / smallestCellHeight));
- }
-
- public String getLabel(PackageManager packageManager) {
- return super.loadLabel(packageManager);
- }
-
- public Point getMinSpans() {
- return new Point((resizeMode & RESIZE_HORIZONTAL) != 0 ? minSpanX : -1,
- (resizeMode & RESIZE_VERTICAL) != 0 ? minSpanY : -1);
- }
-
- public boolean isCustomWidget() {
- return provider.getClassName().startsWith(CLS_CUSTOM_WIDGET_PREFIX);
- }
-
- public int getWidgetFeatures() {
- if (Utilities.ATLEAST_P) {
- return widgetFeatures;
- } else {
- return 0;
- }
- }
-
- @Override
- public final ComponentName getComponent() {
- return provider;
- }
-
- @Override
- public final UserHandle getUser() {
- return getProfile();
- }
-
- @Override
- public Drawable getFullResIcon(IconCache cache) {
- return cache.getFullResIcon(provider.getPackageName(), icon);
- }
-}
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index 25afb55..6c0daa4 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -15,6 +15,7 @@
private static final String XML = ".xml";
public static final String LAUNCHER_DB = "launcher.db";
+ public static final String LAUNCHER_4_BY_5_DB = "launcher_4_by_5.db";
public static final String LAUNCHER_4_BY_4_DB = "launcher_4_by_4.db";
public static final String LAUNCHER_3_BY_3_DB = "launcher_3_by_3.db";
public static final String LAUNCHER_2_BY_2_DB = "launcher_2_by_2.db";
@@ -30,6 +31,7 @@
public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList(
LAUNCHER_DB,
+ LAUNCHER_4_BY_5_DB,
LAUNCHER_4_BY_4_DB,
LAUNCHER_3_BY_3_DB,
LAUNCHER_2_BY_2_DB,
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index f434c91..545f4c3 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,7 +20,6 @@
import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
import android.content.Context;
import android.content.Intent;
@@ -44,9 +43,12 @@
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.CacheDataUpdatedTask;
+import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.model.LoaderResults;
import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDelegate;
import com.android.launcher3.model.ModelWriter;
+import com.android.launcher3.model.PackageIncrementalDownloadUpdatedTask;
import com.android.launcher3.model.PackageInstallStateChangedTask;
import com.android.launcher3.model.PackageUpdatedTask;
import com.android.launcher3.model.ShortcutsChangedTask;
@@ -58,9 +60,8 @@
import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
@@ -71,6 +72,7 @@
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
import java.util.function.Supplier;
/**
@@ -85,7 +87,6 @@
private final LauncherAppState mApp;
private final Object mLock = new Object();
- private final LooperExecutor mMainExecutor = MAIN_EXECUTOR;
private LoaderTask mLoaderTask;
private boolean mIsLoaderTaskRunning;
@@ -112,20 +113,26 @@
*/
private final BgDataModel mBgDataModel = new BgDataModel();
+ private final ModelDelegate mModelDelegate;
+
// Runnable to check if the shortcuts permission has changed.
- private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
+ private final Runnable mDataValidationCheck = new Runnable() {
@Override
public void run() {
- if (mModelLoaded && hasShortcutsPermission(mApp.getContext())
- != mBgAllAppsList.hasShortcutHostPermission()) {
- forceReload();
+ if (mModelLoaded) {
+ mModelDelegate.validateData();
}
}
};
- LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
+ LauncherModel(Context context, LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
mApp = app;
mBgAllAppsList = new AllAppsList(iconCache, appFilter);
+ mModelDelegate = ModelDelegate.newInstance(context, app, mBgAllAppsList, mBgDataModel);
+ }
+
+ public ModelDelegate getModelDelegate() {
+ return mModelDelegate;
}
/**
@@ -195,6 +202,15 @@
}
@Override
+ public void onPackageLoadingProgressChanged(
+ String packageName, UserHandle user, float progress) {
+ if (Utilities.ATLEAST_S) {
+ enqueueModelUpdateTask(new PackageIncrementalDownloadUpdatedTask(
+ packageName, user, progress));
+ }
+ }
+
+ @Override
public void onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts,
UserHandle user) {
enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
@@ -217,6 +233,20 @@
}
}
+ /**
+ * Called when the workspace items have drastically changed
+ */
+ public void onWorkspaceUiChanged() {
+ MODEL_EXECUTOR.execute(mModelDelegate::workspaceLoadComplete);
+ }
+
+ /**
+ * Called when the model is destroyed
+ */
+ public void destroy() {
+ MODEL_EXECUTOR.execute(mModelDelegate::destroy);
+ }
+
public void onBroadcastIntent(Intent intent) {
if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent);
final String action = intent.getAction();
@@ -321,20 +351,21 @@
*/
public boolean startLoader() {
// Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
- InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
+ ItemInstallQueue.INSTANCE.get(mApp.getContext())
+ .pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING);
synchronized (mLock) {
// Don't bother to start the thread if we know it's not going to do anything
final Callbacks[] callbacksList = getCallbacks();
if (callbacksList.length > 0) {
// Clear any pending bind-runnables from the synchronized load process.
for (Callbacks cb : callbacksList) {
- mMainExecutor.execute(cb::clearPendingBinds);
+ MAIN_EXECUTOR.execute(cb::clearPendingBinds);
}
// If there is already one running, tell it to stop.
stopLoader();
LoaderResults loaderResults = new LoaderResults(
- mApp, mBgDataModel, mBgAllAppsList, callbacksList, mMainExecutor);
+ mApp, mBgDataModel, mBgAllAppsList, callbacksList);
if (mModelLoaded && !mIsLoaderTaskRunning) {
// Divide the set of loaded items into those that we are binding synchronously,
// and everything else that is to be bound normally (asynchronously).
@@ -346,7 +377,13 @@
loaderResults.bindWidgets();
return true;
} else {
- startLoaderForResults(loaderResults);
+ stopLoader();
+ mLoaderTask = new LoaderTask(
+ mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, loaderResults);
+
+ // Always post the loader task, instead of running directly
+ // (even on same thread) so that we exit any nested synchronized blocks
+ MODEL_EXECUTOR.post(mLoaderTask);
}
}
}
@@ -369,24 +406,17 @@
}
}
- public void startLoaderForResults(LoaderResults results) {
+ /**
+ * Loads the model if not loaded
+ * @param callback called with the data model upon successful load or null on model thread.
+ */
+ public void loadAsync(Consumer<BgDataModel> callback) {
synchronized (mLock) {
- stopLoader();
- mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, mBgDataModel, results);
-
- // Always post the loader task, instead of running directly (even on same thread) so
- // that we exit any nested synchronized blocks
- MODEL_EXECUTOR.post(mLoaderTask);
- }
- }
-
- public void startLoaderForResultsIfNotLoaded(LoaderResults results) {
- synchronized (mLock) {
- if (!isModelLoaded()) {
- Log.d(TAG, "Workspace not loaded, loading now");
- startLoaderForResults(results);
+ if (!mModelLoaded && !mIsLoaderTaskRunning) {
+ startLoader();
}
}
+ MODEL_EXECUTOR.post(() -> callback.accept(isModelLoaded() ? mBgDataModel : null));
}
@Override
@@ -410,7 +440,7 @@
enqueueModelUpdateTask(new BaseModelUpdateTask() {
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
- final IntSparseArrayMap<Boolean> removedIds = new IntSparseArrayMap<>();
+ final IntSet removedIds = new IntSet();
synchronized (dataModel) {
for (ItemInfo info : dataModel.itemsIdMap) {
if (info instanceof WorkspaceItemInfo
@@ -418,13 +448,13 @@
&& user.equals(info.user)
&& info.getIntent() != null
&& TextUtils.equals(packageName, info.getIntent().getPackage())) {
- removedIds.put(info.id, true /* remove */);
+ removedIds.add(info.id);
}
}
}
if (!removedIds.isEmpty()) {
- deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds, false));
+ deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds));
}
}
});
@@ -491,9 +521,9 @@
* Current implementation simply reloads the workspace, but it can be optimized to
* use partial updates similar to {@link UserCache}
*/
- public void refreshShortcutsIfRequired() {
- MODEL_EXECUTOR.getHandler().removeCallbacks(mShortcutPermissionCheckRunnable);
- MODEL_EXECUTOR.post(mShortcutPermissionCheckRunnable);
+ public void validateModelDataOnResume() {
+ MODEL_EXECUTOR.getHandler().removeCallbacks(mDataValidationCheck);
+ MODEL_EXECUTOR.post(mDataValidationCheck);
}
/**
@@ -520,7 +550,7 @@
}
public void enqueueModelUpdateTask(ModelUpdateTask task) {
- task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
+ task.init(mApp, this, mBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
MODEL_EXECUTOR.execute(task);
}
@@ -588,7 +618,9 @@
+ "\" bitmapIcon=" + info.bitmap.icon
+ " componentName=" + info.componentName.getPackageName());
}
+ writer.println();
}
+ mModelDelegate.dump(prefix, fd, writer, args);
mBgDataModel.dump(prefix, fd, writer, args);
}
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index e8b5568..440e9e3 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -61,6 +61,7 @@
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.DbDowngradeHelper;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.provider.LauncherDbUtils;
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
@@ -71,6 +72,7 @@
import com.android.launcher3.util.NoLocaleSQLiteHelper;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
import org.xmlpull.v1.XmlPullParser;
@@ -97,13 +99,15 @@
* Represents the schema of the database. Changes in scheme need not be backwards compatible.
* When increasing the scheme version, ensure that downgrade_schema.json is updated
*/
- public static final int SCHEMA_VERSION = 28;
+ public static final int SCHEMA_VERSION = 29;
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".settings";
+ public static final String KEY_LAYOUT_PROVIDER_AUTHORITY = "KEY_LAYOUT_PROVIDER_AUTHORITY";
static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
protected DatabaseHelper mOpenHelper;
+ protected String mProviderAuthority;
private long mLastRestoreTimestamp = 0L;
@@ -186,6 +190,9 @@
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
+ final Bundle extra = new Bundle();
+ extra.putString(LauncherSettings.Settings.EXTRA_DB_NAME, mOpenHelper.getDatabaseName());
+ result.setExtras(extra);
result.setNotificationUri(getContext().getContentResolver(), uri);
return result;
@@ -367,7 +374,8 @@
case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
Bundle result = new Bundle();
result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
- Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false));
+ Utilities.getPrefs(getContext()).getBoolean(
+ mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false));
return result;
}
case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
@@ -437,6 +445,7 @@
getContext(), true /* forMigration */)));
return result;
}
+ return null;
}
case LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW: {
if (MULTI_DB_GRID_MIRATION_ALGO.get()) {
@@ -450,6 +459,23 @@
() -> mOpenHelper));
return result;
}
+ return null;
+ }
+ case LauncherSettings.Settings.METHOD_SWITCH_DATABASE: {
+ if (TextUtils.equals(arg, mOpenHelper.getDatabaseName())) return null;
+ final DatabaseHelper helper = mOpenHelper;
+ if (extras == null || !extras.containsKey(KEY_LAYOUT_PROVIDER_AUTHORITY)) {
+ mProviderAuthority = null;
+ } else {
+ mProviderAuthority = extras.getString(KEY_LAYOUT_PROVIDER_AUTHORITY);
+ }
+ mOpenHelper = DatabaseHelper.createDatabaseHelper(
+ getContext(), arg, false /* forMigration */);
+ helper.close();
+ LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+ if (app == null) return null;
+ app.getModel().forceReload();
+ return null;
}
}
return null;
@@ -492,7 +518,8 @@
}
private void clearFlagEmptyDbCreated() {
- Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
+ Utilities.getPrefs(getContext()).edit()
+ .remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit();
}
/**
@@ -505,7 +532,7 @@
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
- if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
+ if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
Log.d(TAG, "loading default workspace");
AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
@@ -553,8 +580,13 @@
*/
private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
Context ctx = getContext();
- String authority = Settings.Secure.getString(ctx.getContentResolver(),
- "launcher3.layout.provider");
+ final String authority;
+ if (!TextUtils.isEmpty(mProviderAuthority)) {
+ authority = mProviderAuthority;
+ } else {
+ authority = Settings.Secure.getString(ctx.getContentResolver(),
+ "launcher3.layout.provider");
+ }
if (TextUtils.isEmpty(authority)) {
return null;
}
@@ -587,7 +619,7 @@
.appendQueryParameter("version", "1")
.appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
.appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
- .appendQueryParameter("hotseatSize", Integer.toString(grid.numHotseatIcons))
+ .appendQueryParameter("hotseatSize", Integer.toString(grid.numDatabaseHotseatIcons))
.build();
}
@@ -694,11 +726,25 @@
}
/**
+ * Re-composite given key in respect to database. If the current db is
+ * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
+ * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
+ * string will be "EMPTY_DATABASE_CREATED@minimal.db".
+ */
+ String getKey(final String key) {
+ if (TextUtils.equals(getDatabaseName(), LauncherFiles.LAUNCHER_DB)) {
+ return key;
+ }
+ return key + "@" + getDatabaseName();
+ }
+
+ /**
* Overriden in tests.
*/
protected void onEmptyDbCreated() {
// Set the flag for empty DB
- Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
+ Utilities.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
+ .commit();
}
public long getSerialNumberForUser(UserHandle user) {
@@ -834,9 +880,18 @@
}
dropTable(db, "workspaceScreens");
}
- case 28:
+ case 28: {
+ boolean columnAdded = addIntegerColumn(
+ db, Favorites.APPWIDGET_SOURCE, Favorites.CONTAINER_UNKNOWN);
+ if (!columnAdded) {
+ // Old version remains, which means we wipe old data
+ break;
+ }
+ }
+ case 29: {
// DB Upgraded successfully
return;
+ }
}
// DB was not upgraded
@@ -872,7 +927,6 @@
* Removes widgets which are registered to the Launcher's host, but are not present
* in our model.
*/
- @TargetApi(Build.VERSION_CODES.O)
public void removeGhostWidgets(SQLiteDatabase db) {
// Get all existing widget ids.
final AppWidgetHost host = newLauncherWidgetHost();
@@ -1056,11 +1110,20 @@
* @return the max _id in the provided table.
*/
@Thunk static int getMaxId(SQLiteDatabase db, String query, Object... args) {
- int max = (int) DatabaseUtils.longForQuery(db,
- String.format(Locale.ENGLISH, query, args),
- null);
- if (max < 0) {
- throw new RuntimeException("Error: could not query max id");
+ int max = 0;
+ try (SQLiteStatement prog = db.compileStatement(
+ String.format(Locale.ENGLISH, query, args))) {
+ max = (int) DatabaseUtils.longForQuery(prog, null);
+ if (max < 0) {
+ throw new RuntimeException("Error: could not query max id");
+ }
+ } catch (IllegalArgumentException exception) {
+ String message = exception.getMessage();
+ if (message.contains("re-open") && message.contains("already-closed")) {
+ // Don't crash trying to end a transaction an an already closed DB. See b/173162852.
+ } else {
+ throw exception;
+ }
}
return max;
}
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 51504ce..f26cfe8 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -4,12 +4,14 @@
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.view.ViewDebug;
import android.view.WindowInsets;
+import com.android.launcher3.graphics.SysUiScrim;
import com.android.launcher3.statemanager.StatefulActivity;
import java.util.Collections;
@@ -31,14 +33,24 @@
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mForceHideBackArrow;
+ private final SysUiScrim mSysUiScrim;
+
public LauncherRootView(Context context, AttributeSet attrs) {
super(context, attrs);
mActivity = StatefulActivity.fromContext(context);
+ mSysUiScrim = new SysUiScrim(this);
}
private void handleSystemWindowInsets(Rect insets) {
- // Update device profile before notifying th children.
- mActivity.getDeviceProfile().updateInsets(insets);
+ DeviceProfile dp = mActivity.getDeviceProfile();
+
+ // Taskbar provides insets, but we don't want that for most Launcher elements so remove it.
+ mTempRect.set(insets);
+ insets = mTempRect;
+ insets.bottom = Math.max(0, insets.bottom - dp.nonOverlappingTaskbarInset);
+
+ // Update device profile before notifying the children.
+ dp.updateInsets(insets);
boolean resetState = !insets.equals(mInsets);
setInsets(insets);
@@ -61,6 +73,7 @@
// modifying child layout params.
if (!insets.equals(mInsets)) {
super.setInsets(insets);
+ mSysUiScrim.onInsetsChanged(insets);
}
}
@@ -90,10 +103,17 @@
}
@Override
+ protected void dispatchDraw(Canvas canvas) {
+ mSysUiScrim.draw(canvas);
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(l, t, r, b);
setDisallowBackGesture(mDisallowBackGesture);
+ mSysUiScrim.setSize(r - l, b - t);
}
@TargetApi(Build.VERSION_CODES.Q)
@@ -113,10 +133,14 @@
: Collections.emptyList());
}
+ public SysUiScrim getSysUiScrim() {
+ return mSysUiScrim;
+ }
+
public interface WindowStateListener {
void onWindowFocusChanged(boolean hasFocus);
void onWindowVisibilityChanged(int visibility);
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 5512654..d663480 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -75,6 +75,37 @@
public static final int ITEM_TYPE_SHORTCUT = 1;
/**
+ * The favorite is a user created folder
+ */
+ public static final int ITEM_TYPE_FOLDER = 2;
+
+ /**
+ * The favorite is a widget
+ */
+ public static final int ITEM_TYPE_APPWIDGET = 4;
+
+ /**
+ * The favorite is a custom widget provided by the launcher
+ */
+ public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5;
+
+ /**
+ * The gesture is an application created deep shortcut
+ */
+ public static final int ITEM_TYPE_DEEP_SHORTCUT = 6;
+
+ /**
+ * Type of the item is recents task.
+ * TODO(hyunyoungs): move constants not related to Favorites DB to a better location.
+ */
+ public static final int ITEM_TYPE_TASK = 7;
+
+ /**
+ * The item is QSB
+ */
+ public static final int ITEM_TYPE_QSB = 8;
+
+ /**
* The icon package name in Intent.ShortcutIconResource
* <P>Type: TEXT</P>
*/
@@ -121,6 +152,12 @@
+ LauncherProvider.AUTHORITY + "/" + TABLE_NAME);
/**
+ * The content:// style URL for "favorites_bakup" table
+ */
+ public static final Uri BACKUP_CONTENT_URI = Uri.parse("content://"
+ + LauncherProvider.AUTHORITY + "/" + BACKUP_TABLE_NAME);
+
+ /**
* The content:// style URL for "favorites_preview" table
*/
public static final Uri PREVIEW_CONTENT_URI = Uri.parse("content://"
@@ -156,14 +193,23 @@
public static final int CONTAINER_DESKTOP = -100;
public static final int CONTAINER_HOTSEAT = -101;
public static final int CONTAINER_PREDICTION = -102;
+ public static final int CONTAINER_WIDGETS_PREDICTION = -111;
public static final int CONTAINER_HOTSEAT_PREDICTION = -103;
public static final int CONTAINER_ALL_APPS = -104;
public static final int CONTAINER_WIDGETS_TRAY = -105;
+ public static final int CONTAINER_BOTTOM_WIDGETS_TRAY = -112;
+ public static final int CONTAINER_PIN_WIDGETS = -113;
// Represents search results view.
public static final int CONTAINER_SEARCH_RESULTS = -106;
public static final int CONTAINER_SHORTCUTS = -107;
public static final int CONTAINER_SETTINGS = -108;
public static final int CONTAINER_TASKSWITCHER = -109;
+ public static final int CONTAINER_QSB = -110;
+
+ // Represents any of the extended containers implemented in non-AOSP variants.
+ public static final int EXTENDED_CONTAINERS = -200;
+
+ public static final int CONTAINER_UNKNOWN = -1;
public static final String containerToString(int container) {
switch (container) {
@@ -186,6 +232,8 @@
case ITEM_TYPE_APPWIDGET: return "WIDGET";
case ITEM_TYPE_CUSTOM_APPWIDGET: return "CUSTOMWIDGET";
case ITEM_TYPE_DEEP_SHORTCUT: return "DEEPSHORTCUT";
+ case ITEM_TYPE_TASK: return "TASK";
+ case ITEM_TYPE_QSB: return "QSB";
default: return String.valueOf(type);
}
}
@@ -231,32 +279,6 @@
public static final String PROFILE_ID = "profileId";
/**
- * The favorite is a user created folder
- */
- public static final int ITEM_TYPE_FOLDER = 2;
-
- /**
- * The favorite is a widget
- */
- public static final int ITEM_TYPE_APPWIDGET = 4;
-
- /**
- * The favorite is a custom widget provided by the launcher
- */
- public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5;
-
- /**
- * The gesture is an application created deep shortcut
- */
- public static final int ITEM_TYPE_DEEP_SHORTCUT = 6;
-
- /**
- * Type of the item is recents task.
- * TODO(hyunyoungs): move constants not related to Favorites DB to a better location.
- */
- public static final int ITEM_TYPE_TASK = 7;
-
- /**
* The appWidgetId of the widget
*
* <P>Type: INTEGER</P>
@@ -288,6 +310,12 @@
*/
public static final String OPTIONS = "options";
+ /**
+ * Stores the source container that the widget was added from.
+ * <p>Type: INTEGER</p>
+ */
+ public static final String APPWIDGET_SOURCE = "appWidgetSource";
+
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
addTableToDb(db, myProfileId, optional, TABLE_NAME);
}
@@ -315,7 +343,8 @@
"restored INTEGER NOT NULL DEFAULT 0," +
"profileId INTEGER DEFAULT " + myProfileId + "," +
"rank INTEGER NOT NULL DEFAULT 0," +
- "options INTEGER NOT NULL DEFAULT 0" +
+ "options INTEGER NOT NULL DEFAULT 0," +
+ APPWIDGET_SOURCE + " INTEGER NOT NULL DEFAULT " + CONTAINER_UNKNOWN +
");");
}
}
@@ -354,14 +383,22 @@
public static final String METHOD_PREP_FOR_PREVIEW = "prep_for_preview";
+ public static final String METHOD_SWITCH_DATABASE = "switch_database";
+
public static final String EXTRA_VALUE = "value";
+ public static final String EXTRA_DB_NAME = "db_name";
+
public static Bundle call(ContentResolver cr, String method) {
- return call(cr, method, null);
+ return call(cr, method, null /* arg */);
}
public static Bundle call(ContentResolver cr, String method, String arg) {
- return cr.call(CONTENT_URI, method, arg, null);
+ return call(cr, method, arg, null /* extras */);
+ }
+
+ public static Bundle call(ContentResolver cr, String method, String arg, Bundle extras) {
+ return cr.call(CONTENT_URI, method, arg, extras);
}
}
}
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index c78df62..3399ce9 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -16,17 +16,21 @@
package com.android.launcher3;
import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.HINT_STATE_TWO_BUTTON_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.OVERVIEW_MODAL_TASK_STATE_ORDINAL;
-import static com.android.launcher3.testing.TestProtocol.OVERVIEW_PEEK_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
import android.content.Context;
+import android.graphics.Color;
import android.view.animation.Interpolator;
import com.android.launcher3.statemanager.BaseState;
@@ -36,7 +40,6 @@
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.uioverrides.states.AllAppsState;
import com.android.launcher3.uioverrides.states.OverviewState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import java.util.Arrays;
@@ -51,16 +54,13 @@
*/
public static final int NONE = 0;
public static final int HOTSEAT_ICONS = 1 << 0;
- public static final int HOTSEAT_SEARCH_BOX = 1 << 1;
- public static final int ALL_APPS_HEADER = 1 << 2;
- public static final int ALL_APPS_HEADER_EXTRA = 1 << 3; // e.g. app predictions
- public static final int ALL_APPS_CONTENT = 1 << 4;
- public static final int VERTICAL_SWIPE_INDICATOR = 1 << 5;
- public static final int OVERVIEW_BUTTONS = 1 << 6;
-
- /** Mask of all the items that are contained in the apps view. */
- public static final int APPS_VIEW_ITEM_MASK =
- HOTSEAT_SEARCH_BOX | ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT;
+ public static final int ALL_APPS_CONTENT = 1 << 1;
+ public static final int VERTICAL_SWIPE_INDICATOR = 1 << 2;
+ public static final int OVERVIEW_ACTIONS = 1 << 3;
+ public static final int TASKBAR = 1 << 4;
+ public static final int CLEAR_ALL_BUTTON = 1 << 5;
+ public static final int WORKSPACE_PAGE_INDICATOR = 1 << 6;
+ public static final int SPLIT_PLACHOLDER_VIEW = 1 << 7;
// Flag indicating workspace has multiple pages visible.
public static final int FLAG_MULTI_PAGE = BaseState.getFlag(0);
@@ -92,13 +92,13 @@
}
};
- private static final LauncherState[] sAllStates = new LauncherState[9];
+ private static final LauncherState[] sAllStates = new LauncherState[10];
/**
* TODO: Create a separate class for NORMAL state.
*/
public static final LauncherState NORMAL = new LauncherState(NORMAL_STATE_ORDINAL,
- ContainerType.WORKSPACE,
+ LAUNCHER_STATE_HOME,
FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED | FLAG_HIDE_BACK_BUTTON |
FLAG_HAS_SYS_UI_SCRIM) {
@Override
@@ -115,23 +115,25 @@
SPRING_LOADED_STATE_ORDINAL);
public static final LauncherState ALL_APPS = new AllAppsState(ALL_APPS_STATE_ORDINAL);
public static final LauncherState HINT_STATE = new HintState(HINT_STATE_ORDINAL);
+ public static final LauncherState HINT_STATE_TWO_BUTTON = new HintState(
+ HINT_STATE_TWO_BUTTON_ORDINAL, LAUNCHER_STATE_OVERVIEW);
public static final LauncherState OVERVIEW = new OverviewState(OVERVIEW_STATE_ORDINAL);
- public static final LauncherState OVERVIEW_PEEK =
- OverviewState.newPeekState(OVERVIEW_PEEK_STATE_ORDINAL);
public static final LauncherState OVERVIEW_MODAL_TASK = OverviewState.newModalTaskState(
OVERVIEW_MODAL_TASK_STATE_ORDINAL);
public static final LauncherState QUICK_SWITCH =
OverviewState.newSwitchState(QUICK_SWITCH_STATE_ORDINAL);
public static final LauncherState BACKGROUND_APP =
OverviewState.newBackgroundState(BACKGROUND_APP_STATE_ORDINAL);
+ public static final LauncherState OVERVIEW_SPLIT_SELECT =
+ OverviewState.newSplitSelectState(OVERVIEW_SPLIT_SELECT_ORDINAL);
public final int ordinal;
/**
- * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
+ * Used for {@link com.android.launcher3.logging.StatsLogManager}
*/
- public final int containerType;
+ public final int statsLogOrdinal;
/**
* True if the state has overview panel visible.
@@ -140,8 +142,8 @@
private final int mFlags;
- public LauncherState(int id, int containerType, int flags) {
- this.containerType = containerType;
+ public LauncherState(int id, int statsLogOrdinal, int flags) {
+ this.statsLogOrdinal = statsLogOrdinal;
this.mFlags = flags;
this.overviewUi = (flags & FLAG_OVERVIEW_UI) != 0;
this.ordinal = id;
@@ -171,16 +173,20 @@
/**
* Returns an array of two elements.
- * The first specifies the scale for the overview
- * The second is the factor ([0, 1], 0 => center-screen; 1 => offscreen) by which overview
- * should be shifted horizontally.
+ * The first specifies the scale for the overview
+ * The second is the factor ([0, 1], 0 => center-screen; 1 => offscreen) by which overview
+ * should be shifted horizontally.
*/
public float[] getOverviewScaleAndOffset(Launcher launcher) {
return launcher.getNormalOverviewScaleAndOffset();
}
- public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
- return new ScaleAndTranslation(NO_SCALE, NO_OFFSET, NO_OFFSET);
+ public float getTaskbarScale(Launcher launcher) {
+ return launcher.getNormalTaskbarScale();
+ }
+
+ public float getTaskbarTranslationY(Launcher launcher) {
+ return -launcher.getHotseat().getTaskbarOffsetY();
}
public float getOverviewFullscreenProgress() {
@@ -188,10 +194,15 @@
}
public int getVisibleElements(Launcher launcher) {
- if (launcher.getDeviceProfile().isVerticalBarLayout()) {
- return HOTSEAT_ICONS | VERTICAL_SWIPE_INDICATOR;
- }
- return HOTSEAT_ICONS | HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR;
+ return HOTSEAT_ICONS | WORKSPACE_PAGE_INDICATOR | VERTICAL_SWIPE_INDICATOR | TASKBAR;
+ }
+
+ /**
+ * A shorthand for checking getVisibleElements() & elements == elements.
+ * @return Whether all of the given elements are visible.
+ */
+ public boolean areElementsVisible(Launcher launcher, int elements) {
+ return (getVisibleElements(launcher) & elements) == elements;
}
/**
@@ -203,12 +214,16 @@
return 1f;
}
- public float getWorkspaceScrimAlpha(Launcher launcher) {
+ public float getWorkspaceBackgroundAlpha(Launcher launcher) {
return 0;
}
- public float getOverviewScrimAlpha(Launcher launcher) {
- return 0;
+ /**
+ * What color should the workspace scrim be in when at rest in this state.
+ * Return {@link Color#TRANSPARENT} for no scrim.
+ */
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return Color.TRANSPARENT;
}
/**
@@ -220,6 +235,14 @@
}
/**
+ * For this state, how much additional translation there should be for each of the
+ * child TaskViews. Note that the translation can be its primary or secondary dimension.
+ */
+ public float getSplitSelectTranslation(Launcher launcher) {
+ return 0;
+ }
+
+ /**
* The amount of blur and wallpaper zoom to apply to the background of either the app
* or Launcher surface in this state. Should be a number between 0 and 1, inclusive.
*
@@ -232,6 +255,7 @@
/**
* Returns the amount of blur and wallpaper zoom for this state with {@param isMultiWindowMode}.
+ *
* @see #getDepth(Context).
*/
public final float getDepth(Context context, boolean isMultiWindowMode) {
@@ -250,14 +274,15 @@
}
public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
- if (this != NORMAL || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
+ if ((this != NORMAL && this != HINT_STATE)
+ || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
return DEFAULT_ALPHA_PROVIDER;
}
final int centerPage = launcher.getWorkspace().getNextPage();
return new PageAlphaProvider(ACCEL_2) {
@Override
public float getPageAlpha(int pageIndex) {
- return pageIndex != centerPage ? 0 : 1f;
+ return pageIndex != centerPage ? 0 : 1f;
}
};
}
diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java
index 5f6ecb5..f2a3de7 100644
--- a/src/com/android/launcher3/MainProcessInitializer.java
+++ b/src/com/android/launcher3/MainProcessInitializer.java
@@ -38,7 +38,6 @@
protected void init(Context context) {
FileLog.setDir(context.getApplicationContext().getFilesDir());
FeatureFlags.initialize(context);
- SessionCommitReceiver.applyDefaultUserPrefs(context);
IconShape.init(context);
if (BitmapCreationCheck.ENABLED) {
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index e29faac..b423871 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -16,16 +16,14 @@
package com.android.launcher3;
+import static com.android.launcher3.anim.Interpolators.SCROLL;
import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
-import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_BY;
import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_TO;
import android.animation.LayoutTransition;
-import android.animation.TimeInterpolator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
@@ -46,23 +44,22 @@
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.animation.Interpolator;
+import android.widget.OverScroller;
import android.widget.ScrollView;
import androidx.annotation.Nullable;
-import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.pageindicators.PageIndicator;
-import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds;
-import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.EdgeEffectCompat;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
+import java.util.function.Consumer;
/**
* An abstraction of the original Workspace which supports browsing through a
@@ -79,9 +76,6 @@
public static final int PAGE_SNAP_ANIMATION_DURATION = 750;
- // OverScroll constants
- private final static int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270;
-
private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f;
// The page is moved more than halfway, automatically move to the next page on touch up.
private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f;
@@ -112,7 +106,6 @@
protected int mMaxScroll;
protected int mMinScroll;
protected OverScroller mScroller;
- private Interpolator mDefaultInterpolator;
private VelocityTracker mVelocityTracker;
protected int mPageSpacing = 0;
@@ -143,12 +136,6 @@
protected boolean mIsPageInTransition = false;
private Runnable mOnPageTransitionEndCallback;
- protected float mSpringOverScroll;
-
- protected boolean mWasInOverscroll = false;
-
- protected int mUnboundedScroll;
-
// Page Indicator
@Thunk int mPageIndicatorViewId;
protected T mPageIndicator;
@@ -161,6 +148,9 @@
private int[] mTmpIntPair = new int[2];
+ protected EdgeEffectCompat mEdgeGlowLeft;
+ protected EdgeEffectCompat mEdgeGlowRight;
+
public PagedView(Context context) {
this(context, null);
}
@@ -180,8 +170,7 @@
setHapticFeedbackEnabled(false);
mIsRtl = Utilities.isRtl(getResources());
- mScroller = new OverScroller(context);
- setDefaultInterpolator(Interpolators.SCROLL);
+ mScroller = new OverScroller(context, SCROLL);
mCurrentPage = 0;
final ViewConfiguration configuration = ViewConfiguration.get(context);
@@ -195,14 +184,14 @@
mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density);
mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density);
- if (Utilities.ATLEAST_OREO) {
- setDefaultFocusHighlightEnabled(false);
- }
+ initEdgeEffect();
+ setDefaultFocusHighlightEnabled(false);
+ setWillNotDraw(false);
}
- protected void setDefaultInterpolator(Interpolator interpolator) {
- mDefaultInterpolator = interpolator;
- mScroller.setInterpolator(mDefaultInterpolator);
+ protected void initEdgeEffect() {
+ mEdgeGlowLeft = new EdgeEffectCompat(getContext());
+ mEdgeGlowRight = new EdgeEffectCompat(getContext());
}
public void initParentViews(View parent) {
@@ -219,7 +208,7 @@
/**
* Returns the index of the currently displayed page. When in free scroll mode, this is the page
* that the user was on before entering free scroll mode (e.g. the home screen page they
- * long-pressed on to enter the overview). Try using {@link #getPageNearestToCenterOfScreen()}
+ * long-pressed on to enter the overview). Try using {@link #getDestinationPage()}
* to get the page the user is currently scrolling over.
*/
public int getCurrentPage() {
@@ -257,10 +246,19 @@
newPosition = getScrollForPage(mCurrentPage);
}
mOrientationHandler.set(this, VIEW_SCROLL_TO, newPosition);
- mOrientationHandler.scrollerStartScroll(mScroller, newPosition);
+ mScroller.startScroll(mScroller.getCurrX(), 0, newPosition - mScroller.getCurrX(), 0);
forceFinishScroller(true);
}
+ /**
+ * Immediately finishes any overscroll effect and jumps to the end of the scroller animation.
+ */
+ public void abortScrollerAnimation() {
+ mEdgeGlowLeft.finish();
+ mEdgeGlowRight.finish();
+ abortScrollerAnimation(true);
+ }
+
private void abortScrollerAnimation(boolean resetNextPage) {
mScroller.abortAnimation();
// We need to clean up the next page here to avoid computeScrollHelper from
@@ -284,7 +282,45 @@
private int validateNewPage(int newPage) {
newPage = ensureWithinScrollBounds(newPage);
// Ensure that it is clamped by the actual set of children in all cases
- return Utilities.boundToRange(newPage, 0, getPageCount() - 1);
+ newPage = Utilities.boundToRange(newPage, 0, getPageCount() - 1);
+
+ if (getPanelCount() > 1) {
+ // Always return left panel as new page
+ newPage = getLeftmostVisiblePageForIndex(newPage);
+ }
+ return newPage;
+ }
+
+ private int getLeftmostVisiblePageForIndex(int pageIndex) {
+ int panelCount = getPanelCount();
+ return (pageIndex / panelCount) * panelCount;
+ }
+
+ /**
+ * Returns the number of pages that are shown at the same time.
+ */
+ protected int getPanelCount() {
+ return 1;
+ }
+
+ /**
+ * Executes the callback against each visible page
+ */
+ public void forEachVisiblePage(Consumer<View> callback) {
+ int panelCount = getPanelCount();
+ for (int i = mCurrentPage; i < mCurrentPage + panelCount; i++) {
+ View page = getPageAt(i);
+ if (page != null) {
+ callback.accept(page);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the view is on one of the current pages, false otherwise.
+ */
+ public boolean isVisible(View child) {
+ return getLeftmostVisiblePageForIndex(indexOfChild(child)) == mCurrentPage;
}
/**
@@ -359,12 +395,19 @@
}
protected void pageEndTransition() {
- if (mIsPageInTransition) {
+ if (mIsPageInTransition && !mIsBeingDragged && mScroller.isFinished()
+ && (!isShown() || (mEdgeGlowLeft.isFinished() && mEdgeGlowRight.isFinished()))) {
mIsPageInTransition = false;
onPageEndTransition();
}
}
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ pageEndTransition();
+ super.onVisibilityAggregated(isVisible);
+ }
+
protected boolean isPageInTransition() {
return mIsPageInTransition;
}
@@ -381,7 +424,6 @@
* to provide custom behavior during animation.
*/
protected void onPageEndTransition() {
- mWasInOverscroll = false;
AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
AccessibilityManagerCompat.sendCustomAccessibilityEvent(getPageAt(mCurrentPage),
AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
@@ -403,56 +445,12 @@
}
}
- protected int getUnboundedScroll() {
- return mUnboundedScroll;
- }
-
- @Override
- public void scrollBy(int x, int y) {
- mOrientationHandler.delegateScrollBy(this, getUnboundedScroll(), x, y);
- }
-
@Override
public void scrollTo(int x, int y) {
- int primaryScroll = mOrientationHandler.getPrimaryValue(x, y);
- int secondaryScroll = mOrientationHandler.getSecondaryValue(x, y);
- mUnboundedScroll = primaryScroll;
-
- boolean isBeforeFirstPage = mIsRtl ?
- (primaryScroll > mMaxScroll) : (primaryScroll < mMinScroll);
- boolean isAfterLastPage = mIsRtl ?
- (primaryScroll < mMinScroll) : (primaryScroll > mMaxScroll);
- if (!isBeforeFirstPage && !isAfterLastPage) {
- mSpringOverScroll = 0;
- }
-
- if (isBeforeFirstPage) {
- mOrientationHandler.delegateScrollTo(this,
- secondaryScroll, mIsRtl ? mMaxScroll : mMinScroll);
- if (mAllowOverScroll) {
- mWasInOverscroll = true;
- overScroll(primaryScroll - (mIsRtl ? mMaxScroll : mMinScroll));
- }
- } else if (isAfterLastPage) {
- mOrientationHandler.delegateScrollTo(this,
- secondaryScroll, mIsRtl ? mMinScroll : mMaxScroll);
- if (mAllowOverScroll) {
- mWasInOverscroll = true;
- overScroll(primaryScroll - (mIsRtl ? mMinScroll : mMaxScroll));
- }
- } else {
- if (mWasInOverscroll) {
- overScroll(0);
- mWasInOverscroll = false;
- }
- super.scrollTo(x, y);
- }
- }
-
- /**
- * Helper for {@link PagedOrientationHandler} to be able to call parent's scrollTo method
- */
- public void superScrollTo(int x, int y) {
+ x = Utilities.boundToRange(x,
+ mOrientationHandler.getPrimaryValue(mMinScroll, 0), mMaxScroll);
+ y = Utilities.boundToRange(y,
+ mOrientationHandler.getPrimaryValue(0, mMinScroll), mMaxScroll);
super.scrollTo(x, y);
}
@@ -470,11 +468,6 @@
}
}
- // we moved this functionality to a helper function so SmoothPagedView can reuse it
- protected boolean computeScrollHelper() {
- return computeScrollHelper(true);
- }
-
protected void announcePageForAccessibility() {
if (isAccessibilityEnabled(getContext())) {
// Notify the user when the page changes
@@ -482,21 +475,37 @@
}
}
- protected boolean computeScrollHelper(boolean shouldInvalidate) {
+ protected boolean computeScrollHelper() {
if (mScroller.computeScrollOffset()) {
// Don't bother scrolling if the page does not need to be moved
- int currentScroll = mOrientationHandler.getPrimaryScroll(this);
- if (mUnboundedScroll != mScroller.getCurrPos()
- || currentScroll != mScroller.getCurrPos()) {
- mOrientationHandler.set(this, VIEW_SCROLL_TO, mScroller.getCurrPos());
+ int oldPos = mOrientationHandler.getPrimaryScroll(this);
+ int newPos = mScroller.getCurrX();
+ if (oldPos != newPos) {
+ mOrientationHandler.set(this, VIEW_SCROLL_TO, mScroller.getCurrX());
}
- if (shouldInvalidate) {
- invalidate();
- }
- return true;
- } else if (mNextPage != INVALID_PAGE && shouldInvalidate) {
- sendScrollAccessibilityEvent();
+ if (mAllowOverScroll) {
+ if (newPos < mMinScroll && oldPos >= mMinScroll) {
+ mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
+ mScroller.abortAnimation();
+ } else if (newPos > mMaxScroll && oldPos <= mMaxScroll) {
+ mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
+ mScroller.abortAnimation();
+ }
+ }
+
+ // If the scroller has scrolled to the final position and there is no edge effect, then
+ // finish the scroller to skip waiting for additional settling
+ int finalPos = mOrientationHandler.getPrimaryValue(mScroller.getFinalX(),
+ mScroller.getFinalY());
+ if (newPos == finalPos && mEdgeGlowLeft.isFinished() && mEdgeGlowRight.isFinished()) {
+ mScroller.abortAnimation();
+ }
+
+ invalidate();
+ return true;
+ } else if (mNextPage != INVALID_PAGE) {
+ sendScrollAccessibilityEvent();
int prevPage = mCurrentPage;
mCurrentPage = validateNewPage(mNextPage);
mNextPage = INVALID_PAGE;
@@ -550,6 +559,10 @@
super.forceLayout();
}
+ private int getPageWidthSize(int widthSize) {
+ return (widthSize - mInsets.left - mInsets.right) / getPanelCount();
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() == 0) {
@@ -580,7 +593,7 @@
if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize);
int myWidthSpec = MeasureSpec.makeMeasureSpec(
- widthSize - mInsets.left - mInsets.right, MeasureSpec.EXACTLY);
+ getPageWidthSize(widthSize), MeasureSpec.EXACTLY);
int myHeightSpec = MeasureSpec.makeMeasureSpec(
heightSize - mInsets.top - mInsets.bottom, MeasureSpec.EXACTLY);
@@ -672,11 +685,10 @@
final int primaryDimension = bounds.primaryDimension;
final int childPrimaryEnd = bounds.childPrimaryEnd;
- // In case the pages are of different width, align the page to left or right edge
- // based on the orientation.
- final int pageScroll = mIsRtl
- ? (childStart - scrollOffsetStart)
- : Math.max(0, childPrimaryEnd - scrollOffsetEnd);
+ // In case the pages are of different width, align the page to left edge for non-RTL
+ // or right edge for RTL.
+ final int pageScroll =
+ mIsRtl ? childPrimaryEnd - scrollOffsetEnd : childStart - scrollOffsetStart;
if (outPageScrolls[i] != pageScroll) {
pageScrollChanged = true;
outPageScrolls[i] = pageScroll;
@@ -684,6 +696,19 @@
childStart += primaryDimension + mPageSpacing + getChildGap();
}
}
+
+ int panelCount = getPanelCount();
+ if (panelCount > 1) {
+ for (int i = 0; i < childCount; i++) {
+ // In case we have multiple panels, always use left panel's page scroll for all
+ // panels on the screen.
+ int adjustedScroll = outPageScrolls[getLeftmostVisiblePageForIndex(i)];
+ if (outPageScrolls[i] != adjustedScroll) {
+ outPageScrolls[i] = adjustedScroll;
+ pageScrollChanged = true;
+ }
+ }
+ }
return pageScrollChanged;
}
@@ -747,6 +772,11 @@
return mOrientationHandler.getChildStart(pageAtIndex);
}
+ protected int getChildVisibleSize(int index) {
+ View layout = getPageAt(index);
+ return mOrientationHandler.getMeasuredSize(layout);
+ }
+
@Override
public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
int page = indexToPage(indexOfChild(child));
@@ -791,14 +821,16 @@
}
if (direction == View.FOCUS_LEFT) {
if (getCurrentPage() > 0) {
- snapToPage(getCurrentPage() - 1);
- getChildAt(getCurrentPage() - 1).requestFocus(direction);
+ int nextPage = validateNewPage(getCurrentPage() - 1);
+ snapToPage(nextPage);
+ getChildAt(nextPage).requestFocus(direction);
return true;
}
} else if (direction == View.FOCUS_RIGHT) {
if (getCurrentPage() < getPageCount() - 1) {
- snapToPage(getCurrentPage() + 1);
- getChildAt(getCurrentPage() + 1).requestFocus(direction);
+ int nextPage = validateNewPage(getCurrentPage() + 1);
+ snapToPage(nextPage);
+ getChildAt(nextPage).requestFocus(direction);
return true;
}
}
@@ -811,17 +843,26 @@
return;
}
+ // Add the current page's views as focusable and the next possible page's too. If the
+ // last focus change action was left then the left neighbour's views will be added, and
+ // if it was right then the right neighbour's views will be added.
+ // Unfortunately mCurrentPage can be outdated if there were multiple control actions in a
+ // short period of time, but mNextPage is up to date because it is always updated by
+ // method snapToPage.
+ int nextPage = getNextPage();
// XXX-RTL: This will be fixed in a future CL
- if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
- getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode);
+ if (nextPage >= 0 && nextPage < getPageCount()) {
+ getPageAt(nextPage).addFocusables(views, direction, focusableMode);
}
if (direction == View.FOCUS_LEFT) {
- if (mCurrentPage > 0) {
- getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode);
+ if (nextPage > 0) {
+ nextPage = validateNewPage(nextPage - 1);
+ getPageAt(nextPage).addFocusables(views, direction, focusableMode);
}
- } else if (direction == View.FOCUS_RIGHT){
- if (mCurrentPage < getPageCount() - 1) {
- getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode);
+ } else if (direction == View.FOCUS_RIGHT) {
+ if (nextPage < getPageCount() - 1) {
+ nextPage = validateNewPage(nextPage + 1);
+ getPageAt(nextPage).addFocusables(views, direction, focusableMode);
}
}
}
@@ -862,8 +903,7 @@
if (disallowIntercept) {
// We need to make sure to cancel our long press if
// a scrollable widget takes over touch events
- final View currentPage = getPageAt(mCurrentPage);
- currentPage.cancelLongPress();
+ cancelCurrentPageLongPress();
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
@@ -919,9 +959,7 @@
mTotalMotion = 0;
mAllowEasyFling = false;
mActivePointerId = ev.getPointerId(0);
-
- updateIsBeingDraggedOnTouchDown();
-
+ updateIsBeingDraggedOnTouchDown(ev);
break;
}
@@ -946,9 +984,9 @@
/**
* If being flinged and user touches the screen, initiate drag; otherwise don't.
*/
- private void updateIsBeingDraggedOnTouchDown() {
+ private void updateIsBeingDraggedOnTouchDown(MotionEvent ev) {
// mScroller.isFinished should be false when being flinged.
- final int xDist = Math.abs(mScroller.getFinalPos() - mScroller.getCurrPos());
+ final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
final boolean finishedScrolling = (mScroller.isFinished() || xDist < mPageSlop / 3);
if (finishedScrolling) {
@@ -957,9 +995,20 @@
setCurrentPage(getNextPage());
pageEndTransition();
}
+ mIsBeingDragged = !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished();
} else {
mIsBeingDragged = true;
}
+
+ // Catch the edge effect if it is active.
+ float displacement = mOrientationHandler.getSecondaryValue(ev.getX(), ev.getY())
+ / mOrientationHandler.getSecondaryValue(getWidth(), getHeight());
+ if (!mEdgeGlowLeft.isFinished()) {
+ mEdgeGlowLeft.onPullDistance(0f, 1f - displacement);
+ }
+ if (!mEdgeGlowRight.isFinished()) {
+ mEdgeGlowRight.onPullDistance(0f, displacement);
+ }
}
public boolean isHandlingTouch() {
@@ -990,7 +1039,6 @@
mTotalMotion += Math.abs(mLastMotion - primaryDirection);
mLastMotion = primaryDirection;
mLastMotionRemainder = 0;
- onScrollInteractionBegin();
pageBeginTransition();
// Stop listening for things like pinches.
requestDisallowInterceptTouchEvent(true);
@@ -1001,10 +1049,7 @@
// Try canceling the long press. It could also have been scheduled
// by a distant descendant, so use the mAllowLongPress flag to block
// everything
- final View currentPage = getPageAt(mCurrentPage);
- if (currentPage != null) {
- currentPage.cancelLongPress();
- }
+ forEachVisiblePage(View::cancelLongPress);
}
protected float getScrollProgress(int screenCenter, View v, int page) {
@@ -1054,69 +1099,6 @@
}
}
- @Override
- protected void dispatchDraw(Canvas canvas) {
- if (mScroller.isSpringing() && mSpringOverScroll != 0) {
- int saveCount = canvas.save();
- mOrientationHandler.set(canvas, CANVAS_TRANSLATE, -mSpringOverScroll);
- super.dispatchDraw(canvas);
-
- canvas.restoreToCount(saveCount);
- } else {
- super.dispatchDraw(canvas);
- }
- }
-
- /**
- * Returns the amount of overscroll caused by the spring in {@link OverScroller}.
- */
- private int getSpringOverScroll(int amount) {
- if (mScroller.isSpringing()) {
- return amount < 0
- ? mScroller.getCurrPos() - mMinScroll
- : Math.max(0, mScroller.getCurrPos() - mMaxScroll);
- } else {
- return 0;
- }
- }
-
- protected void dampedOverScroll(int amount) {
- if (amount == 0) {
- return;
- }
-
- int size = mOrientationHandler.getMeasuredSize(this);
- int overScrollAmount = OverScroll.dampedScroll(amount, size);
- if (mScroller.isSpringing()) {
- mSpringOverScroll = getSpringOverScroll(amount);
- invalidate();
- return;
- }
-
- int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
- int boundedScroll = Utilities.boundToRange(primaryScroll, mMinScroll, mMaxScroll);
- mOrientationHandler.delegateScrollTo(this, boundedScroll + overScrollAmount);
- invalidate();
- }
-
- protected void overScroll(int amount) {
- if (mScroller.isSpringing()) {
- mSpringOverScroll = getSpringOverScroll(amount);
- invalidate();
- return;
- }
-
- if (amount == 0) return;
-
- if (mFreeScroll && !mScroller.isFinished()) {
- int scrollAmount = amount < 0 ? mMinScroll + amount : mMaxScroll + amount;
- mOrientationHandler.delegateScrollTo(this, scrollAmount);
- } else {
- dampedOverScroll(amount);
- }
- }
-
-
public void setEnableFreeScroll(boolean freeScroll) {
if (mFreeScroll == freeScroll) {
return;
@@ -1149,7 +1131,7 @@
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
- updateIsBeingDraggedOnTouchDown();
+ updateIsBeingDraggedOnTouchDown(ev);
/*
* If being flinged and user touches, stop the fling. isFinished
@@ -1168,7 +1150,6 @@
mAllowEasyFling = false;
mActivePointerId = ev.getPointerId(0);
if (mIsBeingDragged) {
- onScrollInteractionBegin();
pageBeginTransition();
}
break;
@@ -1185,19 +1166,62 @@
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) return true;
+ float oldScroll = mOrientationHandler.getPrimaryScroll(this);
+ float dx = ev.getX(pointerIndex);
+ float dy = ev.getY(pointerIndex);
- float direction = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
+ float direction = mOrientationHandler.getPrimaryValue(dx, dy);
float delta = mLastMotion + mLastMotionRemainder - direction;
+
+ int width = getWidth();
+ int height = getHeight();
+ int size = mOrientationHandler.getPrimaryValue(width, height);
+
+ final float displacement = mOrientationHandler.getSecondaryValue(dx, dy)
+ / mOrientationHandler.getSecondaryValue(width, height);
mTotalMotion += Math.abs(delta);
+ if (mAllowOverScroll) {
+ float consumed = 0;
+ if (delta < 0 && mEdgeGlowRight.getDistance() != 0f) {
+ consumed = size * mEdgeGlowRight.onPullDistance(delta / size, displacement);
+ } else if (delta > 0 && mEdgeGlowLeft.getDistance() != 0f) {
+ consumed = -size * mEdgeGlowLeft.onPullDistance(
+ -delta / size, 1 - displacement);
+ }
+ delta -= consumed;
+ }
+
// Only scroll and update mLastMotionX if we have moved some discrete amount. We
// keep the remainder because we are actually testing if we've moved from the last
// scrolled position (which is discrete).
- if (Math.abs(delta) >= 1.0f) {
- mLastMotion = direction;
- mLastMotionRemainder = delta - (int) delta;
+ mLastMotion = direction;
+ int movedDelta = (int) delta;
+ mLastMotionRemainder = delta - movedDelta;
- mOrientationHandler.set(this, VIEW_SCROLL_BY, (int) delta);
+ if (delta != 0) {
+ mOrientationHandler.set(this, VIEW_SCROLL_BY, movedDelta);
+
+ if (mAllowOverScroll) {
+ final float pulledToX = oldScroll + delta;
+
+ if (pulledToX < mMinScroll) {
+ mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement);
+ if (!mEdgeGlowRight.isFinished()) {
+ mEdgeGlowRight.onRelease();
+ }
+ } else if (pulledToX > mMaxScroll) {
+ mEdgeGlowRight.onPullDistance(delta / size, displacement);
+ if (!mEdgeGlowLeft.isFinished()) {
+ mEdgeGlowLeft.onRelease();
+ }
+ }
+
+ if (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished()) {
+ postInvalidateOnAnimation();
+ }
+ }
+
} else {
awakenScrollBars();
}
@@ -1252,12 +1276,14 @@
if (((isSignificantMove && !isDeltaLeft && !isFling) ||
(isFling && !isVelocityLeft)) && mCurrentPage > 0) {
- finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
+ finalPage = returnToOriginalPage
+ ? mCurrentPage : mCurrentPage - getPanelCount();
snapToPageWithVelocity(finalPage, velocity);
} else if (((isSignificantMove && isDeltaLeft && !isFling) ||
(isFling && isVelocityLeft)) &&
mCurrentPage < getChildCount() - 1) {
- finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
+ finalPage = returnToOriginalPage
+ ? mCurrentPage : mCurrentPage + getPanelCount();
snapToPageWithVelocity(finalPage, velocity);
} else {
snapToDestination();
@@ -1273,44 +1299,24 @@
if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) ||
((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) {
- mScroller.springBack(initialScroll, minScroll, maxScroll);
- mNextPage = getPageNearestToCenterOfScreen();
+ mScroller.springBack(initialScroll, 0, minScroll, maxScroll, 0, 0);
+ mNextPage = getDestinationPage();
} else {
- mScroller.setInterpolator(mDefaultInterpolator);
- mScroller.fling(initialScroll, -velocity,
- minScroll, maxScroll,
- Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
+ int velocity1 = -velocity;
+ // Continue a scroll or fling in progress
+ mScroller.fling(initialScroll, 0, velocity1, 0, minScroll, maxScroll, 0, 0,
+ Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR), 0);
- int finalPos = mScroller.getFinalPos();
- mNextPage = getPageNearestToCenterOfScreen(finalPos);
-
- int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
- int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
- if (finalPos > minScroll && finalPos < maxScroll) {
- // If scrolling ends in the half of the added space that is closer to
- // the end, settle to the end. Otherwise snap to the nearest page.
- // If flinging past one of the ends, don't change the velocity as it
- // will get stopped at the end anyway.
- int pageSnapped = finalPos < (firstPageScroll + minScroll) / 2
- ? minScroll
- : finalPos > (lastPageScroll + maxScroll) / 2
- ? maxScroll
- : getScrollForPage(mNextPage);
-
- mScroller.setFinalPos(pageSnapped);
- // Ensure the scroll/snap doesn't happen too fast;
- int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
- - mScroller.getDuration();
- if (extraScrollDuration > 0) {
- mScroller.extendDuration(extraScrollDuration);
- }
- }
+ int finalPos = mScroller.getFinalX();
+ mNextPage = getDestinationPage(finalPos);
+ onNotSnappingToPageInFreeScroll();
}
invalidate();
}
- onScrollInteractionEnd();
}
+ mEdgeGlowLeft.onRelease();
+ mEdgeGlowRight.onRelease();
// End any intermediate reordering states
resetTouchState();
break;
@@ -1318,8 +1324,9 @@
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
snapToDestination();
- onScrollInteractionEnd();
}
+ mEdgeGlowLeft.onRelease();
+ mEdgeGlowRight.onRelease();
resetTouchState();
break;
@@ -1332,6 +1339,8 @@
return true;
}
+ protected void onNotSnappingToPageInFreeScroll() { }
+
protected boolean shouldFlingForVelocity(int velocity) {
float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
return Math.abs(velocity) > threshold;
@@ -1343,15 +1352,6 @@
mActivePointerId = INVALID_POINTER;
}
- /**
- * Triggered by scrolling via touch
- */
- protected void onScrollInteractionBegin() {
- }
-
- protected void onScrollInteractionEnd() {
- }
-
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
@@ -1431,28 +1431,40 @@
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
+
+ // In case the device is controlled by a controller, mCurrentPage isn't updated properly
+ // which results in incorrect navigation
+ int nextPage = getNextPage();
+ if (nextPage != mCurrentPage) {
+ setCurrentPage(nextPage);
+ }
+
int page = indexToPage(indexOfChild(child));
if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) {
snapToPage(page);
}
}
+ public int getDestinationPage() {
+ return getDestinationPage(mOrientationHandler.getPrimaryScroll(this));
+ }
+
+ protected int getDestinationPage(int primaryScroll) {
+ return getPageNearestToCenterOfScreen(primaryScroll);
+ }
+
public int getPageNearestToCenterOfScreen() {
return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this));
}
- private int getPageNearestToCenterOfScreen(int scaledScroll) {
- int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
- int screenCenter = scaledScroll + (pageOrientationSize / 2);
+ private int getPageNearestToCenterOfScreen(int primaryScroll) {
+ int screenCenter = getScreenCenter(primaryScroll);
int minDistanceFromScreenCenter = Integer.MAX_VALUE;
int minDistanceFromScreenCenterIndex = -1;
final int childCount = getChildCount();
for (int i = 0; i < childCount; ++i) {
- View layout = getPageAt(i);
- int childSize = mOrientationHandler.getMeasuredSize(layout);
- int halfChildSize = (childSize / 2);
- int childCenter = getChildOffset(i) + halfChildSize;
- int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
+ int distanceFromScreenCenter = Math.abs(
+ getDisplacementFromScreenCenter(i, screenCenter));
if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
minDistanceFromScreenCenter = distanceFromScreenCenter;
minDistanceFromScreenCenterIndex = i;
@@ -1461,20 +1473,29 @@
return minDistanceFromScreenCenterIndex;
}
+ private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) {
+ int childSize = Math.round(getChildVisibleSize(childIndex));
+ int halfChildSize = (childSize / 2);
+ int childCenter = getChildOffset(childIndex) + halfChildSize;
+ return childCenter - screenCenter;
+ }
+
+ protected int getDisplacementFromScreenCenter(int childIndex) {
+ int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+ int screenCenter = getScreenCenter(primaryScroll);
+ return getDisplacementFromScreenCenter(childIndex, screenCenter);
+ }
+
+ private int getScreenCenter(int primaryScroll) {
+ float primaryScale = mOrientationHandler.getPrimaryScale(this);
+ float primaryPivot = mOrientationHandler.getPrimaryValue(getPivotX(), getPivotY());
+ int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
+ return Math.round(primaryScroll + (pageOrientationSize / 2f - primaryPivot) / primaryScale
+ + primaryPivot);
+ }
+
protected void snapToDestination() {
- snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration());
- }
-
- protected boolean isInOverScroll() {
- int scroll = mOrientationHandler.getPrimaryScroll(this);
- return scroll > mMaxScroll || scroll < mMinScroll;
- }
-
- protected int getPageSnapDuration() {
- if (isInOverScroll()) {
- return OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION;
- }
- return PAGE_SNAP_ANIMATION_DURATION;
+ snapToPage(getDestinationPage(), PAGE_SNAP_ANIMATION_DURATION);
}
// We want the duration of the page snap animation to be influenced by the distance that
@@ -1492,7 +1513,7 @@
int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2;
final int newLoc = getScrollForPage(whichPage);
- int delta = newLoc - getUnboundedScroll();
+ int delta = newLoc - mOrientationHandler.getPrimaryScroll(this);
int duration = 0;
if (Math.abs(velocity) < mMinFlingVelocity) {
@@ -1517,12 +1538,7 @@
// interpolator at zero, ie. 5. We use 4 to make it a little slower.
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
- if (QUICKSTEP_SPRINGS.get() && mCurrentPage != whichPage) {
- return snapToPage(whichPage, delta, duration, false, null,
- velocity * Math.signum(delta), true);
- } else {
- return snapToPage(whichPage, delta, duration);
- }
+ return snapToPage(whichPage, delta, duration);
}
public boolean snapToPage(int whichPage) {
@@ -1530,32 +1546,26 @@
}
public boolean snapToPageImmediately(int whichPage) {
- return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null);
+ return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true);
}
public boolean snapToPage(int whichPage, int duration) {
- return snapToPage(whichPage, duration, false, null);
+ return snapToPage(whichPage, duration, false);
}
- public boolean snapToPage(int whichPage, int duration, TimeInterpolator interpolator) {
- return snapToPage(whichPage, duration, false, interpolator);
- }
-
- protected boolean snapToPage(int whichPage, int duration, boolean immediate,
- TimeInterpolator interpolator) {
+ protected boolean snapToPage(int whichPage, int duration, boolean immediate) {
whichPage = validateNewPage(whichPage);
int newLoc = getScrollForPage(whichPage);
- final int delta = newLoc - getUnboundedScroll();
- return snapToPage(whichPage, delta, duration, immediate, interpolator, 0, false);
+ final int delta = newLoc - mOrientationHandler.getPrimaryScroll(this);
+ return snapToPage(whichPage, delta, duration, immediate);
}
protected boolean snapToPage(int whichPage, int delta, int duration) {
- return snapToPage(whichPage, delta, duration, false, null, 0, false);
+ return snapToPage(whichPage, delta, duration, false);
}
- protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate,
- TimeInterpolator interpolator, float velocity, boolean spring) {
+ protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate) {
if (mFirstLayout) {
setCurrentPage(whichPage);
return false;
@@ -1585,18 +1595,7 @@
abortScrollerAnimation(false);
}
- if (interpolator != null) {
- mScroller.setInterpolator(interpolator);
- } else {
- mScroller.setInterpolator(mDefaultInterpolator);
- }
-
- if (spring && QUICKSTEP_SPRINGS.get()) {
- mScroller.startScrollSpring(getUnboundedScroll(), delta, duration, velocity);
- } else {
- mScroller.startScroll(getUnboundedScroll(), delta, duration);
- }
-
+ mScroller.startScroll(mOrientationHandler.getPrimaryScroll(this), 0, delta, 0, duration);
updatePageIndicator();
// Trigger a compute() to finish switching pages if necessary
@@ -1614,7 +1613,7 @@
snapToPage(getNextPage() - 1);
return true;
}
- return onOverscroll(-getMeasuredWidth());
+ return mAllowOverScroll;
}
public boolean scrollRight() {
@@ -1622,15 +1621,7 @@
snapToPage(getNextPage() + 1);
return true;
}
- return onOverscroll(getMeasuredWidth());
- }
-
- protected boolean onOverscroll(int amount) {
- if (!mAllowOverScroll) return false;
- onScrollInteractionBegin();
- overScroll(amount);
- onScrollInteractionEnd();
- return true;
+ return mAllowOverScroll;
}
@Override
@@ -1774,4 +1765,39 @@
mTmpIntPair[1] = rightChild;
return mTmpIntPair;
}
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ drawEdgeEffect(canvas);
+ pageEndTransition();
+ }
+
+ protected void drawEdgeEffect(Canvas canvas) {
+ if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) {
+ final int width = getWidth();
+ final int height = getHeight();
+ if (!mEdgeGlowLeft.isFinished()) {
+ final int restoreCount = canvas.save();
+ canvas.rotate(-90);
+ canvas.translate(-height, Math.min(mMinScroll, getScrollX()));
+ mEdgeGlowLeft.setSize(height, width);
+ if (mEdgeGlowLeft.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowRight.isFinished()) {
+ final int restoreCount = canvas.save();
+ canvas.rotate(90, width, 0);
+ canvas.translate(width, -(Math.max(mMaxScroll, getScrollX())));
+
+ mEdgeGlowRight.setSize(height, width);
+ if (mEdgeGlowRight.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ }
+ }
}
diff --git a/src/com/android/launcher3/Partner.java b/src/com/android/launcher3/Partner.java
index d79f62d..0bdb37c 100644
--- a/src/com/android/launcher3/Partner.java
+++ b/src/com/android/launcher3/Partner.java
@@ -129,7 +129,7 @@
"dimen", getPackageName());
if (resId > 0) {
int px = getResources().getDimensionPixelSize(resId);
- iconSize = Utilities.dpiFromPx(px, dm);
+ iconSize = Utilities.dpiFromPx((float) px, dm.densityDpi);
}
} catch (Resources.NotFoundException ex) {
Log.e(TAG, "Invalid Partner grid resource!", ex);
diff --git a/src/com/android/launcher3/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java
index 403d779..ece123d 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -22,8 +22,11 @@
public class ResourceUtils {
public static final int DEFAULT_NAVBAR_VALUE = 48;
+ public static final int INVALID_RESOURCE_HANDLE = -1;
public static final String NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE = "navigation_bar_width";
public static final String NAVBAR_BOTTOM_GESTURE_SIZE = "navigation_bar_gesture_height";
+ public static final String NAVBAR_BOTTOM_GESTURE_LARGER_SIZE =
+ "navigation_bar_gesture_larger_height";
public static int getNavbarSize(String resName, Resources res) {
return getDimenByName(resName, res, DEFAULT_NAVBAR_VALUE);
@@ -51,7 +54,17 @@
return val;
}
+ public static int getIntegerByName(String resName, Resources res, int defaultValue) {
+ int resId = res.getIdentifier(resName, "integer", "android");
+ return resId != 0 ? res.getInteger(resId) : defaultValue;
+ }
+
public static int pxFromDp(float size, DisplayMetrics metrics) {
- return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
+ return pxFromDp(size, metrics, 1f);
+ }
+
+ public static int pxFromDp(float size, DisplayMetrics metrics, float scale) {
+ return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(scale
+ * TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
}
}
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 499b54f..cd06414 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -3,6 +3,7 @@
import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
+import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DISMISS_PREDICTION;
import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE;
@@ -33,23 +34,21 @@
import android.view.View;
import android.widget.Toast;
-import com.android.launcher3.Launcher.OnResumeCallback;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.logging.LoggerUtils;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.PendingRequestArgs;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import java.net.URISyntaxException;
-import java.util.ArrayList;
/**
* Drop target which provides a secondary option for an item.
@@ -111,16 +110,13 @@
mCurrentAccessibilityAction = action;
if (action == UNINSTALL) {
- mHoverColor = getResources().getColor(R.color.uninstall_target_hover_tint);
- setDrawable(R.drawable.ic_uninstall_shadow);
+ setDrawable(R.drawable.ic_uninstall_no_shadow);
updateText(R.string.uninstall_drop_target_label);
} else if (action == DISMISS_PREDICTION) {
- mHoverColor = Themes.getColorAccent(getContext());
- setDrawable(R.drawable.ic_block_shadow);
+ setDrawable(R.drawable.ic_block_no_shadow);
updateText(R.string.dismiss_prediction_label);
} else if (action == RECONFIGURE) {
- mHoverColor = Themes.getColorAccent(getContext());
- setDrawable(R.drawable.ic_setup_shadow);
+ setDrawable(R.drawable.ic_setting);
updateText(R.string.gadget_setup_text);
}
}
@@ -136,19 +132,6 @@
}
@Override
- public Target getDropTargetForLogging() {
- Target t = LoggerUtils.newTarget(Target.Type.CONTROL);
- if (mCurrentAccessibilityAction == UNINSTALL) {
- t.controlType = ControlType.UNINSTALL_TARGET;
- } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
- t.controlType = ControlType.DISMISS_PREDICTION;
- } else {
- t.controlType = ControlType.SETTINGS_BUTTON;
- }
- return t;
- }
-
- @Override
protected boolean supportsDrop(ItemInfo info) {
return supportsAccessibilityDrop(info, getViewUnderDrag(info));
}
@@ -218,13 +201,20 @@
public void onDrop(DragObject d, DragOptions options) {
// Defer onComplete
d.dragSource = new DeferredOnComplete(d.dragSource, getContext());
+
super.onDrop(d, options);
+ doLog(d.logInstanceId, d.originalDragInfo);
+ }
+
+ private void doLog(InstanceId logInstanceId, ItemInfo itemInfo) {
+ StatsLogger logger = mStatsLogManager.logger().withInstanceId(logInstanceId);
+ if (itemInfo != null) {
+ logger.withItemInfo(itemInfo);
+ }
if (mCurrentAccessibilityAction == UNINSTALL) {
- mStatsLogManager.logger().withInstanceId(d.logInstanceId)
- .log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL);
+ logger.log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL);
} else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
- mStatsLogManager.logger().withInstanceId(d.logInstanceId)
- .log(LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST);
+ logger.log(LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST);
}
}
@@ -235,7 +225,7 @@
DeferredOnComplete deferred = (DeferredOnComplete) d.dragSource;
if (target != null) {
deferred.mPackageName = target.getPackageName();
- mLauncher.addOnResumeCallback(deferred);
+ mLauncher.addOnResumeCallback(deferred::onLauncherResume);
} else {
deferred.sendFailure();
}
@@ -278,13 +268,15 @@
if (mCurrentAccessibilityAction == RECONFIGURE) {
int widgetId = getReconfigurableWidgetId(view);
if (widgetId != INVALID_APPWIDGET_ID) {
- mLauncher.getAppWidgetHost().startConfigActivity(mLauncher, widgetId, -1);
+ mLauncher.setWaitingForResult(
+ PendingRequestArgs.forWidgetInfo(widgetId, null, info));
+ mLauncher.getAppWidgetHost().startConfigActivity(mLauncher, widgetId,
+ REQUEST_RECONFIGURE_APPWIDGET);
}
return null;
}
if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
- AppLaunchTracker.INSTANCE.get(getContext()).onDismissApp(info.getTargetComponent(),
- info.user, AppLaunchTracker.CONTAINER_PREDICTIONS);
+ // We sent the log event, nothing else left to do
return null;
}
// else: mCurrentAccessibilityAction == UNINSTALL
@@ -311,6 +303,7 @@
@Override
public void onAccessibilityDrop(View view, ItemInfo item) {
+ doLog(new InstanceIdSequence().newInstanceId(), item);
performDropAction(view, item);
}
@@ -318,7 +311,7 @@
* A wrapper around {@link DragSource} which delays the {@link #onDropCompleted} action until
* {@link #onLauncherResume}
*/
- private class DeferredOnComplete implements DragSource, OnResumeCallback {
+ private class DeferredOnComplete implements DragSource {
private final DragSource mOriginal;
private final Context mContext;
@@ -337,13 +330,6 @@
mDragObject = d;
}
- @Override
- public void fillInLogContainerData(ItemInfo childInfo, Target child,
- ArrayList<Target> parents) {
- mOriginal.fillInLogContainerData(childInfo, child, parents);
- }
-
- @Override
public void onLauncherResume() {
// We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well.
if (new PackageManagerHelper(mContext).getApplicationInfo(mPackageName,
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index 89f0a3d..558538c 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -16,55 +16,40 @@
package com.android.launcher3;
-import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle;
-
-import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
import android.os.UserHandle;
-import android.provider.Settings;
import android.text.TextUtils;
-import android.util.Log;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.util.Executors;
-import java.util.List;
-
/**
* BroadcastReceiver to handle session commit intent.
*/
-@TargetApi(Build.VERSION_CODES.O)
public class SessionCommitReceiver extends BroadcastReceiver {
- private static final String TAG = "SessionCommitReceiver";
-
- // The content provider for the add to home screen setting. It should be of the format:
- // <package name>.addtohomescreen
- private static final String MARKER_PROVIDER_PREFIX = ".addtohomescreen";
+ private static final String LOG = "SessionCommitReceiver";
// Preference key for automatically adding icon to homescreen.
public static final String ADD_ICON_PREFERENCE_KEY = "pref_add_icon_to_home";
- public static final String ADD_ICON_PREFERENCE_INITIALIZED_KEY =
- "pref_add_icon_to_home_initialized";
@Override
public void onReceive(Context context, Intent intent) {
- if (!isEnabled(context) || !Utilities.ATLEAST_OREO) {
+ Executors.MODEL_EXECUTOR.execute(() -> processIntent(context, intent));
+ }
+
+ @WorkerThread
+ private static void processIntent(Context context, Intent intent) {
+ if (!isEnabled(context)) {
// User has decided to not add icons on homescreen.
return;
}
@@ -86,105 +71,16 @@
return;
}
- queueAppIconAddition(context, info.getAppPackageName(), user);
- }
+ FileLog.d(LOG,
+ "Adding package name to install queue. Package name: " + info.getAppPackageName()
+ + ", has app icon: " + (info.getAppIcon() != null)
+ + ", has app label: " + !TextUtils.isEmpty(info.getAppLabel()));
- public static void queuePromiseAppIconAddition(Context context, SessionInfo sessionInfo) {
- String packageName = sessionInfo.getAppPackageName();
- if (context.getSystemService(LauncherApps.class)
- .getActivityList(packageName, getUserHandle(sessionInfo)).isEmpty()) {
- // Ensure application isn't already installed.
- queueAppIconAddition(context, packageName, sessionInfo.getAppLabel(),
- sessionInfo.getAppIcon(), getUserHandle(sessionInfo));
- }
- }
-
- public static void queueAppIconAddition(Context context, String packageName, UserHandle user) {
- List<LauncherActivityInfo> activities = context.getSystemService(LauncherApps.class)
- .getActivityList(packageName, user);
- if (activities.isEmpty()) {
- // no activity found
- return;
- }
- queueAppIconAddition(context, packageName, activities.get(0).getLabel(), null, user);
- }
-
- private static void queueAppIconAddition(Context context, String packageName,
- CharSequence label, Bitmap icon, UserHandle user) {
- Intent data = new Intent();
- data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent().setComponent(
- new ComponentName(packageName, "")).setPackage(packageName));
- data.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
- data.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
-
- InstallShortcutReceiver.queueApplication(data, user, context);
+ ItemInstallQueue.INSTANCE.get(context)
+ .queueItem(info.getAppPackageName(), user);
}
public static boolean isEnabled(Context context) {
return Utilities.getPrefs(context).getBoolean(ADD_ICON_PREFERENCE_KEY, true);
}
-
- public static void applyDefaultUserPrefs(final Context context) {
- if (!Utilities.ATLEAST_OREO) {
- return;
- }
- SharedPreferences prefs = Utilities.getPrefs(context);
- if (prefs.getAll().isEmpty()) {
- // This logic assumes that the code is the first thing that is executed (before any
- // shared preference is written).
- // TODO: Move this logic to DB upgrade once we have proper support for db downgrade
- // If it is a fresh start, just apply the default value. We use prefs.isEmpty() to infer
- // a fresh start as put preferences always contain some values corresponding to current
- // grid.
- prefs.edit().putBoolean(ADD_ICON_PREFERENCE_KEY, true).apply();
- } else if (!prefs.contains(ADD_ICON_PREFERENCE_INITIALIZED_KEY)) {
- new PrefInitTask(context).executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
- }
- }
-
- private static class PrefInitTask extends AsyncTask<Void, Void, Void> {
- private final Context mContext;
-
- PrefInitTask(Context context) {
- mContext = context;
- }
-
- @Override
- protected Void doInBackground(Void... voids) {
- boolean addIconToHomeScreenEnabled = readValueFromMarketApp();
- Utilities.getPrefs(mContext).edit()
- .putBoolean(ADD_ICON_PREFERENCE_KEY, addIconToHomeScreenEnabled)
- .putBoolean(ADD_ICON_PREFERENCE_INITIALIZED_KEY, true)
- .apply();
- return null;
- }
-
- public boolean readValueFromMarketApp() {
- // Get the marget package
- ResolveInfo ri = mContext.getPackageManager().resolveActivity(
- new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_MARKET),
- PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_SYSTEM_ONLY);
- if (ri == null) {
- return true;
- }
-
- Cursor c = null;
- try {
- c = mContext.getContentResolver().query(
- Uri.parse("content://" + ri.activityInfo.packageName
- + MARKER_PROVIDER_PREFIX),
- null, null, null, null);
- if (c.moveToNext()) {
- return c.getInt(c.getColumnIndexOrThrow(Settings.NameValueTable.VALUE)) != 0;
- }
- } catch (Exception e) {
- Log.d(TAG, "Error reading add to homescreen preference", e);
- } finally {
- if (c != null) {
- c.close();
- }
- }
- return true;
- }
- }
}
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 6326b7a..519b63d 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -18,6 +18,9 @@
import static android.view.MotionEvent.ACTION_DOWN;
+import static com.android.launcher3.CellLayout.FOLDER;
+import static com.android.launcher3.CellLayout.WORKSPACE;
+
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Rect;
@@ -26,25 +29,31 @@
import android.view.ViewGroup;
import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
-public class ShortcutAndWidgetContainer extends ViewGroup {
+public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.FolderIconParent {
static final String TAG = "ShortcutAndWidgetContainer";
// These are temporary variables to prevent having to allocate a new object just to
// return an (x, y) value from helper functions. Do NOT use them to maintain other state.
private final int[] mTmpCellXY = new int[2];
- @ContainerType private final int mContainerType;
+ private final Rect mTempRect = new Rect();
+
+ @ContainerType
+ private final int mContainerType;
private final WallpaperManager mWallpaperManager;
private int mCellWidth;
private int mCellHeight;
+ private int mBorderSpacing;
private int mCountX;
+ private int mCountY;
- private ActivityContext mActivity;
+ private final ActivityContext mActivity;
private boolean mInvertIfRtl = false;
public ShortcutAndWidgetContainer(Context context, @ContainerType int containerType) {
@@ -54,20 +63,23 @@
mContainerType = containerType;
}
- public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY) {
+ public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY,
+ int borderSpacing) {
mCellWidth = cellWidth;
mCellHeight = cellHeight;
mCountX = countX;
+ mCountY = countY;
+ mBorderSpacing = borderSpacing;
}
- public View getChildAt(int x, int y) {
+ public View getChildAt(int cellX, int cellY) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
- if ((lp.cellX <= x) && (x < lp.cellX + lp.cellHSpan) &&
- (lp.cellY <= y) && (y < lp.cellY + lp.cellVSpan)) {
+ if ((lp.cellX <= cellX) && (cellX < lp.cellX + lp.cellHSpan)
+ && (lp.cellY <= cellY) && (cellY < lp.cellY + lp.cellVSpan)) {
return child;
}
}
@@ -79,7 +91,7 @@
int count = getChildCount();
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSpecSize, heightSpecSize);
for (int i = 0; i < count; i++) {
@@ -94,10 +106,12 @@
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
if (child instanceof LauncherAppWidgetHostView) {
DeviceProfile profile = mActivity.getDeviceProfile();
- lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
- profile.appWidgetScale.x, profile.appWidgetScale.y);
+ ((LauncherAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
+ lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+ profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing, mTempRect);
} else {
- lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX);
+ lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+ mBorderSpacing, null);
}
}
@@ -108,25 +122,34 @@
public int getCellContentHeight() {
return Math.min(getMeasuredHeight(),
- mActivity.getDeviceProfile().getCellHeight(mContainerType));
+ mActivity.getDeviceProfile().getCellContentHeight(mContainerType));
}
public void measureChild(View child) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
- final DeviceProfile profile = mActivity.getDeviceProfile();
+ final DeviceProfile dp = mActivity.getDeviceProfile();
if (child instanceof LauncherAppWidgetHostView) {
- lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
- profile.appWidgetScale.x, profile.appWidgetScale.y);
- // Widgets have their own padding
+ ((LauncherAppWidgetHostView) child).getWidgetInset(dp, mTempRect);
+ lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+ dp.appWidgetScale.x, dp.appWidgetScale.y, mBorderSpacing, mTempRect);
} else {
- lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX);
+ lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+ mBorderSpacing, null);
// Center the icon/folder
int cHeight = getCellContentHeight();
- int cellPaddingY = (int) Math.max(0, ((lp.height - cHeight) / 2f));
- int cellPaddingX = mContainerType == CellLayout.WORKSPACE
- ? profile.workspaceCellPaddingXPx
- : (int) (profile.edgeMarginPx / 2f);
+ int cellPaddingY = dp.isScalableGrid && mContainerType == WORKSPACE
+ ? dp.cellYPaddingPx
+ : (int) Math.max(0, ((lp.height - cHeight) / 2f));
+
+ // No need to add padding when cell layout border spacing is present.
+ boolean noPaddingX = (dp.cellLayoutBorderSpacingPx > 0 && mContainerType == WORKSPACE)
+ || (dp.folderCellLayoutBorderSpacingPx > 0 && mContainerType == FOLDER);
+ int cellPaddingX = noPaddingX
+ ? 0
+ : mContainerType == WORKSPACE
+ ? dp.workspaceCellPaddingXPx
+ : (int) (dp.edgeMarginPx / 2f);
child.setPadding(cellPaddingX, cellPaddingY, cellPaddingX, 0);
}
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
@@ -220,4 +243,24 @@
child.cancelLongPress();
}
}
+
+ @Override
+ public void drawFolderLeaveBehindForIcon(FolderIcon child) {
+ CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+ // While the folder is open, the position of the icon cannot change.
+ lp.canReorder = false;
+ if (mContainerType == CellLayout.HOTSEAT) {
+ CellLayout cl = (CellLayout) getParent();
+ cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
+ }
+ }
+
+ @Override
+ public void clearFolderLeaveBehind(FolderIcon child) {
+ ((CellLayout.LayoutParams) child.getLayoutParams()).canReorder = true;
+ if (mContainerType == CellLayout.HOTSEAT) {
+ CellLayout cl = (CellLayout) getParent();
+ cl.clearFolderLeaveBehind();
+ }
+ }
}
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index bf63788..75f6278 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -18,7 +18,6 @@
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ICON_BADGED;
-import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.Person;
@@ -26,6 +25,7 @@
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
@@ -33,9 +33,13 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
+import android.content.res.Configuration;
import android.content.res.Resources;
+import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.LightingColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
@@ -44,11 +48,11 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
+import android.net.Uri;
import android.os.Build;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.Message;
-import android.os.PowerManager;
import android.os.TransactionTooLargeException;
import android.provider.Settings;
import android.text.Spannable;
@@ -62,14 +66,16 @@
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+import androidx.core.graphics.ColorUtils;
import androidx.core.os.BuildCompat;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
-import com.android.launcher3.graphics.GridOptionsProvider;
+import com.android.launcher3.graphics.GridCustomizationsProvider;
import com.android.launcher3.graphics.TintedDrawableSpan;
-import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.icons.ShortcutCachingLogic;
import com.android.launcher3.model.data.ItemInfo;
@@ -79,12 +85,14 @@
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
+import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -98,6 +106,8 @@
private static final Pattern sTrimPattern =
Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$");
+ private static final float[] sTmpFloatArray = new float[4];
+
private static final int[] sLoc0 = new int[2];
private static final int[] sLoc1 = new int[2];
private static final Matrix sMatrix = new Matrix();
@@ -106,18 +116,14 @@
public static final String[] EMPTY_STRING_ARRAY = new String[0];
public static final Person[] EMPTY_PERSON_ARRAY = new Person[0];
- public static final boolean ATLEAST_R = BuildCompat.isAtLeastR();
+ public static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
public static final boolean ATLEAST_Q = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
- public static final boolean ATLEAST_P =
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+ public static final boolean ATLEAST_R = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
- public static final boolean ATLEAST_OREO_MR1 =
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1;
-
- public static final boolean ATLEAST_OREO =
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+ public static final boolean ATLEAST_S = BuildCompat.isAtLeastS()
+ || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
/**
* Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}.
@@ -132,6 +138,15 @@
Build.TYPE.toLowerCase(Locale.ROOT).contains("debug") ||
Build.TYPE.toLowerCase(Locale.ROOT).equals("eng");
+ /**
+ * Returns true if theme is dark.
+ */
+ public static boolean isDarkTheme(Context context) {
+ Configuration configuration = context.getResources().getConfiguration();
+ int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ return nightMode == Configuration.UI_MODE_NIGHT_YES;
+ }
+
public static boolean isDevelopersOptionsEnabled(Context context) {
return Settings.Global.getInt(context.getApplicationContext().getContentResolver(),
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
@@ -141,6 +156,10 @@
public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET";
public static final String EXTRA_WALLPAPER_FLAVOR = "com.android.launcher3.WALLPAPER_FLAVOR";
+ // An intent extra to indicate the launch source by launcher.
+ public static final String EXTRA_WALLPAPER_LAUNCH_SOURCE =
+ "com.android.wallpaper.LAUNCH_SOURCE";
+
public static boolean IS_RUNNING_IN_TEST_HARNESS =
ActivityManager.isRunningInTestHarness();
@@ -214,6 +233,33 @@
}
/**
+ * Returns bounds for a child view of DragLayer, in drag layer coordinates.
+ *
+ * see {@link com.android.launcher3.dragndrop.DragLayer}.
+ *
+ * @param viewBounds Bounds of the view wanted in drag layer coordinates, relative to the view
+ * itself. eg. (0, 0, view.getWidth, view.getHeight)
+ * @param ignoreTransform If true, view transform is ignored
+ * @param outRect The out rect where we return the bounds of {@param view} in drag layer coords.
+ */
+ public static void getBoundsForViewInDragLayer(BaseDragLayer dragLayer, View view,
+ Rect viewBounds, boolean ignoreTransform, float[] recycle, RectF outRect) {
+ float[] points = recycle == null ? new float[4] : recycle;
+ points[0] = viewBounds.left;
+ points[1] = viewBounds.top;
+ points[2] = viewBounds.right;
+ points[3] = viewBounds.bottom;
+
+ Utilities.getDescendantCoordRelativeToAncestor(view, dragLayer, points,
+ false, ignoreTransform);
+ outRect.set(
+ Math.min(points[0], points[2]),
+ Math.min(points[1], points[3]),
+ Math.max(points[0], points[2]),
+ Math.max(points[1], points[3]));
+ }
+
+ /**
* Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, float[], boolean)}.
*/
public static void mapCoordInSelfToDescendant(View descendant, View root, float[] coord) {
@@ -268,16 +314,28 @@
return new int[] {sLoc1[0] - sLoc0[0], sLoc1[1] - sLoc0[1]};
}
+ /**
+ * Helper method to set rectOut with rectFSrc.
+ */
+ public static void setRect(RectF rectFSrc, Rect rectOut) {
+ rectOut.left = (int) rectFSrc.left;
+ rectOut.top = (int) rectFSrc.top;
+ rectOut.right = (int) rectFSrc.right;
+ rectOut.bottom = (int) rectFSrc.bottom;
+ }
+
public static void scaleRectFAboutCenter(RectF r, float scale) {
+ scaleRectFAboutPivot(r, scale, r.centerX(), r.centerY());
+ }
+
+ public static void scaleRectFAboutPivot(RectF r, float scale, float px, float py) {
if (scale != 1.0f) {
- float cx = r.centerX();
- float cy = r.centerY();
- r.offset(-cx, -cy);
+ r.offset(-px, -py);
r.left = r.left * scale;
r.top = r.top * scale ;
r.right = r.right * scale;
r.bottom = r.bottom * scale;
- r.offset(cx, cy);
+ r.offset(px, py);
}
}
@@ -340,6 +398,13 @@
return mapRange(interpolator.getInterpolation(progress), toMin, toMax);
}
+ /** Bounds t between a lower and upper bound and maps the result to a range. */
+ public static float mapBoundToRange(float t, float lowerBound, float upperBound,
+ float toMin, float toMax, Interpolator interpolator) {
+ return mapToRange(boundToRange(t, lowerBound, upperBound), lowerBound, upperBound,
+ toMin, toMax, interpolator);
+ }
+
public static float getProgress(float current, float min, float max) {
return Math.abs(current - min) / Math.abs(max - min);
}
@@ -400,14 +465,24 @@
return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
- public static float dpiFromPx(float size, DisplayMetrics metrics) {
- float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+ public static float dpiFromPx(float size, int densityDpi) {
+ float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
return (size / densityRatio);
}
+ /** Converts a dp value to pixels for the current device. */
+ public static int dpToPx(float dp) {
+ return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
+ }
+
+
public static int pxFromSp(float size, DisplayMetrics metrics) {
- return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
- size, metrics));
+ return pxFromSp(size, metrics, 1f);
+ }
+
+ public static int pxFromSp(float size, DisplayMetrics metrics, float scale) {
+ return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
+ size, metrics) * scale);
}
public static String createDbSelectionQuery(String columnName, IntArray values) {
@@ -456,6 +531,15 @@
}
/**
+ * Returns an intent for starting the default home activity
+ */
+ public static Intent createHomeIntent() {
+ return new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_HOME)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+
+ /**
* Wraps a message with a TTS span, so that a different message is spoken than
* what is getting displayed.
* @param msg original message
@@ -494,21 +578,6 @@
LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
}
- /**
- * @return {@link SharedPreferences} that backs {@link FeatureFlags}
- */
- public static SharedPreferences getFeatureFlagsPrefs(Context context) {
- // Use application context for shared preferences, so that we use a single cached instance
- return context.getApplicationContext().getSharedPreferences(
- FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
- }
-
- public static boolean areAnimationsEnabled(Context context) {
- return ATLEAST_OREO
- ? ValueAnimator.areAnimatorsEnabled()
- : !context.getSystemService(PowerManager.class).isPowerSaveMode();
- }
-
public static boolean isWallpaperAllowed(Context context) {
return context.getSystemService(WallpaperManager.class).isSetWallpaperAllowed();
}
@@ -521,7 +590,7 @@
public static boolean isGridOptionsEnabled(Context context) {
return isComponentEnabled(context.getPackageManager(),
context.getPackageName(),
- GridOptionsProvider.class.getName());
+ GridCustomizationsProvider.class.getName());
}
private static boolean isComponentEnabled(PackageManager pm, String pkgName, String clsName) {
@@ -592,13 +661,23 @@
*/
public static Drawable getFullDrawable(Launcher launcher, ItemInfo info, int width, int height,
Object[] outObj) {
+ Drawable icon = loadFullDrawableWithoutTheme(launcher, info, width, height, outObj);
+ if (icon instanceof BitmapInfo.Extender) {
+ icon = ((BitmapInfo.Extender) icon).getThemedDrawable(launcher);
+ }
+ return icon;
+ }
+
+ private static Drawable loadFullDrawableWithoutTheme(Launcher launcher, ItemInfo info,
+ int width, int height, Object[] outObj) {
LauncherAppState appState = LauncherAppState.getInstance(launcher);
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
LauncherActivityInfo activityInfo = launcher.getSystemService(LauncherApps.class)
.resolveActivity(info.getIntent(), info.user);
outObj[0] = activityInfo;
- return activityInfo == null ? null : new IconProvider(launcher).getIconForUI(
- activityInfo, launcher.getDeviceProfile().inv.fillResIconDpi);
+ return activityInfo == null ? null : LauncherAppState.getInstance(launcher)
+ .getIconProvider().getIcon(
+ activityInfo, launcher.getDeviceProfile().inv.fillResIconDpi);
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
if (info instanceof PendingAddShortcutInfo) {
ShortcutConfigActivityInfo activityInfo =
@@ -606,7 +685,6 @@
outObj[0] = activityInfo;
return activityInfo.getFullResIcon(appState.getIconCache());
}
- if (info.getIntent() == null || info.getIntent().getPackage() == null) return null;
List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
.buildRequest(launcher)
.query(ShortcutRequest.ALL);
@@ -663,6 +741,14 @@
}
}
+ /**
+ * @return true is the extra is either null or is of type {@param type}
+ */
+ public static boolean isValidExtraType(Intent intent, String key, Class type) {
+ Object extra = intent.getParcelableExtra(key);
+ return extra == null || type.isInstance(extra);
+ }
+
public static float squaredHypot(float x, float y) {
return x * x + y * y;
}
@@ -672,6 +758,86 @@
return slop * slop;
}
+ /**
+ * Helper method to create a content provider
+ */
+ public static ContentObserver newContentObserver(Handler handler, Consumer<Uri> command) {
+ return new ContentObserver(handler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ command.accept(uri);
+ }
+ };
+ }
+
+ /**
+ * Compares the ratio of two quantities and returns whether that ratio is greater than the
+ * provided bound. Order of quantities does not matter. Bound should be a decimal representation
+ * of a percentage.
+ */
+ public static boolean isRelativePercentDifferenceGreaterThan(float first, float second,
+ float bound) {
+ return (Math.abs(first - second) / Math.abs((first + second) / 2.0f)) > bound;
+ }
+
+ /**
+ * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent
+ * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine
+ * the final bounds.
+ */
+ public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight,
+ int delta) {
+ int rdelta = ((delta % 4) + 4) % 4;
+ int origLeft = inOutBounds.left;
+ switch (rdelta) {
+ case 0:
+ return;
+ case 1:
+ inOutBounds.left = inOutBounds.top;
+ inOutBounds.top = parentWidth - inOutBounds.right;
+ inOutBounds.right = inOutBounds.bottom;
+ inOutBounds.bottom = parentWidth - origLeft;
+ return;
+ case 2:
+ inOutBounds.left = parentWidth - inOutBounds.right;
+ inOutBounds.right = parentWidth - origLeft;
+ return;
+ case 3:
+ inOutBounds.left = parentHeight - inOutBounds.bottom;
+ inOutBounds.bottom = inOutBounds.right;
+ inOutBounds.right = parentHeight - inOutBounds.top;
+ inOutBounds.top = origLeft;
+ return;
+ }
+ }
+
+ /**
+ * Make a color filter that blends a color into the destination based on a scalable amout.
+ *
+ * @param color to blend in.
+ * @param tintAmount [0-1] 0 no tinting, 1 full color.
+ * @return ColorFilter for tinting, or {@code null} if no filter is needed.
+ */
+ public static ColorFilter makeColorTintingColorFilter(int color, float tintAmount) {
+ if (tintAmount == 0f) {
+ return null;
+ }
+ return new LightingColorFilter(
+ // This isn't blending in white, its making a multiplication mask for the base color
+ ColorUtils.blendARGB(Color.WHITE, 0, tintAmount),
+ ColorUtils.blendARGB(0, color, tintAmount));
+ }
+
+ /**
+ * Sets start margin on the provided {@param view} to be {@param margin}.
+ * Assumes {@param view} is a child of {@link LinearLayout}
+ */
+ public static void setStartMarginForView(View view, int margin) {
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) view.getLayoutParams();
+ lp.setMarginStart(margin);
+ view.setLayoutParams(lp);
+ }
+
private static class FixedSizeEmptyDrawable extends ColorDrawable {
private final int mSize;
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
deleted file mode 100644
index 2c45c77..0000000
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ /dev/null
@@ -1,758 +0,0 @@
-package com.android.launcher3;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.content.ComponentName;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
-import android.os.CancellationSignal;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.util.LongSparseArray;
-import android.util.Pair;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.icons.GraphicsUtils;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.icons.ShadowGenerator;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.pm.ShortcutConfigActivityInfo;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.SQLiteCacheHelper;
-import com.android.launcher3.util.Thunk;
-import com.android.launcher3.widget.WidgetCell;
-import com.android.launcher3.widget.WidgetManagerHelper;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.WeakHashMap;
-import java.util.concurrent.ExecutionException;
-
-public class WidgetPreviewLoader {
-
- private static final String TAG = "WidgetPreviewLoader";
- private static final boolean DEBUG = false;
-
- private final HashMap<String, long[]> mPackageVersions = new HashMap<>();
-
- /**
- * Weak reference objects, do not prevent their referents from being made finalizable,
- * finalized, and then reclaimed.
- * Note: synchronized block used for this variable is expensive and the block should always
- * be posted to a background thread.
- */
- @Thunk final Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<>());
-
- private final Context mContext;
- private final IconCache mIconCache;
- private final UserCache mUserCache;
- private final CacheDb mDb;
-
- private final UserHandle mMyUser = Process.myUserHandle();
- private final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
-
- public WidgetPreviewLoader(Context context, IconCache iconCache) {
- mContext = context;
- mIconCache = iconCache;
- mUserCache = UserCache.INSTANCE.get(context);
- mDb = new CacheDb(context);
- }
-
- /**
- * Returns a drawable that can be used as a badge for the user or null.
- */
- @UiThread
- public Drawable getBadgeForUser(UserHandle user, int badgeSize) {
- if (mMyUser.equals(user)) {
- return null;
- }
-
- Bitmap badgeBitmap = getUserBadge(user, badgeSize);
- FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
- d.setFilterBitmap(true);
- d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
- return d;
- }
-
- private Bitmap getUserBadge(UserHandle user, int badgeSize) {
- synchronized (mUserBadges) {
- Bitmap badgeBitmap = mUserBadges.get(user);
- if (badgeBitmap != null) {
- return badgeBitmap;
- }
-
- final Resources res = mContext.getResources();
- badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
-
- Drawable drawable = mContext.getPackageManager().getUserBadgedDrawableForDensity(
- new BitmapDrawable(res, badgeBitmap), user,
- new Rect(0, 0, badgeSize, badgeSize),
- 0);
- if (drawable instanceof BitmapDrawable) {
- badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
- } else {
- badgeBitmap.eraseColor(Color.TRANSPARENT);
- Canvas c = new Canvas(badgeBitmap);
- drawable.setBounds(0, 0, badgeSize, badgeSize);
- drawable.draw(c);
- c.setBitmap(null);
- }
-
- mUserBadges.put(user, badgeBitmap);
- return badgeBitmap;
- }
- }
-
- /**
- * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
- * called on UI thread
- *
- * @return a request id which can be used to cancel the request.
- */
- public CancellationSignal getPreview(WidgetItem item, int previewWidth,
- int previewHeight, WidgetCell caller) {
- String size = previewWidth + "x" + previewHeight;
- WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
-
- PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller);
- task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
-
- CancellationSignal signal = new CancellationSignal();
- signal.setOnCancelListener(task);
- return signal;
- }
-
- public void refresh() {
- mDb.clear();
- }
-
- /**
- * The DB holds the generated previews for various components. Previews can also have different
- * sizes (landscape vs portrait).
- */
- private static class CacheDb extends SQLiteCacheHelper {
- private static final int DB_VERSION = 9;
-
- private static final String TABLE_NAME = "shortcut_and_widget_previews";
- private static final String COLUMN_COMPONENT = "componentName";
- private static final String COLUMN_USER = "profileId";
- private static final String COLUMN_SIZE = "size";
- private static final String COLUMN_PACKAGE = "packageName";
- private static final String COLUMN_LAST_UPDATED = "lastUpdated";
- private static final String COLUMN_VERSION = "version";
- private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
-
- public CacheDb(Context context) {
- super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
- }
-
- @Override
- public void onCreateTable(SQLiteDatabase database) {
- database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
- COLUMN_COMPONENT + " TEXT NOT NULL, " +
- COLUMN_USER + " INTEGER NOT NULL, " +
- COLUMN_SIZE + " TEXT NOT NULL, " +
- COLUMN_PACKAGE + " TEXT NOT NULL, " +
- COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
- COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
- COLUMN_PREVIEW_BITMAP + " BLOB, " +
- "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " +
- ");");
- }
- }
-
- @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) {
- ContentValues values = new ContentValues();
- values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
- values.put(CacheDb.COLUMN_USER, mUserCache.getSerialNumberForUser(key.user));
- values.put(CacheDb.COLUMN_SIZE, key.size);
- values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
- values.put(CacheDb.COLUMN_VERSION, versions[0]);
- values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
- values.put(CacheDb.COLUMN_PREVIEW_BITMAP, GraphicsUtils.flattenBitmap(preview));
- mDb.insertOrReplace(values);
- }
-
- public void removePackage(String packageName, UserHandle user) {
- removePackage(packageName, user, mUserCache.getSerialNumberForUser(user));
- }
-
- private void removePackage(String packageName, UserHandle user, long userSerial) {
- synchronized(mPackageVersions) {
- mPackageVersions.remove(packageName);
- }
-
- mDb.delete(
- CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
- new String[]{packageName, Long.toString(userSerial)});
- }
-
- /**
- * Updates the persistent DB:
- * 1. Any preview generated for an old package version is removed
- * 2. Any preview for an absent package is removed
- * This ensures that we remove entries for packages which changed while the launcher was dead.
- *
- * @param packageUser if provided, specifies that list only contains previews for the
- * given package/user, otherwise the list contains all previews
- */
- public void removeObsoletePreviews(ArrayList<? extends ComponentKey> list,
- @Nullable PackageUserKey packageUser) {
- Preconditions.assertWorkerThread();
-
- LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>();
-
- for (ComponentKey key : list) {
- final long userId = mUserCache.getSerialNumberForUser(key.user);
- HashSet<String> packages = validPackages.get(userId);
- if (packages == null) {
- packages = new HashSet<>();
- validPackages.put(userId, packages);
- }
- packages.add(key.componentName.getPackageName());
- }
-
- LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
- long passedUserId = packageUser == null ? 0
- : mUserCache.getSerialNumberForUser(packageUser.mUser);
- Cursor c = null;
- try {
- c = mDb.query(
- new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
- CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
- null, null);
- while (c.moveToNext()) {
- long userId = c.getLong(0);
- String pkg = c.getString(1);
- long lastUpdated = c.getLong(2);
- long version = c.getLong(3);
-
- if (packageUser != null && (!pkg.equals(packageUser.mPackageName)
- || userId != passedUserId)) {
- // This preview is associated with a different package/user, no need to remove.
- continue;
- }
-
- HashSet<String> packages = validPackages.get(userId);
- if (packages != null && packages.contains(pkg)) {
- long[] versions = getPackageVersion(pkg);
- if (versions[0] == version && versions[1] == lastUpdated) {
- // Every thing checks out
- continue;
- }
- }
-
- // We need to delete this package.
- packages = packagesToDelete.get(userId);
- if (packages == null) {
- packages = new HashSet<>();
- packagesToDelete.put(userId, packages);
- }
- packages.add(pkg);
- }
-
- for (int i = 0; i < packagesToDelete.size(); i++) {
- long userId = packagesToDelete.keyAt(i);
- UserHandle user = mUserCache.getUserForSerialNumber(userId);
- for (String pkg : packagesToDelete.valueAt(i)) {
- removePackage(pkg, user, userId);
- }
- }
- } catch (SQLException e) {
- Log.e(TAG, "Error updating widget previews", e);
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- /**
- * Reads the preview bitmap from the DB or null if the preview is not in the DB.
- */
- @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
- Cursor cursor = null;
- try {
- cursor = mDb.query(
- new String[]{CacheDb.COLUMN_PREVIEW_BITMAP},
- CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND "
- + CacheDb.COLUMN_SIZE + " = ?",
- new String[]{
- key.componentName.flattenToShortString(),
- Long.toString(mUserCache.getSerialNumberForUser(key.user)),
- key.size
- });
- // If cancelled, skip getting the blob and decoding it into a bitmap
- if (loadTask.isCancelled()) {
- return null;
- }
- if (cursor.moveToNext()) {
- byte[] blob = cursor.getBlob(0);
- BitmapFactory.Options opts = new BitmapFactory.Options();
- opts.inBitmap = recycle;
- try {
- if (!loadTask.isCancelled()) {
- return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
- }
- } catch (Exception e) {
- return null;
- }
- }
- } catch (SQLException e) {
- Log.w(TAG, "Error loading preview from DB", e);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- return null;
- }
-
- /**
- * Returns generatedPreview for a widget and if the preview should be saved in persistent
- * storage.
- * @param launcher
- * @param item
- * @param recycle
- * @param previewWidth
- * @param previewHeight
- * @return Pair<Bitmap, Boolean>
- */
- private Pair<Bitmap, Boolean> generatePreview(BaseActivity launcher, WidgetItem item,
- Bitmap recycle,
- int previewWidth, int previewHeight) {
- if (item.widgetInfo != null) {
- return generateWidgetPreview(launcher, item.widgetInfo,
- previewWidth, recycle, null);
- } else {
- return new Pair<>(generateShortcutPreview(launcher, item.activityInfo,
- previewWidth, previewHeight, recycle), false);
- }
- }
-
- /**
- * Generates the widget preview from either the {@link WidgetManagerHelper} or cache
- * and add badge at the bottom right corner.
- *
- * @param launcher
- * @param info information about the widget
- * @param maxPreviewWidth width of the preview on either workspace or tray
- * @param preview bitmap that can be recycled
- * @param preScaledWidthOut return the width of the returned bitmap
- * @return Pair<Bitmap (the preview) , Boolean (should be stored in db)>
- */
- public Pair<Bitmap, Boolean> generateWidgetPreview(BaseActivity launcher,
- LauncherAppWidgetProviderInfo info,
- int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
- // Load the preview image if possible
- if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
-
- Drawable drawable = null;
- if (info.previewImage != 0) {
- try {
- drawable = info.loadPreviewImage(mContext, 0);
- } catch (OutOfMemoryError e) {
- Log.w(TAG, "Error loading widget preview for: " + info.provider, e);
- // During OutOfMemoryError, the previous heap stack is not affected. Catching
- // an OOM error here should be safe & not affect other parts of launcher.
- drawable = null;
- }
- if (drawable != null) {
- drawable = mutateOnMainThread(drawable);
- } else {
- Log.w(TAG, "Can't load widget preview drawable 0x" +
- Integer.toHexString(info.previewImage) + " for provider: " + info.provider);
- }
- }
-
- final boolean widgetPreviewExists = (drawable != null);
- final int spanX = info.spanX;
- final int spanY = info.spanY;
-
- int previewWidth;
- int previewHeight;
-
- boolean savePreviewImage = widgetPreviewExists || info.previewImage == 0;
-
- if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
- && drawable.getIntrinsicHeight() > 0) {
- previewWidth = drawable.getIntrinsicWidth();
- previewHeight = drawable.getIntrinsicHeight();
- } else {
- DeviceProfile dp = launcher.getDeviceProfile();
- int tileSize = Math.min(dp.cellWidthPx, dp.cellHeightPx);
- previewWidth = tileSize * spanX;
- previewHeight = tileSize * spanY;
- }
-
- // Scale to fit width only - let the widget preview be clipped in the
- // vertical dimension
- float scale = 1f;
- if (preScaledWidthOut != null) {
- preScaledWidthOut[0] = previewWidth;
- }
- if (previewWidth > maxPreviewWidth) {
- scale = maxPreviewWidth / (float) (previewWidth);
- }
- if (scale != 1f) {
- previewWidth = Math.max((int)(scale * previewWidth), 1);
- previewHeight = Math.max((int)(scale * previewHeight), 1);
- }
-
- // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size
- final Canvas c = new Canvas();
- if (preview == null) {
- preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
- c.setBitmap(preview);
- } else {
- // We use the preview bitmap height to determine where the badge will be drawn in the
- // UI. If its larger than what we need, resize the preview bitmap so that there are
- // no transparent pixels between the preview and the badge.
- if (preview.getHeight() > previewHeight) {
- preview.reconfigure(preview.getWidth(), previewHeight, preview.getConfig());
- }
- // Reusing bitmap. Clear it.
- c.setBitmap(preview);
- c.drawColor(0, PorterDuff.Mode.CLEAR);
- }
-
- // Draw the scaled preview into the final bitmap
- int x = (preview.getWidth() - previewWidth) / 2;
- if (widgetPreviewExists) {
- drawable.setBounds(x, 0, x + previewWidth, previewHeight);
- drawable.draw(c);
- } else {
- RectF boxRect = drawBoxWithShadow(c, previewWidth, previewHeight);
-
- // Draw horizontal and vertical lines to represent individual columns.
- final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
- p.setStyle(Paint.Style.STROKE);
- p.setStrokeWidth(mContext.getResources()
- .getDimension(R.dimen.widget_preview_cell_divider_width));
- p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-
- float t = boxRect.left;
- float tileSize = boxRect.width() / spanX;
- for (int i = 1; i < spanX; i++) {
- t += tileSize;
- c.drawLine(t, 0, t, previewHeight, p);
- }
-
- t = boxRect.top;
- tileSize = boxRect.height() / spanY;
- for (int i = 1; i < spanY; i++) {
- t += tileSize;
- c.drawLine(0, t, previewWidth, t, p);
- }
-
- // Draw icon in the center.
- try {
- Drawable icon =
- mIconCache.getFullResIcon(info.provider.getPackageName(), info.icon);
- if (icon != null) {
- int appIconSize = launcher.getDeviceProfile().iconSizePx;
- int iconSize = (int) Math.min(appIconSize * scale,
- Math.min(boxRect.width(), boxRect.height()));
-
- icon = mutateOnMainThread(icon);
- int hoffset = (previewWidth - iconSize) / 2;
- int yoffset = (previewHeight - iconSize) / 2;
- icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
- icon.draw(c);
- }
- } catch (Resources.NotFoundException e) {
- savePreviewImage = false;
- }
- c.setBitmap(null);
- }
- return new Pair<>(preview, savePreviewImage);
- }
-
- private RectF drawBoxWithShadow(Canvas c, int width, int height) {
- Resources res = mContext.getResources();
-
- ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.WHITE);
- builder.shadowBlur = res.getDimension(R.dimen.widget_preview_shadow_blur);
- builder.radius = res.getDimension(R.dimen.widget_preview_corner_radius);
- builder.keyShadowDistance = res.getDimension(R.dimen.widget_preview_key_shadow_distance);
-
- builder.bounds.set(builder.shadowBlur, builder.shadowBlur,
- width - builder.shadowBlur,
- height - builder.shadowBlur - builder.keyShadowDistance);
- builder.drawShadow(c);
- return builder.bounds;
- }
-
- private Bitmap generateShortcutPreview(BaseActivity launcher, ShortcutConfigActivityInfo info,
- int maxWidth, int maxHeight, Bitmap preview) {
- int iconSize = launcher.getDeviceProfile().allAppsIconSizePx;
- int padding = launcher.getResources()
- .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
-
- int size = iconSize + 2 * padding;
- if (maxHeight < size || maxWidth < size) {
- throw new RuntimeException("Max size is too small for preview");
- }
- final Canvas c = new Canvas();
- if (preview == null || preview.getWidth() < size || preview.getHeight() < size) {
- preview = Bitmap.createBitmap(size, size, Config.ARGB_8888);
- c.setBitmap(preview);
- } else {
- if (preview.getWidth() > size || preview.getHeight() > size) {
- preview.reconfigure(size, size, preview.getConfig());
- }
-
- // Reusing bitmap. Clear it.
- c.setBitmap(preview);
- c.drawColor(0, PorterDuff.Mode.CLEAR);
- }
- RectF boxRect = drawBoxWithShadow(c, size, size);
-
- LauncherIcons li = LauncherIcons.obtain(mContext);
- Bitmap icon = li.createBadgedIconBitmap(
- mutateOnMainThread(info.getFullResIcon(mIconCache)),
- Process.myUserHandle(), 0).icon;
- li.recycle();
-
- Rect src = new Rect(0, 0, icon.getWidth(), icon.getHeight());
-
- boxRect.set(0, 0, iconSize, iconSize);
- boxRect.offset(padding, padding);
- c.drawBitmap(icon, src, boxRect,
- new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
- c.setBitmap(null);
- return preview;
- }
-
- private Drawable mutateOnMainThread(final Drawable drawable) {
- try {
- return MAIN_EXECUTOR.submit(drawable::mutate).get();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException(e);
- } catch (ExecutionException e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * @return an array of containing versionCode and lastUpdatedTime for the package.
- */
- @Thunk long[] getPackageVersion(String packageName) {
- synchronized (mPackageVersions) {
- long[] versions = mPackageVersions.get(packageName);
- if (versions == null) {
- versions = new long[2];
- try {
- PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName,
- PackageManager.GET_UNINSTALLED_PACKAGES);
- versions[0] = info.versionCode;
- versions[1] = info.lastUpdateTime;
- } catch (NameNotFoundException e) {
- Log.e(TAG, "PackageInfo not found", e);
- }
- mPackageVersions.put(packageName, versions);
- }
- return versions;
- }
- }
-
- public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
- implements CancellationSignal.OnCancelListener {
- @Thunk final WidgetCacheKey mKey;
- private final WidgetItem mInfo;
- private final int mPreviewHeight;
- private final int mPreviewWidth;
- private final WidgetCell mCaller;
- private final BaseActivity mActivity;
- @Thunk long[] mVersions;
- @Thunk Bitmap mBitmapToRecycle;
-
- private boolean mSaveToDB = false;
-
- PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth,
- int previewHeight, WidgetCell caller) {
- mKey = key;
- mInfo = info;
- mPreviewHeight = previewHeight;
- mPreviewWidth = previewWidth;
- mCaller = caller;
- mActivity = BaseActivity.fromContext(mCaller.getContext());
- if (DEBUG) {
- Log.d(TAG, String.format("%s, %s, %d, %d",
- mKey, mInfo, mPreviewHeight, mPreviewWidth));
- }
- }
-
- @Override
- protected Bitmap doInBackground(Void... params) {
- Bitmap unusedBitmap = null;
-
- // If already cancelled before this gets to run in the background, then return early
- if (isCancelled()) {
- return null;
- }
- synchronized (mUnusedBitmaps) {
- // Check if we can re-use a bitmap
- for (Bitmap candidate : mUnusedBitmaps) {
- if (candidate != null && candidate.isMutable() &&
- candidate.getWidth() == mPreviewWidth &&
- candidate.getHeight() == mPreviewHeight) {
- unusedBitmap = candidate;
- mUnusedBitmaps.remove(unusedBitmap);
- break;
- }
- }
- }
-
- // creating a bitmap is expensive. Do not do this inside synchronized block.
- if (unusedBitmap == null) {
- unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888);
- }
- // If cancelled now, don't bother reading the preview from the DB
- if (isCancelled()) {
- return unusedBitmap;
- }
- Bitmap preview = readFromDb(mKey, unusedBitmap, this);
- // Only consider generating the preview if we have not cancelled the task already
- if (!isCancelled() && preview == null) {
- // Fetch the version info before we generate the preview, so that, in-case the
- // app was updated while we are generating the preview, we use the old version info,
- // which would gets re-written next time.
- boolean persistable = mInfo.activityInfo == null
- || mInfo.activityInfo.isPersistable();
- mVersions = persistable ? getPackageVersion(mKey.componentName.getPackageName())
- : null;
-
- // it's not in the db... we need to generate it
- Pair<Bitmap, Boolean> pair = generatePreview(mActivity, mInfo, unusedBitmap,
- mPreviewWidth, mPreviewHeight);
- preview = pair.first;
- this.mSaveToDB = pair.second;
- }
- return preview;
- }
-
- @Override
- protected void onPostExecute(final Bitmap preview) {
- mCaller.applyPreview(preview);
-
- // Write the generated preview to the DB in the worker thread
- if (mVersions != null) {
- MODEL_EXECUTOR.post(new Runnable() {
- @Override
- public void run() {
- if (!isCancelled() && mSaveToDB) {
- // If we are still using this preview, then write it to the DB and then
- // let the normal clear mechanism recycle the bitmap
- writeToDb(mKey, mVersions, preview);
- mBitmapToRecycle = preview;
- } else {
- // If we've already cancelled, then skip writing the bitmap to the DB
- // and manually add the bitmap back to the recycled set
- synchronized (mUnusedBitmaps) {
- mUnusedBitmaps.add(preview);
- }
- }
- }
- });
- } else {
- // If we don't need to write to disk, then ensure the preview gets recycled by
- // the normal clear mechanism
- mBitmapToRecycle = preview;
- }
- }
-
- @Override
- protected void onCancelled(final Bitmap preview) {
- // If we've cancelled while the task is running, then can return the bitmap to the
- // recycled set immediately. Otherwise, it will be recycled after the preview is written
- // to disk.
- if (preview != null) {
- MODEL_EXECUTOR.post(new Runnable() {
- @Override
- public void run() {
- synchronized (mUnusedBitmaps) {
- mUnusedBitmaps.add(preview);
- }
- }
- });
- }
- }
-
- @Override
- public void onCancel() {
- cancel(true);
-
- // This only handles the case where the PreviewLoadTask is cancelled after the task has
- // successfully completed (including having written to disk when necessary). In the
- // other cases where it is cancelled while the task is running, it will be cleaned up
- // in the tasks's onCancelled() call, and if cancelled while the task is writing to
- // disk, it will be cancelled in the task's onPostExecute() call.
- if (mBitmapToRecycle != null) {
- MODEL_EXECUTOR.post(new Runnable() {
- @Override
- public void run() {
- synchronized (mUnusedBitmaps) {
- mUnusedBitmaps.add(mBitmapToRecycle);
- }
- mBitmapToRecycle = null;
- }
- });
- }
- }
- }
-
- private static final class WidgetCacheKey extends ComponentKey {
-
- @Thunk final String size;
-
- public WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
- super(componentName, user);
- this.size = size;
- }
-
- @Override
- public int hashCode() {
- return super.hashCode() ^ size.hashCode();
- }
-
- @Override
- public boolean equals(Object o) {
- return super.equals(o) && ((WidgetCacheKey) o).size.equals(size);
- }
- }
-}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 1441e0b..5bdc402 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -22,12 +22,12 @@
import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED;
import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_INACCESSIBLE;
+import static com.android.launcher3.LauncherState.HINT_STATE;
import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.LauncherState.SPRING_LOADED;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
@@ -63,7 +63,6 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;
-import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
import com.android.launcher3.anim.Interpolators;
@@ -82,10 +81,10 @@
import com.android.launcher3.graphics.DragPreviewProvider;
import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
-import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
@@ -93,29 +92,37 @@
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pageindicators.WorkspacePageIndicator;
import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.WorkspaceTouchListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.EdgeEffectCompat;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.OverlayEdgeEffect;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.WallpaperOffsetInterpolator;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.PendingAppWidgetHostView;
import com.android.launcher3.widget.WidgetManagerHelper;
+import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
+import com.android.launcher3.widget.util.WidgetSizes;
import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
/**
* The workspace is a wide area with a wallpaper and a finite number of pages.
@@ -141,6 +148,10 @@
public static final int DEFAULT_PAGE = 0;
+ private static final int DEFAULT_SMARTSPACE_HEIGHT = 1;
+
+ private static final int EXPANDED_SMARTSPACE_HEIGHT = 2;
+
private LayoutTransition mLayoutTransition;
@Thunk final WallpaperManager mWallpaperManager;
@@ -180,7 +191,6 @@
@Thunk final Launcher mLauncher;
@Thunk DragController mDragController;
- private final Rect mTempRect = new Rect();
private final int[] mTempXY = new int[2];
private final float[] mTempFXY = new float[2];
@Thunk float[] mDragViewVisualCenter = new float[2];
@@ -195,6 +205,7 @@
private boolean mStripScreensOnPageStopMoving = false;
private DragPreviewProvider mOutlineProvider = null;
+
private boolean mWorkspaceFadeInAdjacentScreens;
final WallpaperOffsetInterpolator mWallpaperOffset;
@@ -238,10 +249,7 @@
private float mTransitionProgress;
// State related to Launcher Overlay
- LauncherOverlay mLauncherOverlay;
- boolean mScrollInteractionBegan;
- boolean mStartedSendingScrollEvents;
- float mLastOverlayScroll = 0;
+ private OverlayEdgeEffect mOverlayEdgeEffect;
boolean mOverlayShown = false;
private Runnable mOnOverlayHiddenCallback;
@@ -301,23 +309,47 @@
Rect padding = grid.workspacePadding;
setPadding(padding.left, padding.top, padding.right, padding.bottom);
mInsets.set(insets);
+ // Increase our bottom insets so we don't overlap with the taskbar.
+ mInsets.bottom += grid.nonOverlappingTaskbarInset;
- if (mWorkspaceFadeInAdjacentScreens) {
+ if (isTwoPanelEnabled()) {
+ setPageSpacing(0); // we have two pages and we don't want any spacing
+ } else if (mWorkspaceFadeInAdjacentScreens) {
// In landscape mode the page spacing is set to the default.
setPageSpacing(grid.edgeMarginPx);
} else {
// In portrait, we want the pages spaced such that there is no
// overhang of the previous / next page into the current page viewport.
// We assume symmetrical padding in portrait mode.
- setPageSpacing(Math.max(grid.edgeMarginPx, padding.left + 1));
+ int maxInsets = Math.max(insets.left, insets.right);
+ int maxPadding = Math.max(grid.edgeMarginPx, padding.left + 1);
+ setPageSpacing(Math.max(maxInsets, maxPadding));
}
-
int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
int paddingBottom = grid.cellLayoutBottomPaddingPx;
+ int twoPanelLandscapeSidePadding = paddingLeftRight * 2;
+ int twoPanelPortraitSidePadding = paddingLeftRight / 2;
+
+ int panelCount = getPanelCount();
for (int i = mWorkspaceScreens.size() - 1; i >= 0; i--) {
- mWorkspaceScreens.valueAt(i)
- .setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
+ int paddingLeft = paddingLeftRight;
+ int paddingRight = paddingLeftRight;
+ if (panelCount > 1) {
+ if (i % panelCount == 0) { // left side panel
+ paddingLeft = grid.isLandscape ? twoPanelLandscapeSidePadding
+ : twoPanelPortraitSidePadding;
+ paddingRight = 0;
+ } else if (i % panelCount == panelCount - 1) { // right side panel
+ paddingLeft = 0;
+ paddingRight = grid.isLandscape ? twoPanelLandscapeSidePadding
+ : twoPanelPortraitSidePadding;
+ } else { // middle panel
+ paddingLeft = 0;
+ paddingRight = 0;
+ }
+ }
+ mWorkspaceScreens.valueAt(i).setPadding(paddingLeft, 0, paddingRight, paddingBottom);
}
}
@@ -356,10 +388,19 @@
}
public float getWallpaperOffsetForCenterPage() {
- int pageScroll = getScrollForPage(getPageNearestToCenterOfScreen());
+ return getWallpaperOffsetForPage(getPageNearestToCenterOfScreen());
+ }
+
+ private float getWallpaperOffsetForPage(int page) {
+ int pageScroll = getScrollForPage(page);
return mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
}
+ /** Returns the number of pages used for the wallpaper parallax. */
+ public int getNumPagesForWallpaperParallax() {
+ return mWallpaperOffset.getNumPagesForWallpaperParallax();
+ }
+
public Rect estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan) {
Rect r = new Rect();
cl.cellToRect(hCell, vCell, hSpan, vSpan, r);
@@ -373,19 +414,12 @@
}
if (mDragInfo != null && mDragInfo.cell != null) {
- CellLayout layout = (CellLayout) mDragInfo.cell.getParent().getParent();
+ CellLayout layout = (CellLayout) (mDragInfo.cell instanceof LauncherAppWidgetHostView
+ ? dragObject.dragView.getContentViewParent().getParent()
+ : mDragInfo.cell.getParent().getParent());
layout.markCellsAsUnoccupiedForView(mDragInfo.cell);
}
- if (mOutlineProvider != null) {
- if (dragObject.dragView != null) {
- Bitmap preview = dragObject.dragView.getPreviewBitmap();
-
- // The outline is used to visualize where the item will land if dropped
- mOutlineProvider.generateDragOutline(preview);
- }
- }
-
updateChildrenLayersEnabled();
// Do not add a new page if it is a accessible drag which was not started by the workspace.
@@ -394,10 +428,9 @@
// When a accessible drag is started by the folder, we only allow rearranging withing the
// folder.
boolean addNewPage = !(options.isAccessibleDrag && dragObject.dragSource != this);
-
if (addNewPage) {
mDeferRemoveExtraEmptyScreen = false;
- addExtraEmptyScreenOnDrag();
+ addExtraEmptyScreenOnDrag(dragObject);
if (dragObject.dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
&& dragObject.dragSource != this) {
@@ -406,7 +439,7 @@
// widgets as they cannot be placed inside a folder.
// Start at the current page and search right (on LTR) until finding a page with
// enough space. Since an empty screen is the furthest right, a page must be found.
- int currentPage = getPageNearestToCenterOfScreen();
+ int currentPage = getDestinationPage();
for (int pageIndex = currentPage; pageIndex < getPageCount(); pageIndex++) {
CellLayout page = (CellLayout) getPageAt(pageIndex);
if (page.hasReorderSolution(dragObject.dragInfo)) {
@@ -424,6 +457,15 @@
.log(LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
}
+ private boolean isTwoPanelEnabled() {
+ return mLauncher.mDeviceProfile.isTwoPanels;
+ }
+
+ @Override
+ protected int getPanelCount() {
+ return isTwoPanelEnabled() ? 2 : super.getPanelCount();
+ }
+
public void deferRemoveExtraEmptyScreen() {
mDeferRemoveExtraEmptyScreen = true;
}
@@ -434,11 +476,20 @@
enforceDragParity("onDragEnd", 0, 0);
}
- if (!mDeferRemoveExtraEmptyScreen) {
- removeExtraEmptyScreen(mDragSourceInternal != null);
- }
-
updateChildrenLayersEnabled();
+ StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+ stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (finalState == NORMAL) {
+ if (!mDeferRemoveExtraEmptyScreen) {
+ removeExtraEmptyScreen(true /* stripEmptyScreens */);
+ }
+ stateManager.removeStateListener(this);
+ }
+ }
+ });
+
mDragInfo = null;
mOutlineProvider = null;
mDragSourceInternal = null;
@@ -511,7 +562,10 @@
.inflate(R.layout.search_container_workspace, firstPage, false);
}
- CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(), 1);
+ int cellVSpan = FeatureFlags.EXPANDED_SMARTSPACE.get()
+ ? EXPANDED_SMARTSPACE_HEIGHT : DEFAULT_SMARTSPACE_HEIGHT;
+ CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(),
+ cellVSpan);
lp.canReorder = false;
if (!firstPage.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true)) {
Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
@@ -578,15 +632,24 @@
addView(newScreen, insertIndex);
mStateTransitionAnimation.applyChildState(
mLauncher.getStateManager().getState(), newScreen, insertIndex);
+
+ updatePageScrollValues();
return newScreen;
}
- public void addExtraEmptyScreenOnDrag() {
+ private void addExtraEmptyScreenOnDrag(DragObject dragObject) {
boolean lastChildOnScreen = false;
boolean childOnFinalScreen = false;
if (mDragSourceInternal != null) {
- if (mDragSourceInternal.getChildCount() == 1) {
+ // When the drag view content is a LauncherAppWidgetHostView, we should increment the
+ // drag source child count by 1 because the widget in drag has been detached from its
+ // original parent, ShortcutAndWidgetContainer, and reattached to the DragView.
+ int dragSourceChildCount =
+ dragObject.dragView.getContentView() instanceof LauncherAppWidgetHostView
+ ? mDragSourceInternal.getChildCount() + 1
+ : mDragSourceInternal.getChildCount();
+ if (dragSourceChildCount == 1) {
lastChildOnScreen = true;
}
CellLayout cl = (CellLayout) mDragSourceInternal.getParent();
@@ -655,6 +718,7 @@
convertFinalScreenToEmptyScreenIfNecessary();
if (hasExtraEmptyScreen()) {
removeView(mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID));
+ setCurrentPage(getNextPage());
mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID);
@@ -798,7 +862,7 @@
private boolean shouldConsumeTouch(View v) {
return !workspaceIconsCanBeDragged()
- || (!workspaceInModalState() && indexOfChild(v) != mCurrentPage);
+ || (!workspaceInModalState() && !isVisible(v));
}
public boolean isSwitchingState() {
@@ -888,47 +952,25 @@
}
}
- protected void onScrollInteractionBegin() {
- super.onScrollInteractionBegin();
- mScrollInteractionBegan = true;
- }
-
- protected void onScrollInteractionEnd() {
- super.onScrollInteractionEnd();
- mScrollInteractionBegan = false;
- if (mStartedSendingScrollEvents) {
- mStartedSendingScrollEvents = false;
- mLauncherOverlay.onScrollInteractionEnd();
- }
- }
-
public void setLauncherOverlay(LauncherOverlay overlay) {
- mLauncherOverlay = overlay;
- // A new overlay has been set. Reset event tracking
- mStartedSendingScrollEvents = false;
+ mOverlayEdgeEffect = overlay == null ? null : new OverlayEdgeEffect(getContext(), overlay);
+ EdgeEffectCompat newEffect = overlay == null
+ ? new EdgeEffectCompat(getContext()) : mOverlayEdgeEffect;
+ if (mIsRtl) {
+ mEdgeGlowRight = newEffect;
+ } else {
+ mEdgeGlowLeft = newEffect;
+ }
onOverlayScrollChanged(0);
}
public boolean hasOverlay() {
- return mLauncherOverlay != null;
- }
-
- private boolean isScrollingOverlay() {
- return mLauncherOverlay != null &&
- ((mIsRtl && getUnboundedScroll() > mMaxScroll)
- || (!mIsRtl && getUnboundedScroll() < mMinScroll));
+ return mOverlayEdgeEffect != null;
}
@Override
protected void snapToDestination() {
- // If we're overscrolling the overlay, we make sure to immediately reset the PagedView
- // to it's baseline position instead of letting the overscroll settle. The overlay handles
- // it's own settling, and every gesture to the overlay should be self-contained and start
- // from 0, so we zero it out here.
- if (isScrollingOverlay()) {
- // We reset mWasInOverscroll so that PagedView doesn't zero out the overscroll
- // interaction when we call snapToPageImmediately.
- mWasInOverscroll = false;
+ if (mOverlayEdgeEffect != null && !mOverlayEdgeEffect.isFinished()) {
snapToPageImmediately(0);
} else {
super.snapToDestination();
@@ -940,13 +982,17 @@
super.onScrollChanged(l, t, oldl, oldt);
// Update the page indicator progress.
- boolean isTransitioning = mIsSwitchingState
+ // Unlike from other states, we show the page indicator when transitioning from HINT_STATE.
+ boolean isSwitchingState = mIsSwitchingState
+ && mLauncher.getStateManager().getCurrentStableState() != HINT_STATE;
+ boolean isTransitioning = isSwitchingState
|| (getLayoutTransition() != null && getLayoutTransition().isRunning());
if (!isTransitioning) {
showPageIndicatorAtCurrentScroll();
}
updatePageAlphaValues();
+ updatePageScrollValues();
enableHwLayersOnVisiblePages();
}
@@ -957,38 +1003,6 @@
}
@Override
- protected void overScroll(int amount) {
- boolean shouldScrollOverlay = mLauncherOverlay != null && !mScroller.isSpringing() &&
- ((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl));
-
- boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlayScroll != 0 &&
- ((amount >= 0 && !mIsRtl) || (amount <= 0 && mIsRtl));
-
- if (shouldScrollOverlay) {
- if (!mStartedSendingScrollEvents && mScrollInteractionBegan) {
- mStartedSendingScrollEvents = true;
- mLauncherOverlay.onScrollInteractionBegin();
- }
-
- mLastOverlayScroll = Math.abs(((float) amount) / getMeasuredWidth());
- mLauncherOverlay.onScrollChange(mLastOverlayScroll, mIsRtl);
- } else {
- dampedOverScroll(amount);
- }
-
- if (shouldZeroOverlay) {
- mLauncherOverlay.onScrollChange(0, mIsRtl);
- }
- }
-
- @Override
- protected boolean onOverscroll(int amount) {
- // Enforce overscroll on -1 direction
- if ((amount > 0 && !mIsRtl) || (amount < 0 && mIsRtl)) return false;
- return super.onOverscroll(amount);
- }
-
- @Override
protected boolean shouldFlingForVelocity(int velocityX) {
// When the overlay is moving, the fling or settle transition is controlled by the overlay.
return Float.compare(Math.abs(mOverlayTranslation), 0) == 0 &&
@@ -1001,8 +1015,6 @@
public void onOverlayScrollChanged(float scroll) {
if (Float.compare(scroll, 1f) == 0) {
if (!mOverlayShown) {
- mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
- Action.Direction.LEFT, ContainerType.WORKSPACE, 0);
mLauncher.getStatsLogManager().logger()
.withSrcState(LAUNCHER_STATE_HOME)
.withDstState(LAUNCHER_STATE_HOME)
@@ -1017,20 +1029,16 @@
// Not announcing the overlay page for accessibility since it announces itself.
} else if (Float.compare(scroll, 0f) == 0) {
if (mOverlayShown) {
- UserEventDispatcher ued = mLauncher.getUserEventDispatcher();
- if (!ued.isPreviousHomeGesture()) {
- mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
- Action.Direction.RIGHT, ContainerType.WORKSPACE, -1);
- mLauncher.getStatsLogManager().logger()
- .withSrcState(LAUNCHER_STATE_HOME)
- .withDstState(LAUNCHER_STATE_HOME)
- .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
- .setWorkspace(
- LauncherAtom.WorkspaceContainer.newBuilder()
- .setPageIndex(-1))
- .build())
- .log(LAUNCHER_SWIPERIGHT);
- }
+ // TODO: this is logged unnecessarily on home gesture.
+ mLauncher.getStatsLogManager().logger()
+ .withSrcState(LAUNCHER_STATE_HOME)
+ .withDstState(LAUNCHER_STATE_HOME)
+ .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+ .setWorkspace(
+ LauncherAtom.WorkspaceContainer.newBuilder()
+ .setPageIndex(-1))
+ .build())
+ .log(LAUNCHER_SWIPERIGHT);
} else if (Float.compare(mOverlayTranslation, 0f) != 0) {
// When arriving to 0 overscroll from non-zero overscroll, announce page for
// accessibility since default announcements were disabled while in overscroll
@@ -1121,12 +1129,8 @@
protected void notifyPageSwitchListener(int prevPage) {
super.notifyPageSwitchListener(prevPage);
if (prevPage != mCurrentPage) {
- int swipeDirection = (prevPage < mCurrentPage)
- ? Action.Direction.RIGHT : Action.Direction.LEFT;
StatsLogManager.EventEnum event = (prevPage < mCurrentPage)
? LAUNCHER_SWIPERIGHT : LAUNCHER_SWIPELEFT;
- mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
- swipeDirection, ContainerType.WORKSPACE, prevPage);
mLauncher.getStatsLogManager().logger()
.withSrcState(LAUNCHER_STATE_HOME)
.withDstState(LAUNCHER_STATE_HOME)
@@ -1168,17 +1172,6 @@
mWallpaperOffset.syncWithScroll();
}
- public void computeScrollWithoutInvalidation() {
- computeScrollHelper(false);
- }
-
- @Override
- protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
- if (!isSwitchingState()) {
- super.determineScrollingStart(ev, touchSlopScale);
- }
- }
-
@Override
public void announceForAccessibility(CharSequence text) {
// Don't announce if apps is on top of us.
@@ -1210,6 +1203,17 @@
}
}
+ private void updatePageScrollValues() {
+ int screenCenter = getScrollX() + getMeasuredWidth() / 2;
+ for (int i = 0; i < getChildCount(); i++) {
+ CellLayout child = (CellLayout) getChildAt(i);
+ if (child != null) {
+ float scrollProgress = getScrollProgress(screenCenter, child, i);
+ child.setScrollProgress(scrollProgress);
+ }
+ }
+ }
+
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mWallpaperOffset.setWindowToken(getWindowToken());
@@ -1322,10 +1326,6 @@
mOutlineProvider = outlineProvider;
}
- public void snapToPageFromOverView(int whichPage) {
- snapToPage(whichPage, OVERVIEW.getTransitionDuration(mLauncher), Interpolators.ZOOM_IN);
- }
-
private void onStartStateTransition(LauncherState state) {
mIsSwitchingState = true;
mTransitionProgress = 0;
@@ -1400,7 +1400,7 @@
// TAPL can work only if UIDevice is set up as setCompressedLayoutHeirarchy(false).
// Hiding workspace from the tests when it's
// IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS.
- return null;
+ return AccessibilityNodeInfo.obtain();
}
return super.createAccessibilityNodeInfo();
}
@@ -1473,10 +1473,19 @@
draggableView = (DraggableView) child;
}
- // The drag bitmap follows the touch point around on the screen
- final Bitmap b = previewProvider.createDragBitmap();
+ final View contentView = previewProvider.getContentView();
+ final float scale;
+ // The draggable drawable follows the touch point around on the screen
+ final Drawable drawable;
+ if (contentView == null) {
+ drawable = previewProvider.createDrawable();
+ scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
+ } else {
+ drawable = null;
+ scale = previewProvider.getScaleAndPosition(contentView, mTempXY);
+ }
+
int halfPadding = previewProvider.previewPadding / 2;
- float scale = previewProvider.getScaleAndPosition(b, mTempXY);
int dragLayerX = mTempXY[0];
int dragLayerY = mTempXY[1];
@@ -1499,14 +1508,40 @@
.showForIcon((BubbleTextView) child);
if (popupContainer != null) {
dragOptions.preDragCondition = popupContainer.createPreDragCondition();
- mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("dragging started");
}
}
- DragView dv = mDragController.startDrag(b, draggableView, dragLayerX, dragLayerY, source,
- dragObject, dragVisualizeOffset, dragRect, scale * iconScale,
- scale, dragOptions);
- dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
+ final DragView dv;
+ if (contentView instanceof View) {
+ if (contentView instanceof LauncherAppWidgetHostView) {
+ mDragController.addDragListener(new AppWidgetHostViewDragListener(mLauncher));
+ }
+ dv = mDragController.startDrag(
+ contentView,
+ draggableView,
+ dragLayerX,
+ dragLayerY,
+ source,
+ dragObject,
+ dragVisualizeOffset,
+ dragRect,
+ scale * iconScale,
+ scale,
+ dragOptions);
+ } else {
+ dv = mDragController.startDrag(
+ drawable,
+ draggableView,
+ dragLayerX,
+ dragLayerY,
+ source,
+ dragObject,
+ dragVisualizeOffset,
+ dragRect,
+ scale * iconScale,
+ scale,
+ dragOptions);
+ }
return dv;
}
@@ -1699,7 +1734,6 @@
fi.addItem(destInfo);
fi.addItem(sourceInfo);
}
- mLauncher.folderCreatedFromItem(fi.getFolder(), destInfo);
return true;
}
return false;
@@ -1717,9 +1751,8 @@
FolderIcon fi = (FolderIcon) dropOverView;
if (fi.acceptDrop(d.dragInfo)) {
mStatsLogManager.logger().withItemInfo(fi.mInfo).withInstanceId(d.logInstanceId)
- .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
+ .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED_ON_FOLDER_ICON);
fi.onDrop(d, false /* itemReturnedOnFailedDrop */);
-
// if the drag started here, we need to remove it from the workspace
if (!external) {
getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
@@ -1753,8 +1786,11 @@
onDropExternal(touchXY, dropTargetLayout, d);
} else {
final View cell = mDragInfo.cell;
+ final DragView dragView = d.dragView;
boolean droppedOnOriginalCellDuringTransition = false;
- Runnable onCompleteRunnable = null;
+ Runnable onCompleteRunnable = dragView::resumeColorExtraction;
+
+ dragView.disableColorExtraction();
if (dropTargetLayout != null && !d.cancelled) {
// Move internally
@@ -1822,7 +1858,7 @@
item.spanX = resultSpan[0];
item.spanY = resultSpan[1];
AppWidgetHostView awhv = (AppWidgetHostView) cell;
- AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
+ WidgetSizes.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
resultSpan[1]);
}
@@ -1838,6 +1874,8 @@
CellLayout parentCell = getParentCellLayoutForView(cell);
if (parentCell != null) {
parentCell.removeView(cell);
+ } else if (mDragInfo.cell instanceof LauncherAppWidgetHostView) {
+ d.dragView.detachContentView(/* reattachToPreviousParent= */ false);
} else if (FeatureFlags.IS_STUDIO_BUILD) {
throw new NullPointerException("mDragInfo.cell has null parent");
}
@@ -1863,20 +1901,24 @@
AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
&& !options.isAccessibleDrag) {
+ final Runnable previousRunnable = onCompleteRunnable;
onCompleteRunnable = () -> {
+ previousRunnable.run();
if (!isPageInTransition()) {
AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
}
};
}
}
-
mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
lp.cellX, lp.cellY, item.spanX, item.spanY);
} else {
if (!returnToOriginalCellToPreventShuffling) {
onNoCellFound(dropTargetLayout);
}
+ if (mDragInfo.cell instanceof LauncherAppWidgetHostView) {
+ d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
+ }
// If we can't find a drop location, we return the item to its original position
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
@@ -1885,6 +1927,11 @@
CellLayout layout = (CellLayout) cell.getParent().getParent();
layout.markCellsAsOccupiedForView(cell);
}
+ } else {
+ // When drag is cancelled, reattach content view back to its original parent.
+ if (mDragInfo.cell instanceof LauncherAppWidgetHostView) {
+ d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
+ }
}
final CellLayout parent = (CellLayout) cell.getParent().getParent();
@@ -1892,10 +1939,16 @@
if (droppedOnOriginalCellDuringTransition) {
// Animate the item to its original position, while simultaneously exiting
// spring-loaded mode so the page meets the icon where it was picked up.
+ final RunnableList callbackList = new RunnableList();
+ final Runnable onCompleteCallback = onCompleteRunnable;
mLauncher.getDragController().animateDragViewToOriginalPosition(
- onCompleteRunnable, cell,
+ /* onComplete= */ callbackList::executeAllAndDestroy, cell,
SPRING_LOADED.getTransitionDuration(mLauncher));
- mLauncher.getStateManager().goToState(NORMAL);
+ mLauncher.getStateManager().goToState(NORMAL, /* delay= */ 0,
+ onCompleteCallback == null
+ ? null
+ : forSuccessCallback(
+ () -> callbackList.add(onCompleteCallback)));
mLauncher.getDropTargetBar().onDragEnd();
parent.onDropChild(cell);
return;
@@ -1918,8 +1971,8 @@
}
parent.onDropChild(cell);
- mLauncher.getStateManager().goToState(
- NORMAL, SPRING_LOADED_EXIT_DELAY, onCompleteRunnable);
+ mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY,
+ forSuccessCallback(onCompleteRunnable));
mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
.log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
}
@@ -1936,16 +1989,26 @@
}
/**
- * Computes the area relative to dragLayer which is used to display a page.
+ * Computes and returns the area relative to dragLayer which is used to display a page.
+ * In case we have multiple pages displayed at the same time, we return the union of the areas.
*/
- public void getPageAreaRelativeToDragLayer(Rect outArea) {
- CellLayout child = (CellLayout) getChildAt(getNextPage());
- if (child == null) {
- return;
+ public Rect getPageAreaRelativeToDragLayer() {
+ Rect area = new Rect();
+ int nextPage = getNextPage();
+ int panelCount = getPanelCount();
+ for (int page = nextPage; page < nextPage + panelCount; page++) {
+ CellLayout child = (CellLayout) getChildAt(page);
+ if (child == null) {
+ break;
+ }
+
+ ShortcutAndWidgetContainer boundingLayout = child.getShortcutsAndWidgets();
+ Rect tmpRect = new Rect();
+ mLauncher.getDragLayer().getDescendantRectRelativeToSelf(boundingLayout, tmpRect);
+ area.union(tmpRect);
}
- ShortcutAndWidgetContainer boundingLayout = child.getShortcutsAndWidgets();
- mLauncher.getDragLayer().getDescendantRectRelativeToSelf(boundingLayout, outArea);
+ return area;
}
@Override
@@ -2024,9 +2087,6 @@
if (mDragOverlappingLayout != null) {
mDragOverlappingLayout.setIsDragOverlapping(true);
}
- // Invalidating the scrim will also force this CellLayout
- // to be invalidated so that it is highlighted if necessary.
- mLauncher.getDragLayer().getScrim().invalidate();
}
public CellLayout getCurrentDragOverlappingLayout() {
@@ -2085,40 +2145,40 @@
mLastReorderY = -1;
}
- /*
- *
- * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's
- * coordinate space. The argument xy is modified with the return result.
- */
- private void mapPointFromSelfToChild(View v, float[] xy) {
- xy[0] = xy[0] - v.getLeft();
- xy[1] = xy[1] - v.getTop();
- }
+ /*
+ *
+ * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's
+ * coordinate space. The argument xy is modified with the return result.
+ */
+ private void mapPointFromSelfToChild(View v, float[] xy) {
+ xy[0] = xy[0] - v.getLeft();
+ xy[1] = xy[1] - v.getTop();
+ }
- boolean isPointInSelfOverHotseat(int x, int y) {
- mTempFXY[0] = x;
- mTempFXY[1] = y;
- mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempFXY, true);
- View hotseat = mLauncher.getHotseat();
- return mTempFXY[0] >= hotseat.getLeft() &&
- mTempFXY[0] <= hotseat.getRight() &&
- mTempFXY[1] >= hotseat.getTop() &&
- mTempFXY[1] <= hotseat.getBottom();
- }
+ boolean isPointInSelfOverHotseat(int x, int y) {
+ mTempFXY[0] = x;
+ mTempFXY[1] = y;
+ mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempFXY, true);
+ View hotseat = mLauncher.getHotseat();
+ return mTempFXY[0] >= hotseat.getLeft()
+ && mTempFXY[0] <= hotseat.getRight()
+ && mTempFXY[1] >= hotseat.getTop()
+ && mTempFXY[1] <= hotseat.getBottom();
+ }
/**
* Updates the point in {@param xy} to point to the co-ordinate space of {@param layout}
* @param layout either hotseat of a page in workspace
* @param xy the point location in workspace co-ordinate space
*/
- private void mapPointFromDropLayout(CellLayout layout, float[] xy) {
- if (mLauncher.isHotseatLayout(layout)) {
- mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, xy, true);
- mLauncher.getDragLayer().mapCoordInSelfToDescendant(layout, xy);
- } else {
- mapPointFromSelfToChild(layout, xy);
- }
- }
+ private void mapPointFromDropLayout(CellLayout layout, float[] xy) {
+ if (mLauncher.isHotseatLayout(layout)) {
+ mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, xy, true);
+ mLauncher.getDragLayer().mapCoordInSelfToDescendant(layout, xy);
+ } else {
+ mapPointFromSelfToChild(layout, xy);
+ }
+ }
private boolean isDragWidget(DragObject d) {
return (d.dragInfo instanceof LauncherAppWidgetInfo ||
@@ -2180,8 +2240,8 @@
item.spanY, child, mTargetCell);
if (!nearestDropOccupied) {
- mDragTargetLayout.visualizeDropLocation(d.originalView, mOutlineProvider,
- mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, d);
+ mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1],
+ item.spanX, item.spanY, d);
} else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
&& !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX ||
mLastReorderY != reorderY)) {
@@ -2230,19 +2290,27 @@
int nextPage = getNextPage();
if (layout == null && !isPageInTransition()) {
- // Check if the item is dragged over left page
+ // Check if the item is dragged over currentPage - 1 page
mTempTouchCoordinates[0] = Math.min(centerX, d.x);
mTempTouchCoordinates[1] = d.y;
layout = verifyInsidePage(nextPage + (mIsRtl ? 1 : -1), mTempTouchCoordinates);
}
if (layout == null && !isPageInTransition()) {
- // Check if the item is dragged over right page
+ // Check if the item is dragged over currentPage + 1 page
mTempTouchCoordinates[0] = Math.max(centerX, d.x);
mTempTouchCoordinates[1] = d.y;
layout = verifyInsidePage(nextPage + (mIsRtl ? -1 : 1), mTempTouchCoordinates);
}
+ // If two panel is enabled, users can also drag items to currentPage + 2
+ if (isTwoPanelEnabled() && layout == null && !isPageInTransition()) {
+ // Check if the item is dragged over currentPage + 2 page
+ mTempTouchCoordinates[0] = Math.max(centerX, d.x);
+ mTempTouchCoordinates[1] = d.y;
+ layout = verifyInsidePage(nextPage + (mIsRtl ? -2 : 2), mTempTouchCoordinates);
+ }
+
// Always pick the current page.
if (layout == null && nextPage >= 0 && nextPage < getPageCount()) {
layout = (CellLayout) getChildAt(nextPage);
@@ -2363,8 +2431,8 @@
}
boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
- mDragTargetLayout.visualizeDropLocation(dragObject.originalView, mOutlineProvider,
- mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, dragObject);
+ mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1],
+ resultSpan[0], resultSpan[1], dragObject);
}
}
@@ -2400,9 +2468,9 @@
spanY = mDragInfo.spanY;
}
- final int container = mLauncher.isHotseatLayout(cellLayout) ?
- LauncherSettings.Favorites.CONTAINER_HOTSEAT :
- LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ final int container = mLauncher.isHotseatLayout(cellLayout)
+ ? LauncherSettings.Favorites.CONTAINER_HOTSEAT
+ : LauncherSettings.Favorites.CONTAINER_DESKTOP;
final int screenId = getIdForScreen(cellLayout);
if (!mLauncher.isHotseatLayout(cellLayout)
&& screenId != getScreenIdForPageIndex(mCurrentPage)
@@ -2450,7 +2518,7 @@
Runnable onAnimationCompleteRunnable = new Runnable() {
@Override
public void run() {
- // Normally removeExtraEmptyScreen is called in Workspace#onDragEnd, but when
+ // Normally removeExtraEmptyScreen is called in Workspace#onDrop, but when
// adding an item that may not be dropped right away (due to a config activity)
// we defer the removal until the activity returns.
deferRemoveExtraEmptyScreen();
@@ -2471,8 +2539,7 @@
((PendingAddWidgetInfo) pendingInfo).boundWidget : null;
if (finalView != null && updateWidgetSize) {
- AppWidgetResizeFrame.updateWidgetSizeRanges(finalView, mLauncher, item.spanX,
- item.spanY);
+ WidgetSizes.updateWidgetSizeRanges(finalView, mLauncher, item.spanX, item.spanY);
}
int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR;
@@ -2556,7 +2623,7 @@
}
- public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) {
+ private Drawable createWidgetDrawable(ItemInfo widgetInfo, View layout) {
int[] unScaledSize = estimateItemSize(widgetInfo);
int visibility = layout.getVisibility();
layout.setVisibility(VISIBLE);
@@ -2568,7 +2635,7 @@
Bitmap b = BitmapRenderer.createHardwareBitmap(
unScaledSize[0], unScaledSize[1], layout::draw);
layout.setVisibility(visibility);
- return b;
+ return new FastBitmapDrawable(b);
}
private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY,
@@ -2637,10 +2704,11 @@
boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET ||
info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
- if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) {
- Bitmap crossFadeBitmap = createWidgetBitmap(info, finalView);
- dragView.setCrossFadeBitmap(crossFadeBitmap);
- dragView.crossFade((int) (duration * 0.8f));
+ if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external)
+ && finalView != null
+ && dragView.getContentView() != finalView) {
+ Drawable crossFadeDrawable = createWidgetDrawable(info, finalView);
+ dragView.crossFadeContent(crossFadeDrawable, (int) (duration * 0.8f));
} else if (isWidget && external) {
scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]);
}
@@ -2728,6 +2796,10 @@
removeWorkspaceItem(mDragInfo.cell);
}
} else if (mDragInfo != null) {
+ // When drag is cancelled, reattach content view back to its original parent.
+ if (mDragInfo.cell instanceof LauncherAppWidgetHostView && d.dragView != null) {
+ d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
+ }
final CellLayout cellLayout = mLauncher.getCellLayout(
mDragInfo.container, mDragInfo.screenId);
if (cellLayout != null) {
@@ -2888,18 +2960,31 @@
* Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close
* animation.
*
+ * @param preferredItemId The id of the preferred item to match to if it exists.
* @param packageName The package name of the app to match.
* @param user The user of the app to match.
*/
- public View getFirstMatchForAppClose(String packageName, UserHandle user) {
- final int curPage = getCurrentPage();
- final CellLayout currentPage = (CellLayout) getPageAt(curPage);
- final Workspace.ItemOperator packageAndUser = (ItemInfo info, View view) -> info != null
- && info.getTargetComponent() != null
- && TextUtils.equals(info.getTargetComponent().getPackageName(), packageName)
- && info.user.equals(user);
+ public View getFirstMatchForAppClose(int preferredItemId, String packageName, UserHandle user) {
+ final Workspace.ItemOperator preferredItem = (ItemInfo info, View view) ->
+ info != null && info.id == preferredItemId;
+ final Workspace.ItemOperator preferredItemInFolder = (info, view) -> {
+ if (info instanceof FolderInfo) {
+ FolderInfo folderInfo = (FolderInfo) info;
+ for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) {
+ if (preferredItem.evaluate(shortcutInfo, view)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
final Workspace.ItemOperator packageAndUserAndApp = (ItemInfo info, View view) ->
- packageAndUser.evaluate(info, view) && info.itemType == ITEM_TYPE_APPLICATION;
+ info != null
+ && info.itemType == ITEM_TYPE_APPLICATION
+ && info.user.equals(user)
+ && info.getTargetComponent() != null
+ && TextUtils.equals(info.getTargetComponent().getPackageName(),
+ packageName);
final Workspace.ItemOperator packageAndUserAndAppInFolder = (info, view) -> {
if (info instanceof FolderInfo) {
FolderInfo folderInfo = (FolderInfo) info;
@@ -2912,15 +2997,18 @@
return false;
};
- // Order: App icons, app in folder. Items in hotseat get returned first.
+ List<CellLayout> cellLayouts = new ArrayList<>(getPanelCount() + 1);
+ cellLayouts.add(getHotseat());
+ forEachVisiblePage(page -> cellLayouts.add((CellLayout) page));
+
+ // Order: Preferred item, App icons in hotseat/workspace, app in folder in hotseat/workspace
if (ADAPTIVE_ICON_WINDOW_ANIM.get()) {
- return getFirstMatch(new CellLayout[] { getHotseat(), currentPage },
+ return getFirstMatch(cellLayouts, preferredItem, preferredItemInFolder,
packageAndUserAndApp, packageAndUserAndAppInFolder);
} else {
// Do not use Folder as a criteria, since it'll cause a crash when trying to draw
// FolderAdaptiveIcon as the background.
- return getFirstMatch(new CellLayout[] { getHotseat(), currentPage },
- packageAndUserAndApp);
+ return getFirstMatch(cellLayouts, preferredItem, packageAndUserAndApp);
}
}
@@ -2950,34 +3038,17 @@
}
/**
+ * Finds the first view matching the ordered operators across the given cell layouts by order.
* @param cellLayouts List of CellLayouts to scan, in order of preference.
* @param operators List of operators, in order starting from best matching operator.
- * @return
*/
- private View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
- // This array is filled with the first match for each operator.
- final View[] matches = new View[operators.length];
- // For efficiency, the outer loop should be CellLayout.
- for (CellLayout cellLayout : cellLayouts) {
- mapOverCellLayout(cellLayout, (info, v) -> {
- for (int i = 0; i < operators.length; ++i) {
- if (matches[i] == null && operators[i].evaluate(info, v)) {
- matches[i] = v;
- if (i == 0) {
- // We can return since this is the best match possible.
- return true;
- }
- }
+ View getFirstMatch(Iterable<CellLayout> cellLayouts, final ItemOperator... operators) {
+ for (ItemOperator operator : operators) {
+ for (CellLayout cellLayout : cellLayouts) {
+ View match = mapOverCellLayout(cellLayout, operator);
+ if (match != null) {
+ return match;
}
- return false;
- });
- if (matches[0] != null) {
- break;
- }
- }
- for (View match : matches) {
- if (match != null) {
- return match;
}
}
return null;
@@ -3002,38 +3073,27 @@
* shortcuts are not removed.
*/
public void removeItemsByMatcher(final ItemInfoMatcher matcher) {
- for (final CellLayout layoutParent: getWorkspaceAndHotseatCellLayouts()) {
- final ViewGroup layout = layoutParent.getShortcutsAndWidgets();
+ for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
+ ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
+ // Iterate in reverse order as we are removing items
+ for (int i = container.getChildCount() - 1; i >= 0; i--) {
+ View child = container.getChildAt(i);
+ ItemInfo info = (ItemInfo) child.getTag();
- IntSparseArrayMap<View> idToViewMap = new IntSparseArrayMap<>();
- ArrayList<ItemInfo> items = new ArrayList<>();
- for (int j = 0; j < layout.getChildCount(); j++) {
- final View view = layout.getChildAt(j);
- if (view.getTag() instanceof ItemInfo) {
- ItemInfo item = (ItemInfo) view.getTag();
- items.add(item);
- idToViewMap.put(item.id, view);
- }
- }
-
- for (ItemInfo itemToRemove : matcher.filterItemInfos(items)) {
- View child = idToViewMap.get(itemToRemove.id);
-
- if (child != null) {
- // Note: We can not remove the view directly from CellLayoutChildren as this
- // does not re-mark the spaces as unoccupied.
- layoutParent.removeViewInLayout(child);
+ if (matcher.matchesInfo(info)) {
+ layout.removeViewInLayout(child);
if (child instanceof DropTarget) {
mDragController.removeDropTarget((DropTarget) child);
}
- } else if (itemToRemove.container >= 0) {
- // The item may belong to a folder.
- View parent = idToViewMap.get(itemToRemove.container);
- if (parent instanceof FolderIcon) {
- FolderInfo folderInfo = (FolderInfo) parent.getTag();
- folderInfo.remove((WorkspaceItemInfo) itemToRemove, false);
- if (((FolderIcon) parent).getFolder().isOpen()) {
- ((FolderIcon) parent).getFolder().close(false /* animate */);
+ } else if (child instanceof FolderIcon) {
+ FolderInfo folderInfo = (FolderInfo) info;
+ List<WorkspaceItemInfo> matches = folderInfo.contents.stream()
+ .filter(matcher::matchesInfo)
+ .collect(Collectors.toList());
+ if (!matches.isEmpty()) {
+ folderInfo.removeAll(matches, false);
+ if (((FolderIcon) child).getFolder().isOpen()) {
+ ((FolderIcon) child).getFolder().close(false /* animate */);
}
}
}
@@ -3062,16 +3122,16 @@
*/
public void mapOverItems(ItemOperator op) {
for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
- if (mapOverCellLayout(layout, op)) {
+ if (mapOverCellLayout(layout, op) != null) {
return;
}
}
}
- private boolean mapOverCellLayout(CellLayout layout, ItemOperator op) {
+ private View mapOverCellLayout(CellLayout layout, ItemOperator op) {
// TODO(b/128460496) Potential race condition where layout is not yet loaded
if (layout == null) {
- return false;
+ return null;
}
ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
// map over all the shortcuts on the workspace
@@ -3079,13 +3139,13 @@
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
View item = container.getChildAt(itemIdx);
if (op.evaluate((ItemInfo) item.getTag(), item)) {
- return true;
+ return item;
}
}
- return false;
+ return null;
}
- void updateShortcuts(ArrayList<WorkspaceItemInfo> shortcuts) {
+ void updateShortcuts(List<WorkspaceItemInfo> shortcuts) {
final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
ItemOperator op = (info, v) -> {
if (v instanceof BubbleTextView && updates.contains(info)) {
@@ -3143,9 +3203,8 @@
}
public void removeAbandonedPromise(String packageName, UserHandle user) {
- HashSet<String> packages = new HashSet<>(1);
- packages.add(packageName);
- ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packages, user);
+ ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(
+ Collections.singleton(packageName), user);
mLauncher.getModelWriter().deleteItemsFromDatabase(matcher);
removeItemsByMatcher(matcher);
}
@@ -3154,7 +3213,7 @@
ItemOperator op = (info, v) -> {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
&& updates.contains(info)) {
- ((BubbleTextView) v).applyPromiseState(false /* promiseStateChanged */);
+ ((BubbleTextView) v).applyLoadingState(false /* promiseStateChanged */);
} else if (v instanceof PendingAppWidgetHostView
&& info instanceof LauncherAppWidgetInfo
&& updates.contains(info)) {
@@ -3224,6 +3283,18 @@
}
}
+ /**
+ * Set the given view's pivot point to match the workspace's, so that it scales together. Since
+ * both this view and workspace can move, transform the point manually instead of using
+ * dragLayer.getDescendantCoordRelativeToSelf and related methods.
+ */
+ public void setPivotToScaleWithSelf(View sibling) {
+ sibling.setPivotY(getPivotY() + getTop()
+ - sibling.getTop() - sibling.getTranslationY());
+ sibling.setPivotX(getPivotX() + getLeft()
+ - sibling.getLeft() - sibling.getTranslationX());
+ }
+
@Override
public int getExpectedHeight() {
return getMeasuredHeight() <= 0 || !mIsLayoutValid
@@ -3260,29 +3331,11 @@
}
if (nScreens == 0) {
// When the workspace is not loaded, we do not know how many screen will be bound.
- return getContext().getString(R.string.all_apps_home_button_label);
+ return getContext().getString(R.string.home_screen);
}
return getContext().getString(R.string.workspace_scroll_format, page + 1, nScreens);
}
- @Override
- public void fillInLogContainerData(ItemInfo childInfo, Target child,
- ArrayList<Target> parents) {
- if (childInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
- || childInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
- getHotseat().fillInLogContainerData(childInfo, child, parents);
- return;
- } else if (childInfo.container >= 0) {
- FolderIcon icon = (FolderIcon) getHomescreenIconByItemId(childInfo.container);
- icon.getFolder().fillInLogContainerData(childInfo, child, parents);
- return;
- }
- child.gridX = childInfo.cellX;
- child.gridY = childInfo.cellY;
- child.pageIndex = getCurrentPage();
- parents.add(newContainerTarget(ContainerType.WORKSPACE));
- }
-
/**
* Used as a workaround to ensure that the AppWidgetService receives the
* PACKAGE_ADDED broadcast before updating widgets.
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index c3d4aeb..d6302ce 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -28,7 +28,7 @@
String TAG = "Launcher.Workspace";
- // The screen id used for the empty screen always present to the right.
+ // The screen id used for the empty screen always present at the end.
int EXTRA_EMPTY_SCREEN_ID = -201;
// The is the first screen. It is always present, even if its empty.
int FIRST_SCREEN_ID = 0;
@@ -130,12 +130,16 @@
}
child.setHapticFeedbackEnabled(false);
- child.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+ child.setOnLongClickListener(getWorkspaceChildOnLongClickListener());
if (child instanceof DropTarget) {
onAddDropTarget((DropTarget) child);
}
}
+ default View.OnLongClickListener getWorkspaceChildOnLongClickListener() {
+ return ItemLongClickListener.INSTANCE_WORKSPACE;
+ }
+
Hotseat getHotseat();
CellLayout getScreenWithId(int screenId);
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index cd938e1..1b9647a 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -18,26 +18,28 @@
import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE;
-import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.LauncherState.FLAG_HAS_SYS_UI_SCRIM;
-import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_HAS_BACKGROUNDS;
import static com.android.launcher3.LauncherState.HINT_STATE;
import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.WORKSPACE_PAGE_INDICATOR;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.ZOOM_OUT;
import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
-import static com.android.launcher3.graphics.WorkspaceAndHotseatScrim.SYSUI_PROGRESS;
+import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
import android.animation.ValueAnimator;
import android.view.View;
@@ -45,11 +47,12 @@
import com.android.launcher3.LauncherState.PageAlphaProvider;
import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
+import com.android.launcher3.graphics.Scrim;
+import com.android.launcher3.graphics.SysUiScrim;
+import com.android.launcher3.states.SpringLoadedState;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.DynamicResource;
import com.android.systemui.plugins.ResourceProvider;
@@ -93,7 +96,6 @@
ScaleAndTranslation scaleAndTranslation = state.getWorkspaceScaleAndTranslation(mLauncher);
ScaleAndTranslation hotseatScaleAndTranslation = state.getHotseatScaleAndTranslation(
mLauncher);
- ScaleAndTranslation qsbScaleAndTranslation = state.getQsbScaleAndTranslation(mLauncher);
mNewScale = scaleAndTranslation.scale;
PageAlphaProvider pageAlphaProvider = state.getWorkspacePageAlphaProvider(mLauncher);
final int childCount = mWorkspace.getChildCount();
@@ -105,54 +107,39 @@
int elements = state.getVisibleElements(mLauncher);
Interpolator fadeInterpolator = config.getInterpolator(ANIM_WORKSPACE_FADE,
pageAlphaProvider.interpolator);
- boolean playAtomicComponent = config.playAtomicOverviewScaleComponent();
Hotseat hotseat = mWorkspace.getHotseat();
- // Since we set the pivot relative to mWorkspace, we need to scale a sibling of Workspace.
- AllAppsContainerView qsbScaleView = mLauncher.getAppsView();
- View qsbView = qsbScaleView.getSearchView();
- if (playAtomicComponent) {
- Interpolator scaleInterpolator = config.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT);
- LauncherState fromState = mLauncher.getStateManager().getState();
- boolean shouldSpring = propertySetter instanceof PendingAnimation
- && fromState == HINT_STATE && state == NORMAL;
- if (shouldSpring) {
- ((PendingAnimation) propertySetter).add(getSpringScaleAnimator(mLauncher,
- mWorkspace, mNewScale));
- } else {
- propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
- }
+ Interpolator scaleInterpolator = config.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT);
+ LauncherState fromState = mLauncher.getStateManager().getState();
- setPivotToScaleWithWorkspace(hotseat);
- setPivotToScaleWithWorkspace(qsbScaleView);
- float hotseatScale = hotseatScaleAndTranslation.scale;
- if (shouldSpring) {
- PendingAnimation pa = (PendingAnimation) propertySetter;
- pa.add(getSpringScaleAnimator(mLauncher, hotseat, hotseatScale));
- pa.add(getSpringScaleAnimator(mLauncher, qsbScaleView,
- qsbScaleAndTranslation.scale));
- } else {
- Interpolator hotseatScaleInterpolator = config.getInterpolator(ANIM_HOTSEAT_SCALE,
- scaleInterpolator);
- propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
- hotseatScaleInterpolator);
- propertySetter.setFloat(qsbScaleView, SCALE_PROPERTY, qsbScaleAndTranslation.scale,
- hotseatScaleInterpolator);
- }
-
- float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
- propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator);
- propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
- hotseatIconsAlpha, fadeInterpolator);
+ boolean shouldSpring = propertySetter instanceof PendingAnimation
+ && fromState == HINT_STATE && state == NORMAL;
+ if (shouldSpring) {
+ ((PendingAnimation) propertySetter).add(getSpringScaleAnimator(mLauncher,
+ mWorkspace, mNewScale));
+ } else {
+ propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
}
- if (config.onlyPlayAtomicComponent()) {
- // Only the alpha and scale, handled above, are included in the atomic animation.
- return;
+ mWorkspace.setPivotToScaleWithSelf(hotseat);
+ float hotseatScale = hotseatScaleAndTranslation.scale;
+ if (shouldSpring) {
+ PendingAnimation pa = (PendingAnimation) propertySetter;
+ pa.add(getSpringScaleAnimator(mLauncher, hotseat, hotseatScale));
+ } else {
+ Interpolator hotseatScaleInterpolator = config.getInterpolator(ANIM_HOTSEAT_SCALE,
+ scaleInterpolator);
+ propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
+ hotseatScaleInterpolator);
}
- Interpolator translationInterpolator = !playAtomicComponent
- ? LINEAR
- : config.getInterpolator(ANIM_WORKSPACE_TRANSLATE, ZOOM_OUT);
+ float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
+ propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator);
+ float workspacePageIndicatorAlpha = (elements & WORKSPACE_PAGE_INDICATOR) != 0 ? 1 : 0;
+ propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
+ workspacePageIndicatorAlpha, fadeInterpolator);
+
+ Interpolator translationInterpolator =
+ config.getInterpolator(ANIM_WORKSPACE_TRANSLATE, ZOOM_OUT);
propertySetter.setFloat(mWorkspace, VIEW_TRANSLATE_X,
scaleAndTranslation.translationX, translationInterpolator);
propertySetter.setFloat(mWorkspace, VIEW_TRANSLATE_Y,
@@ -164,30 +151,25 @@
hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator);
propertySetter.setFloat(mWorkspace.getPageIndicator(), VIEW_TRANSLATE_Y,
hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator);
- propertySetter.setFloat(qsbView, VIEW_TRANSLATE_Y,
- qsbScaleAndTranslation.translationY, hotseatTranslationInterpolator);
- setScrim(propertySetter, state);
+ if (!config.hasAnimationFlag(SKIP_SCRIM)) {
+ setScrim(propertySetter, state, config);
+ }
}
- /**
- * Set the given view's pivot point to match the workspace's, so that it scales together. Since
- * both this view and workspace can move, transform the point manually instead of using
- * dragLayer.getDescendantCoordRelativeToSelf and related methods.
- */
- private void setPivotToScaleWithWorkspace(View sibling) {
- sibling.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop()
- - sibling.getTop() - sibling.getTranslationY());
- sibling.setPivotX(mWorkspace.getPivotX() + mWorkspace.getLeft()
- - sibling.getLeft() - sibling.getTranslationX());
- }
+ public void setScrim(PropertySetter propertySetter, LauncherState state,
+ StateAnimationConfig config) {
+ Scrim workspaceDragScrim = mLauncher.getDragLayer().getWorkspaceDragScrim();
+ propertySetter.setFloat(workspaceDragScrim, SCRIM_PROGRESS,
+ state.getWorkspaceBackgroundAlpha(mLauncher), LINEAR);
- public void setScrim(PropertySetter propertySetter, LauncherState state) {
- WorkspaceAndHotseatScrim scrim = mLauncher.getDragLayer().getScrim();
- propertySetter.setFloat(scrim, SCRIM_PROGRESS, state.getWorkspaceScrimAlpha(mLauncher),
- LINEAR);
- propertySetter.setFloat(scrim, SYSUI_PROGRESS,
+ SysUiScrim sysUiScrim = mLauncher.getRootView().getSysUiScrim();
+ propertySetter.setFloat(sysUiScrim, SYSUI_PROGRESS,
state.hasFlag(FLAG_HAS_SYS_UI_SCRIM) ? 1 : 0, LINEAR);
+
+ propertySetter.setViewBackgroundColor(mLauncher.getScrimView(),
+ state.getWorkspaceScrimColor(mLauncher),
+ config.getInterpolator(ANIM_SCRIM_FADE, ACCEL_2));
}
public void applyChildState(LauncherState state, CellLayout cl, int childIndex) {
@@ -199,20 +181,14 @@
PageAlphaProvider pageAlphaProvider, PropertySetter propertySetter,
StateAnimationConfig config) {
float pageAlpha = pageAlphaProvider.getPageAlpha(childIndex);
- int drawableAlpha = state.hasFlag(FLAG_WORKSPACE_HAS_BACKGROUNDS)
- ? Math.round(pageAlpha * 255) : 0;
+ float springLoadedProgress = (state instanceof SpringLoadedState) ? 1.0f : 0f;
- if (!config.onlyPlayAtomicComponent()) {
- // Don't update the scrim during the atomic animation.
- propertySetter.setInt(cl.getScrimBackground(),
- DRAWABLE_ALPHA, drawableAlpha, ZOOM_OUT);
- }
- if (config.playAtomicOverviewScaleComponent()) {
- Interpolator fadeInterpolator = config.getInterpolator(ANIM_WORKSPACE_FADE,
- pageAlphaProvider.interpolator);
- propertySetter.setFloat(cl.getShortcutsAndWidgets(), VIEW_ALPHA,
- pageAlpha, fadeInterpolator);
- }
+ propertySetter.setFloat(cl,
+ CellLayout.SPRING_LOADED_PROGRESS, springLoadedProgress, ZOOM_OUT);
+ Interpolator fadeInterpolator = config.getInterpolator(ANIM_WORKSPACE_FADE,
+ pageAlphaProvider.interpolator);
+ propertySetter.setFloat(cl.getShortcutsAndWidgets(), VIEW_ALPHA,
+ pageAlpha, fadeInterpolator);
}
/**
diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
index ddb547f..d0fc175 100644
--- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
@@ -31,6 +31,7 @@
import com.android.launcher3.CellLayout;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DragLayer;
import java.util.List;
@@ -41,30 +42,32 @@
implements OnClickListener, OnHoverListener {
protected static final int INVALID_POSITION = -1;
- private static final int[] sTempArray = new int[2];
+ protected final Rect mTempRect = new Rect();
+ protected final int[] mTempCords = new int[2];
protected final CellLayout mView;
protected final Context mContext;
protected final LauncherAccessibilityDelegate mDelegate;
-
- private final Rect mTempRect = new Rect();
+ protected final DragLayer mDragLayer;
public DragAndDropAccessibilityDelegate(CellLayout forView) {
super(forView);
mView = forView;
mContext = mView.getContext();
- mDelegate = Launcher.getLauncher(mContext).getAccessibilityDelegate();
+ Launcher launcher = Launcher.getLauncher(mContext);
+ mDelegate = launcher.getAccessibilityDelegate();
+ mDragLayer = launcher.getDragLayer();
}
@Override
- protected int getVirtualViewAt(float x, float y) {
+ public int getVirtualViewAt(float x, float y) {
if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) {
return INVALID_ID;
}
- mView.pointToCellExact((int) x, (int) y, sTempArray);
+ mView.pointToCellExact((int) x, (int) y, mTempCords);
// Map cell to id
- int id = sTempArray[0] + sTempArray[1] * mView.getCountX();
+ int id = mTempCords[0] + mTempCords[1] * mView.getCountX();
return intersectsValidDropTarget(id);
}
@@ -75,7 +78,7 @@
protected abstract int intersectsValidDropTarget(int id);
@Override
- protected void getVisibleVirtualViews(List<Integer> virtualViews) {
+ public void getVisibleVirtualViews(List<Integer> virtualViews) {
// We create a virtual view for each cell of the grid
// The cell ids correspond to cells in reading order.
int nCells = mView.getCountX() * mView.getCountY();
@@ -88,7 +91,7 @@
}
@Override
- protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
+ public boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) {
String confirmation = getConfirmationForIconDrop(viewId);
mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation);
@@ -112,13 +115,25 @@
}
@Override
- protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
+ public void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
if (id == INVALID_ID) {
throw new IllegalArgumentException("Invalid virtual view id");
}
node.setContentDescription(getLocationDescriptionForIconDrop(id));
- node.setBoundsInParent(getItemBounds(id));
+
+ Rect itemBounds = getItemBounds(id);
+ node.setBoundsInParent(itemBounds);
+
+ // ExploreByTouchHelper does not currently handle view scale.
+ // Update BoundsInScreen to appropriate value.
+ mTempCords[0] = mTempCords[1] = 0;
+ float scale = mDragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords);
+ mTempRect.left = mTempCords[0] + (int) (itemBounds.left * scale);
+ mTempRect.right = mTempCords[0] + (int) (itemBounds.right * scale);
+ mTempRect.top = mTempCords[1] + (int) (itemBounds.top * scale);
+ mTempRect.bottom = mTempCords[1] + (int) (itemBounds.bottom * scale);
+ node.setBoundsInScreen(mTempRect);
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
node.setClickable(true);
@@ -130,6 +145,13 @@
return dispatchHoverEvent(motionEvent);
}
+ /**
+ * Returns the target host container
+ */
+ public View getHost() {
+ return mView;
+ }
+
protected abstract String getLocationDescriptionForIconDrop(int id);
protected abstract String getConfirmationForIconDrop(int id);
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 136d43e..9faac5b 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -3,51 +3,58 @@
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
-import android.app.AlertDialog;
import android.appwidget.AppWidgetProviderInfo;
-import android.content.DialogInterface;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
+import android.view.KeyEvent;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import com.android.launcher3.AppWidgetResizeFrame;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.ButtonDropTarget;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.R;
import com.android.launcher3.Workspace;
import com.android.launcher3.dragndrop.DragController.DragListener;
import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.folder.Folder;
-import com.android.launcher3.keyboard.CustomActionsPopup;
+import com.android.launcher3.keyboard.KeyboardDragAndDropView;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.notification.NotificationListener;
+import com.android.launcher3.popup.ArrowPopup;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.ShortcutUtil;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.OptionsPopupView;
+import com.android.launcher3.views.OptionsPopupView.OptionItem;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener {
@@ -77,93 +84,105 @@
public View item;
}
- protected final SparseArray<AccessibilityAction> mActions = new SparseArray<>();
- @Thunk final Launcher mLauncher;
+ protected final SparseArray<LauncherAction> mActions = new SparseArray<>();
+ protected final Launcher mLauncher;
private DragInfo mDragInfo = null;
public LauncherAccessibilityDelegate(Launcher launcher) {
mLauncher = launcher;
- mActions.put(REMOVE, new AccessibilityAction(REMOVE,
- launcher.getText(R.string.remove_drop_target_label)));
- mActions.put(UNINSTALL, new AccessibilityAction(UNINSTALL,
- launcher.getText(R.string.uninstall_drop_target_label)));
- mActions.put(DISMISS_PREDICTION, new AccessibilityAction(DISMISS_PREDICTION,
- launcher.getText(R.string.dismiss_prediction_label)));
- mActions.put(RECONFIGURE, new AccessibilityAction(RECONFIGURE,
- launcher.getText(R.string.gadget_setup_text)));
- mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE,
- launcher.getText(R.string.action_add_to_workspace)));
- mActions.put(MOVE, new AccessibilityAction(MOVE,
- launcher.getText(R.string.action_move)));
- mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE,
- launcher.getText(R.string.action_move_to_workspace)));
- mActions.put(RESIZE, new AccessibilityAction(RESIZE,
- launcher.getText(R.string.action_resize)));
- mActions.put(DEEP_SHORTCUTS, new AccessibilityAction(DEEP_SHORTCUTS,
- launcher.getText(R.string.action_deep_shortcut)));
- mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new AccessibilityAction(DEEP_SHORTCUTS,
- launcher.getText(R.string.shortcuts_menu_with_notifications_description)));
- }
-
- public void addAccessibilityAction(int action, int actionLabel) {
- mActions.put(action, new AccessibilityAction(action, mLauncher.getText(actionLabel)));
+ mActions.put(REMOVE, new LauncherAction(
+ REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X));
+ mActions.put(UNINSTALL, new LauncherAction(
+ UNINSTALL, R.string.uninstall_drop_target_label, KeyEvent.KEYCODE_U));
+ mActions.put(DISMISS_PREDICTION, new LauncherAction(DISMISS_PREDICTION,
+ R.string.dismiss_prediction_label, KeyEvent.KEYCODE_X));
+ mActions.put(RECONFIGURE, new LauncherAction(
+ RECONFIGURE, R.string.gadget_setup_text, KeyEvent.KEYCODE_E));
+ mActions.put(ADD_TO_WORKSPACE, new LauncherAction(
+ ADD_TO_WORKSPACE, R.string.action_add_to_workspace, KeyEvent.KEYCODE_P));
+ mActions.put(MOVE, new LauncherAction(
+ MOVE, R.string.action_move, KeyEvent.KEYCODE_M));
+ mActions.put(MOVE_TO_WORKSPACE, new LauncherAction(MOVE_TO_WORKSPACE,
+ R.string.action_move_to_workspace, KeyEvent.KEYCODE_P));
+ mActions.put(RESIZE, new LauncherAction(
+ RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R));
+ mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS,
+ R.string.action_deep_shortcut, KeyEvent.KEYCODE_S));
+ mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new LauncherAction(DEEP_SHORTCUTS,
+ R.string.shortcuts_menu_with_notifications_description, KeyEvent.KEYCODE_S));
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
- addSupportedActions(host, info, false);
+ if (host.getTag() instanceof ItemInfo) {
+ ItemInfo item = (ItemInfo) host.getTag();
+
+ List<LauncherAction> actions = new ArrayList<>();
+ getSupportedActions(host, item, actions);
+ actions.forEach(la -> info.addAction(la.accessibilityAction));
+
+ if (!itemSupportsLongClick(host, item)) {
+ info.setLongClickable(false);
+ info.removeAction(AccessibilityAction.ACTION_LONG_CLICK);
+ }
+ }
}
- public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) {
- if (!(host.getTag() instanceof ItemInfo)) return;
- ItemInfo item = (ItemInfo) host.getTag();
-
- if (host instanceof AccessibilityActionHandler) {
- ((AccessibilityActionHandler) host).addSupportedAccessibilityActions(info);
- }
-
+ /**
+ * Adds all the accessibility actions that can be handled.
+ */
+ protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
// If the request came from keyboard, do not add custom shortcuts as that is already
// exposed as a direct shortcut
- if (!fromKeyboard && ShortcutUtil.supportsShortcuts(item)) {
- info.addAction(mActions.get(NotificationListener.getInstanceIfConnected() != null
+ if (ShortcutUtil.supportsShortcuts(item)) {
+ out.add(mActions.get(NotificationListener.getInstanceIfConnected() != null
? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS));
}
for (ButtonDropTarget target : mLauncher.getDropTargetBar().getDropTargets()) {
if (target.supportsAccessibilityDrop(item, host)) {
- info.addAction(mActions.get(target.getAccessibilityAction()));
+ out.add(mActions.get(target.getAccessibilityAction()));
}
}
// Do not add move actions for keyboard request as this uses virtual nodes.
- if (!fromKeyboard && itemSupportsAccessibleDrag(item)) {
- info.addAction(mActions.get(MOVE));
+ if (itemSupportsAccessibleDrag(item)) {
+ out.add(mActions.get(MOVE));
if (item.container >= 0) {
- info.addAction(mActions.get(MOVE_TO_WORKSPACE));
+ out.add(mActions.get(MOVE_TO_WORKSPACE));
} else if (item instanceof LauncherAppWidgetInfo) {
if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) {
- info.addAction(mActions.get(RESIZE));
+ out.add(mActions.get(RESIZE));
}
}
}
- if (!fromKeyboard && !itemSupportsLongClick(host, item)) {
- info.setLongClickable(false);
- info.removeAction(AccessibilityAction.ACTION_LONG_CLICK);
- }
-
if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) {
- info.addAction(mActions.get(ADD_TO_WORKSPACE));
+ out.add(mActions.get(ADD_TO_WORKSPACE));
}
}
+ /**
+ * Returns all the accessibility actions that can be handled by the host.
+ */
+ public static List<LauncherAction> getSupportedActions(Launcher launcher, View host) {
+ if (host == null || !(host.getTag() instanceof ItemInfo)) {
+ return Collections.emptyList();
+ }
+ PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
+ LauncherAccessibilityDelegate delegate = container != null
+ ? container.getAccessibilityDelegate() : launcher.getAccessibilityDelegate();
+ List<LauncherAction> result = new ArrayList<>();
+ delegate.getSupportedActions(host, (ItemInfo) host.getTag(), result);
+ return result;
+ }
+
private boolean itemSupportsLongClick(View host, ItemInfo info) {
- return PopupContainerWithArrow.canShow(host, info)
- || new CustomActionsPopup(mLauncher, host).canShow();
+ return PopupContainerWithArrow.canShow(host, info);
}
private boolean itemSupportsAccessibleDrag(ItemInfo item) {
@@ -178,13 +197,17 @@
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if ((host.getTag() instanceof ItemInfo)
- && performAction(host, (ItemInfo) host.getTag(), action)) {
+ && performAction(host, (ItemInfo) host.getTag(), action, false)) {
return true;
}
return super.performAccessibilityAction(host, action, args);
}
- public boolean performAction(final View host, final ItemInfo item, int action) {
+ /**
+ * Performs the provided action on the host
+ */
+ protected boolean performAction(final View host, final ItemInfo item, int action,
+ boolean fromKeyboard) {
if (action == ACTION_LONG_CLICK) {
if (PopupContainerWithArrow.canShow(host, item)) {
// Long press should be consumed for workspace items, and it should invoke the
@@ -192,46 +215,32 @@
// standard long press path does.
PopupContainerWithArrow.showForIcon((BubbleTextView) host);
return true;
- } else {
- CustomActionsPopup popup = new CustomActionsPopup(mLauncher, host);
- if (popup.canShow()) {
- popup.show();
- return true;
- }
}
- }
- if (host instanceof AccessibilityActionHandler
- && ((AccessibilityActionHandler) host).performAccessibilityAction(action, item)) {
- return true;
- }
- if (action == MOVE) {
- beginAccessibleDrag(host, item);
+ } else if (action == MOVE) {
+ return beginAccessibleDrag(host, item, fromKeyboard);
} else if (action == ADD_TO_WORKSPACE) {
final int[] coordinates = new int[2];
final int screenId = findSpaceOnWorkspace(item, coordinates);
- mLauncher.getStateManager().goToState(NORMAL, true, new Runnable() {
+ mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
+ if (item instanceof AppInfo) {
+ WorkspaceItemInfo info = ((AppInfo) item).makeWorkspaceItem();
+ mLauncher.getModelWriter().addItemToDatabase(info,
+ Favorites.CONTAINER_DESKTOP,
+ screenId, coordinates[0], coordinates[1]);
- @Override
- public void run() {
- if (item instanceof AppInfo) {
- WorkspaceItemInfo info = ((AppInfo) item).makeWorkspaceItem();
- mLauncher.getModelWriter().addItemToDatabase(info,
- Favorites.CONTAINER_DESKTOP,
- screenId, coordinates[0], coordinates[1]);
-
- ArrayList<ItemInfo> itemList = new ArrayList<>();
- itemList.add(info);
- mLauncher.bindItems(itemList, true);
- announceConfirmation(R.string.item_added_to_workspace);
- } else if (item instanceof PendingAddItemInfo) {
- PendingAddItemInfo info = (PendingAddItemInfo) item;
- Workspace workspace = mLauncher.getWorkspace();
- workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
- mLauncher.addPendingItem(info, Favorites.CONTAINER_DESKTOP,
- screenId, coordinates, info.spanX, info.spanY);
- }
+ mLauncher.bindItems(
+ Collections.singletonList(info),
+ /* forceAnimateIcons= */ true,
+ /* focusFirstItemForAccessibility= */ true);
+ announceConfirmation(R.string.item_added_to_workspace);
+ } else if (item instanceof PendingAddItemInfo) {
+ PendingAddItemInfo info = (PendingAddItemInfo) item;
+ Workspace workspace = mLauncher.getWorkspace();
+ workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
+ mLauncher.addPendingItem(info, Favorites.CONTAINER_DESKTOP,
+ screenId, coordinates, info.spanX, info.spanY);
}
- });
+ }));
return true;
} else if (action == MOVE_TO_WORKSPACE) {
Folder folder = Folder.getOpen(mLauncher);
@@ -242,47 +251,31 @@
final int[] coordinates = new int[2];
final int screenId = findSpaceOnWorkspace(item, coordinates);
mLauncher.getModelWriter().moveItemInDatabase(info,
- LauncherSettings.Favorites.CONTAINER_DESKTOP,
+ Favorites.CONTAINER_DESKTOP,
screenId, coordinates[0], coordinates[1]);
// Bind the item in next frame so that if a new workspace page was created,
// it will get laid out.
- new Handler().post(new Runnable() {
-
- @Override
- public void run() {
- ArrayList<ItemInfo> itemList = new ArrayList<>();
- itemList.add(item);
- mLauncher.bindItems(itemList, true);
- announceConfirmation(R.string.item_moved);
- }
+ new Handler().post(() -> {
+ mLauncher.bindItems(Collections.singletonList(item), true);
+ announceConfirmation(R.string.item_moved);
});
+ return true;
} else if (action == RESIZE) {
final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item;
- final IntArray actions = getSupportedResizeActions(host, info);
- CharSequence[] labels = new CharSequence[actions.size()];
- for (int i = 0; i < actions.size(); i++) {
- labels[i] = mLauncher.getText(actions.get(i));
- }
-
- new AlertDialog.Builder(mLauncher)
- .setTitle(R.string.action_resize)
- .setItems(labels, new DialogInterface.OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- performResizeAction(actions.get(which), host, info);
- dialog.dismiss();
- }
- })
- .show();
+ List<OptionItem> actions = getSupportedResizeActions(host, info);
+ Rect pos = new Rect();
+ mLauncher.getDragLayer().getDescendantRectRelativeToSelf(host, pos);
+ ArrowPopup popup = OptionsPopupView.show(mLauncher, new RectF(pos), actions, false);
+ popup.requestFocus();
+ popup.setOnCloseCallback(host::requestFocus);
return true;
- } else if (action == DEEP_SHORTCUTS) {
+ } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) {
return PopupContainerWithArrow.showForIcon((BubbleTextView) host) != null;
} else {
for (ButtonDropTarget dropTarget : mLauncher.getDropTargetBar().getDropTargets()) {
- if (dropTarget.supportsAccessibilityDrop(item, host) &&
- action == dropTarget.getAccessibilityAction()) {
+ if (dropTarget.supportsAccessibilityDrop(item, host)
+ && action == dropTarget.getAccessibilityAction()) {
dropTarget.onAccessibilityDrop(host, item);
return true;
}
@@ -291,40 +284,60 @@
return false;
}
- private IntArray getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
- IntArray actions = new IntArray();
-
+ private List<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
+ List<OptionItem> actions = new ArrayList<>();
AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo();
if (providerInfo == null) {
return actions;
}
- CellLayout layout = (CellLayout) host.getParent().getParent();
+ CellLayout layout;
+ if (host.getParent() instanceof DragView) {
+ layout = (CellLayout) ((DragView) host.getParent()).getContentViewParent().getParent();
+ } else {
+ layout = (CellLayout) host.getParent().getParent();
+ }
if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) {
if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) ||
layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) {
- actions.add(R.string.action_increase_width);
+ actions.add(new OptionItem(mLauncher,
+ R.string.action_increase_width,
+ R.drawable.ic_widget_width_increase,
+ IGNORE,
+ v -> performResizeAction(R.string.action_increase_width, host, info)));
}
if (info.spanX > info.minSpanX && info.spanX > 1) {
- actions.add(R.string.action_decrease_width);
+ actions.add(new OptionItem(mLauncher,
+ R.string.action_decrease_width,
+ R.drawable.ic_widget_width_decrease,
+ IGNORE,
+ v -> performResizeAction(R.string.action_decrease_width, host, info)));
}
}
if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) {
if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) ||
layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) {
- actions.add(R.string.action_increase_height);
+ actions.add(new OptionItem(mLauncher,
+ R.string.action_increase_height,
+ R.drawable.ic_widget_height_increase,
+ IGNORE,
+ v -> performResizeAction(R.string.action_increase_height, host, info)));
}
if (info.spanY > info.minSpanY && info.spanY > 1) {
- actions.add(R.string.action_decrease_height);
+ actions.add(new OptionItem(mLauncher,
+ R.string.action_decrease_height,
+ R.drawable.ic_widget_height_decrease,
+ IGNORE,
+ v -> performResizeAction(R.string.action_decrease_height, host, info)));
}
}
return actions;
}
- @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
+ private boolean performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams();
CellLayout layout = (CellLayout) host.getParent().getParent();
layout.markCellsAsUnoccupiedForView(host);
@@ -354,13 +367,12 @@
}
layout.markCellsAsOccupiedForView(host);
- Rect sizeRange = new Rect();
- AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, sizeRange);
- ((LauncherAppWidgetHostView) host).updateAppWidgetSize(null,
- sizeRange.left, sizeRange.top, sizeRange.right, sizeRange.bottom);
+ WidgetSizes.updateWidgetSizeRanges(((LauncherAppWidgetHostView) host), mLauncher,
+ info.spanX, info.spanY);
host.requestLayout();
mLauncher.getModelWriter().updateItemInDatabase(info);
announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY));
+ return true;
}
@Thunk void announceConfirmation(int resId) {
@@ -406,7 +418,11 @@
}
}
- public void beginAccessibleDrag(View item, ItemInfo info) {
+ private boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) {
+ if (!itemSupportsAccessibleDrag(info)) {
+ return false;
+ }
+
mDragInfo = new DragInfo();
mDragInfo.info = info;
mDragInfo.item = item;
@@ -423,8 +439,17 @@
DragOptions options = new DragOptions();
options.isAccessibleDrag = true;
+ options.isKeyboardDrag = fromKeyboard;
options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
- ItemLongClickListener.beginDrag(item, mLauncher, info, options);
+
+ if (fromKeyboard) {
+ KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mLauncher.getLayoutInflater()
+ .inflate(R.layout.keyboard_drag_and_drop, mLauncher.getDragLayer(), false);
+ popup.showForIcon(item, info, options);
+ } else {
+ ItemLongClickListener.beginDrag(item, mLauncher, info, options);
+ }
+ return true;
}
@Override
@@ -475,19 +500,28 @@
return screenId;
}
- /**
- * An interface allowing views to handle their own action.
- */
- public interface AccessibilityActionHandler {
+ public class LauncherAction {
+ public final int keyCode;
+ public final AccessibilityAction accessibilityAction;
+
+ private final LauncherAccessibilityDelegate mDelegate;
+
+ public LauncherAction(int id, int labelRes, int keyCode) {
+ this.keyCode = keyCode;
+ accessibilityAction = new AccessibilityAction(id, mLauncher.getString(labelRes));
+ mDelegate = LauncherAccessibilityDelegate.this;
+ }
/**
- * performs accessibility action and returns true on success
+ * Invokes the action for the provided host
*/
- boolean performAccessibilityAction(int action, ItemInfo itemInfo);
-
- /**
- * adds all the accessibility actions that can be handled.
- */
- void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo);
+ public boolean invokeFromKeyboard(View host) {
+ if (host != null && host.getTag() instanceof ItemInfo) {
+ return mDelegate.performAction(
+ host, (ItemInfo) host.getTag(), accessibilityAction.getId(), true);
+ } else {
+ return false;
+ }
+ }
}
}
diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index d4ba11e..f96afa8 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -17,10 +17,10 @@
package com.android.launcher3.accessibility;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import android.view.KeyEvent;
import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
@@ -31,7 +31,8 @@
import com.android.launcher3.notification.NotificationMainView;
import com.android.launcher3.shortcuts.DeepShortcutView;
-import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
/**
* Extension of {@link LauncherAccessibilityDelegate} with actions specific to shortcuts in
@@ -43,23 +44,23 @@
public ShortcutMenuAccessibilityDelegate(Launcher launcher) {
super(launcher);
- mActions.put(DISMISS_NOTIFICATION, new AccessibilityAction(DISMISS_NOTIFICATION,
- launcher.getText(R.string.action_dismiss_notification)));
+ mActions.put(DISMISS_NOTIFICATION, new LauncherAction(DISMISS_NOTIFICATION,
+ R.string.action_dismiss_notification, KeyEvent.KEYCODE_X));
}
@Override
- public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) {
+ protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
if ((host.getParent() instanceof DeepShortcutView)) {
- info.addAction(mActions.get(ADD_TO_WORKSPACE));
+ out.add(mActions.get(ADD_TO_WORKSPACE));
} else if (host instanceof NotificationMainView) {
if (((NotificationMainView) host).canChildBeDismissed()) {
- info.addAction(mActions.get(DISMISS_NOTIFICATION));
+ out.add(mActions.get(DISMISS_NOTIFICATION));
}
}
}
@Override
- public boolean performAction(View host, ItemInfo item, int action) {
+ protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
if (action == ADD_TO_WORKSPACE) {
if (!(host.getParent() instanceof DeepShortcutView)) {
return false;
@@ -67,21 +68,14 @@
final WorkspaceItemInfo info = ((DeepShortcutView) host.getParent()).getFinalInfo();
final int[] coordinates = new int[2];
final int screenId = findSpaceOnWorkspace(item, coordinates);
- Runnable onComplete = new Runnable() {
- @Override
- public void run() {
- mLauncher.getModelWriter().addItemToDatabase(info,
- LauncherSettings.Favorites.CONTAINER_DESKTOP,
- screenId, coordinates[0], coordinates[1]);
- ArrayList<ItemInfo> itemList = new ArrayList<>();
- itemList.add(info);
- mLauncher.bindItems(itemList, true);
- AbstractFloatingView.closeAllOpenViews(mLauncher);
- announceConfirmation(R.string.item_added_to_workspace);
- }
- };
-
- mLauncher.getStateManager().goToState(NORMAL, true, onComplete);
+ mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
+ mLauncher.getModelWriter().addItemToDatabase(info,
+ LauncherSettings.Favorites.CONTAINER_DESKTOP,
+ screenId, coordinates[0], coordinates[1]);
+ mLauncher.bindItems(Collections.singletonList(info), true);
+ AbstractFloatingView.closeAllOpenViews(mLauncher);
+ announceConfirmation(R.string.item_added_to_workspace);
+ }));
return true;
} else if (action == DISMISS_NOTIFICATION) {
if (!(host instanceof NotificationMainView)) {
diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
index 65a261d..a331924 100644
--- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
+++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
@@ -17,17 +17,12 @@
package com.android.launcher3.accessibility;
import android.content.Context;
-import android.graphics.Rect;
import android.text.TextUtils;
import android.view.View;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-
import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType;
-import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
@@ -38,9 +33,6 @@
*/
public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelegate {
- private final Rect mTempRect = new Rect();
- private final int[] mTempCords = new int[2];
-
public WorkspaceAccessibilityHelper(CellLayout layout) {
super(layout);
}
@@ -134,26 +126,6 @@
}
return "";
}
-
- @Override
- protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
- super.onPopulateNodeForVirtualView(id, node);
-
-
- // ExploreByTouchHelper does not currently handle view scale.
- // Update BoundsInScreen to appropriate value.
- DragLayer dragLayer = Launcher.getLauncher(mView.getContext()).getDragLayer();
- mTempCords[0] = mTempCords[1] = 0;
- float scale = dragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords);
-
- node.getBoundsInParent(mTempRect);
- mTempRect.left = mTempCords[0] + (int) (mTempRect.left * scale);
- mTempRect.right = mTempCords[0] + (int) (mTempRect.right * scale);
- mTempRect.top = mTempCords[1] + (int) (mTempRect.top * scale);
- mTempRect.bottom = mTempCords[1] + (int) (mTempRect.bottom * scale);
- node.setBoundsInScreen(mTempRect);
- }
-
@Override
protected String getLocationDescriptionForIconDrop(int id) {
int x = id % mView.getCountX();
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index c989e7b..f420ec2 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -15,23 +15,30 @@
*/
package com.android.launcher3.allapps;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
-import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Parcelable;
import android.os.Process;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -42,7 +49,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
-import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -55,37 +63,46 @@
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.search.SearchAdapterProvider;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.keyboard.FocusedItemDecorator;
import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.RecyclerViewFastScroller;
+import com.android.launcher3.views.ScrimView;
import com.android.launcher3.views.SpringRelativeLayout;
-
-import java.util.ArrayList;
+import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
/**
* The all apps view container.
*/
public class AllAppsContainerView extends SpringRelativeLayout implements DragSource,
- Insettable, OnDeviceProfileChangeListener {
+ Insettable, OnDeviceProfileChangeListener, OnActivePageChangedListener,
+ ScrimView.ScrimDrawingController {
- private static final float FLING_VELOCITY_MULTIPLIER = 135f;
- // Starts the springs after at least 55% of the animation has passed.
- private static final float FLING_ANIMATION_THRESHOLD = 0.55f;
- private static final int ALPHA_CHANNEL_COUNT = 2;
+ private static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
+
+ public static final float PULL_MULTIPLIER = .02f;
+ public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
+
+ private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
protected final BaseDraggingActivity mLauncher;
protected final AdapterHolder[] mAH;
private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle());
- private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher);
+ private final ItemInfoMatcher mWorkMatcher = mPersonalMatcher.negate();
private final AllAppsStore mAllAppsStore = new AllAppsStore();
+ private final RecyclerView.OnScrollListener mScrollListener =
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ updateHeaderScroll(((AllAppsRecyclerView) recyclerView).getCurrentScrollY());
+ }
+ };
+
private final Paint mNavBarScrimPaint;
private int mNavBarScrimHeight = 0;
@@ -93,7 +110,8 @@
private View mSearchContainer;
private AllAppsPagedView mViewPager;
- private FloatingHeaderView mHeader;
+ protected FloatingHeaderView mHeader;
+ private float mHeaderTop;
private WorkModeSwitch mWorkModeSwitch;
@@ -105,9 +123,15 @@
protected RecyclerViewFastScroller mTouchHandler;
protected final Point mFastScrollerOffset = new Point();
- private final MultiValueAlpha mMultiValueAlpha;
+ private Rect mInsets = new Rect();
- Rect mInsets = new Rect();
+ private SearchAdapterProvider mSearchAdapterProvider;
+ private WorkAdapterProvider mWorkAdapterProvider;
+ private final int mScrimColor;
+ private final int mHeaderProtectionColor;
+ private final float mHeaderThreshold;
+ private ScrimView mScrimView;
+ private int mHeaderColor;
public AllAppsContainerView(Context context) {
this(context, null);
@@ -121,12 +145,24 @@
super(context, attrs, defStyleAttr);
mLauncher = BaseDraggingActivity.fromContext(context);
+
+ mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
+ mHeaderThreshold = getResources().getDimensionPixelSize(
+ R.dimen.dynamic_grid_cell_border_spacing);
+ mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor);
+
mLauncher.addOnDeviceProfileChangeListener(this);
+ mSearchAdapterProvider = mLauncher.createSearchAdapterProvider(this);
mSearchQueryBuilder = new SpannableStringBuilder();
Selection.setSelection(mSearchQueryBuilder, 0);
mAH = new AdapterHolder[2];
+ mWorkAdapterProvider = new WorkAdapterProvider(mLauncher, () -> {
+ if (mAH[AdapterHolder.WORK] != null) {
+ mAH[AdapterHolder.WORK].appsList.updateAdapterItems();
+ }
+ });
mAH[AdapterHolder.MAIN] = new AdapterHolder(false /* isWork */);
mAH[AdapterHolder.WORK] = new AdapterHolder(true /* isWork */);
@@ -134,12 +170,39 @@
mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor));
mAllAppsStore.addUpdateListener(this::onAppsUpdated);
+ }
- addSpringView(R.id.all_apps_header);
- addSpringView(R.id.apps_list_view);
- addSpringView(R.id.all_apps_tabs_view_pager);
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray) {
+ try {
+ // Many slice view id is not properly assigned, and hence throws null
+ // pointer exception in the underneath method. Catching the exception
+ // simply doesn't restore these slice views. This doesn't have any
+ // user visible effect because because we query them again.
+ super.dispatchRestoreInstanceState(sparseArray);
+ } catch (Exception e) {
+ Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e);
+ }
- mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
+ Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null);
+ if (state != null) {
+ int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0);
+ if (currentPage != 0 && mViewPager != null) {
+ mViewPager.setCurrentPage(currentPage);
+ rebindAdapters(true);
+ } else {
+ reset(true);
+ }
+ }
+
+ }
+
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchSaveInstanceState(container);
+ Bundle state = new Bundle();
+ state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage());
+ container.put(R.id.work_tab_state_id, state);
}
/**
@@ -155,25 +218,14 @@
return mAllAppsStore;
}
- public AlphaProperty getAlphaProperty(int index) {
- return mMultiValueAlpha.getProperty(index);
- }
-
public WorkModeSwitch getWorkModeSwitch() {
return mWorkModeSwitch;
}
-
- @Override
- protected void setDampedScrollShift(float shift) {
- // Bound the shift amount to avoid content from drawing on top (Y-val) of the QSB.
- float maxShift = getSearchView().getHeight() / 2f;
- super.setDampedScrollShift(Utilities.boundToRange(shift, -maxShift, maxShift));
- }
-
@Override
public void onDeviceProfileChanged(DeviceProfile dp) {
for (AdapterHolder holder : mAH) {
+ holder.adapter.setAppsPerRow(dp.numShownAllAppsColumns);
if (holder.recyclerView != null) {
// Remove all views and clear the pool, while keeping the data same. After this
// call, all the viewHolders will be recreated.
@@ -191,16 +243,20 @@
break;
}
}
- rebindAdapters(hasWorkApps);
- if (hasWorkApps) {
- resetWorkProfile();
+ if (!mAH[AdapterHolder.MAIN].appsList.hasFilter()) {
+ rebindAdapters(hasWorkApps);
+ if (hasWorkApps) {
+ resetWorkProfile();
+ }
}
}
private void resetWorkProfile() {
- mWorkModeSwitch.update(!mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED));
- mAH[AdapterHolder.WORK].setupOverlay();
- mAH[AdapterHolder.WORK].applyPadding();
+ boolean isEnabled = !mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED);
+ if (mWorkModeSwitch != null) {
+ mWorkModeSwitch.updateCurrentState(isEnabled);
+ }
+ mWorkAdapterProvider.updateCurrentState(isEnabled);
}
/**
@@ -298,6 +354,7 @@
}
// Reset the search bar and base recycler view after transitioning home
mSearchUiManager.resetSearch();
+ updateHeaderScroll(0);
}
@Override
@@ -317,7 +374,7 @@
mSearchContainer = findViewById(R.id.search_container_all_apps);
mSearchUiManager = (SearchUiManager) mSearchContainer;
- mSearchUiManager.initialize(this);
+ mSearchUiManager.initializeSearch(this);
}
public SearchUiManager getSearchUiManager() {
@@ -335,13 +392,6 @@
}
@Override
- public void fillInLogContainerData(ItemInfo childInfo, Target child,
- ArrayList<Target> parents) {
- parents.add(newContainerTarget(
- getApps().hasFilter() ? ContainerType.SEARCHRESULT : ContainerType.ALLAPPS));
- }
-
- @Override
public void setInsets(Rect insets) {
mInsets.set(insets);
DeviceProfile grid = mLauncher.getDeviceProfile();
@@ -352,7 +402,6 @@
mAH[i].padding.bottom = insets.bottom;
mAH[i].padding.left = mAH[i].padding.right = leftRightPadding;
mAH[i].applyPadding();
- mAH[i].setupOverlay();
}
ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
@@ -372,7 +421,8 @@
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
if (Utilities.ATLEAST_Q) {
- mNavBarScrimHeight = insets.getTappableElementInsets().bottom;
+ mNavBarScrimHeight = insets.getTappableElementInsets().bottom
+ - mLauncher.getDeviceProfile().nonOverlappingTaskbarInset;
} else {
mNavBarScrimHeight = insets.getStableInsetBottom();
}
@@ -389,12 +439,6 @@
}
}
- @Override
- public int getCanvasClipTopForOverscroll() {
- // Do not clip if the QSB is attached to the spring, otherwise the QSB will get clipped.
- return mSpringViews.get(getSearchView().getId()) ? 0 : mHeader.getTop();
- }
-
private void rebindAdapters(boolean showTabs) {
rebindAdapters(showTabs, false /* force */);
}
@@ -413,19 +457,26 @@
setupWorkToggle();
mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher);
mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
+ mAH[AdapterHolder.WORK].recyclerView.setId(R.id.apps_list_view_work);
mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
findViewById(R.id.tab_personal)
- .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.MAIN));
+ .setOnClickListener((View view) -> {
+ if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
+ mLauncher.getStatsLogManager().logger()
+ .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
+ }
+ });
findViewById(R.id.tab_work)
- .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK));
- onTabChanged(mViewPager.getNextPage());
+ .setOnClickListener((View view) -> {
+ if (mViewPager.snapToPage(AdapterHolder.WORK)) {
+ mLauncher.getStatsLogManager().logger()
+ .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
+ }
+ });
+ onActivePageChanged(mViewPager.getNextPage());
} else {
mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
mAH[AdapterHolder.WORK].recyclerView = null;
- if (mWorkModeSwitch != null) {
- ((ViewGroup) mWorkModeSwitch.getParent()).removeView(mWorkModeSwitch);
- mWorkModeSwitch = null;
- }
}
setupHeader();
@@ -436,26 +487,22 @@
private void setupWorkToggle() {
if (Utilities.ATLEAST_P) {
mWorkModeSwitch = (WorkModeSwitch) mLauncher.getLayoutInflater().inflate(
- R.layout.work_mode_switch, this, false);
+ R.layout.work_mode_fab, this, false);
this.addView(mWorkModeSwitch);
mWorkModeSwitch.setInsets(mInsets);
- mWorkModeSwitch.post(() -> mAH[AdapterHolder.WORK].applyPadding());
+ mWorkModeSwitch.post(() -> {
+ mAH[AdapterHolder.WORK].applyPadding();
+ resetWorkProfile();
+ });
}
}
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- View overlay = mAH[AdapterHolder.WORK].getOverlayView();
- int v = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? GONE : VISIBLE;
- overlay.findViewById(R.id.work_apps_paused_title).setVisibility(v);
- overlay.findViewById(R.id.work_apps_paused_content).setVisibility(v);
- }
-
private void replaceRVContainer(boolean showTabs) {
for (int i = 0; i < mAH.length; i++) {
- if (mAH[i].recyclerView != null) {
- mAH[i].recyclerView.setLayoutManager(null);
+ AllAppsRecyclerView rv = mAH[i].recyclerView;
+ if (rv != null) {
+ rv.setLayoutManager(null);
+ rv.setAdapter(null);
}
}
View oldView = getRecyclerViewContainer();
@@ -464,10 +511,14 @@
int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
View newView = getLayoutInflater().inflate(layout, this, false);
addView(newView, index);
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED, "should show tabs:" + showTabs,
+ new Exception());
+ }
if (showTabs) {
mViewPager = (AllAppsPagedView) newView;
mViewPager.initParentViews(this);
- mViewPager.getPageIndicator().setContainerView(this);
+ mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
} else {
mViewPager = null;
}
@@ -477,16 +528,25 @@
return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
}
- public void onTabChanged(int pos) {
- mHeader.setMainActive(pos == 0);
- if (mAH[pos].recyclerView != null) {
- mAH[pos].recyclerView.bindFastScrollbar();
+ @Override
+ public void onActivePageChanged(int currentActivePage) {
+ mHeader.setMainActive(currentActivePage == AdapterHolder.MAIN);
+ if (mAH[currentActivePage].recyclerView != null) {
+ mAH[currentActivePage].recyclerView.bindFastScrollbar();
}
reset(true /* animate */);
if (mWorkModeSwitch != null) {
- mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK
+ mWorkModeSwitch.setWorkTabVisible(currentActivePage == AdapterHolder.WORK
&& mAllAppsStore.hasModelFlag(
- FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
+ FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
+
+ if (currentActivePage == AdapterHolder.WORK) {
+ if (mWorkModeSwitch.getParent() == null) {
+ addView(mWorkModeSwitch);
+ }
+ } else {
+ removeView(mWorkModeSwitch);
+ }
}
}
@@ -500,7 +560,7 @@
return view.getGlobalVisibleRect(new Rect());
}
- // Used by tests only
+ @VisibleForTesting
public boolean isPersonalTabVisible() {
return isDescendantViewVisible(R.id.tab_personal);
}
@@ -526,6 +586,22 @@
return mViewPager == null ? getActiveRecyclerView() : mViewPager;
}
+ public int getCurrentPage() {
+ return mViewPager != null ? mViewPager.getCurrentPage() : AdapterHolder.MAIN;
+ }
+
+ /**
+ * Handles selection on focused view and returns success
+ */
+ public boolean launchHighlightedItem() {
+ if (mSearchAdapterProvider == null) return false;
+ return mSearchAdapterProvider.launchHighlightedItem();
+ }
+
+ public SearchAdapterProvider getSearchAdapterProvider() {
+ return mSearchAdapterProvider;
+ }
+
public RecyclerViewFastScroller getScrollBar() {
AllAppsRecyclerView rv = getActiveRecyclerView();
return rv == null ? null : rv.getScrollbar();
@@ -540,6 +616,7 @@
mAH[i].padding.top = padding;
mAH[i].applyPadding();
}
+ mHeaderTop = mHeader.getTop();
}
public void setLastSearchQuery(String query) {
@@ -550,6 +627,7 @@
mSearchModeWhileUsingTabs = true;
rebindAdapters(false); // hide tabs
}
+ mHeader.setCollapsed(true);
}
public void onClearSearchResult() {
@@ -586,31 +664,61 @@
/**
* Adds an update listener to {@param animator} that adds springs to the animation.
*/
- public void addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity) {
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- boolean shouldSpring = true;
-
+ public void addSpringFromFlingUpdateListener(ValueAnimator animator,
+ float velocity /* release velocity */,
+ float progress /* portion of the distance to travel*/) {
+ animator.addListener(new AnimatorListenerAdapter() {
@Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- if (shouldSpring
- && valueAnimator.getAnimatedFraction() >= FLING_ANIMATION_THRESHOLD) {
- int searchViewId = getSearchView().getId();
- addSpringView(searchViewId);
- finishWithShiftAndVelocity(1, velocity * FLING_VELOCITY_MULTIPLIER,
- (anim, canceled, value, velocity) -> removeSpringView(searchViewId));
-
- shouldSpring = false;
- }
+ public void onAnimationStart(Animator animator) {
+ float distance = (float) ((1 - progress) * getHeight()); // px
+ float settleVelocity = Math.min(0, distance
+ / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration())
+ + velocity);
+ absorbSwipeUpVelocity(Math.max(1000, Math.abs(
+ Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER))));
}
});
}
+ public void onPull(float deltaDistance, float displacement) {
+ absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement);
+ // Current motion spec is to actually push and not pull
+ // on this surface. However, until EdgeEffect.onPush (b/190612804) is
+ // implemented at view level, we will simply pull
+ }
+
@Override
public void getDrawingRect(Rect outRect) {
super.getDrawingRect(outRect);
outRect.offset(0, (int) getTranslationY());
}
+ @Override
+ public void setTranslationY(float translationY) {
+ super.setTranslationY(translationY);
+ invalidateHeader();
+ }
+
+ public void setScrimView(ScrimView scrimView) {
+ mScrimView = scrimView;
+ }
+
+ @Override
+ public void drawOnScrim(Canvas canvas) {
+ mHeaderPaint.setColor(mHeaderColor);
+ mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor)));
+ if (mHeaderPaint.getColor() != mScrimColor && mHeaderPaint.getColor() != 0) {
+ int bottom = mUsingTabs && mHeader.mHeaderCollapsed ? mHeader.getVisibleBottomBound()
+ : mSearchContainer.getBottom();
+ canvas.drawRect(0, 0, canvas.getWidth(), bottom + getTranslationY(),
+ mHeaderPaint);
+
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && getTranslationY() == 0) {
+ mSearchUiManager.getEditText().setBackground(null);
+ }
+ }
+ }
+
public class AdapterHolder {
public static final int MAIN = 0;
public static final int WORK = 1;
@@ -629,8 +737,15 @@
AdapterHolder(boolean isWork) {
mIsWork = isWork;
- appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore, isWork);
- adapter = new AllAppsGridAdapter(mLauncher, getLayoutInflater(), appsList);
+ appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore,
+ isWork ? mWorkAdapterProvider : null);
+
+ BaseAdapterProvider[] adapterProviders =
+ isWork ? new BaseAdapterProvider[]{mSearchAdapterProvider, mWorkAdapterProvider}
+ : new BaseAdapterProvider[]{mSearchAdapterProvider};
+
+ adapter = new AllAppsGridAdapter(mLauncher, getLayoutInflater(), appsList,
+ adapterProviders);
appsList.setAdapter(adapter);
layoutManager = adapter.getLayoutManager();
}
@@ -646,38 +761,15 @@
recyclerView.setHasFixedSize(true);
// No animations will occur when changes occur to the items in this RecyclerView.
recyclerView.setItemAnimator(null);
+ recyclerView.addOnScrollListener(mScrollListener);
FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(recyclerView);
recyclerView.addItemDecoration(focusedItemDecorator);
adapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
applyVerticalFadingEdgeEnabled(verticalFadingEdge);
applyPadding();
- setupOverlay();
- }
-
- void setupOverlay() {
- if (!mIsWork || recyclerView == null) return;
- boolean workDisabled = mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED);
- if (mWorkDisabled == workDisabled) return;
- recyclerView.setContentDescription(workDisabled ? mLauncher.getString(
- R.string.work_apps_paused_content_description) : null);
- View overlayView = getOverlayView();
- recyclerView.setItemAnimator(new DefaultItemAnimator());
- if (workDisabled) {
- overlayView.setAlpha(0);
- recyclerView.addAutoSizedOverlay(overlayView);
- overlayView.animate().alpha(1).withEndAction(
- () -> {
- appsList.updateItemFilter((info, cn) -> false);
- recyclerView.setItemAnimator(null);
- }).start();
- } else if (mInfoMatcher != null) {
- appsList.updateItemFilter(mInfoMatcher);
- overlayView.animate().alpha(0).withEndAction(() -> {
- recyclerView.setItemAnimator(null);
- recyclerView.clearAutoSizedOverlays();
- }).start();
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ recyclerView.addItemDecoration(mSearchAdapterProvider.getDecorator());
}
- mWorkDisabled = workDisabled;
}
void applyPadding() {
@@ -698,12 +790,31 @@
mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs
&& verticalFadingEdge);
}
+ }
- private View getOverlayView() {
- if (mOverlay == null) {
- mOverlay = mLauncher.getLayoutInflater().inflate(R.layout.work_apps_paused, null);
+
+ protected void updateHeaderScroll(int scrolledOffset) {
+ float prog = Math.max(0, Math.min(1, (float) scrolledOffset / mHeaderThreshold));
+ int viewBG = ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, prog);
+ int headerColor = ColorUtils.setAlphaComponent(viewBG,
+ (int) (getSearchView().getAlpha() * 255));
+ if (headerColor != mHeaderColor) {
+ mHeaderColor = headerColor;
+ getSearchView().setBackgroundColor(viewBG);
+ getFloatingHeaderView().setHeaderColor(viewBG);
+ invalidateHeader();
+ if (scrolledOffset == 0 && mSearchUiManager.getEditText() != null) {
+ mSearchUiManager.getEditText().show();
}
- return mOverlay;
+ }
+ }
+
+ /**
+ * redraws header protection
+ */
+ public void invalidateHeader() {
+ if (mScrimView != null && FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ mScrimView.invalidate();
}
}
}
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 8ec4d27..874fe80 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -30,6 +30,7 @@
import android.view.accessibility.AccessibilityEvent;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -40,17 +41,17 @@
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
-import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
-import com.android.launcher3.model.AppLaunchTracker;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.PackageManagerHelper;
+import java.util.Arrays;
import java.util.List;
/**
* The grid view adapter of all the apps.
*/
-public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
+public class AllAppsGridAdapter extends
+ RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
public static final String TAG = "AppsGridAdapter";
@@ -71,6 +72,9 @@
public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
+
+ private final BaseAdapterProvider[] mAdapterProviders;
+
/**
* ViewHolder for each icon.
*/
@@ -82,6 +86,80 @@
}
/**
+ * Info about a particular adapter item (can be either section or app)
+ */
+ public static class AdapterItem {
+ /** Common properties */
+ // The index of this adapter item in the list
+ public int position;
+ // The type of this item
+ public int viewType;
+
+ /** App-only properties */
+ // The section name of this app. Note that there can be multiple items with different
+ // sectionNames in the same section
+ public String sectionName = null;
+ // The row that this item shows up on
+ public int rowIndex;
+ // The index of this app in the row
+ public int rowAppIndex;
+ // The associated AppInfo for the app
+ public AppInfo appInfo = null;
+ // The index of this app not including sections
+ public int appIndex = -1;
+ // Search section associated to result
+ public DecorationInfo decorationInfo = null;
+
+ /**
+ * Factory method for AppIcon AdapterItem
+ */
+ public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
+ int appIndex) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = VIEW_TYPE_ICON;
+ item.position = pos;
+ item.sectionName = sectionName;
+ item.appInfo = appInfo;
+ item.appIndex = appIndex;
+ return item;
+ }
+
+ /**
+ * Factory method for empty search results view
+ */
+ public static AdapterItem asEmptySearch(int pos) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = VIEW_TYPE_EMPTY_SEARCH;
+ item.position = pos;
+ return item;
+ }
+
+ /**
+ * Factory method for a dividerView in AllAppsSearch
+ */
+ public static AdapterItem asAllAppsDivider(int pos) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = VIEW_TYPE_ALL_APPS_DIVIDER;
+ item.position = pos;
+ return item;
+ }
+
+ /**
+ * Factory method for a market search button
+ */
+ public static AdapterItem asMarketSearch(int pos) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = VIEW_TYPE_SEARCH_MARKET;
+ item.position = pos;
+ return item;
+ }
+
+ protected boolean isCountedForAccessibility() {
+ return viewType == VIEW_TYPE_ICON || viewType == VIEW_TYPE_SEARCH_MARKET;
+ }
+ }
+
+ /**
* A subclass of GridLayoutManager that overrides accessibility values during app search.
*/
public class AppsGridLayoutManager extends GridLayoutManager {
@@ -161,11 +239,18 @@
@Override
public int getSpanSize(int position) {
- if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) {
- return 1;
+ int viewType = mApps.getAdapterItems().get(position).viewType;
+ int totalSpans = mGridLayoutMgr.getSpanCount();
+ if (isIconViewType(viewType)) {
+ return totalSpans / mAppsPerRow;
} else {
+ BaseAdapterProvider adapterProvider = getAdapterProvider(viewType);
+ if (adapterProvider != null) {
+ return totalSpans / adapterProvider.getItemsPerRow(viewType, mAppsPerRow);
+ }
+
// Section breaks span the full width
- return mAppsPerRow;
+ return totalSpans;
}
}
}
@@ -189,7 +274,7 @@
private Intent mMarketSearchIntent;
public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater,
- AlphabeticalAppsList apps) {
+ AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders) {
Resources res = launcher.getResources();
mLauncher = launcher;
mApps = apps;
@@ -201,12 +286,21 @@
mOnIconClickListener = launcher.getItemOnClickListener();
- setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns);
+ mAdapterProviders = adapterProviders;
+ setAppsPerRow(mLauncher.getDeviceProfile().numShownAllAppsColumns);
}
public void setAppsPerRow(int appsPerRow) {
mAppsPerRow = appsPerRow;
- mGridLayoutMgr.setSpanCount(mAppsPerRow);
+ int totalSpans = mAppsPerRow;
+ for (BaseAdapterProvider adapterProvider : mAdapterProviders) {
+ for (int itemPerRow : adapterProvider.getSupportedItemsPerRowArray()) {
+ if (totalSpans % itemPerRow != 0) {
+ totalSpans *= itemPerRow;
+ }
+ }
+ }
+ mGridLayoutMgr.setSpanCount(totalSpans);
}
/**
@@ -255,11 +349,10 @@
case VIEW_TYPE_ICON:
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.all_apps_icon, parent, false);
- icon.setOnClickListener(mOnIconClickListener);
- icon.setOnLongClickListener(mOnIconLongClickListener);
icon.setLongPressTimeoutFactor(1f);
icon.setOnFocusChangeListener(mIconFocusListener);
-
+ icon.setOnClickListener(mOnIconClickListener);
+ icon.setOnLongClickListener(mOnIconLongClickListener);
// Ensure the all apps icon height matches the workspace icons in portrait mode.
icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
return new ViewHolder(icon);
@@ -270,12 +363,16 @@
View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
parent, false);
searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely(
- v, mMarketSearchIntent, null, AppLaunchTracker.CONTAINER_SEARCH));
+ v, mMarketSearchIntent, null));
return new ViewHolder(searchMarketView);
case VIEW_TYPE_ALL_APPS_DIVIDER:
return new ViewHolder(mLayoutInflater.inflate(
R.layout.all_apps_divider, parent, false));
default:
+ BaseAdapterProvider adapterProvider = getAdapterProvider(viewType);
+ if (adapterProvider != null) {
+ return adapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType);
+ }
throw new RuntimeException("Unexpected view type");
}
}
@@ -284,7 +381,8 @@
public void onBindViewHolder(ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case VIEW_TYPE_ICON:
- AppInfo info = mApps.getAdapterItems().get(position).appInfo;
+ AdapterItem adapterItem = mApps.getAdapterItems().get(position);
+ AppInfo info = adapterItem.appInfo;
BubbleTextView icon = (BubbleTextView) holder.itemView;
icon.reset();
icon.applyFromApplicationInfo(info);
@@ -306,10 +404,20 @@
case VIEW_TYPE_ALL_APPS_DIVIDER:
// nothing to do
break;
+ default:
+ BaseAdapterProvider adapterProvider = getAdapterProvider(holder.getItemViewType());
+ if (adapterProvider != null) {
+ adapterProvider.onBindView(holder, position);
+ }
}
}
@Override
+ public void onViewRecycled(@NonNull ViewHolder holder) {
+ super.onViewRecycled(holder);
+ }
+
+ @Override
public boolean onFailedToRecycleView(ViewHolder holder) {
// Always recycle and we will reset the view when it is bound
return true;
@@ -322,8 +430,14 @@
@Override
public int getItemViewType(int position) {
- AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+ AdapterItem item = mApps.getAdapterItems().get(position);
return item.viewType;
}
+ @Nullable
+ private BaseAdapterProvider getAdapterProvider(int viewType) {
+ return Arrays.stream(mAdapterProviders).filter(
+ adapterProvider -> adapterProvider.isViewSupported(viewType)).findFirst().orElse(
+ null);
+ }
}
diff --git a/src/com/android/launcher3/allapps/AllAppsPagedView.java b/src/com/android/launcher3/allapps/AllAppsPagedView.java
index f640c3e..3cc9ce6 100644
--- a/src/com/android/launcher3/allapps/AllAppsPagedView.java
+++ b/src/com/android/launcher3/allapps/AllAppsPagedView.java
@@ -15,19 +15,23 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB;
+
import android.content.Context;
import android.util.AttributeSet;
-import android.view.MotionEvent;
+import com.android.launcher3.Launcher;
import com.android.launcher3.PagedView;
+import com.android.launcher3.workprofile.PersonalWorkPagedView;
-public class AllAppsPagedView extends PagedView<PersonalWorkSlidingTabStrip> {
+/**
+ * A {@link PagedView} for showing different views for the personal and work profile respectively
+ * in the {@link AllAppsContainerView}.
+ */
+public class AllAppsPagedView extends PersonalWorkPagedView {
- final static float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
- final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
- final static float TOUCH_SLOP_DAMPING_FACTOR = 4;
-
- public AllAppsPagedView(Context context) {
+ public AllAppsPagedView(Context context) {
this(context, null);
}
@@ -40,50 +44,14 @@
}
@Override
- protected String getCurrentPageDescription() {
- // Not necessary, tab-bar already has two tabs with their own descriptions.
- return "";
- }
-
- @Override
- protected void onScrollChanged(int l, int t, int oldl, int oldt) {
- super.onScrollChanged(l, t, oldl, oldt);
- mPageIndicator.setScroll(l, mMaxScroll);
- }
-
- @Override
- protected void determineScrollingStart(MotionEvent ev) {
- float absDeltaX = Math.abs(ev.getX() - getDownMotionX());
- float absDeltaY = Math.abs(ev.getY() - getDownMotionY());
-
- if (Float.compare(absDeltaX, 0f) == 0) return;
-
- float slope = absDeltaY / absDeltaX;
- float theta = (float) Math.atan(slope);
-
- if (absDeltaX > mTouchSlop || absDeltaY > mTouchSlop) {
- cancelCurrentPageLongPress();
+ protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
+ boolean resp = super.snapToPageWithVelocity(whichPage, velocity);
+ if (resp && whichPage != mCurrentPage) {
+ Launcher.getLauncher(getContext()).getStatsLogManager().logger()
+ .log(mCurrentPage < whichPage
+ ? LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB
+ : LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB);
}
-
- if (theta > MAX_SWIPE_ANGLE) {
- return;
- } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
- theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
- float extraRatio = (float)
- Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
- super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
- } else {
- super.determineScrollingStart(ev);
- }
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- @Override
- protected boolean canScroll(float absVScroll, float absHScroll) {
- return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
+ return resp;
}
}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index cbf02b7..bddbbd0 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -19,13 +19,16 @@
import static android.view.View.MeasureSpec.UNSPECIFIED;
import static android.view.View.MeasureSpec.makeMeasureSpec;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END;
+import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
+import android.util.Log;
import android.util.SparseIntArray;
import android.view.MotionEvent;
import android.view.View;
@@ -37,11 +40,9 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsGridAdapter.AppsGridLayoutManager;
-import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.RecyclerViewFastScroller;
import java.util.ArrayList;
@@ -50,7 +51,9 @@
/**
* A RecyclerView with custom fast scroll support for the all apps view.
*/
-public class AllAppsRecyclerView extends BaseRecyclerView implements LogContainerProvider {
+public class AllAppsRecyclerView extends BaseRecyclerView {
+ private static final String TAG = "AllAppsContainerView";
+ private static final boolean DEBUG = false;
private AlphabeticalAppsList mApps;
private final int mNumAppsPerRow;
@@ -60,6 +63,13 @@
private final SparseIntArray mCachedScrollPositions = new SparseIntArray();
private final AllAppsFastScrollHelper mFastScrollHelper;
+
+ private final AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
+ public void onChanged() {
+ mCachedScrollPositions.clear();
+ }
+ };
+
// The empty-search result background
private AllAppsBackgroundDrawable mEmptySearchBackground;
private int mEmptySearchBackgroundTopOffset;
@@ -106,29 +116,13 @@
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1);
- pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow);
+ pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows
+ * (mNumAppsPerRow + 1));
mViewHeights.clear();
mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx);
}
- /**
- * Scrolls this recycler view to the top.
- */
- public void scrollToTop() {
- // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
- if (mScrollbar != null) {
- mScrollbar.reattachThumbToScroll();
- }
- if (getLayoutManager() instanceof AppsGridLayoutManager) {
- AppsGridLayoutManager layoutManager = (AppsGridLayoutManager) getLayoutManager();
- if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
- // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
- return;
- }
- }
- scrollToPosition(0);
- }
@Override
public void onDraw(Canvas c) {
@@ -136,7 +130,9 @@
if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
mEmptySearchBackground.draw(c);
}
-
+ if (DEBUG) {
+ Log.d(TAG, "onDraw at = " + System.currentTimeMillis());
+ }
super.onDraw(c);
}
@@ -175,18 +171,11 @@
mAutoSizedOverlays.clear();
}
- @Override
- public void fillInLogContainerData(ItemInfo childInfo, Target child,
- ArrayList<Target> parents) {
- parents.add(newContainerTarget(
- getApps().hasFilter() ? ContainerType.SEARCHRESULT : ContainerType.ALLAPPS));
- }
-
public void onSearchResultsChanged() {
// Always scroll the view to the top so the user can see the changed results
scrollToTop();
- if (mApps.hasNoFilteredResults()) {
+ if (mApps.hasNoFilteredResults() && !FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
if (mEmptySearchBackground == null) {
mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext());
mEmptySearchBackground.setAlpha(0);
@@ -202,12 +191,32 @@
}
@Override
+ public void onScrollStateChanged(int state) {
+ super.onScrollStateChanged(state);
+
+ StatsLogManager mgr = BaseDraggingActivity.fromContext(getContext()).getStatsLogManager();
+ switch (state) {
+ case SCROLL_STATE_DRAGGING:
+ requestFocus();
+ mgr.logger().sendToInteractionJankMonitor(
+ LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this);
+ break;
+ case SCROLL_STATE_IDLE:
+ mgr.logger().sendToInteractionJankMonitor(
+ LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END, this);
+ break;
+ }
+ }
+
+ @Override
public boolean onInterceptTouchEvent(MotionEvent e) {
boolean result = super.onInterceptTouchEvent(e);
if (!result && e.getAction() == MotionEvent.ACTION_DOWN
&& mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
mEmptySearchBackground.setHotspot(e.getX(), e.getY());
}
+ hideKeyboardAsync(ActivityContext.lookupContext(getContext()),
+ getApplicationWindowToken());
return result;
}
@@ -245,12 +254,13 @@
@Override
public void setAdapter(Adapter adapter) {
+ if (getAdapter() != null) {
+ getAdapter().unregisterAdapterDataObserver(mObserver);
+ }
super.setAdapter(adapter);
- adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
- public void onChanged() {
- mCachedScrollPositions.clear();
- }
- });
+ if (adapter != null) {
+ adapter.registerAdapterDataObserver(mObserver);
+ }
}
@Override
@@ -277,7 +287,7 @@
if (mApps == null) {
return;
}
- List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems();
// Skip early if there are no items or we haven't been measured
if (items.isEmpty() || mNumAppsPerRow == 0) {
@@ -351,7 +361,7 @@
@Override
public int getCurrentScrollY() {
// Return early if there are no items or we haven't been measured
- List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+ List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems();
if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) {
return -1;
}
@@ -367,14 +377,14 @@
}
public int getCurrentScrollY(int position, int offset) {
- List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
- AlphabeticalAppsList.AdapterItem posItem = position < items.size() ?
- items.get(position) : null;
+ List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems();
+ AllAppsGridAdapter.AdapterItem posItem = position < items.size()
+ ? items.get(position) : null;
int y = mCachedScrollPositions.get(position, -1);
if (y < 0) {
y = 0;
for (int i = 0; i < position; i++) {
- AlphabeticalAppsList.AdapterItem item = items.get(i);
+ AllAppsGridAdapter.AdapterItem item = items.get(i);
if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
// Break once we reach the desired row
if (posItem != null && posItem.viewType == item.viewType &&
@@ -412,7 +422,7 @@
/**
* Returns the available scroll height:
- * AvailableScrollHeight = Total height of the all items - last page height
+ * AvailableScrollHeight = Total height of the all items - last page height
*/
@Override
protected int getAvailableScrollHeight() {
@@ -448,4 +458,14 @@
public boolean hasOverlappingRendering() {
return false;
}
+
+ /**
+ * Returns distance between left and right app icons
+ */
+ public int getTabWidth() {
+ DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile();
+ int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ int iconPadding = totalWidth / grid.numShownAllAppsColumns - grid.allAppsIconSizePx;
+ return totalWidth - iconPadding - grid.allAppsIconDrawablePaddingPx;
+ }
}
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 3ae0a18..7bc3eec 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -17,14 +17,16 @@
import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageUserKey;
@@ -79,6 +81,11 @@
return (mModelFlags & mask) != 0;
}
+ /**
+ * Returns {@link AppInfo} if any apps matches with provided {@link ComponentKey}, otherwise
+ * null.
+ */
+ @Nullable
public AppInfo getApp(ComponentKey key) {
mTempInfo.componentName = key.componentName;
mTempInfo.user = key.user;
@@ -125,7 +132,7 @@
}
public void registerIconContainer(ViewGroup container) {
- if (container != null) {
+ if (container != null && !mIconContainers.contains(container)) {
mIconContainers.add(container);
}
}
@@ -145,10 +152,23 @@
});
}
- public void updatePromiseAppProgress(PromiseAppInfo app) {
+ /**
+ * Sets the AppInfo's associated icon's progress bar.
+ *
+ * If this app is installed and supports incremental downloads, the progress bar will be updated
+ * the app's total download progress. Otherwise, the progress bar will be updated to the app's
+ * installation progress.
+ *
+ * If this app is fully downloaded, the app icon will be reapplied.
+ */
+ public void updateProgressBar(AppInfo app) {
updateAllIcons((child) -> {
if (child.getTag() == app) {
- child.applyProgressLevel(app.level);
+ if ((app.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) == 0) {
+ child.applyFromApplicationInfo(app);
+ } else {
+ child.applyProgressLevel();
+ }
}
});
}
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index a9b030e..a0551f0 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,44 +1,49 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.launcher3.allapps;
+import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
-import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
-import static com.android.launcher3.LauncherState.APPS_VIEW_ITEM_MASK;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
-import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_HEADER_FADE;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_ALLAPPS;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
-import android.content.Context;
import android.util.FloatProperty;
import android.view.View;
-import android.view.ViewGroup;
import android.view.animation.Interpolator;
-import android.widget.EditText;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.views.ScrimView;
-import com.android.systemui.plugins.AllAppsSearchPlugin;
-import com.android.systemui.plugins.PluginListener;
/**
* Handles AllApps view transition.
@@ -50,27 +55,27 @@
* If release velocity < THRES1, snap according to either top or bottom depending on whether it's
* closer to top or closer to the page indicator.
*/
-public class AllAppsTransitionController implements StateHandler<LauncherState>,
- OnDeviceProfileChangeListener, PluginListener<AllAppsSearchPlugin> {
+public class AllAppsTransitionController
+ implements StateHandler<LauncherState>, OnDeviceProfileChangeListener {
+ // This constant should match the second derivative of the animator interpolator.
+ public static final float INTERP_COEFF = 1.7f;
+ private static final float CONTENT_VISIBLE_MAX_THRESHOLD = 0.5f;
public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
- @Override
- public Float get(AllAppsTransitionController controller) {
- return controller.mProgress;
- }
+ @Override
+ public Float get(AllAppsTransitionController controller) {
+ return controller.mProgress;
+ }
- @Override
- public void setValue(AllAppsTransitionController controller, float progress) {
- controller.setProgress(progress);
- }
- };
-
- private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 0;
+ @Override
+ public void setValue(AllAppsTransitionController controller, float progress) {
+ controller.setProgress(progress);
+ }
+ };
private AllAppsContainerView mAppsView;
- private ScrimView mScrimView;
private final Launcher mLauncher;
private boolean mIsVerticalLayout;
@@ -85,10 +90,7 @@
private float mProgress; // [0, 1], mShiftRange * mProgress = shiftCurrent
private float mScrollRangeDelta = 0;
-
- // plugin related variables
- private AllAppsSearchPlugin mPlugin;
- private View mPluginContent;
+ private ScrimView mScrimView;
public AllAppsTransitionController(Launcher l) {
mLauncher = l;
@@ -109,7 +111,6 @@
setScrollRangeDelta(mScrollRangeDelta);
if (mIsVerticalLayout) {
- mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1);
mLauncher.getHotseat().setTranslationY(0);
mLauncher.getWorkspace().getPageIndicator().setTranslationY(0);
}
@@ -120,19 +121,12 @@
* in xml-based animations which also handle updating the appropriate UI.
*
* @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace
- *
* @see #setState(LauncherState)
* @see #setStateWithAnimation(LauncherState, StateAnimationConfig, PendingAnimation)
*/
public void setProgress(float progress) {
mProgress = progress;
- mScrimView.setProgress(progress);
- float shiftCurrent = progress * mShiftRange;
-
- mAppsView.setTranslationY(shiftCurrent);
- if (mPlugin != null) {
- mPlugin.setProgress(progress);
- }
+ mAppsView.setTranslationY(mProgress * mShiftRange);
}
public float getProgress() {
@@ -159,22 +153,14 @@
StateAnimationConfig config, PendingAnimation builder) {
float targetProgress = toState.getVerticalProgress(mLauncher);
if (Float.compare(mProgress, targetProgress) == 0) {
- if (!config.onlyPlayAtomicComponent()) {
- setAlphas(toState, config, builder);
- }
+ setAlphas(toState, config, builder);
// Fail fast
onProgressAnimationEnd();
return;
}
- if (config.onlyPlayAtomicComponent()) {
- // There is no atomic component for the all apps transition, so just return early.
- return;
- }
-
- Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW
- ? config.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN)
- : FAST_OUT_SLOW_IN;
+ // need to decide depending on the release velocity
+ Interpolator interpolator = (config.userControlled ? LINEAR : DEACCEL_1_7);
Animator anim = createSpringAnimation(mProgress, targetProgress);
anim.setInterpolator(config.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
@@ -193,55 +179,41 @@
*/
public void setAlphas(LauncherState state, StateAnimationConfig config, PropertySetter setter) {
int visibleElements = state.getVisibleElements(mLauncher);
- boolean hasHeaderExtra = (visibleElements & ALL_APPS_HEADER_EXTRA) != 0;
boolean hasAllAppsContent = (visibleElements & ALL_APPS_CONTENT) != 0;
- boolean hasAnyVisibleItem = (visibleElements & APPS_VIEW_ITEM_MASK) != 0;
+ Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE,
+ Interpolators.clampToProgress(LINEAR, 0, CONTENT_VISIBLE_MAX_THRESHOLD));
+ setter.setViewAlpha(mAppsView, hasAllAppsContent ? 1 : 0, allAppsFade);
- Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
- Interpolator headerFade = config.getInterpolator(ANIM_ALL_APPS_HEADER_FADE, allAppsFade);
-
- if (mPlugin == null) {
- setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
- setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
- mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra,
- hasAllAppsContent, setter, headerFade, allAppsFade);
- } else {
- setter.setViewAlpha(mPluginContent, hasAllAppsContent ? 1 : 0, allAppsFade);
- setter.setViewAlpha(mAppsView.getContentView(), 0, allAppsFade);
- setter.setViewAlpha(mAppsView.getScrollBar(), 0, allAppsFade);
- }
- mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
-
- setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA,
- (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade);
-
- // Set visibility of the container at the very beginning or end of the transition.
- setter.setViewAlpha(mAppsView, hasAnyVisibleItem ? 1 : 0,
- hasAnyVisibleItem ? INSTANT : FINAL_FRAME);
+ boolean shouldProtectHeader =
+ ALL_APPS == state || mLauncher.getStateManager().getState() == ALL_APPS;
+ mScrimView.setDrawingController(shouldProtectHeader ? mAppsView : null);
}
- public AnimatorListenerAdapter getProgressAnimatorListener() {
- return AnimationSuccessListener.forRunnable(this::onProgressAnimationEnd);
+ public AnimatorListener getProgressAnimatorListener() {
+ return AnimatorListeners.forSuccessCallback(this::onProgressAnimationEnd);
}
- public void setupViews(AllAppsContainerView appsView, ScrimView scrimView) {
- mAppsView = appsView;
+ /**
+ * see Launcher#setupViews
+ */
+ public void setupViews(ScrimView scrimView, AllAppsContainerView appsView) {
mScrimView = scrimView;
- PluginManagerWrapper.INSTANCE.get(mLauncher)
- .addPluginListener(this, AllAppsSearchPlugin.class, false);
+ mAppsView = appsView;
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && Utilities.ATLEAST_R) {
+ mLauncher.getSystemUiController().updateUiState(UI_STATE_ALLAPPS,
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ mAppsView.setScrimView(scrimView);
}
/**
* Updates the total scroll range but does not update the UI.
*/
- void setScrollRangeDelta(float delta) {
+ public void setScrollRangeDelta(float delta) {
mScrollRangeDelta = delta;
mShiftRange = mLauncher.getDeviceProfile().heightPx - mScrollRangeDelta;
-
- if (mScrimView != null) {
- mScrimView.reInitUi();
- }
}
/**
@@ -249,50 +221,9 @@
* TODO: This logic should go in {@link LauncherState}
*/
private void onProgressAnimationEnd() {
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) return;
if (Float.compare(mProgress, 1f) == 0) {
mAppsView.reset(false /* animate */);
}
- updatePluginAnimationEnd();
- }
-
- @Override
- public void onPluginConnected(AllAppsSearchPlugin plugin, Context context) {
- mPlugin = plugin;
- mPluginContent = mLauncher.getLayoutInflater().inflate(
- R.layout.all_apps_content_layout, mAppsView, false);
- mAppsView.addView(mPluginContent);
- mPluginContent.setAlpha(0f);
- mPlugin.setup((ViewGroup) mPluginContent, mLauncher, mShiftRange);
- }
-
- @Override
- public void onPluginDisconnected(AllAppsSearchPlugin plugin) {
- mPlugin = null;
- mAppsView.removeView(mPluginContent);
- }
-
- public void onActivityDestroyed() {
- PluginManagerWrapper.INSTANCE.get(mLauncher).removePluginListener(this);
- }
-
- /** Used for the plugin to signal when drag starts happens
- * @param toAllApps*/
- public void onDragStart(boolean toAllApps) {
- if (mPlugin == null) return;
-
- if (toAllApps) {
- EditText editText = mAppsView.getSearchUiManager().setTextSearchEnabled(true);
- mPlugin.setEditText(editText);
- }
- mPlugin.onDragStart(toAllApps ? 1f : 0f);
- }
-
- private void updatePluginAnimationEnd() {
- if (mPlugin == null) return;
- mPlugin.onAnimationEnd(mProgress);
- if (Float.compare(mProgress, 1f) == 0) {
- mAppsView.getSearchUiManager().setTextSearchEnabled(false);
- mPlugin.setEditText(null);
- }
}
}
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 06209bb..ce5c589 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -19,8 +19,9 @@
import android.content.Context;
import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LabelComparator;
@@ -29,6 +30,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.TreeMap;
/**
@@ -42,6 +44,7 @@
private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1;
private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS;
+ private final WorkAdapterProvider mWorkAdapterProvider;
/**
* Info about a fast scroller section, depending if sections are merged, the fast scroller
@@ -60,61 +63,6 @@
}
}
- /**
- * Info about a particular adapter item (can be either section or app)
- */
- public static class AdapterItem {
- /** Common properties */
- // The index of this adapter item in the list
- public int position;
- // The type of this item
- public int viewType;
-
- /** App-only properties */
- // The section name of this app. Note that there can be multiple items with different
- // sectionNames in the same section
- public String sectionName = null;
- // The row that this item shows up on
- public int rowIndex;
- // The index of this app in the row
- public int rowAppIndex;
- // The associated AppInfo for the app
- public AppInfo appInfo = null;
- // The index of this app not including sections
- public int appIndex = -1;
-
- public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
- int appIndex) {
- AdapterItem item = new AdapterItem();
- item.viewType = AllAppsGridAdapter.VIEW_TYPE_ICON;
- item.position = pos;
- item.sectionName = sectionName;
- item.appInfo = appInfo;
- item.appIndex = appIndex;
- return item;
- }
-
- public static AdapterItem asEmptySearch(int pos) {
- AdapterItem item = new AdapterItem();
- item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH;
- item.position = pos;
- return item;
- }
-
- public static AdapterItem asAllAppsDivider(int pos) {
- AdapterItem item = new AdapterItem();
- item.viewType = AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER;
- item.position = pos;
- return item;
- }
-
- public static AdapterItem asMarketSearch(int pos) {
- AdapterItem item = new AdapterItem();
- item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET;
- item.position = pos;
- return item;
- }
- }
private final BaseDraggingActivity mLauncher;
@@ -122,28 +70,27 @@
private final List<AppInfo> mApps = new ArrayList<>();
private final AllAppsStore mAllAppsStore;
- // The set of filtered apps with the current filter
- private final List<AppInfo> mFilteredApps = new ArrayList<>();
+ // The number of results in current adapter
+ private int mAccessibilityResultsCount = 0;
// The current set of adapter items
private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>();
// The set of sections that we allow fast-scrolling to (includes non-merged sections)
private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
- // Is it the work profile app list.
- private final boolean mIsWork;
// The of ordered component names as a result of a search query
- private ArrayList<ComponentKey> mSearchResults;
+ private ArrayList<AdapterItem> mSearchResults;
private AllAppsGridAdapter mAdapter;
private AppInfoComparator mAppNameComparator;
private final int mNumAppsPerRow;
private int mNumAppRowsInAdapter;
private ItemInfoMatcher mItemFilter;
- public AlphabeticalAppsList(Context context, AllAppsStore appsStore, boolean isWork) {
+ public AlphabeticalAppsList(Context context, AllAppsStore appsStore,
+ WorkAdapterProvider adapterProvider) {
mAllAppsStore = appsStore;
mLauncher = BaseDraggingActivity.fromContext(context);
mAppNameComparator = new AppInfoComparator(context);
- mIsWork = isWork;
+ mWorkAdapterProvider = adapterProvider;
mNumAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns;
mAllAppsStore.addUpdateListener(this);
}
@@ -182,6 +129,28 @@
}
/**
+ * Returns the child adapter item with IME launch focus.
+ */
+ public AdapterItem getFocusedChild() {
+ if (mAdapterItems.size() == 0 || getFocusedChildIndex() == -1) {
+ return null;
+ }
+ return mAdapterItems.get(getFocusedChildIndex());
+ }
+
+ /**
+ * Returns the index of the child with IME launch focus.
+ */
+ public int getFocusedChildIndex() {
+ for (AdapterItem item : mAdapterItems) {
+ if (item.isCountedForAccessibility()) {
+ return mAdapterItems.indexOf(item);
+ }
+ }
+ return -1;
+ }
+
+ /**
* Returns the number of rows of applications
*/
public int getNumAppRows() {
@@ -192,7 +161,7 @@
* Returns the number of applications in this list.
*/
public int getNumFilteredApps() {
- return mFilteredApps.size();
+ return mAccessibilityResultsCount;
}
/**
@@ -206,22 +175,42 @@
* Returns whether there are no filtered results.
*/
public boolean hasNoFilteredResults() {
- return (mSearchResults != null) && mFilteredApps.isEmpty();
+ return (mSearchResults != null) && mAccessibilityResultsCount == 0;
}
/**
- * Sets the sorted list of filtered components.
+ * Sets results list for search
*/
- public boolean setOrderedFilter(ArrayList<ComponentKey> f) {
- if (mSearchResults != f) {
- boolean same = mSearchResults != null && mSearchResults.equals(f);
- mSearchResults = f;
- onAppsUpdated();
- return !same;
+ public boolean setSearchResults(ArrayList<AdapterItem> results) {
+ if (!Objects.equals(results, mSearchResults)) {
+ mSearchResults = results;
+ updateAdapterItems();
+ return true;
}
return false;
}
+ public boolean appendSearchResults(ArrayList<AdapterItem> results) {
+ if (mSearchResults != null && results != null && results.size() > 0) {
+ updateSearchAdapterItems(results, mSearchResults.size());
+ refreshRecyclerView();
+ return true;
+ }
+ return false;
+ }
+
+ void updateSearchAdapterItems(ArrayList<AdapterItem> list, int offset) {
+ for (int i = 0; i < list.size(); i++) {
+ AdapterItem adapterItem = list.get(i);
+ adapterItem.position = offset + i;
+ mAdapterItems.add(adapterItem);
+
+ if (adapterItem.isCountedForAccessibility()) {
+ mAccessibilityResultsCount++;
+ }
+ }
+ }
+
/**
* Updates internals when the set of apps are updated.
*/
@@ -267,14 +256,16 @@
}
// Recompose the set of adapter items from the current set of apps
- updateAdapterItems();
+ if (mSearchResults == null) {
+ updateAdapterItems();
+ }
}
/**
- * Updates the set of filtered apps with the current filter. At this point, we expect
+ * Updates the set of filtered apps with the current filter. At this point, we expect
* mCachedSectionNames to have been calculated for the set of all apps in mApps.
*/
- private void updateAdapterItems() {
+ public void updateAdapterItems() {
refillAdapterItems();
refreshRecyclerView();
}
@@ -292,41 +283,53 @@
int appIndex = 0;
// Prepare to update the list of sections, filtered apps, etc.
- mFilteredApps.clear();
+ mAccessibilityResultsCount = 0;
mFastScrollerSections.clear();
mAdapterItems.clear();
// Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
// ordered set of sections
- for (AppInfo info : getFiltersAppInfos()) {
- String sectionName = info.sectionName;
- // Create a new section if the section names do not match
- if (!sectionName.equals(lastSectionName)) {
- lastSectionName = sectionName;
- lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
- mFastScrollerSections.add(lastFastScrollerSectionInfo);
+ if (!hasFilter()) {
+ mAccessibilityResultsCount = mApps.size();
+ if (mWorkAdapterProvider != null) {
+ position += mWorkAdapterProvider.addWorkItems(mAdapterItems);
+ if (!mWorkAdapterProvider.shouldShowWorkApps()) {
+ return;
+ }
}
+ for (AppInfo info : mApps) {
+ String sectionName = info.sectionName;
- // Create an app item
- AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
- if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
- lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+ // Create a new section if the section names do not match
+ if (!sectionName.equals(lastSectionName)) {
+ lastSectionName = sectionName;
+ lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
+ mFastScrollerSections.add(lastFastScrollerSectionInfo);
+ }
+
+ // Create an app item
+ AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info,
+ appIndex++);
+ if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
+ lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+ }
+
+ mAdapterItems.add(appItem);
}
- mAdapterItems.add(appItem);
- mFilteredApps.add(info);
+ } else {
+ updateSearchAdapterItems(mSearchResults, 0);
+ if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ // Append the search market item
+ if (hasNoFilteredResults()) {
+ mAdapterItems.add(AdapterItem.asEmptySearch(position++));
+ } else {
+ mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
+ }
+ mAdapterItems.add(AdapterItem.asMarketSearch(position++));
+
+ }
}
-
- if (hasFilter()) {
- // Append the search market item
- if (hasNoFilteredResults()) {
- mAdapterItems.add(AdapterItem.asEmptySearch(position++));
- } else {
- mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
- }
- mAdapterItems.add(AdapterItem.asMarketSearch(position++));
- }
-
if (mNumAppsPerRow != 0) {
// Update the number of rows in the adapter after we do all the merging (otherwise, we
// would have to shift the values again)
@@ -381,18 +384,4 @@
}
}
}
-
- private List<AppInfo> getFiltersAppInfos() {
- if (mSearchResults == null) {
- return mApps;
- }
- ArrayList<AppInfo> result = new ArrayList<>();
- for (ComponentKey key : mSearchResults) {
- AppInfo match = mAllAppsStore.getApp(key);
- if (match != null) {
- result.add(match);
- }
- }
- return result;
- }
}
diff --git a/src/com/android/launcher3/allapps/BaseAdapterProvider.java b/src/com/android/launcher3/allapps/BaseAdapterProvider.java
new file mode 100644
index 0000000..308294c
--- /dev/null
+++ b/src/com/android/launcher3/allapps/BaseAdapterProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+/**
+ * A UI expansion wrapper providing for providing dynamic recyclerview items
+ */
+public abstract class BaseAdapterProvider {
+
+ /**
+ * Returns whether or not viewType can be handled by searchProvider
+ */
+ public abstract boolean isViewSupported(int viewType);
+
+ /**
+ * Called from RecyclerView.Adapter#onBindViewHolder
+ */
+ public abstract void onBindView(AllAppsGridAdapter.ViewHolder holder, int position);
+
+ /**
+ * Called from RecyclerView.Adapter#onCreateViewHolder
+ */
+ public abstract AllAppsGridAdapter.ViewHolder onCreateViewHolder(LayoutInflater layoutInflater,
+ ViewGroup parent, int viewType);
+
+ /**
+ * Returns supported item per row combinations supported
+ */
+ public int[] getSupportedItemsPerRowArray() {
+ return new int[]{};
+ }
+
+ /**
+ * Returns how many cells a view should span
+ */
+ public int getItemsPerRow(int viewType, int appsPerRow) {
+ return appsPerRow;
+ }
+
+}
diff --git a/src/com/android/launcher3/allapps/DecorationInfo.java b/src/com/android/launcher3/allapps/DecorationInfo.java
new file mode 100644
index 0000000..50b250c
--- /dev/null
+++ b/src/com/android/launcher3/allapps/DecorationInfo.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+public class DecorationInfo {
+}
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index b4ff5ea..be261f7 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -17,24 +17,21 @@
package com.android.launcher3.allapps;
import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.HOTSEAT;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.PREDICTION;
import android.animation.Animator;
import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
import android.os.Handler;
import android.os.UserManager;
import android.view.MotionEvent;
+import android.view.View;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.OnboardingPrefs;
/**
@@ -57,21 +54,15 @@
public void onStateTransitionComplete(LauncherState finalState) {}
};
- public DiscoveryBounce(Launcher launcher, float delta) {
+ public DiscoveryBounce(Launcher launcher) {
super(launcher, null);
mLauncher = launcher;
- AllAppsTransitionController controller = mLauncher.getAllAppsController();
mDiscoBounceAnimation =
AnimatorInflater.loadAnimator(launcher, R.animator.discovery_bounce);
- mDiscoBounceAnimation.setTarget(new VerticalProgressWrapper(controller, delta));
- mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- handleClose(false);
- }
- });
- mDiscoBounceAnimation.addListener(controller.getProgressAnimatorListener());
+ mDiscoBounceAnimation.setTarget(new VerticalProgressWrapper(
+ launcher.getHotseat(), mLauncher.getDragLayer().getHeight()));
+ mDiscoBounceAnimation.addListener(AnimatorListeners.forEndCallback(this::handleClose));
launcher.getStateManager().addStateListener(mStateListener);
}
@@ -108,27 +99,22 @@
if (mIsOpen) {
mIsOpen = false;
mLauncher.getDragLayer().removeView(this);
- // Reset the all-apps progress to what ever it was previously.
- mLauncher.getAllAppsController().setProgress(mLauncher.getStateManager()
- .getState().getVerticalProgress(mLauncher));
+ // Reset the translation to what ever it was previously.
+ mLauncher.getHotseat().setTranslationY(mLauncher.getStateManager().getState()
+ .getHotseatScaleAndTranslation(mLauncher).translationY);
mLauncher.getStateManager().removeStateListener(mStateListener);
}
}
@Override
- public void logActionCommand(int command) {
- // Since this is on-boarding popup, it is not a user controlled action.
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_DISCOVERY_BOUNCE) != 0;
}
- private void show(int containerType) {
+ private void show() {
mIsOpen = true;
mLauncher.getDragLayer().addView(this);
- mLauncher.getUserEventDispatcher().logActionBounceTip(containerType);
+ // TODO: add WW log for discovery bounce tip show event.
}
public static void showForHomeIfNeeded(Launcher launcher) {
@@ -150,62 +136,28 @@
return;
}
onboardingPrefs.incrementEventCount(OnboardingPrefs.HOME_BOUNCE_COUNT);
-
- new DiscoveryBounce(launcher, 0).show(HOTSEAT);
- }
-
- public static void showForOverviewIfNeeded(Launcher launcher,
- PagedOrientationHandler orientationHandler) {
- showForOverviewIfNeeded(launcher, true, orientationHandler);
- }
-
- private static void showForOverviewIfNeeded(Launcher launcher, boolean withDelay,
- PagedOrientationHandler orientationHandler) {
- OnboardingPrefs onboardingPrefs = launcher.getOnboardingPrefs();
- if (!launcher.isInState(OVERVIEW)
- || !launcher.hasBeenResumed()
- || launcher.isForceInvisible()
- || launcher.getDeviceProfile().isVerticalBarLayout()
- || !orientationHandler.isLayoutNaturalToLauncher()
- || onboardingPrefs.getBoolean(OnboardingPrefs.SHELF_BOUNCE_SEEN)
- || launcher.getSystemService(UserManager.class).isDemoUser()
- || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
- return;
- }
-
- if (withDelay) {
- new Handler().postDelayed(() -> showForOverviewIfNeeded(launcher, false,
- orientationHandler), DELAY_MS);
- return;
- } else if (AbstractFloatingView.getTopOpenView(launcher) != null) {
- // TODO: Move these checks to the top and call this method after invalidate handler.
- return;
- }
- onboardingPrefs.incrementEventCount(OnboardingPrefs.SHELF_BOUNCE_COUNT);
-
- new DiscoveryBounce(launcher, (1 - OVERVIEW.getVerticalProgress(launcher)))
- .show(PREDICTION);
+ new DiscoveryBounce(launcher).show();
}
/**
- * A wrapper around {@link AllAppsTransitionController} allowing a fixed shift in the value.
+ * A wrapper around hotseat animator allowing a fixed shift in the value.
*/
public static class VerticalProgressWrapper {
- private final float mDelta;
- private final AllAppsTransitionController mController;
+ private final View mView;
+ private final float mLimit;
- private VerticalProgressWrapper(AllAppsTransitionController controller, float delta) {
- mController = controller;
- mDelta = delta;
+ private VerticalProgressWrapper(View view, float limit) {
+ mView = view;
+ mLimit = limit;
}
public float getProgress() {
- return mController.getProgress() + mDelta;
+ return 1 + mView.getTranslationY() / mLimit;
}
public void setProgress(float progress) {
- mController.setProgress(progress - mDelta);
+ mView.setTranslationY(mLimit * (progress - 1));
}
}
}
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderRow.java b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
index f899587..9bf6043 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderRow.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
@@ -16,10 +16,9 @@
package com.android.launcher3.allapps;
import android.graphics.Rect;
-import android.view.animation.Interpolator;
+import android.view.View;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.anim.PropertySetter;
/**
* A abstract representation of a row in all-apps view
@@ -46,13 +45,22 @@
*/
boolean hasVisibleContent();
- void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
- PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade);
-
/**
* Scrolls the content vertically.
*/
void setVerticalScroll(int scroll, boolean isScrolledOut);
Class<? extends FloatingHeaderRow> getTypeClass();
+
+ /**
+ * Returns a child that has focus to be launched by the IME.
+ */
+ View getFocusedChild();
+
+ /**
+ * Returns true if view is currently visible
+ */
+ default boolean isVisible() {
+ return shouldDraw();
+ }
}
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 81e1b94..8ea83d5 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -15,10 +15,11 @@
*/
package com.android.launcher3.allapps;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-
import android.animation.ValueAnimator;
import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.ArrayMap;
@@ -26,7 +27,6 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.animation.Interpolator;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
@@ -37,7 +37,7 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
-import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.systemui.plugins.AllAppsRow;
import com.android.systemui.plugins.AllAppsRow.OnHeightUpdatedListener;
@@ -52,27 +52,35 @@
private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
+ private final ValueAnimator mHeaderAnimator = ValueAnimator.ofInt(0, 1).setDuration(100);
private final Point mTempOffset = new Point();
- private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- }
+ private final Paint mBGPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final RecyclerView.OnScrollListener mOnScrollListener =
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ }
- @Override
- public void onScrolled(RecyclerView rv, int dx, int dy) {
- if (rv != mCurrentRV) {
- return;
- }
+ @Override
+ public void onScrolled(RecyclerView rv, int dx, int dy) {
+ if (rv != mCurrentRV) {
+ return;
+ }
- if (mAnimator.isStarted()) {
- mAnimator.cancel();
- }
+ if (mAnimator.isStarted()) {
+ mAnimator.cancel();
+ }
- int current = -mCurrentRV.getCurrentScrollY();
- moved(current);
- applyVerticalMove();
- }
- };
+ int current = -mCurrentRV.getCurrentScrollY();
+ boolean headerCollapsed = mHeaderCollapsed;
+ moved(current);
+ applyVerticalMove();
+ if (headerCollapsed != mHeaderCollapsed) {
+ AllAppsContainerView parent = (AllAppsContainerView) getParent();
+ parent.invalidateHeader();
+ }
+ }
+ };
private final int mHeaderTopPadding;
@@ -83,11 +91,11 @@
private AllAppsRecyclerView mWorkRV;
private AllAppsRecyclerView mCurrentRV;
private ViewGroup mParent;
- private boolean mHeaderCollapsed;
+ public boolean mHeaderCollapsed;
private int mSnappedScrolledY;
private int mTranslationY;
+ private int mHeaderColor;
- private boolean mAllowTouchForwarding;
private boolean mForwardToRecyclerView;
protected boolean mTabsHidden;
@@ -130,6 +138,7 @@
}
mFixedRows = rows.toArray(new FloatingHeaderRow[rows.size()]);
mAllRows = mFixedRows;
+ mHeaderAnimator.addUpdateListener(valueAnimator -> invalidate());
}
@Override
@@ -145,6 +154,14 @@
PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
}
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mMainRV != null) {
+ mTabLayout.getLayoutParams().width = mMainRV.getTabWidth();
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
private void recreateAllRowsArray() {
int pluginCount = mPluginRows.size();
if (pluginCount == 0) {
@@ -194,6 +211,19 @@
onHeightUpdated();
}
+ @Override
+ public View getFocusedChild() {
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ for (FloatingHeaderRow row : mAllRows) {
+ if (row.hasVisibleContent() && row.isVisible()) {
+ return row.getFocusedChild();
+ }
+ }
+ return null;
+ }
+ return super.getFocusedChild();
+ }
+
public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) {
for (FloatingHeaderRow row : mAllRows) {
row.setup(this, mAllRows, tabsHidden);
@@ -210,7 +240,7 @@
}
private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) {
- if (old != updated && updated != null ) {
+ if (old != updated && updated != null) {
updated.addOnScrollListener(mOnScrollListener);
}
return updated;
@@ -265,10 +295,31 @@
} else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden
mHeaderCollapsed = true;
mSnappedScrolledY = -mMaxTranslation;
+ mHeaderAnimator.setCurrentFraction(0);
+ mHeaderAnimator.start();
}
}
}
+ /**
+ * Set current header protection background color
+ */
+ public void setHeaderColor(int color) {
+ mHeaderColor = color;
+ invalidate();
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mHeaderCollapsed && !mCollapsed && mTabLayout.getVisibility() == VISIBLE
+ && mHeaderColor != Color.TRANSPARENT && FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ mBGPaint.setColor(mHeaderColor);
+ mBGPaint.setAlpha((int) (255 * mHeaderAnimator.getAnimatedFraction()));
+ canvas.drawRect(0, 0, getWidth(), getHeight() + mTranslationY, mBGPaint);
+ }
+ super.dispatchDraw(canvas);
+ }
+
protected void applyVerticalMove() {
int uncappedTranslationY = mTranslationY;
mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
@@ -333,10 +384,6 @@
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (!mAllowTouchForwarding) {
- mForwardToRecyclerView = false;
- return super.onInterceptTouchEvent(ev);
- }
calcOffset(mTempOffset);
ev.offsetLocation(mTempOffset.x, mTempOffset.y);
mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev);
@@ -365,20 +412,6 @@
p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
}
- public void setContentVisibility(boolean hasHeader, boolean hasAllAppsContent,
- PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
- for (FloatingHeaderRow row : mAllRows) {
- row.setContentVisibility(hasHeader, hasAllAppsContent, setter, headerFade, allAppsFade);
- }
-
- allowTouchForwarding(hasAllAppsContent);
- setter.setFloat(mTabLayout, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
- }
-
- protected void allowTouchForwarding(boolean allow) {
- mAllowTouchForwarding = allow;
- }
-
public boolean hasVisibleContent() {
for (FloatingHeaderRow row : mAllRows) {
if (row.hasVisibleContent()) {
@@ -409,6 +442,13 @@
}
return null;
}
+
+ /**
+ * Returns visible height of FloatingHeaderView contents
+ */
+ public int getVisibleBottomBound() {
+ return getBottom() + mTranslationY;
+ }
}
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index a6bc6cf..f64b7cb 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -23,7 +23,6 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.views.WorkEduView;
/**
* AllAppsContainerView with launcher specific callbacks
@@ -70,8 +69,9 @@
@Override
public void setInsets(Rect insets) {
super.setInsets(insets);
- mLauncher.getAllAppsController()
- .setScrollRangeDelta(mSearchUiManager.getScrollRangeDelta(insets));
+ int allAppsStartingPositionY = mLauncher.getDeviceProfile().availableHeightPx
+ - mLauncher.getDeviceProfile().allAppsOpenVerticalTranslate;
+ mLauncher.getAllAppsController().setScrollRangeDelta(allAppsStartingPositionY);
}
@Override
@@ -83,14 +83,7 @@
}
@Override
- public void onTabChanged(int pos) {
- super.onTabChanged(pos);
- if (mUsingTabs) {
- if (pos == AdapterHolder.WORK) {
- WorkEduView.showWorkEduIfNeeded(mLauncher);
- } else {
- mWorkTabListener = WorkEduView.showEduFlowIfNeeded(mLauncher, mWorkTabListener);
- }
- }
+ public void onActivePageChanged(int currentActivePage) {
+ super.onActivePageChanged(currentActivePage);
}
}
diff --git a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
deleted file mode 100644
index 2de425e..0000000
--- a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.allapps;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.LinearLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.pageindicators.PageIndicator;
-import com.android.launcher3.util.Themes;
-
-/**
- * Supports two indicator colors, dedicated for personal and work tabs.
- */
-public class PersonalWorkSlidingTabStrip extends LinearLayout implements PageIndicator {
- private static final int POSITION_PERSONAL = 0;
- private static final int POSITION_WORK = 1;
-
- private final Paint mSelectedIndicatorPaint;
- private final Paint mDividerPaint;
-
- private int mSelectedIndicatorHeight;
- private int mIndicatorLeft = -1;
- private int mIndicatorRight = -1;
- private float mScrollOffset;
- private int mSelectedPosition = 0;
-
- private AllAppsContainerView mContainerView;
- private int mLastActivePage = 0;
- private boolean mIsRtl;
-
- public PersonalWorkSlidingTabStrip(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- setOrientation(HORIZONTAL);
- setWillNotDraw(false);
-
- mSelectedIndicatorHeight =
- getResources().getDimensionPixelSize(R.dimen.all_apps_tabs_indicator_height);
-
- mSelectedIndicatorPaint = new Paint();
- mSelectedIndicatorPaint.setColor(
- Themes.getAttrColor(context, android.R.attr.colorAccent));
-
- mDividerPaint = new Paint();
- mDividerPaint.setColor(Themes.getAttrColor(context, android.R.attr.colorControlHighlight));
- mDividerPaint.setStrokeWidth(
- getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
-
- mIsRtl = Utilities.isRtl(getResources());
- }
-
- /**
- * Highlights tab with index pos
- */
- public void updateTabTextColor(int pos) {
- mSelectedPosition = pos;
- for (int i = 0; i < getChildCount(); i++) {
- Button tab = (Button) getChildAt(i);
- tab.setSelected(i == pos);
- }
- }
-
- private void updateIndicatorPosition(float scrollOffset) {
- mScrollOffset = scrollOffset;
- updateIndicatorPosition();
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- updateTabTextColor(mSelectedPosition);
- updateIndicatorPosition(mScrollOffset);
- }
-
- private void updateIndicatorPosition() {
- int left = -1, right = -1;
- final View leftTab = getLeftTab();
- if (leftTab != null) {
- left = (int) (leftTab.getLeft() + leftTab.getWidth() * mScrollOffset);
- right = left + leftTab.getWidth();
- }
- setIndicatorPosition(left, right);
- }
-
- private View getLeftTab() {
- return mIsRtl ? getChildAt(1) : getChildAt(0);
- }
-
- private void setIndicatorPosition(int left, int right) {
- if (left != mIndicatorLeft || right != mIndicatorRight) {
- mIndicatorLeft = left;
- mIndicatorRight = right;
- invalidate();
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- float y = getHeight() - mDividerPaint.getStrokeWidth();
- canvas.drawLine(getPaddingLeft(), y, getWidth() - getPaddingRight(), y, mDividerPaint);
- canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
- mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
- }
-
- @Override
- public void setScroll(int currentScroll, int totalScroll) {
- float scrollOffset = ((float) currentScroll) / totalScroll;
- updateIndicatorPosition(scrollOffset);
- }
-
- @Override
- public void setActiveMarker(int activePage) {
- updateTabTextColor(activePage);
- if (mContainerView != null && mLastActivePage != activePage) {
- updateIndicatorPosition(activePage);
- mContainerView.onTabChanged(activePage);
- }
- mLastActivePage = activePage;
- }
-
- public void setContainerView(AllAppsContainerView containerView) {
- mContainerView = containerView;
- }
-
- @Override
- public void setMarkersCount(int numMarkers) { }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-}
diff --git a/src/com/android/launcher3/allapps/PluginHeaderRow.java b/src/com/android/launcher3/allapps/PluginHeaderRow.java
index 3089b18..5b5fbb7 100644
--- a/src/com/android/launcher3/allapps/PluginHeaderRow.java
+++ b/src/com/android/launcher3/allapps/PluginHeaderRow.java
@@ -18,14 +18,10 @@
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-
import android.graphics.Rect;
import android.view.View;
-import android.view.animation.Interpolator;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.anim.PropertySetter;
import com.android.systemui.plugins.AllAppsRow;
/**
@@ -65,13 +61,6 @@
}
@Override
- public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
- PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
- // Don't use setViewAlpha as we want to control the visibility ourselves.
- setter.setFloat(mView, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
- }
-
- @Override
public void setVerticalScroll(int scroll, boolean isScrolledOut) {
mView.setVisibility(isScrolledOut ? INVISIBLE : VISIBLE);
if (!isScrolledOut) {
@@ -83,4 +72,9 @@
public Class<PluginHeaderRow> getTypeClass() {
return PluginHeaderRow.class;
}
+
+ @Override
+ public View getFocusedChild() {
+ return null;
+ }
}
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index 7d5363f..924a392 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -15,16 +15,11 @@
*/
package com.android.launcher3.allapps;
-import static com.android.launcher3.LauncherState.ALL_APPS_HEADER;
-
-import android.graphics.Rect;
import android.view.KeyEvent;
-import android.view.animation.Interpolator;
-import android.widget.EditText;
import androidx.annotation.Nullable;
-import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.ExtendedEditText;
/**
* Interface for controlling the Apps search UI.
@@ -34,7 +29,7 @@
/**
* Initializes the search manager.
*/
- void initialize(AllAppsContainerView containerView);
+ void initializeSearch(AllAppsContainerView containerView);
/**
* Notifies the search manager to close any active search session.
@@ -45,33 +40,16 @@
* Called before dispatching a key event, in case the search manager wants to initialize
* some UI beforehand.
*/
- void preDispatchKeyEvent(KeyEvent keyEvent);
+ default void preDispatchKeyEvent(KeyEvent keyEvent) { };
/**
- * Returns the vertical shift for the all-apps view, so that it aligns with the hotseat.
- */
- float getScrollRangeDelta(Rect insets);
-
- /**
- * Called as part of state transition to update the content UI
- */
- void setContentVisibility(int visibleElements, PropertySetter setter,
- Interpolator interpolator);
-
- /**
- * Returns true if the QSB should be visible for the given set of visible elements
- */
- default boolean isQsbVisible(int visibleElements) {
- return (visibleElements & ALL_APPS_HEADER) != 0;
- }
-
- /**
- * Called to control how the search UI result should be handled.
- *
- * @param isEnabled when {@code true}, the search is all handled inside AOSP
- * and is not overlayable.
- * @return the searchbox edit text object
+ * @return the edit text object
*/
@Nullable
- EditText setTextSearchEnabled(boolean isEnabled);
+ ExtendedEditText getEditText();
+
+ /**
+ * sets highlight result's title
+ */
+ default void setFocusedResultTitle(@Nullable CharSequence title) { }
}
diff --git a/src/com/android/launcher3/allapps/WorkAdapterProvider.java b/src/com/android/launcher3/allapps/WorkAdapterProvider.java
new file mode 100644
index 0000000..13444dd
--- /dev/null
+++ b/src/com/android/launcher3/allapps/WorkAdapterProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+
+import java.util.ArrayList;
+
+/**
+ * A UI expansion wrapper providing for providing work profile specific views
+ */
+public class WorkAdapterProvider extends BaseAdapterProvider {
+
+ public static final String KEY_WORK_EDU_STEP = "showed_work_profile_edu";
+
+ private static final int VIEW_TYPE_WORK_EDU_CARD = 1 << 20;
+ private static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 21;
+ private final Runnable mRefreshCB;
+ private final BaseDraggingActivity mLauncher;
+ private boolean mEnabled;
+
+ WorkAdapterProvider(BaseDraggingActivity launcher, Runnable refreshCallback) {
+ mLauncher = launcher;
+ mRefreshCB = refreshCallback;
+ }
+
+ @Override
+ public void onBindView(AllAppsGridAdapter.ViewHolder holder, int position) {
+ if (holder.itemView instanceof WorkEduCard) {
+ ((WorkEduCard) holder.itemView).setPosition(position);
+ }
+ }
+
+ @Override
+ public AllAppsGridAdapter.ViewHolder onCreateViewHolder(LayoutInflater layoutInflater,
+ ViewGroup parent, int viewType) {
+ int viewId = viewType == VIEW_TYPE_WORK_DISABLED_CARD ? R.layout.work_apps_paused
+ : R.layout.work_apps_edu;
+ return new AllAppsGridAdapter.ViewHolder(layoutInflater.inflate(viewId, parent, false));
+ }
+
+ /**
+ * returns whether or not work apps should be visible in work tab.
+ */
+ public boolean shouldShowWorkApps() {
+ return mEnabled;
+ }
+
+ /**
+ * Adds work profile specific adapter items to adapterItems and returns number of items added
+ */
+ public int addWorkItems(ArrayList<AllAppsGridAdapter.AdapterItem> adapterItems) {
+ if (!mEnabled) {
+ //add disabled card here.
+ AllAppsGridAdapter.AdapterItem disabledCard = new AllAppsGridAdapter.AdapterItem();
+ disabledCard.viewType = VIEW_TYPE_WORK_DISABLED_CARD;
+ adapterItems.add(disabledCard);
+ } else if (!isEduSeen()) {
+ AllAppsGridAdapter.AdapterItem eduCard = new AllAppsGridAdapter.AdapterItem();
+ eduCard.viewType = VIEW_TYPE_WORK_EDU_CARD;
+ adapterItems.add(eduCard);
+ }
+
+ return adapterItems.size();
+ }
+
+ /**
+ * Sets the current state of work profile
+ */
+ public void updateCurrentState(boolean isEnabled) {
+ mEnabled = isEnabled;
+ mRefreshCB.run();
+ }
+
+ @Override
+ public boolean isViewSupported(int viewType) {
+ return viewType == VIEW_TYPE_WORK_DISABLED_CARD || viewType == VIEW_TYPE_WORK_EDU_CARD;
+ }
+
+ @Override
+ public int getItemsPerRow(int viewType, int appsPerRow) {
+ return 1;
+ }
+
+ private boolean isEduSeen() {
+ return Utilities.getPrefs(mLauncher).getInt(KEY_WORK_EDU_STEP, 0) != 0;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/WorkEduCard.java b/src/com/android/launcher3/allapps/WorkEduCard.java
new file mode 100644
index 0000000..9db7bf0
--- /dev/null
+++ b/src/com/android/launcher3/allapps/WorkEduCard.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+
+/**
+ * Work profile toggle switch shown at the bottom of AllApps work tab
+ */
+public class WorkEduCard extends FrameLayout implements View.OnClickListener,
+ Animation.AnimationListener {
+
+ private final Launcher mLauncher;
+ Animation mDismissAnim;
+ private int mPosition = -1;
+
+ public WorkEduCard(Context context) {
+ this(context, null, 0);
+ }
+
+ public WorkEduCard(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WorkEduCard(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(getContext());
+ mDismissAnim = AnimationUtils.loadAnimation(context, android.R.anim.fade_out);
+ mDismissAnim.setDuration(500);
+ mDismissAnim.setAnimationListener(this);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mDismissAnim.reset();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mDismissAnim.cancel();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ findViewById(R.id.action_btn).setOnClickListener(this);
+ MarginLayoutParams lp = ((MarginLayoutParams) findViewById(R.id.wrapper).getLayoutParams());
+ lp.width = mLauncher.getAppsView().getActiveRecyclerView().getTabWidth();
+ }
+
+ @Override
+ public void onClick(View view) {
+ startAnimation(mDismissAnim);
+ mLauncher.getSharedPrefs().edit().putInt(WorkAdapterProvider.KEY_WORK_EDU_STEP, 1).apply();
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ removeCard();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+
+ }
+
+ private void removeCard() {
+ if (mPosition == -1) {
+ if (getParent() != null) ((ViewGroup) getParent()).removeView(WorkEduCard.this);
+ } else {
+ AllAppsRecyclerView rv = mLauncher.getAppsView()
+ .mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView;
+ rv.getApps().getAdapterItems().remove(mPosition);
+ rv.getAdapter().notifyItemRemoved(mPosition);
+ }
+ }
+
+ public void setPosition(int position) {
+ mPosition = position;
+ }
+
+}
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 4567ee6..a800d34 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -15,108 +15,75 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_OFF_WORK_APPS_TAP;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
import android.content.Context;
-import android.content.SharedPreferences;
+import android.graphics.Insets;
import android.graphics.Rect;
-import android.os.AsyncTask;
+import android.os.Build;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-import android.widget.Switch;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import com.android.launcher3.Insettable;
-import com.android.launcher3.R;
+import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.views.ArrowTipView;
-
-import java.lang.ref.WeakReference;
/**
* Work profile toggle switch shown at the bottom of AllApps work tab
*/
-public class WorkModeSwitch extends Switch implements Insettable {
-
- private static final int WORK_TIP_THRESHOLD = 2;
- public static final String KEY_WORK_TIP_COUNTER = "worked_tip_counter";
+public class WorkModeSwitch extends Button implements Insettable, View.OnClickListener {
private Rect mInsets = new Rect();
+ private boolean mWorkEnabled;
- private final float[] mTouch = new float[2];
- private int mTouchSlop;
+
+ @Nullable
+ private KeyboardInsetAnimationCallback mKeyboardInsetAnimationCallback;
public WorkModeSwitch(Context context) {
- super(context);
- init();
+ this(context, null, 0);
}
public WorkModeSwitch(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
+ this(context, attrs, 0);
}
public WorkModeSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- init();
- }
-
- private void init() {
- ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
- mTouchSlop = viewConfiguration.getScaledTouchSlop();
}
@Override
- public void setChecked(boolean checked) { }
-
- @Override
- public void toggle() {
- // don't show tip if user uses toggle
- Utilities.getPrefs(getContext()).edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
- trySetQuietModeEnabledToAllProfilesAsync(isChecked());
- }
-
- /**
- * Sets the enabled or disabled state of the button
- * @param isChecked
- */
- public void update(boolean isChecked) {
- super.setChecked(isChecked);
- setCompoundDrawablesRelativeWithIntrinsicBounds(
- isChecked ? R.drawable.ic_corp : R.drawable.ic_corp_off, 0, 0, 0);
- setEnabled(true);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mTouch[0] = ev.getX();
- mTouch[1] = ev.getY();
- } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
- if (Math.abs(mTouch[0] - ev.getX()) > mTouchSlop
- || Math.abs(mTouch[1] - ev.getY()) > mTouchSlop) {
- int action = ev.getAction();
- ev.setAction(MotionEvent.ACTION_CANCEL);
- super.onTouchEvent(ev);
- ev.setAction(action);
- return false;
- }
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ setSelected(true);
+ setOnClickListener(this);
+ if (Utilities.ATLEAST_R) {
+ mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
+ setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
}
- return super.onTouchEvent(ev);
- }
-
- private void trySetQuietModeEnabledToAllProfilesAsync(boolean enabled) {
- new SetQuietModeEnabledAsyncTask(enabled, new WeakReference<>(this)).execute();
}
@Override
public void setInsets(Rect insets) {
int bottomInset = insets.bottom - mInsets.bottom;
mInsets.set(insets);
- setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
- getPaddingBottom() + bottomInset);
+ ViewGroup.MarginLayoutParams marginLayoutParams =
+ (ViewGroup.MarginLayoutParams) getLayoutParams();
+ if (marginLayoutParams != null) {
+ marginLayoutParams.bottomMargin = bottomInset + marginLayoutParams.bottomMargin;
+ }
}
/**
@@ -125,78 +92,58 @@
public void setWorkTabVisible(boolean workTabVisible) {
clearAnimation();
if (workTabVisible) {
- setVisibility(VISIBLE);
+ setEnabled(true);
+ if (mWorkEnabled) {
+ setVisibility(VISIBLE);
+ }
setAlpha(0);
animate().alpha(1).start();
- showTipIfNeeded();
} else {
animate().alpha(0).withEndAction(() -> this.setVisibility(GONE)).start();
}
}
- private static final class SetQuietModeEnabledAsyncTask
- extends AsyncTask<Void, Void, Boolean> {
-
- private final boolean enabled;
- private final WeakReference<WorkModeSwitch> switchWeakReference;
-
- SetQuietModeEnabledAsyncTask(boolean enabled,
- WeakReference<WorkModeSwitch> switchWeakReference) {
- this.enabled = enabled;
- this.switchWeakReference = switchWeakReference;
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- WorkModeSwitch workModeSwitch = switchWeakReference.get();
- if (workModeSwitch != null) {
- workModeSwitch.setEnabled(false);
- }
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
- WorkModeSwitch workModeSwitch = switchWeakReference.get();
- if (workModeSwitch == null || !Utilities.ATLEAST_P) {
- return false;
- }
-
- Context context = workModeSwitch.getContext();
- UserManager userManager = context.getSystemService(UserManager.class);
- boolean showConfirm = false;
- for (UserHandle userProfile : UserCache.INSTANCE.get(context).getUserProfiles()) {
- if (Process.myUserHandle().equals(userProfile)) {
- continue;
- }
- showConfirm |= !userManager.requestQuietModeEnabled(enabled, userProfile);
- }
- return showConfirm;
- }
-
- @Override
- protected void onPostExecute(Boolean showConfirm) {
- if (showConfirm) {
- WorkModeSwitch workModeSwitch = switchWeakReference.get();
- if (workModeSwitch != null) {
- workModeSwitch.setEnabled(true);
- }
- }
+ @Override
+ public void onClick(View view) {
+ if (Utilities.ATLEAST_P) {
+ setEnabled(false);
+ Launcher.fromContext(getContext()).getStatsLogManager().logger().log(
+ LAUNCHER_TURN_OFF_WORK_APPS_TAP);
+ UI_HELPER_EXECUTOR.post(() -> setWorkProfileEnabled(getContext(), false));
}
}
/**
- * Shows a work tip on the Nth work tab open
+ * Sets the enabled or disabled state of the button
*/
- public void showTipIfNeeded() {
- Context context = getContext();
- SharedPreferences prefs = Utilities.getPrefs(context);
- int tipCounter = prefs.getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD);
- if (tipCounter < 0) return;
- if (tipCounter == 0) {
- new ArrowTipView(context)
- .show(context.getString(R.string.work_switch_tip), getTop());
+ public void updateCurrentState(boolean active) {
+ mWorkEnabled = active;
+ setEnabled(true);
+ setVisibility(active ? VISIBLE : GONE);
+ }
+
+ @RequiresApi(Build.VERSION_CODES.P)
+ public static Boolean setWorkProfileEnabled(Context context, boolean enabled) {
+ UserManager userManager = context.getSystemService(UserManager.class);
+ boolean showConfirm = false;
+ for (UserHandle userProfile : UserCache.INSTANCE.get(context).getUserProfiles()) {
+ if (Process.myUserHandle().equals(userProfile)) {
+ continue;
+ }
+ showConfirm |= !userManager.requestQuietModeEnabled(!enabled, userProfile);
}
- prefs.edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
+ return showConfirm;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (Utilities.ATLEAST_R) {
+ setTranslationY(0);
+ if (insets.isVisible(WindowInsets.Type.ime())) {
+ Insets keyboardInsets = insets.getInsets(WindowInsets.Type.ime());
+ setTranslationY(mInsets.bottom - keyboardInsets.bottom);
+ }
+ }
+ return insets;
}
}
diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java
new file mode 100644
index 0000000..7908b63
--- /dev/null
+++ b/src/com/android/launcher3/allapps/WorkPausedCard.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_ON_WORK_APPS_TAP;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+
+/**
+ * Work profile toggle switch shown at the bottom of AllApps work tab
+ */
+public class WorkPausedCard extends LinearLayout implements View.OnClickListener {
+
+ private final Launcher mLauncher;
+ private Button mBtn;
+
+ public WorkPausedCard(Context context) {
+ this(context, null, 0);
+ }
+
+ public WorkPausedCard(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WorkPausedCard(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(getContext());
+ }
+
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mBtn = findViewById(R.id.enable_work_apps);
+ mBtn.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (Utilities.ATLEAST_P) {
+ setEnabled(false);
+ mLauncher.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP);
+ UI_HELPER_EXECUTOR.post(() -> WorkModeSwitch.setWorkProfileEnabled(getContext(), true));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int orientation = getResources().getConfiguration().orientation;
+ getLayoutParams().height = orientation == Configuration.ORIENTATION_PORTRAIT
+ ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT;
+ super.onLayout(changed, l, t, r, b);
+ }
+}
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index ed45749..79718fb 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.allapps.search;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME;
+
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -27,12 +29,12 @@
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
-import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.PackageManagerHelper;
-
-import java.util.ArrayList;
+import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
/**
* An interface to a search box that AllApps can command.
@@ -42,22 +44,23 @@
OnFocusChangeListener {
protected BaseDraggingActivity mLauncher;
- protected Callbacks mCb;
+ protected SearchCallback<AdapterItem> mCallback;
protected ExtendedEditText mInput;
protected String mQuery;
- protected SearchAlgorithm mSearchAlgorithm;
+ protected SearchAlgorithm<AdapterItem> mSearchAlgorithm;
public void setVisibility(int visibility) {
mInput.setVisibility(visibility);
}
+
/**
* Sets the references to the apps model and the search result callback.
*/
public final void initialize(
- SearchAlgorithm searchAlgorithm, ExtendedEditText input,
- BaseDraggingActivity launcher, Callbacks cb) {
- mCb = cb;
+ SearchAlgorithm<AdapterItem> searchAlgorithm, ExtendedEditText input,
+ BaseDraggingActivity launcher, SearchCallback<AdapterItem> callback) {
+ mCallback = callback;
mLauncher = launcher;
mInput = input;
@@ -69,7 +72,7 @@
}
@Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
// Do nothing
}
@@ -83,10 +86,10 @@
mQuery = s.toString();
if (mQuery.isEmpty()) {
mSearchAlgorithm.cancel(true);
- mCb.clearSearchResult();
+ mCallback.clearSearchResult();
} else {
mSearchAlgorithm.cancel(false);
- mSearchAlgorithm.doSearch(mQuery, mCb);
+ mSearchAlgorithm.doSearch(mQuery, mCallback);
}
}
@@ -96,24 +99,19 @@
}
// If play store continues auto updating an app, we want to show partial result.
mSearchAlgorithm.cancel(false);
- mSearchAlgorithm.doSearch(mQuery, mCb);
+ mSearchAlgorithm.doSearch(mQuery, mCallback);
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- // Skip if it's not the right action
- if (actionId != EditorInfo.IME_ACTION_SEARCH) {
- return false;
- }
- // Skip if the query is empty
- String query = v.getText().toString();
- if (query.isEmpty()) {
- return false;
+ if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) {
+ mLauncher.getStatsLogManager().logger()
+ .log(LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME);
+ // selectFocusedView should return SearchTargetEvent that is passed onto onClick
+ return Launcher.getLauncher(mLauncher).getAppsView().launchHighlightedItem();
}
- return mLauncher.startActivitySafely(v,
- PackageManagerHelper.getMarketSearchIntent(mLauncher, query), null,
- AppLaunchTracker.CONTAINER_SEARCH);
+ return false;
}
@Override
@@ -129,7 +127,7 @@
@Override
public void onFocusChange(View view, boolean hasFocus) {
- if (!hasFocus) {
+ if (!hasFocus && !FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
mInput.hideKeyboard();
}
}
@@ -138,7 +136,7 @@
* Resets the search bar state.
*/
public void reset() {
- mCb.clearSearchResult();
+ mCallback.clearSearchResult();
mInput.reset();
mQuery = null;
}
@@ -156,23 +154,4 @@
public boolean isSearchFieldFocused() {
return mInput.isFocused();
}
-
- /**
- * Callback for getting search results.
- */
- public interface Callbacks {
-
- /**
- * Called when the search is complete.
- *
- * @param apps sorted list of matching components or null if in case of failure.
- */
- void onSearchResult(String query, ArrayList<ComponentKey> apps);
-
- /**
- * Called when the search results should be cleared.
- */
- void clearSearchResult();
- }
-
}
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 356c52c..2491217 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -31,8 +31,6 @@
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
-import android.view.animation.Interpolator;
-import android.widget.EditText;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
@@ -40,11 +38,11 @@
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.allapps.AlphabeticalAppsList;
import com.android.launcher3.allapps.SearchUiManager;
-import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.search.SearchCallback;
import java.util.ArrayList;
@@ -52,7 +50,7 @@
* Layout to contain the All-apps search UI.
*/
public class AppsSearchContainerLayout extends ExtendedEditText
- implements SearchUiManager, AllAppsSearchBarController.Callbacks,
+ implements SearchUiManager, SearchCallback<AdapterItem>,
AllAppsStore.OnUpdateListener, Insettable {
private final BaseDraggingActivity mLauncher;
@@ -107,7 +105,8 @@
int rowWidth = myRequestedWidth - mAppsView.getActiveRecyclerView().getPaddingLeft()
- mAppsView.getActiveRecyclerView().getPaddingRight();
- int cellWidth = DeviceProfile.calculateCellWidth(rowWidth, dp.inv.numHotseatIcons);
+ int cellWidth = DeviceProfile.calculateCellWidth(rowWidth, dp.cellLayoutBorderSpacingPx,
+ dp.numShownHotseatIcons);
int iconVisibleSize = Math.round(ICON_VISIBLE_AREA_FACTOR * dp.iconSizePx);
int iconPadding = cellWidth - iconVisibleSize;
@@ -131,11 +130,12 @@
}
@Override
- public void initialize(AllAppsContainerView appsView) {
+ public void initializeSearch(AllAppsContainerView appsView) {
mApps = appsView.getApps();
mAppsView = appsView;
mSearchBarController.initialize(
- new DefaultAppSearchAlgorithm(mApps.getApps()), this, mLauncher, this);
+ new DefaultAppSearchAlgorithm(mLauncher),
+ this, mLauncher, this);
}
@Override
@@ -168,17 +168,25 @@
}
@Override
- public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
- if (apps != null) {
- mApps.setOrderedFilter(apps);
+ public void onSearchResult(String query, ArrayList<AdapterItem> items) {
+ if (items != null) {
+ mApps.setSearchResults(items);
notifyResultChanged();
mAppsView.setLastSearchQuery(query);
}
}
@Override
+ public void onAppendSearchResult(String query, ArrayList<AdapterItem> items) {
+ if (items != null) {
+ mApps.appendSearchResults(items);
+ notifyResultChanged();
+ }
+ }
+
+ @Override
public void clearSearchResult() {
- if (mApps.setOrderedFilter(null)) {
+ if (mApps.setSearchResults(null)) {
notifyResultChanged();
}
@@ -201,22 +209,7 @@
}
@Override
- public float getScrollRangeDelta(Rect insets) {
- if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
- return 0;
- } else {
- return insets.bottom + insets.top;
- }
- }
-
- @Override
- public void setContentVisibility(int visibleElements, PropertySetter setter,
- Interpolator interpolator) {
- setter.setViewAlpha(this, isQsbVisible(visibleElements) ? 1 : 0, interpolator);
- }
-
- @Override
- public EditText setTextSearchEnabled(boolean isEnabled) {
+ public ExtendedEditText getEditText() {
return this;
}
}
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index f72a988..1f854c6 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -15,26 +15,39 @@
*/
package com.android.launcher3.allapps.search;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.content.Context;
import android.os.Handler;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.util.ComponentKey;
+import androidx.annotation.AnyThread;
-import java.text.Collator;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BaseModelUpdateTask;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.search.StringMatcherUtility;
+
import java.util.ArrayList;
import java.util.List;
/**
* The default search implementation.
*/
-public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
+public class DefaultAppSearchAlgorithm implements SearchAlgorithm<AdapterItem> {
- private final List<AppInfo> mApps;
- protected final Handler mResultHandler;
+ private static final int MAX_RESULTS_COUNT = 5;
- public DefaultAppSearchAlgorithm(List<AppInfo> apps) {
- mApps = apps;
- mResultHandler = new Handler();
+ private final LauncherAppState mAppState;
+ private final Handler mResultHandler;
+
+ public DefaultAppSearchAlgorithm(Context context) {
+ mAppState = LauncherAppState.getInstance(context);
+ mResultHandler = new Handler(MAIN_EXECUTOR.getLooper());
}
@Override
@@ -45,140 +58,38 @@
}
@Override
- public void doSearch(final String query,
- final AllAppsSearchBarController.Callbacks callback) {
- final ArrayList<ComponentKey> result = getTitleMatchResult(query);
- mResultHandler.post(new Runnable() {
-
+ public void doSearch(String query, SearchCallback<AdapterItem> callback) {
+ mAppState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
@Override
- public void run() {
- callback.onSearchResult(query, result);
+ public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+ ArrayList<AdapterItem> result = getTitleMatchResult(apps.data, query);
+ mResultHandler.post(() -> callback.onSearchResult(query, result));
}
});
}
- private ArrayList<ComponentKey> getTitleMatchResult(String query) {
+ /**
+ * Filters {@link AppInfo}s matching specified query
+ */
+ @AnyThread
+ public static ArrayList<AdapterItem> getTitleMatchResult(List<AppInfo> apps, String query) {
// Do an intersection of the words in the query and each title, and filter out all the
// apps that don't match all of the words in the query.
final String queryTextLower = query.toLowerCase();
- final ArrayList<ComponentKey> result = new ArrayList<>();
- StringMatcher matcher = StringMatcher.getInstance();
- for (AppInfo info : mApps) {
- if (matches(info, queryTextLower, matcher)) {
- result.add(info.toComponentKey());
+ final ArrayList<AdapterItem> result = new ArrayList<>();
+ StringMatcherUtility.StringMatcher matcher =
+ StringMatcherUtility.StringMatcher.getInstance();
+
+ int resultCount = 0;
+ int total = apps.size();
+ for (int i = 0; i < total && resultCount < MAX_RESULTS_COUNT; i++) {
+ AppInfo info = apps.get(i);
+ if (StringMatcherUtility.matches(queryTextLower, info.title.toString(), matcher)) {
+ AdapterItem appItem = AdapterItem.asApp(resultCount, "", info, resultCount);
+ result.add(appItem);
+ resultCount++;
}
}
return result;
}
-
- public static boolean matches(AppInfo info, String query, StringMatcher matcher) {
- int queryLength = query.length();
-
- String title = info.title.toString();
- int titleLength = title.length();
-
- if (titleLength < queryLength || queryLength <= 0) {
- return false;
- }
-
- int lastType;
- int thisType = Character.UNASSIGNED;
- int nextType = Character.getType(title.codePointAt(0));
-
- int end = titleLength - queryLength;
- for (int i = 0; i <= end; i++) {
- lastType = thisType;
- thisType = nextType;
- nextType = i < (titleLength - 1) ?
- Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED;
- if (isBreak(thisType, lastType, nextType) &&
- matcher.matches(query, title.substring(i, i + queryLength))) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Returns true if the current point should be a break point. Following cases
- * are considered as break points:
- * 1) Any non space character after a space character
- * 2) Any digit after a non-digit character
- * 3) Any capital character after a digit or small character
- * 4) Any capital character before a small character
- */
- private static boolean isBreak(int thisType, int prevType, int nextType) {
- switch (prevType) {
- case Character.UNASSIGNED:
- case Character.SPACE_SEPARATOR:
- case Character.LINE_SEPARATOR:
- case Character.PARAGRAPH_SEPARATOR:
- return true;
- }
- switch (thisType) {
- case Character.UPPERCASE_LETTER:
- if (nextType == Character.UPPERCASE_LETTER) {
- return true;
- }
- // Follow through
- case Character.TITLECASE_LETTER:
- // Break point if previous was not a upper case
- return prevType != Character.UPPERCASE_LETTER;
- case Character.LOWERCASE_LETTER:
- // Break point if previous was not a letter.
- return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
- case Character.DECIMAL_DIGIT_NUMBER:
- case Character.LETTER_NUMBER:
- case Character.OTHER_NUMBER:
- // Break point if previous was not a number
- return !(prevType == Character.DECIMAL_DIGIT_NUMBER
- || prevType == Character.LETTER_NUMBER
- || prevType == Character.OTHER_NUMBER);
- case Character.MATH_SYMBOL:
- case Character.CURRENCY_SYMBOL:
- case Character.OTHER_PUNCTUATION:
- case Character.DASH_PUNCTUATION:
- // Always a break point for a symbol
- return true;
- default:
- return false;
- }
- }
-
- public static class StringMatcher {
-
- private static final char MAX_UNICODE = '\uFFFF';
-
- private final Collator mCollator;
-
- StringMatcher() {
- // On android N and above, Collator uses ICU implementation which has a much better
- // support for non-latin locales.
- mCollator = Collator.getInstance();
- mCollator.setStrength(Collator.PRIMARY);
- mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
- }
-
- /**
- * Returns true if {@param query} is a prefix of {@param target}
- */
- public boolean matches(String query, String target) {
- switch (mCollator.compare(query, target)) {
- case 0:
- return true;
- case -1:
- // The target string can contain a modifier which would make it larger than
- // the query string (even though the length is same). If the query becomes
- // larger after appending a unicode character, it was originally a prefix of
- // the target string and hence should match.
- return mCollator.compare(query + MAX_UNICODE, target) > -1;
- default:
- return false;
- }
- }
-
- public static StringMatcher getInstance() {
- return new StringMatcher();
- }
- }
}
diff --git a/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java
new file mode 100644
index 0000000..7abd555
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps.search;
+
+import android.graphics.Canvas;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsGridAdapter;
+import com.android.launcher3.model.data.ItemInfo;
+
+/**
+ * Provides views for local search results
+ */
+public class DefaultSearchAdapterProvider extends SearchAdapterProvider {
+
+ private final RecyclerView.ItemDecoration mDecoration;
+ private View mHighlightedView;
+
+ public DefaultSearchAdapterProvider(BaseDraggingActivity launcher,
+ AllAppsContainerView appsContainerView) {
+ super(launcher, appsContainerView);
+ mDecoration = new RecyclerView.ItemDecoration() {
+ @Override
+ public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent,
+ @NonNull RecyclerView.State state) {
+ super.onDraw(c, parent, state);
+ }
+ };
+ }
+
+ @Override
+ public void onBindView(AllAppsGridAdapter.ViewHolder holder, int position) {
+ if (position == 0) {
+ mHighlightedView = holder.itemView;
+ }
+ }
+
+ @Override
+ public boolean isViewSupported(int viewType) {
+ return false;
+ }
+
+ @Override
+ public AllAppsGridAdapter.ViewHolder onCreateViewHolder(LayoutInflater layoutInflater,
+ ViewGroup parent, int viewType) {
+ return null;
+ }
+
+ @Override
+ public boolean launchHighlightedItem() {
+ if (mHighlightedView instanceof BubbleTextView
+ && mHighlightedView.getTag() instanceof ItemInfo) {
+ ItemInfo itemInfo = (ItemInfo) mHighlightedView.getTag();
+ return mLauncher.startActivitySafely(mHighlightedView, itemInfo.getIntent(), itemInfo);
+ }
+ return false;
+ }
+
+ @Override
+ public View getHighlightedItem() {
+ return mHighlightedView;
+ }
+
+ @Override
+ public RecyclerView.ItemDecoration getDecorator() {
+ return mDecoration;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
new file mode 100644
index 0000000..7af0406
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.allapps.search;
+
+import android.net.Uri;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.BaseAdapterProvider;
+
+/**
+ * A UI expansion wrapper providing for search results
+ */
+public abstract class SearchAdapterProvider extends BaseAdapterProvider {
+
+ protected final BaseDraggingActivity mLauncher;
+
+ public SearchAdapterProvider(BaseDraggingActivity launcher, AllAppsContainerView appsView) {
+ mLauncher = launcher;
+ }
+
+ /**
+ * Called from LiveSearchManager to notify slice status updates.
+ */
+ public void onSliceStatusUpdate(Uri sliceUri) {
+ }
+
+ /**
+ * Handles selection event on search adapter item. Returns false if provider can not handle
+ * event
+ */
+ public abstract boolean launchHighlightedItem();
+
+ /**
+ * Returns the current highlighted view
+ */
+ public abstract View getHighlightedItem();
+
+ /**
+ * Returns the item decorator.
+ */
+ public abstract RecyclerView.ItemDecoration getDecorator();
+}
diff --git a/src/com/android/launcher3/allapps/search/SearchAlgorithm.java b/src/com/android/launcher3/allapps/search/SearchAlgorithm.java
deleted file mode 100644
index c409b1c..0000000
--- a/src/com/android/launcher3/allapps/search/SearchAlgorithm.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.allapps.search;
-
-/**
- * An interface for handling search.
- */
-public interface SearchAlgorithm {
-
- /**
- * Performs search and sends the result to the callback.
- */
- void doSearch(String query, AllAppsSearchBarController.Callbacks callback);
-
- /**
- * Cancels any active request.
- */
- void cancel(boolean interruptActiveRequests);
-}
diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java
index eabd283..8dad1b4 100644
--- a/src/com/android/launcher3/anim/AlphaUpdateListener.java
+++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java
@@ -17,6 +17,7 @@
package com.android.launcher3.anim;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.view.View;
@@ -25,7 +26,7 @@
/**
* A convenience class to update a view's visibility state after an alpha animation.
*/
-public class AlphaUpdateListener extends AnimationSuccessListener
+public class AlphaUpdateListener extends AnimatorListenerAdapter
implements AnimatorUpdateListener {
public static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
@@ -41,7 +42,7 @@
}
@Override
- public void onAnimationSuccess(Animator animator) {
+ public void onAnimationEnd(Animator animator) {
updateVisibility(mView);
}
diff --git a/src/com/android/launcher3/anim/AnimationSuccessListener.java b/src/com/android/launcher3/anim/AnimationSuccessListener.java
index 9905e81..a312070 100644
--- a/src/com/android/launcher3/anim/AnimationSuccessListener.java
+++ b/src/com/android/launcher3/anim/AnimationSuccessListener.java
@@ -40,24 +40,4 @@
public abstract void onAnimationSuccess(Animator animator);
- /**
- * Returns an AnimationSuccessListener which runs the provided action on success
- */
- public static AnimationSuccessListener forRunnable(Runnable r) {
- return new RunnableSuccessListener(r);
- }
-
- private static class RunnableSuccessListener extends AnimationSuccessListener {
-
- private final Runnable mRunnable;
-
- private RunnableSuccessListener(Runnable r) {
- mRunnable = r;
- }
-
- @Override
- public void onAnimationSuccess(Animator animator) {
- mRunnable.run();
- }
- }
}
diff --git a/src/com/android/launcher3/anim/AnimatorListeners.java b/src/com/android/launcher3/anim/AnimatorListeners.java
new file mode 100644
index 0000000..d9046b9
--- /dev/null
+++ b/src/com/android/launcher3/anim/AnimatorListeners.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.anim;
+
+import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+
+import java.util.function.Consumer;
+
+/**
+ * Utility class for creating common {@link AnimatorListener}
+ */
+public class AnimatorListeners {
+
+ /**
+ * Returns an AnimatorListener which executes the callback on successful animation completion
+ */
+ public static AnimatorListener forSuccessCallback(Runnable callback) {
+ return new RunnableSuccessListener(callback);
+ }
+
+ /**
+ * Returns an AnimatorListener which executes the callback on animation completion,
+ * with the boolean representing success
+ */
+ public static AnimatorListener forEndCallback(Consumer<Boolean> callback) {
+ return new EndStateCallbackWrapper(callback);
+ }
+
+ /**
+ * Returns an AnimatorListener which executes the callback on animation completion
+ */
+ public static AnimatorListener forEndCallback(Runnable callback) {
+ return new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ callback.run();
+ }
+ };
+ }
+
+ private static class EndStateCallbackWrapper extends AnimatorListenerAdapter {
+
+ private final Consumer<Boolean> mListener;
+ private boolean mListenerCalled = false;
+
+ EndStateCallbackWrapper(Consumer<Boolean> listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (!mListenerCalled) {
+ mListenerCalled = true;
+ mListener.accept(false);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ if (!mListenerCalled) {
+ mListenerCalled = true;
+ mListener.accept(anim instanceof ValueAnimator
+ ? ((ValueAnimator) anim).getAnimatedFraction() > SUCCESS_TRANSITION_PROGRESS
+ : true);
+ }
+ }
+ }
+
+ private static class RunnableSuccessListener extends AnimationSuccessListener {
+
+ private final Runnable mRunnable;
+
+ private RunnableSuccessListener(Runnable r) {
+ mRunnable = r;
+ }
+
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ mRunnable.run();
+ }
+ }
+}
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index ea0ff8b..85ca280 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -19,7 +19,7 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.clampToProgress;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
@@ -29,7 +29,7 @@
import android.animation.ValueAnimator;
import android.content.Context;
-import androidx.annotation.Nullable;
+import com.android.launcher3.Utilities;
import java.util.ArrayList;
import java.util.Collections;
@@ -72,7 +72,6 @@
private Runnable mEndAction;
protected boolean mTargetCancelled = false;
- protected Runnable mOnCancelRunnable;
/** package private */
AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList<Holder> childAnims) {
@@ -88,16 +87,11 @@
@Override
public void onAnimationCancel(Animator animation) {
mTargetCancelled = true;
- if (mOnCancelRunnable != null) {
- mOnCancelRunnable.run();
- mOnCancelRunnable = null;
- }
}
@Override
public void onAnimationEnd(Animator animation) {
mTargetCancelled = false;
- mOnCancelRunnable = null;
}
@Override
@@ -141,15 +135,20 @@
/**
* Starts playing the animation with the provided velocity optionally playing any
- * physics based animations
+ * physics based animations.
+ * @param goingToEnd Whether we are going to the end (progress = 1) or not (progress = 0).
+ * @param velocityPxPerMs The velocity at which to start the animation, in pixels / millisecond.
+ * @param endDistance The distance (pixels) that the animation will travel from progress 0 to 1.
+ * @param animationDuration The duration of the non-physics based animation.
*/
public void startWithVelocity(Context context, boolean goingToEnd,
- float velocity, float scale, long animationDuration) {
- float scaleInverse = 1 / Math.abs(scale);
- float scaledVelocity = velocity * scaleInverse;
+ float velocityPxPerMs, float endDistance, long animationDuration) {
+ float distanceInverse = 1 / Math.abs(endDistance);
+ float velocityProgressPerMs = velocityPxPerMs * distanceInverse;
+ float oneFrameProgress = velocityProgressPerMs * getSingleFrameMs(context);
float nextFrameProgress = boundToRange(getProgressFraction()
- + scaledVelocity * getSingleFrameMs(context), 0f, 1f);
+ + oneFrameProgress, 0f, 1f);
// Update setters for spring
int springFlag = goingToEnd
@@ -162,8 +161,8 @@
SpringAnimationBuilder s = new SpringAnimationBuilder(context)
.setStartValue(mCurrentFraction)
.setEndValue(goingToEnd ? 1 : 0)
- .setStartVelocity(scaledVelocity)
- .setMinimumVisibleChange(scaleInverse)
+ .setStartVelocity(velocityProgressPerMs)
+ .setMinimumVisibleChange(distanceInverse)
.setDampingRatio(h.springProperty.mDampingRatio)
.setStiffness(h.springProperty.mStiffness)
.computeParams();
@@ -172,8 +171,18 @@
springDuration = Math.max(expectedDurationL, springDuration);
float expectedDuration = expectedDurationL;
- h.mapper = (progress, globalEndProgress) ->
- mAnimationPlayer.getCurrentPlayTime() / expectedDuration;
+ h.mapper = (progress, globalEndProgress) -> {
+ if (expectedDuration <= 0 || oneFrameProgress >= 1) {
+ return 1;
+ } else {
+ // Start from one frame ahead of the current position.
+ return Utilities.mapToRange(
+ mAnimationPlayer.getCurrentPlayTime() / expectedDuration,
+ 0, 1,
+ Math.abs(oneFrameProgress), 1,
+ LINEAR);
+ }
+ };
h.anim.setInterpolator(s::getInterpolatedValue);
}
}
@@ -182,7 +191,7 @@
if (springDuration <= animationDuration) {
mAnimationPlayer.setDuration(animationDuration);
- mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocity));
+ mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocityPxPerMs));
} else {
// Since spring requires more time to run, we let the other animations play with
// current time and interpolation and by clamping the duration.
@@ -190,7 +199,7 @@
float cutOff = animationDuration / (float) springDuration;
mAnimationPlayer.setInterpolator(
- clampToProgress(scrollInterpolatorForVelocity(velocity), 0, cutOff));
+ clampToProgress(scrollInterpolatorForVelocity(velocityPxPerMs), 0, cutOff));
}
mAnimationPlayer.start();
}
@@ -269,46 +278,29 @@
}
}
- /** @see #dispatchOnCancelWithoutCancelRunnable(Runnable) */
- public void dispatchOnCancelWithoutCancelRunnable() {
- dispatchOnCancelWithoutCancelRunnable(null);
- }
-
- /**
- * Sets mOnCancelRunnable = null before dispatching the cancel and restoring the runnable. This
- * is intended to be used only if you need to cancel but want to defer cleaning up yourself.
- * @param callback An optional callback to run after dispatching the cancel but before resetting
- * the onCancelRunnable.
- */
- public void dispatchOnCancelWithoutCancelRunnable(@Nullable Runnable callback) {
- Runnable onCancel = mOnCancelRunnable;
- setOnCancelRunnable(null);
- dispatchOnCancel();
- if (callback != null) {
- callback.run();
- }
- setOnCancelRunnable(onCancel);
- }
-
-
- public AnimatorPlaybackController setOnCancelRunnable(Runnable runnable) {
- mOnCancelRunnable = runnable;
+ public AnimatorPlaybackController dispatchOnStart() {
+ callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
return this;
}
- public void dispatchOnStart() {
- callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
+ public AnimatorPlaybackController dispatchOnCancel() {
+ callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel);
+ return this;
}
- public void dispatchOnCancel() {
- callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel);
+ public AnimatorPlaybackController dispatchOnEnd() {
+ callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
+ return this;
}
public void dispatchSetInterpolator(TimeInterpolator interpolator) {
callAnimatorCommandRecursively(mAnim, a -> a.setInterpolator(interpolator));
}
- private static void callListenerCommandRecursively(
+ /**
+ * Recursively calls a command on all the listeners of the provided animation
+ */
+ public static void callListenerCommandRecursively(
Animator anim, BiConsumer<AnimatorListener, Animator> command) {
callAnimatorCommandRecursively(anim, a-> {
for (AnimatorListener l : nonNullList(a.getListeners())) {
@@ -343,7 +335,7 @@
public void onAnimationSuccess(Animator animator) {
// We wait for the spring (if any) to finish running before completing the end callback.
if (!mDispatched) {
- callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
+ dispatchOnEnd();
if (mEndAction != null) {
mEndAction.run();
}
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 860cceb..1e7b224 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -16,9 +16,6 @@
package com.android.launcher3.anim;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
-
-import android.content.Context;
import android.graphics.Path;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
@@ -30,7 +27,6 @@
import com.android.launcher3.Utilities;
-
/**
* Common interpolators used in Launcher
*/
@@ -39,6 +35,7 @@
public static final Interpolator LINEAR = new LinearInterpolator();
public static final Interpolator ACCEL = new AccelerateInterpolator();
+ public static final Interpolator ACCEL_0_5 = new AccelerateInterpolator(0.5f);
public static final Interpolator ACCEL_0_75 = new AccelerateInterpolator(0.75f);
public static final Interpolator ACCEL_1_5 = new AccelerateInterpolator(1.5f);
public static final Interpolator ACCEL_2 = new AccelerateInterpolator(2);
@@ -49,7 +46,6 @@
public static final Interpolator DEACCEL_2 = new DecelerateInterpolator(2);
public static final Interpolator DEACCEL_2_5 = new DecelerateInterpolator(2.5f);
public static final Interpolator DEACCEL_3 = new DecelerateInterpolator(3f);
- public static final Interpolator DEACCEL_5 = new DecelerateInterpolator(5f);
public static final Interpolator ACCEL_DEACCEL = new AccelerateDecelerateInterpolator();
@@ -58,6 +54,9 @@
public static final Interpolator AGGRESSIVE_EASE = new PathInterpolator(0.2f, 0f, 0f, 1f);
public static final Interpolator AGGRESSIVE_EASE_IN_OUT = new PathInterpolator(0.6f,0, 0.4f, 1);
+ public static final Interpolator DECELERATED_EASE = new PathInterpolator(0, 0, .2f, 1f);
+ public static final Interpolator ACCELERATED_EASE = new PathInterpolator(0.4f, 0, 1f, 1f);
+
public static final Interpolator EXAGGERATED_EASE;
public static final Interpolator INSTANT = t -> 1;
@@ -67,9 +66,6 @@
*/
public static final Interpolator FINAL_FRAME = t -> t < 1 ? 0 : 1;
- private static final int MIN_SETTLE_DURATION = 200;
- private static final float OVERSHOOT_FACTOR = 0.9f;
-
static {
Path exaggeratedEase = new Path();
exaggeratedEase.moveTo(0, 0);
@@ -83,6 +79,9 @@
public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR =
new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+ public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR_ACCEL_DEACCEL =
+ v -> ACCEL_DEACCEL.getInterpolation(TOUCH_RESPONSE_INTERPOLATOR.getInterpolation(v));
+
/**
* Inversion of ZOOM_OUT, compounded with an ease-out.
@@ -151,11 +150,15 @@
*/
public static Interpolator clampToProgress(Interpolator interpolator, float lowerBound,
float upperBound) {
- if (upperBound <= lowerBound) {
- throw new IllegalArgumentException(String.format(
- "lowerBound (%f) must be less than upperBound (%f)", lowerBound, upperBound));
+ if (upperBound < lowerBound) {
+ throw new IllegalArgumentException(
+ String.format("upperBound (%f) must be greater than lowerBound (%f)",
+ upperBound, lowerBound));
}
return t -> {
+ if (t == lowerBound && t == upperBound) {
+ return t == 0f ? 0 : 1;
+ }
if (t < lowerBound) {
return 0;
}
@@ -175,75 +178,4 @@
float upperBound) {
return t -> Utilities.mapRange(interpolator.getInterpolation(t), lowerBound, upperBound);
}
-
- /**
- * Computes parameters necessary for an overshoot effect.
- */
- public static class OvershootParams {
- public Interpolator interpolator;
- public float start;
- public float end;
- public long duration;
-
- /**
- * Given the input params, sets OvershootParams variables to be used by the caller.
- * @param startProgress The progress from 0 to 1 that the overshoot starts from.
- * @param overshootPastProgress The progress from 0 to 1 where we overshoot past (should
- * either be equal to startProgress or endProgress, depending on if we want to
- * overshoot immediately or only once we reach the end).
- * @param endProgress The final progress from 0 to 1 that we will settle to.
- * @param velocityPxPerMs The initial velocity that causes this overshoot.
- * @param totalDistancePx The distance against which progress is calculated.
- */
- public OvershootParams(float startProgress, float overshootPastProgress,
- float endProgress, float velocityPxPerMs, int totalDistancePx, Context context) {
- velocityPxPerMs = Math.abs(velocityPxPerMs);
- start = startProgress;
- int startPx = (int) (start * totalDistancePx);
- // Overshoot by about half a frame.
- float overshootBy = OVERSHOOT_FACTOR * velocityPxPerMs *
- getSingleFrameMs(context) / totalDistancePx / 2;
- overshootBy = Utilities.boundToRange(overshootBy, 0.02f, 0.15f);
- end = overshootPastProgress + overshootBy;
- int endPx = (int) (end * totalDistancePx);
- int overshootDistance = endPx - startPx;
- // Calculate deceleration necessary to reach overshoot distance.
- // Formula: velocityFinal^2 = velocityInitial^2 + 2 * acceleration * distance
- // 0 = v^2 + 2ad (velocityFinal == 0)
- // a = v^2 / -2d
- float decelerationPxPerMs = velocityPxPerMs * velocityPxPerMs / (2 * overshootDistance);
- // Calculate time necessary to reach peak of overshoot.
- // Formula: acceleration = velocity / time
- // time = velocity / acceleration
- duration = (long) (velocityPxPerMs / decelerationPxPerMs);
-
- // Now that we're at the top of the overshoot, need to settle back to endProgress.
- float settleDistance = end - endProgress;
- int settleDistancePx = (int) (settleDistance * totalDistancePx);
- // Calculate time necessary for the settle.
- // Formula: distance = velocityInitial * time + 1/2 * acceleration * time^2
- // d = 1/2at^2 (velocityInitial = 0, since we just stopped at the top)
- // t = sqrt(2d/a)
- // Above formula assumes constant acceleration. Since we use ACCEL_DEACCEL, we actually
- // have acceleration to halfway then deceleration the rest. So the formula becomes:
- // t = sqrt(d/a) * 2 (half the distance for accel, half for deaccel)
- long settleDuration = (long) Math.sqrt(settleDistancePx / decelerationPxPerMs) * 4;
-
- settleDuration = Math.max(MIN_SETTLE_DURATION, settleDuration);
- // How much of the animation to devote to playing the overshoot (the rest is for settle).
- float overshootFraction = (float) duration / (duration + settleDuration);
- duration += settleDuration;
- // Finally, create the interpolator, composed of two interpolators: an overshoot, which
- // reaches end > 1, and then a settle to endProgress.
- Interpolator overshoot = Interpolators.clampToProgress(DEACCEL, 0, overshootFraction);
- // The settle starts at 1, where 1 is the top of the overshoot, and maps to a fraction
- // such that final progress is endProgress. For example, if we overshot to 1.1 but want
- // to end at 1, we need to map to 1/1.1.
- Interpolator settle = Interpolators.clampToProgress(Interpolators.mapToProgress(
- ACCEL_DEACCEL, 1, (endProgress - start) / (end - start)), overshootFraction, 1);
- interpolator = t -> t <= overshootFraction
- ? overshoot.getInterpolation(t)
- : settle.getInterpolation(t);
- }
- }
}
diff --git a/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java b/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java
new file mode 100644
index 0000000..ef4ada3
--- /dev/null
+++ b/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.anim;
+
+import android.os.Build;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowInsetsAnimation;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.launcher3.Utilities;
+
+import java.util.List;
+
+/**
+ * Callback that animates views above the IME
+ */
+@RequiresApi(api = Build.VERSION_CODES.R)
+public class KeyboardInsetAnimationCallback extends WindowInsetsAnimation.Callback {
+ private final View mView;
+
+ private float mInitialTranslation;
+ private float mTerminalTranslation;
+
+ public KeyboardInsetAnimationCallback(View view) {
+ super(DISPATCH_MODE_STOP);
+ mView = view;
+ }
+
+ @Override
+ public void onPrepare(WindowInsetsAnimation animation) {
+ mInitialTranslation = mView.getTranslationY();
+ }
+
+
+ @Override
+ public WindowInsets onProgress(WindowInsets windowInsets, List<WindowInsetsAnimation> list) {
+ if (list.size() == 0) {
+ mView.setTranslationY(mInitialTranslation);
+ return windowInsets;
+ }
+ float progress = list.get(0).getInterpolatedFraction();
+
+ mView.setTranslationY(
+ Utilities.mapRange(progress, mInitialTranslation, mTerminalTranslation));
+
+ return windowInsets;
+ }
+
+ @Override
+ public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation,
+ WindowInsetsAnimation.Bounds bounds) {
+ mTerminalTranslation = mView.getTranslationY();
+ mView.setTranslationY(mInitialTranslation);
+ return super.onStart(animation, bounds);
+ }
+}
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index 4195933..01f7de6 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.anim;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
import static com.android.launcher3.anim.AnimatorPlaybackController.addAnimationHoldersRecur;
import android.animation.Animator;
@@ -23,6 +24,7 @@
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
+import android.graphics.drawable.ColorDrawable;
import android.util.FloatProperty;
import android.util.IntProperty;
import android.view.View;
@@ -43,8 +45,6 @@
*/
public class PendingAnimation implements PropertySetter {
- private final ArrayList<Consumer<EndState>> mEndListeners = new ArrayList<>();
-
private final ArrayList<Holder> mAnimHolders = new ArrayList<>();
private final AnimatorSet mAnim;
private final long mDuration;
@@ -73,13 +73,6 @@
addAnimationHoldersRecur(a, mDuration, springProperty, mAnimHolders);
}
- public void finish(boolean isSuccess, int logAction) {
- for (Consumer<EndState> listeners : mEndListeners) {
- listeners.accept(new EndState(isSuccess, logAction));
- }
- mEndListeners.clear();
- }
-
@Override
public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
if (view == null || view.getAlpha() == alpha) {
@@ -92,6 +85,17 @@
}
@Override
+ public void setViewBackgroundColor(View view, int color, TimeInterpolator interpolator) {
+ if (view == null || (view.getBackground() instanceof ColorDrawable
+ && ((ColorDrawable) view.getBackground()).getColor() == color)) {
+ return;
+ }
+ ObjectAnimator anim = ObjectAnimator.ofArgb(view, VIEW_BACKGROUND_COLOR, color);
+ anim.setInterpolator(interpolator);
+ add(anim);
+ }
+
+ @Override
public <T> void setFloat(T target, FloatProperty<T> property, float value,
TimeInterpolator interpolator) {
if (property.get(target) == value) {
@@ -124,11 +128,18 @@
* Adds a callback to be run on every frame of the animation
*/
public void addOnFrameCallback(Runnable runnable) {
+ addOnFrameListener(anim -> runnable.run());
+ }
+
+ /**
+ * Adds a listener to be run on every frame of the animation
+ */
+ public void addOnFrameListener(ValueAnimator.AnimatorUpdateListener listener) {
if (mProgressAnimator == null) {
mProgressAnimator = ValueAnimator.ofFloat(0, 1);
}
- mProgressAnimator.addUpdateListener(anim -> runnable.run());
+ mProgressAnimator.addUpdateListener(listener);
}
/**
@@ -149,7 +160,7 @@
mProgressAnimator = null;
}
if (mAnimHolders.isEmpty()) {
- // Add a dummy animation to that the duration is respected
+ // Add a placeholder animation to that the duration is respected
add(ValueAnimator.ofFloat(0, 1).setDuration(mDuration));
}
return mAnim;
@@ -163,21 +174,12 @@
}
/**
- * Add a listener of receiving the end state.
- * Note that the listeners are called as a result of calling {@link #finish(boolean, int)}
- * and not automatically
+ * Add a listener of receiving the success/failure callback in the end.
*/
- public void addEndListener(Consumer<EndState> listener) {
- mEndListeners.add(listener);
- }
-
- public static class EndState {
- public boolean isSuccess;
- public int logAction;
-
- public EndState(boolean isSuccess, int logAction) {
- this.isSuccess = isSuccess;
- this.logAction = logAction;
+ public void addEndListener(Consumer<Boolean> listener) {
+ if (mProgressAnimator == null) {
+ mProgressAnimator = ValueAnimator.ofFloat(0, 1);
}
+ mProgressAnimator.addListener(AnimatorListeners.forEndCallback(listener));
}
}
diff --git a/src/com/android/launcher3/anim/PropertySetter.java b/src/com/android/launcher3/anim/PropertySetter.java
index 2ce620b..729523f 100644
--- a/src/com/android/launcher3/anim/PropertySetter.java
+++ b/src/com/android/launcher3/anim/PropertySetter.java
@@ -41,6 +41,15 @@
}
/**
+ * Sets the background color of the provided view using the provided interpolator.
+ */
+ default void setViewBackgroundColor(View view, int color, TimeInterpolator interpolator) {
+ if (view != null) {
+ view.setBackgroundColor(color);
+ }
+ }
+
+ /**
* Updates the float property of the target using the provided interpolator
*/
default <T> void setFloat(T target, FloatProperty<T> property, float value,
diff --git a/src/com/android/launcher3/anim/SpringAnimationBuilder.java b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
index a9702b4..bd52158 100644
--- a/src/com/android/launcher3/anim/SpringAnimationBuilder.java
+++ b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
@@ -25,7 +25,7 @@
import androidx.annotation.FloatRange;
import androidx.dynamicanimation.animation.SpringForce;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController;
/**
* Utility class to build an object animator which follows the same path as a spring animation for
@@ -134,7 +134,7 @@
}
public SpringAnimationBuilder computeParams() {
- int singleFrameMs = DefaultDisplay.getSingleFrameMs(mContext);
+ int singleFrameMs = DisplayController.getSingleFrameMs(mContext);
double naturalFreq = Math.sqrt(mStiffness);
double dampedFreq = naturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 1d32d1d..30c3417 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -70,7 +70,8 @@
final Bundle parcel = new Bundle();
parcel.putInt(TestProtocol.STATE_FIELD, stateOrdinal);
- sendEventToTest(accessibilityManager, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
+ sendEventToTest(
+ accessibilityManager, context, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
Log.d(TestProtocol.PERMANENT_DIAG_TAG, "sendStateEventToTest: " + stateOrdinal);
}
@@ -78,22 +79,24 @@
final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
if (accessibilityManager == null) return;
- sendEventToTest(accessibilityManager, TestProtocol.SCROLL_FINISHED_MESSAGE, null);
+ sendEventToTest(accessibilityManager, context, TestProtocol.SCROLL_FINISHED_MESSAGE, null);
}
public static void sendPauseDetectedEventToTest(Context context) {
final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
if (accessibilityManager == null) return;
- sendEventToTest(accessibilityManager, TestProtocol.PAUSE_DETECTED_MESSAGE, null);
+ sendEventToTest(accessibilityManager, context, TestProtocol.PAUSE_DETECTED_MESSAGE, null);
}
private static void sendEventToTest(
- AccessibilityManager accessibilityManager, String eventTag, Bundle data) {
+ AccessibilityManager accessibilityManager,
+ Context context, String eventTag, Bundle data) {
final AccessibilityEvent e = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_ANNOUNCEMENT);
e.setClassName(eventTag);
e.setParcelableData(data);
+ e.setPackageName(context.getApplicationContext().getPackageName());
accessibilityManager.sendAccessibilityEvent(e);
}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5c4a492..f066dc1 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -72,15 +72,15 @@
"PROMISE_APPS_NEW_INSTALLS", true,
"Adds a promise icon to the home screen for new install sessions.");
- public static final BooleanFlag APPLY_CONFIG_AT_RUNTIME = getDebugFlag(
- "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
-
public static final BooleanFlag QUICKSTEP_SPRINGS = getDebugFlag(
"QUICKSTEP_SPRINGS", true, "Enable springs for quickstep animations");
public static final BooleanFlag UNSTABLE_SPRINGS = getDebugFlag(
"UNSTABLE_SPRINGS", false, "Enable unstable springs for quickstep animations");
+ public static final BooleanFlag ENABLE_LOCAL_COLOR_POPUPS = getDebugFlag(
+ "ENABLE_LOCAL_COLOR_POPUPS", false, "Enable local color extraction for popups.");
+
public static final BooleanFlag KEYGUARD_ANIMATION = getDebugFlag(
"KEYGUARD_ANIMATION", false, "Enable animation for keyguard going away on wallpaper");
@@ -88,12 +88,31 @@
"ADAPTIVE_ICON_WINDOW_ANIM", true, "Use adaptive icons for window animations.");
public static final BooleanFlag ENABLE_QUICKSTEP_LIVE_TILE = getDebugFlag(
- "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
+ "ENABLE_QUICKSTEP_LIVE_TILE", true, "Enable live tile in Quickstep overview");
+
+ public static final BooleanFlag ENABLE_QUICKSTEP_WIDGET_APP_START = getDebugFlag(
+ "ENABLE_QUICKSTEP_WIDGET_APP_START", true,
+ "Enable Quickstep animation when launching activities from an app widget");
// Keep as DeviceFlag to allow remote disable in emergency.
public static final BooleanFlag ENABLE_SUGGESTED_ACTIONS_OVERVIEW = new DeviceFlag(
"ENABLE_SUGGESTED_ACTIONS_OVERVIEW", false, "Show chip hints on the overview screen");
+
+ public static final BooleanFlag ENABLE_DEVICE_SEARCH = new DeviceFlag(
+ "ENABLE_DEVICE_SEARCH", true, "Allows on device search in all apps");
+
+ public static final BooleanFlag ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING = new DeviceFlag(
+ "ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING", true,
+ "Allows on device search in all apps logging");
+
+ public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(
+ "IME_STICKY_SNACKBAR_EDU", true, "Show sticky IME edu in AllApps");
+
+ public static final BooleanFlag ENABLE_PEOPLE_TILE_PREVIEW = getDebugFlag(
+ "ENABLE_PEOPLE_TILE_PREVIEW", false,
+ "Experimental: Shows conversation shortcuts on home screen as search results");
+
public static final BooleanFlag FOLDER_NAME_SUGGEST = new DeviceFlag(
"FOLDER_NAME_SUGGEST", true,
"Suggests folder names instead of blank text.");
@@ -102,10 +121,6 @@
"FOLDER_NAME_MAJORITY_RANKING", true,
"Suggests folder names based on majority based ranking.");
- public static final BooleanFlag APP_SEARCH_IMPROVEMENTS = new DeviceFlag(
- "APP_SEARCH_IMPROVEMENTS", true,
- "Adds localized title and keyword search and ranking");
-
public static final BooleanFlag ENABLE_PREDICTION_DISMISS = getDebugFlag(
"ENABLE_PREDICTION_DISMISS", true, "Allow option to dimiss apps from predicted list");
@@ -123,9 +138,6 @@
"ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
"Allow Launcher to handle nav bar gestures while Assistant is running over it");
- public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = getDebugFlag(
- "ENABLE_HYBRID_HOTSEAT", true, "Fill gaps in hotseat with predicted apps");
-
public static final BooleanFlag HOTSEAT_MIGRATE_TO_FOLDER = getDebugFlag(
"HOTSEAT_MIGRATE_TO_FOLDER", false, "Should move hotseat items into a folder");
@@ -135,31 +147,46 @@
public static final BooleanFlag MULTI_DB_GRID_MIRATION_ALGO = getDebugFlag(
"MULTI_DB_GRID_MIRATION_ALGO", true, "Use the multi-db grid migration algorithm");
- public static final BooleanFlag ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER = getDebugFlag(
- "ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER", true, "Show launcher preview in grid picker");
-
- public static final BooleanFlag ENABLE_OVERVIEW_ACTIONS = getDebugFlag(
- "ENABLE_OVERVIEW_ACTIONS", true, "Show app actions instead of the shelf in Overview."
- + " As part of this decoupling, also distinguish swipe up from nav bar vs above it.");
+ public static final BooleanFlag ENABLE_THEMED_ICONS = getDebugFlag(
+ "ENABLE_THEMED_ICONS", true, "Enable themed icons on workspace");
// Keep as DeviceFlag for remote disable in emergency.
public static final BooleanFlag ENABLE_OVERVIEW_SELECTIONS = new DeviceFlag(
"ENABLE_OVERVIEW_SELECTIONS", true, "Show Select Mode button in Overview Actions");
+ public static final BooleanFlag ENABLE_WIDGETS_PICKER_AIAI_SEARCH = new DeviceFlag(
+ "ENABLE_WIDGETS_PICKER_AIAI_SEARCH", false, "Enable AiAi search in the widgets picker");
+
public static final BooleanFlag ENABLE_OVERVIEW_SHARE = getDebugFlag(
"ENABLE_OVERVIEW_SHARE", false, "Show Share button in Overview Actions");
+ public static final BooleanFlag ENABLE_OVERVIEW_SHARING_TO_PEOPLE = getDebugFlag(
+ "ENABLE_OVERVIEW_SHARING_TO_PEOPLE", true,
+ "Show indicators for content on Overview to share with top people. ");
+
+ public static final BooleanFlag ENABLE_OVERVIEW_CONTENT_PUSH = getDebugFlag(
+ "ENABLE_OVERVIEW_CONTENT_PUSH", false, "Show Content Push button in Overview Actions");
+
public static final BooleanFlag ENABLE_DATABASE_RESTORE = getDebugFlag(
- "ENABLE_DATABASE_RESTORE", true,
+ "ENABLE_DATABASE_RESTORE", false,
"Enable database restore when new restore session is created");
- public static final BooleanFlag ENABLE_UNIVERSAL_SMARTSPACE = getDebugFlag(
- "ENABLE_UNIVERSAL_SMARTSPACE", false,
+ public static final BooleanFlag ENABLE_SMARTSPACE_UNIVERSAL = getDebugFlag(
+ "ENABLE_SMARTSPACE_UNIVERSAL", false,
"Replace Smartspace with a version rendered by System UI.");
- public static final BooleanFlag ENABLE_LSQ_VELOCITY_PROVIDER = getDebugFlag(
- "ENABLE_LSQ_VELOCITY_PROVIDER", true,
- "Use Least Square algorithm for motion pause detection.");
+ public static final BooleanFlag ENABLE_SMARTSPACE_ENHANCED = getDebugFlag(
+ "ENABLE_SMARTSPACE_ENHANCED", true,
+ "Replace Smartspace with the enhanced version. "
+ + "Ignored if ENABLE_SMARTSPACE_UNIVERSAL is enabled.");
+
+ public static final BooleanFlag ENABLE_SMARTSPACE_FEEDBACK = getDebugFlag(
+ "ENABLE_SMARTSPACE_FEEDBACK", true,
+ "Adds a menu option to send feedback for Enhanced Smartspace.");
+
+ public static final BooleanFlag ENABLE_SMARTSPACE_DISMISS = getDebugFlag(
+ "ENABLE_SMARTSPACE_DISMISS", true,
+ "Adds a menu option to dismiss the current Enhanced Smartspace card.");
public static final BooleanFlag ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS =
getDebugFlag(
@@ -174,8 +201,58 @@
"SEPARATE_RECENTS_ACTIVITY", false,
"Uses a separate recents activity instead of using the integrated recents+Launcher UI");
- public static final BooleanFlag USER_EVENT_DISPATCHER = new DeviceFlag(
- "USER_EVENT_DISPATCHER", true, "User event dispatcher collects logs.");
+ public static final BooleanFlag ENABLE_MINIMAL_DEVICE = getDebugFlag(
+ "ENABLE_MINIMAL_DEVICE", false,
+ "Allow user to toggle minimal device mode in launcher.");
+
+ public static final BooleanFlag EXPANDED_SMARTSPACE = new DeviceFlag(
+ "EXPANDED_SMARTSPACE", false, "Expands smartspace height to two rows. "
+ + "Any apps occupying the first row will be removed from workspace.");
+
+ // TODO: b/172467144 Remove ENABLE_LAUNCHER_ACTIVITY_THEME_CROSSFADE feature flag.
+ public static final BooleanFlag ENABLE_LAUNCHER_ACTIVITY_THEME_CROSSFADE = new DeviceFlag(
+ "ENABLE_LAUNCHER_ACTIVITY_THEME_CROSSFADE", false, "Enables a "
+ + "crossfade animation when the system these changes.");
+
+ // TODO: b/174174514 Remove ENABLE_APP_PREDICTIONS_WHILE_VISIBLE feature flag.
+ public static final BooleanFlag ENABLE_APP_PREDICTIONS_WHILE_VISIBLE = new DeviceFlag(
+ "ENABLE_APP_PREDICTIONS_WHILE_VISIBLE", true, "Allows app "
+ + "predictions to be updated while they are visible to the user.");
+
+ public static final BooleanFlag ENABLE_TASKBAR = getDebugFlag(
+ "ENABLE_TASKBAR", false, "Allows a system Taskbar to be shown on larger devices.");
+
+ public static final BooleanFlag ENABLE_OVERVIEW_GRID = getDebugFlag(
+ "ENABLE_OVERVIEW_GRID", false, "Uses grid overview layout. "
+ + "Only applicable on large screen devices.");
+
+ public static final BooleanFlag ENABLE_TWO_PANEL_HOME = getDebugFlag(
+ "ENABLE_TWO_PANEL_HOME", false,
+ "Uses two panel on home screen. Only applicable on large screen devices.");
+
+ public static final BooleanFlag ENABLE_SCRIM_FOR_APP_LAUNCH = getDebugFlag(
+ "ENABLE_SCRIM_FOR_APP_LAUNCH", false,
+ "Enables scrim during app launch animation.");
+
+ public static final BooleanFlag ENABLE_SPLIT_SELECT = getDebugFlag(
+ "ENABLE_SPLIT_SELECT", false, "Uses new split screen selection overview UI");
+
+ public static final BooleanFlag ENABLE_ENFORCED_ROUNDED_CORNERS = new DeviceFlag(
+ "ENABLE_ENFORCED_ROUNDED_CORNERS", true, "Enforce rounded corners on all App Widgets");
+
+ public static final BooleanFlag ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER = new DeviceFlag(
+ "ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER", true,
+ "Enables a local filter for recommended widgets.");
+
+ public static final BooleanFlag NOTIFY_CRASHES = getDebugFlag("NOTIFY_CRASHES", false,
+ "Sends a notification whenever launcher encounters an uncaught exception.");
+
+ public static final BooleanFlag PROTOTYPE_APP_CLOSE = getDebugFlag(
+ "PROTOTYPE_APP_CLOSE", false, "Enables new app close");
+
+ public static final BooleanFlag ENABLE_WALLPAPER_SCRIM = getDebugFlag(
+ "ENABLE_WALLPAPER_SCRIM", false,
+ "Enables scrim over wallpaper for text protection.");
public static void initialize(Context context) {
synchronized (sDebugFlags) {
@@ -235,6 +312,8 @@
}
public void addChangeListener(Context context, Runnable r) { }
+
+ public void removeChangeListener(Runnable r) {}
}
public static class DebugFlag extends BooleanFlag {
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 0df6713..55be4a4 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -16,19 +16,24 @@
package com.android.launcher3.dragndrop;
-import static com.android.launcher3.logging.LoggerUtils.newCommandAction;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.logging.LoggerUtils.newItemTarget;
-import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PIN_WIDGETS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_BACK;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_CANCELLED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_EXTERNAL_ITEM_START;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.annotation.TargetApi;
import android.app.ActivityOptions;
import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
import android.content.pm.LauncherApps.PinItemRequest;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.PointF;
@@ -36,36 +41,49 @@
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
+import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.DragShadowBuilder;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.TextView;
import com.android.launcher3.BaseActivity;
-import com.android.launcher3.InstallShortcutReceiver;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.R;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.PinRequestHelper;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.InstantAppResolver;
+import com.android.launcher3.util.SystemUiController;
+import com.android.launcher3.views.AbstractSlideInView;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.AddItemWidgetsBottomSheet;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.NavigableAppWidgetHostView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
-import com.android.launcher3.widget.WidgetHostViewLoader;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetCellPreview;
import com.android.launcher3.widget.WidgetImageView;
import com.android.launcher3.widget.WidgetManagerHelper;
import java.util.function.Supplier;
+/**
+ * Activity to show pin widget dialog.
+ */
@TargetApi(Build.VERSION_CODES.O)
-public class AddItemActivity extends BaseActivity implements OnLongClickListener, OnTouchListener {
+public class AddItemActivity extends BaseActivity
+ implements OnLongClickListener, OnTouchListener, AbstractSlideInView.OnCloseListener {
private static final int SHADOW_SIZE = 10;
@@ -77,8 +95,11 @@
private PinItemRequest mRequest;
private LauncherAppState mApp;
private InvariantDeviceProfile mIdp;
+ private BaseDragLayer<AddItemActivity> mDragLayer;
+ private AddItemWidgetsBottomSheet mSlideInView;
+ private AccessibilityManager mAccessibilityManager;
- private LivePreviewWidgetCell mWidgetCell;
+ private WidgetCell mWidgetCell;
// Widget request specific options.
private LauncherAppWidgetHost mAppWidgetHost;
@@ -87,7 +108,6 @@
private Bundle mWidgetOptions;
private boolean mFinishOnPause = false;
- private InstantAppResolver mInstantAppResolver;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -101,14 +121,20 @@
mApp = LauncherAppState.getInstance(this);
mIdp = mApp.getInvariantDeviceProfile();
- mInstantAppResolver = InstantAppResolver.newInstance(this);
// Use the application context to get the device profile, as in multiwindow-mode, the
// confirmation activity might be rotated.
mDeviceProfile = mIdp.getDeviceProfile(getApplicationContext());
setContentView(R.layout.add_item_confirmation_activity);
+ // Set flag to allow activity to draw over navigation and status bar.
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
+ mDragLayer = findViewById(R.id.add_item_drag_layer);
+ mDragLayer.recreateControllers();
mWidgetCell = findViewById(R.id.widget_cell);
+ mAccessibilityManager =
+ getApplicationContext().getSystemService(AccessibilityManager.class);
if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) {
setupShortcut();
@@ -119,14 +145,24 @@
}
}
- mWidgetCell.setOnTouchListener(this);
- mWidgetCell.setOnLongClickListener(this);
+ WidgetCellPreview previewContainer = mWidgetCell.findViewById(
+ R.id.widget_preview_container);
+ previewContainer.setOnTouchListener(this);
+ previewContainer.setOnLongClickListener(this);
// savedInstanceState is null when the activity is created the first time (i.e., avoids
// duplicate logging during rotation)
if (savedInstanceState == null) {
- logCommand(Action.Command.ENTRY);
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_START);
}
+
+ TextView widgetAppName = findViewById(R.id.widget_appName);
+ widgetAppName.setText(getApplicationInfo().labelRes);
+
+ mSlideInView = findViewById(R.id.add_item_bottom_sheet);
+ mSlideInView.addOnCloseListener(this);
+ mSlideInView.show();
+ setupNavBarColor();
}
@Override
@@ -139,20 +175,31 @@
public boolean onLongClick(View view) {
// Find the position of the preview relative to the touch location.
WidgetImageView img = mWidgetCell.getWidgetView();
+ NavigableAppWidgetHostView appWidgetHostView = mWidgetCell.getAppWidgetHostViewPreview();
// If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and
// we abort the drag.
- if (img.getBitmap() == null) {
+ if (img.getDrawable() == null && appWidgetHostView == null) {
return false;
}
- Rect bounds = img.getBitmapBounds();
- bounds.offset(img.getLeft() - (int) mLastTouchPos.x, img.getTop() - (int) mLastTouchPos.y);
-
+ final Rect bounds;
// Start home and pass the draw request params
- PinItemDragListener listener = new PinItemDragListener(mRequest, bounds,
- img.getBitmap().getWidth(), img.getWidth());
-
+ final PinItemDragListener listener;
+ if (appWidgetHostView != null) {
+ bounds = new Rect();
+ appWidgetHostView.getSourceVisualDragBounds(bounds);
+ bounds.offset(appWidgetHostView.getLeft() - (int) mLastTouchPos.x,
+ appWidgetHostView.getTop() - (int) mLastTouchPos.y);
+ listener = new PinItemDragListener(mRequest, bounds,
+ appWidgetHostView.getMeasuredWidth(), appWidgetHostView.getMeasuredWidth());
+ } else {
+ bounds = img.getBitmapBounds();
+ bounds.offset(img.getLeft() - (int) mLastTouchPos.x,
+ img.getTop() - (int) mLastTouchPos.y);
+ listener = new PinItemDragListener(mRequest, bounds,
+ img.getDrawable().getIntrinsicWidth(), img.getWidth());
+ }
// Start a system drag and drop. We use a transparent bitmap as preview for system drag
// as the preview is handled internally by launcher.
@@ -174,10 +221,11 @@
.addCategory(Intent.CATEGORY_HOME)
.setPackage(getPackageName())
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Launcher.ACTIVITY_TRACKER.runCallbackWhenActivityExists(listener, homeIntent);
+ Launcher.ACTIVITY_TRACKER.registerCallback(listener);
startActivity(homeIntent,
ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out)
.toBundle());
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED);
mFinishOnPause = true;
return false;
}
@@ -205,15 +253,16 @@
// Cannot add widget
return false;
}
- mWidgetCell.setPreview(PinItemDragListener.getPreview(mRequest));
+ mWidgetCell.setRemoteViewsPreview(PinItemDragListener.getPreview(mRequest));
mAppWidgetManager = new WidgetManagerHelper(this);
mAppWidgetHost = new LauncherAppWidgetHost(this);
- PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(widgetInfo);
+ PendingAddWidgetInfo pendingInfo =
+ new PendingAddWidgetInfo(widgetInfo, CONTAINER_PIN_WIDGETS);
pendingInfo.spanX = Math.min(mIdp.numColumns, widgetInfo.spanX);
pendingInfo.spanY = Math.min(mIdp.numRows, widgetInfo.spanY);
- mWidgetOptions = WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo);
+ mWidgetOptions = pendingInfo.getDefaultSizeOptions(this);
mWidgetCell.getWidgetView().setTag(pendingInfo);
applyWidgetItemAsync(() -> new WidgetItem(widgetInfo, mIdp, mApp.getIconCache()));
@@ -229,6 +278,7 @@
@Override
protected void onPostExecute(WidgetItem item) {
+ mWidgetCell.setPreviewSize(item);
mWidgetCell.applyFromCellItem(item, mApp.getWidgetCache());
mWidgetCell.ensurePreview();
}
@@ -240,8 +290,8 @@
* Called when the cancel button is clicked.
*/
public void onCancelClick(View v) {
- logCommand(Action.Command.CANCEL);
- finish();
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_CANCELLED);
+ mSlideInView.close(/* animate= */ true);
}
/**
@@ -249,17 +299,25 @@
*/
public void onPlaceAutomaticallyClick(View v) {
if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) {
- InstallShortcutReceiver.queueShortcut(mRequest.getShortcutInfo(), this);
- logCommand(Action.Command.CONFIRM);
+ ShortcutInfo shortcutInfo = mRequest.getShortcutInfo();
+ ItemInstallQueue.INSTANCE.get(this).queueItem(shortcutInfo);
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY);
mRequest.accept();
- finish();
+ CharSequence label = shortcutInfo.getLongLabel();
+ if (TextUtils.isEmpty(label)) {
+ label = shortcutInfo.getShortLabel();
+ }
+ sendWidgetAddedToScreenAccessibilityEvent(label.toString());
+ mSlideInView.close(/* animate= */ true);
return;
}
mPendingBindWidgetId = mAppWidgetHost.allocateAppWidgetId();
+ AppWidgetProviderInfo widgetProviderInfo = mRequest.getAppWidgetProviderInfo(this);
boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
- mPendingBindWidgetId, mRequest.getAppWidgetProviderInfo(this), mWidgetOptions);
+ mPendingBindWidgetId, widgetProviderInfo, mWidgetOptions);
if (success) {
+ sendWidgetAddedToScreenAccessibilityEvent(widgetProviderInfo.label);
acceptWidget(mPendingBindWidgetId);
return;
}
@@ -270,17 +328,18 @@
}
private void acceptWidget(int widgetId) {
- InstallShortcutReceiver.queueWidget(mRequest.getAppWidgetProviderInfo(this), widgetId, this);
+ ItemInstallQueue.INSTANCE.get(this)
+ .queueItem(mRequest.getAppWidgetProviderInfo(this), widgetId);
mWidgetOptions.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
mRequest.accept(mWidgetOptions);
- logCommand(Action.Command.CONFIRM);
- finish();
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY);
+ mSlideInView.close(/* animate= */ true);
}
@Override
public void onBackPressed() {
- logCommand(Action.Command.BACK);
- super.onBackPressed();
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_BACK);
+ mSlideInView.close(/* animate= */ true);
}
@Override
@@ -316,13 +375,36 @@
@Override
public BaseDragLayer getDragLayer() {
- throw new UnsupportedOperationException();
+ return mDragLayer;
}
- private void logCommand(int command) {
- getUserEventDispatcher().dispatchUserEvent(newLauncherEvent(
- newCommandAction(command),
- newItemTarget(mWidgetCell.getWidgetView(), mInstantAppResolver),
- newContainerTarget(ContainerType.PINITEM)), null);
+ @Override
+ public void onSlideInViewClosed() {
+ finish();
+ }
+
+ protected void setupNavBarColor() {
+ boolean isSheetDark = (getApplicationContext().getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
+ getSystemUiController().updateUiState(
+ SystemUiController.UI_STATE_BASE_WINDOW,
+ isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV);
+ }
+
+ private void sendWidgetAddedToScreenAccessibilityEvent(String widgetName) {
+ if (mAccessibilityManager.isEnabled()) {
+ AccessibilityEvent event =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ event.setContentDescription(
+ getApplicationContext().getResources().getString(
+ R.string.added_to_home_screen_accessibility_text, widgetName));
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+ }
+
+ private void logCommand(StatsLogManager.EventEnum command) {
+ getStatsLogManager().logger()
+ .withItemInfo((ItemInfo) mWidgetCell.getWidgetView().getTag())
+ .log(command);
}
}
diff --git a/src/com/android/launcher3/dragndrop/AddItemDragLayer.java b/src/com/android/launcher3/dragndrop/AddItemDragLayer.java
new file mode 100644
index 0000000..5b52c3d
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/AddItemDragLayer.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.dragndrop;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+
+/**
+ * Drag layer for {@link AddItemActivity}.
+ */
+public class AddItemDragLayer extends BaseDragLayer<AddItemActivity> {
+
+ public AddItemDragLayer(Context context, AttributeSet attrs) {
+ this(context, attrs, /*alphaChannelCount= */ 1);
+ }
+
+ public AddItemDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
+ super(context, attrs, alphaChannelCount);
+ }
+
+ @Override
+ public void recreateControllers() {
+ mControllers = new TouchController[] {};
+ }
+}
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index 707fd06..981e3a6 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -34,7 +34,6 @@
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
import com.android.launcher3.util.ActivityTracker.SchedulerCallback;
import com.android.launcher3.widget.PendingItemDragHelper;
@@ -143,15 +142,13 @@
// the dragLayer alpha to 0 to have a nice fade-in animation. But that will prevent the
// dragView from being visible. Instead just skip the fade-in animation here.
mLauncher.getDragLayer().setAlpha(1);
-
- dragObject.dragView.setColor(
- mLauncher.getResources().getColor(R.color.delete_target_hover_tint));
+ dragObject.dragView.setAlpha(.5f);
}
@Override
public void onPreDragEnd(DragObject dragObject, boolean dragStarted) {
if (dragStarted) {
- dragObject.dragView.setColor(0);
+ dragObject.dragView.setAlpha(1f);
}
}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 03028d3..5731db4 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -16,42 +16,37 @@
package com.android.launcher3.dragndrop;
-import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
-import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
-import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.Utilities.ATLEAST_Q;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import android.animation.ValueAnimator;
import android.content.ComponentName;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.view.DragEvent;
-import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
-import com.android.launcher3.AbstractFloatingView;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
+import java.util.Optional;
/**
* Class for initiating a drag within a view or across multiple views.
+ * @param <T>
*/
-public class DragController implements DragDriver.EventListener, TouchController {
- private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
+public abstract class DragController<T extends ActivityContext>
+ implements DragDriver.EventListener, TouchController {
/**
* When a drag is started from a deep press, you need to drag this much farther than normal to
@@ -59,8 +54,7 @@
*/
private static final int DEEP_PRESS_DISTANCE_FACTOR = 3;
- private final Launcher mLauncher;
- private final FlingToDeleteHelper mFlingToDeleteHelper;
+ protected final T mActivity;
// temporaries to avoid gc thrash
private final Rect mRectTemp = new Rect();
@@ -70,30 +64,30 @@
* Drag driver for the current drag/drop operation, or null if there is no active DND operation.
* It's null during accessible drag operations.
*/
- private DragDriver mDragDriver = null;
+ protected DragDriver mDragDriver = null;
/** Options controlling the drag behavior. */
- private DragOptions mOptions;
+ protected DragOptions mOptions;
/** Coordinate for motion down event */
- private final Point mMotionDown = new Point();
+ protected final Point mMotionDown = new Point();
/** Coordinate for last touch event **/
- private final Point mLastTouch = new Point();
+ protected final Point mLastTouch = new Point();
private final Point mTmpPoint = new Point();
- private DropTarget.DragObject mDragObject;
+ protected DropTarget.DragObject mDragObject;
/** Who can receive drop events */
private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
private final ArrayList<DragListener> mListeners = new ArrayList<>();
- private DropTarget mLastDropTarget;
+ protected DropTarget mLastDropTarget;
private int mLastTouchClassification;
- private int mDistanceSinceScroll = 0;
+ protected int mDistanceSinceScroll = 0;
- private boolean mIsInPreDrag;
+ protected boolean mIsInPreDrag;
/**
* Interface to receive notifications when a drag starts or stops
@@ -116,119 +110,112 @@
/**
* Used to create a new DragLayer from XML.
*/
- public DragController(Launcher launcher) {
- mLauncher = launcher;
- mFlingToDeleteHelper = new FlingToDeleteHelper(launcher);
+ public DragController(T activity) {
+ mActivity = activity;
}
/**
* Starts a drag.
- * When the drag is started, the UI automatically goes into spring loaded mode. On a successful
- * drop, it is the responsibility of the {@link DropTarget} to exit out of the spring loaded
- * mode. If the drop was cancelled for some reason, the UI will automatically exit out of this mode.
*
- * @param b The bitmap to display as the drag image. It will be re-scaled to the
- * enlarged size.
- * @param originalView The source view (ie. icon, widget etc.) that is being dragged
- * and which the DragView represents
+ * <p>When the drag is started, the UI automatically goes into spring loaded mode. On a
+ * successful drop, it is the responsibility of the {@link DropTarget} to exit out of the spring
+ * loaded mode. If the drop was cancelled for some reason, the UI will automatically exit out of
+ * this mode.
+ *
+ * @param drawable The drawable to be displayed in the drag view. It will be re-scaled to the
+ * enlarged size.
+ * @param originalView The source view (ie. icon, widget etc.) that is being dragged and which
+ * the DragView represents
* @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
* @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
* @param source An object representing where the drag originated
* @param dragInfo The data associated with the object that is being dragged
* @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
- * Makes dragging feel more precise, e.g. you can clip out a transparent border
+ * Makes dragging feel more precise, e.g. you can clip out a transparent
+ * border
*/
- public DragView startDrag(Bitmap b, DraggableView originalView, int dragLayerX, int dragLayerY,
- DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
- float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) {
- if (PROFILE_DRAWING_DURING_DRAG) {
- android.os.Debug.startMethodTracing("Launcher");
- }
-
- mLauncher.hideKeyboard();
- AbstractFloatingView.closeOpenViews(mLauncher, false, TYPE_DISCOVERY_BOUNCE);
-
- mOptions = options;
- if (mOptions.simulatedDndStartPoint != null) {
- mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x;
- mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y;
- }
-
- final int registrationX = mMotionDown.x - dragLayerX;
- final int registrationY = mMotionDown.y - dragLayerY;
-
- final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
- final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
-
- mLastDropTarget = null;
-
- mDragObject = new DropTarget.DragObject(mLauncher.getApplicationContext());
- mDragObject.originalView = originalView;
-
- mIsInPreDrag = mOptions.preDragCondition != null
- && !mOptions.preDragCondition.shouldStartDrag(0);
-
- final Resources res = mLauncher.getResources();
- final float scaleDps = mIsInPreDrag
- ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
- final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
- registrationY, initialDragViewScale, dragViewScaleOnDrop, scaleDps);
- dragView.setItemInfo(dragInfo);
- mDragObject.dragComplete = false;
-
- mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
- mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
-
- mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
- if (!mOptions.isAccessibleDrag) {
- mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
- }
-
- mDragObject.dragSource = source;
- mDragObject.dragInfo = dragInfo;
- mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
-
- if (dragOffset != null) {
- dragView.setDragVisualizeOffset(new Point(dragOffset));
- }
- if (dragRegion != null) {
- dragView.setDragRegion(new Rect(dragRegion));
- }
-
- mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- dragView.show(mLastTouch.x, mLastTouch.y);
- mDistanceSinceScroll = 0;
-
- if (!mIsInPreDrag) {
- callOnDragStart();
- } else if (mOptions.preDragCondition != null) {
- mOptions.preDragCondition.onPreDragStart(mDragObject);
- }
-
- handleMoveEvent(mLastTouch.x, mLastTouch.y);
- mLauncher.getUserEventDispatcher().resetActionDurationMillis();
-
- if (!mLauncher.isTouchInProgress() && options.simulatedDndStartPoint == null) {
- // If it is an internal drag and the touch is already complete, cancel immediately
- MAIN_EXECUTOR.submit(this::cancelDrag);
- }
- return dragView;
+ public DragView startDrag(
+ Drawable drawable,
+ DraggableView originalView,
+ int dragLayerX,
+ int dragLayerY,
+ DragSource source,
+ ItemInfo dragInfo,
+ Point dragOffset,
+ Rect dragRegion,
+ float initialDragViewScale,
+ float dragViewScaleOnDrop,
+ DragOptions options) {
+ return startDrag(drawable, /* view= */ null, originalView, dragLayerX, dragLayerY,
+ source, dragInfo, dragOffset, dragRegion, initialDragViewScale, dragViewScaleOnDrop,
+ options);
}
- private void callOnDragStart() {
+ /**
+ * Starts a drag.
+ *
+ * <p>When the drag is started, the UI automatically goes into spring loaded mode. On a
+ * successful drop, it is the responsibility of the {@link DropTarget} to exit out of the spring
+ * loaded mode. If the drop was cancelled for some reason, the UI will automatically exit out of
+ * this mode.
+ *
+ * @param view The view to be displayed in the drag view. It will be re-scaled to the
+ * enlarged size.
+ * @param originalView The source view (ie. icon, widget etc.) that is being dragged and which
+ * the DragView represents
+ * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
+ * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
+ * @param source An object representing where the drag originated
+ * @param dragInfo The data associated with the object that is being dragged
+ * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
+ * Makes dragging feel more precise, e.g. you can clip out a transparent
+ * border
+ */
+ public DragView startDrag(
+ View view,
+ DraggableView originalView,
+ int dragLayerX,
+ int dragLayerY,
+ DragSource source,
+ ItemInfo dragInfo,
+ Point dragOffset,
+ Rect dragRegion,
+ float initialDragViewScale,
+ float dragViewScaleOnDrop,
+ DragOptions options) {
+ return startDrag(/* drawable= */ null, view, originalView, dragLayerX, dragLayerY,
+ source, dragInfo, dragOffset, dragRegion, initialDragViewScale, dragViewScaleOnDrop,
+ options);
+ }
+
+ protected abstract DragView startDrag(
+ @Nullable Drawable drawable,
+ @Nullable View view,
+ DraggableView originalView,
+ int dragLayerX,
+ int dragLayerY,
+ DragSource source,
+ ItemInfo dragInfo,
+ Point dragOffset,
+ Rect dragRegion,
+ float initialDragViewScale,
+ float dragViewScaleOnDrop,
+ DragOptions options);
+
+ protected void callOnDragStart() {
if (mOptions.preDragCondition != null) {
mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/);
}
mIsInPreDrag = false;
+ mDragObject.dragView.onDragStart();
for (DragListener listener : new ArrayList<>(mListeners)) {
listener.onDragStart(mDragObject, mOptions);
}
}
- public void addFirstFrameAnimationHelper(ValueAnimator anim) {
- if (mDragObject != null && mDragObject.dragView != null) {
- mDragObject.dragView.mFirstFrameAnimatorHelper.addTo(anim);
- }
+ public Optional<InstanceId> getLogInstanceId() {
+ return Optional.ofNullable(mDragObject)
+ .map(dragObject -> dragObject.logInstanceId);
}
/**
@@ -271,13 +258,15 @@
if (!accepted) {
// If it was not accepted, cleanup the state. If it was accepted, it is the
// responsibility of the drop target to cleanup the state.
- mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+ exitDrag();
mDragObject.deferDragViewCleanupPostAnimation = false;
}
mDragObject.dragSource.onDropCompleted(dropTarget, mDragObject, accepted);
}
+ protected abstract void exitDrag();
+
public void onAppsRemoved(ItemInfoMatcher matcher) {
// Cancel the current drag if we are removing an app that we are dragging
if (mDragObject != null) {
@@ -291,7 +280,7 @@
}
}
- private void endDrag() {
+ protected void endDrag() {
if (isDragging()) {
mDragDriver = null;
boolean isDeferred = false;
@@ -310,8 +299,6 @@
callOnDragEnd();
}
}
-
- mFlingToDeleteHelper.releaseVelocityTracker();
}
public void animateDragViewToOriginalPosition(final Runnable onComplete,
@@ -357,7 +344,7 @@
* Clamps the position to the drag layer bounds.
*/
private Point getClampedDragLayerPos(float x, float y) {
- mLauncher.getDragLayer().getLocalVisibleRect(mRectTemp);
+ mActivity.getDragLayer().getLocalVisibleRect(mRectTemp);
mTmpPoint.x = (int) Math.max(mRectTemp.left, Math.min(x, mRectTemp.right - 1));
mTmpPoint.y = (int) Math.max(mRectTemp.top, Math.min(y, mRectTemp.bottom - 1));
return mTmpPoint;
@@ -379,19 +366,16 @@
@Override
public void onDriverDragEnd(float x, float y) {
- DropTarget dropTarget;
- Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject, mOptions);
- if (flingAnimation != null) {
- dropTarget = mFlingToDeleteHelper.getDropTarget();
- } else {
- dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp);
+ if (!endWithFlingAnimation()) {
+ drop(findDropTarget((int) x, (int) y, mCoordinatesTemp), null);
}
-
- drop(dropTarget, flingAnimation);
-
endDrag();
}
+ protected boolean endWithFlingAnimation() {
+ return false;
+ }
+
@Override
public void onDriverDragCancel() {
cancelDrag();
@@ -434,7 +418,7 @@
return mDragDriver != null && mDragDriver.onDragEvent(event);
}
- private void handleMoveEvent(int x, int y) {
+ protected void handleMoveEvent(int x, int y) {
mDragObject.dragView.move(x, y);
// Drop on someone?
@@ -463,10 +447,10 @@
}
public void forceTouchMove() {
- int[] dummyCoordinates = mCoordinatesTemp;
- DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, dummyCoordinates);
- mDragObject.x = dummyCoordinates[0];
- mDragObject.y = dummyCoordinates[1];
+ int[] placeholderCoordinates = mCoordinatesTemp;
+ DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, placeholderCoordinates);
+ mDragObject.x = placeholderCoordinates[0];
+ mDragObject.y = placeholderCoordinates[1];
checkTouchMove(dropTarget);
}
@@ -506,7 +490,7 @@
endDrag();
}
- private void drop(DropTarget dropTarget, Runnable flingAnimation) {
+ protected void drop(DropTarget dropTarget, Runnable flingAnimation) {
final int[] coordinates = mCoordinatesTemp;
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
@@ -544,7 +528,6 @@
}
}
final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
- mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView);
dispatchDropComplete(dropTargetAsView, accepted);
}
@@ -564,7 +547,7 @@
if (r.contains(x, y)) {
dropCoordinates[0] = x;
dropCoordinates[1] = y;
- mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates);
+ mActivity.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates);
return target;
}
}
@@ -572,11 +555,11 @@
// cell layout to drop to in the existing drag/drop logic.
dropCoordinates[0] = x;
dropCoordinates[1] = y;
- mLauncher.getDragLayer().mapCoordInSelfToDescendant(mLauncher.getWorkspace(),
- dropCoordinates);
- return mLauncher.getWorkspace();
+ return getDefaultDropTarget(dropCoordinates);
}
+ protected abstract DropTarget getDefaultDropTarget(int[] dropCoordinates);
+
/**
* Sets the drag listener which will be notified when a drag starts or ends.
*/
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index ddf44ca..011325d 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -45,10 +45,10 @@
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Workspace;
import com.android.launcher3.folder.Folder;
-import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
+import com.android.launcher3.graphics.Scrim;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
import java.util.ArrayList;
@@ -82,11 +82,7 @@
// Related to adjacent page hints
private final ViewGroupFocusHelper mFocusIndicatorHelper;
- private final WorkspaceAndHotseatScrim mWorkspaceScrim;
- private final OverviewScrim mOverviewScrim;
-
- // View that should handle move events
- private View mMoveTarget;
+ private Scrim mWorkspaceDragScrim;
/**
* Used to create a new DragLayer from XML.
@@ -102,15 +98,12 @@
setChildrenDrawingOrderEnabled(true);
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
- mWorkspaceScrim = new WorkspaceAndHotseatScrim(this);
- mOverviewScrim = new OverviewScrim(this);
}
public void setup(DragController dragController, Workspace workspace) {
mDragController = dragController;
- mWorkspaceScrim.setWorkspace(workspace);
- mMoveTarget = workspace;
recreateControllers();
+ mWorkspaceDragScrim = new Scrim(this);
}
@Override
@@ -215,12 +208,6 @@
}
@Override
- public boolean dispatchUnhandledMove(View focused, int direction) {
- return super.dispatchUnhandledMove(focused, direction)
- || mMoveTarget.dispatchUnhandledMove(focused, direction);
- }
-
- @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
ev.offsetLocation(getTranslationX(), 0);
try {
@@ -525,41 +512,22 @@
@Override
protected void dispatchDraw(Canvas canvas) {
// Draw the background below children.
- mWorkspaceScrim.draw(canvas);
- mOverviewScrim.updateCurrentScrimmedView(this);
+ mWorkspaceDragScrim.draw(canvas);
mFocusIndicatorHelper.draw(canvas);
super.dispatchDraw(canvas);
- if (mOverviewScrim.getScrimmedView() == null) {
- mOverviewScrim.draw(canvas);
+ }
+
+ public Scrim getWorkspaceDragScrim() {
+ return mWorkspaceDragScrim;
+ }
+
+ /**
+ * Called when one handed mode state changed.
+ * @param activated true if one handed mode activated, false otherwise.
+ */
+ public void onOneHandedModeStateChanged(boolean activated) {
+ for (TouchController controller : mControllers) {
+ controller.onOneHandedModeStateChanged(activated);
}
}
-
- @Override
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- if (child == mOverviewScrim.getScrimmedView()) {
- mOverviewScrim.draw(canvas);
- }
- return super.drawChild(canvas, child, drawingTime);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mWorkspaceScrim.setSize(w, h);
- }
-
- @Override
- public void setInsets(Rect insets) {
- super.setInsets(insets);
- mWorkspaceScrim.onInsetsChanged(insets, mAllowSysuiScrims);
- mOverviewScrim.onInsetsChanged(insets);
- }
-
- public WorkspaceAndHotseatScrim getScrim() {
- return mWorkspaceScrim;
- }
-
- public OverviewScrim getOverviewScrim() {
- return mOverviewScrim;
- }
}
diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java
index 959602b..e8ff8da 100644
--- a/src/com/android/launcher3/dragndrop/DragOptions.java
+++ b/src/com/android/launcher3/dragndrop/DragOptions.java
@@ -28,6 +28,9 @@
/** Whether or not an accessible drag operation is in progress. */
public boolean isAccessibleDrag = false;
+ /** Whether or not the drag operation is controlled by keyboard. */
+ public boolean isKeyboardDrag = false;
+
/**
* Specifies the start location for a simulated DnD (like system drag or accessibility drag),
* null when using internal DnD
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index de0fa1a..3fdb256 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -16,61 +16,71 @@
package com.android.launcher3.dragndrop;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.Utilities.getBadge;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import android.animation.FloatArrayEvaluator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
+import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.graphics.Paint;
import android.graphics.Path;
+import android.graphics.Picture;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PictureDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
-import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.FirstFrameAnimatorHelper;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import java.util.Arrays;
+/** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */
+public class DragView extends FrameLayout implements StateListener<LauncherState> {
-public class DragView extends View implements StateListener<LauncherState> {
- private static final ColorMatrix sTempMatrix1 = new ColorMatrix();
- private static final ColorMatrix sTempMatrix2 = new ColorMatrix();
-
- public static final int COLOR_CHANGE_DURATION = 120;
public static final int VIEW_ZOOM_DURATION = 150;
- private boolean mDrawBitmap = true;
- private Bitmap mBitmap;
- private Bitmap mCrossFadeBitmap;
- @Thunk Paint mPaint;
+ private final View mContent;
+ // The following are only used for rendering mContent directly during drag-n-drop.
+ @Nullable private ViewGroup.LayoutParams mContentViewLayoutParams;
+ @Nullable private ViewGroup mContentViewParent;
+ private int mContentViewInParentViewIndex = -1;
+ private final int mWidth;
+ private final int mHeight;
+
private final int mBlurSizeOutline;
private final int mRegistrationX;
private final int mRegistrationY;
@@ -78,23 +88,16 @@
private final float mScaleOnDrop;
private final int[] mTempLoc = new int[2];
+ private final RunnableList mOnDragStartCallback = new RunnableList();
+
private Point mDragVisualizeOffset = null;
private Rect mDragRegion = null;
private final Launcher mLauncher;
private final DragLayer mDragLayer;
@Thunk final DragController mDragController;
- final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper;
private boolean mHasDrawn = false;
- @Thunk float mCrossFadeProgress = 0f;
- private boolean mAnimationCancelled = false;
- ValueAnimator mAnim;
- // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace
- // size. This is ignored for non-icons.
- private float mIntrinsicIconScale = 1f;
-
- @Thunk float[] mCurrentFilter;
- private ValueAnimator mFilterAnimator;
+ final ValueAnimator mAnim;
private int mLastTouchX;
private int mLastTouchY;
@@ -106,7 +109,14 @@
private SpringFloatValue mTranslateX, mTranslateY;
private Path mScaledMaskPath;
private Drawable mBadge;
- private ColorMatrixColorFilter mBaseFilter;
+
+ public DragView(Launcher launcher, Drawable drawable, int registrationX,
+ int registrationY, final float initialScale, final float scaleOnDrop,
+ final float finalScaleDps) {
+ this(launcher, getViewFromDrawable(launcher, drawable),
+ drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
+ registrationX, registrationY, initialScale, scaleOnDrop, finalScaleDps);
+ }
/**
* Construct the drag view.
@@ -114,19 +124,36 @@
* The registration point is the point inside our view that the touch events should
* be centered upon.
* @param launcher The Launcher instance
- * @param bitmap The view that we're dragging around. We scale it up when we draw it.
+ * @param content the view content that is attached to the drag view.
+ * @param width the width of the dragView
+ * @param height the height of the dragView
+ * @param initialScale The view that we're dragging around. We scale it up when we draw it.
* @param registrationX The x coordinate of the registration point.
* @param registrationY The y coordinate of the registration point.
+ * @param scaleOnDrop the scale used in the drop animation.
+ * @param finalScaleDps the scale used in the zoom out animation when the drag view is shown.
*/
- public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY,
- final float initialScale, final float scaleOnDrop, final float finalScaleDps) {
+ public DragView(Launcher launcher, View content, int width, int height, int registrationX,
+ int registrationY, final float initialScale, final float scaleOnDrop,
+ final float finalScaleDps) {
super(launcher);
mLauncher = launcher;
mDragLayer = launcher.getDragLayer();
mDragController = launcher.getDragController();
- mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this);
- final float scale = (bitmap.getWidth() + finalScaleDps) / bitmap.getWidth();
+ mContent = content;
+ mWidth = width;
+ mHeight = height;
+ mContentViewLayoutParams = mContent.getLayoutParams();
+ if (mContent.getParent() instanceof ViewGroup) {
+ mContentViewParent = (ViewGroup) mContent.getParent();
+ mContentViewInParentViewIndex = mContentViewParent.indexOfChild(mContent);
+ mContentViewParent.removeView(mContent);
+ }
+
+ addView(content, new LayoutParams(width, height));
+
+ final float scale = (width + finalScaleDps) / width;
// Set the initial scale to avoid any jumps
setScaleX(initialScale);
@@ -144,8 +171,7 @@
}
});
- mBitmap = bitmap;
- setDragRegion(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()));
+ setDragRegion(new Rect(0, 0, width, height));
// The point in our scaled bitmap that the touch events are located
mRegistrationX = registrationX;
@@ -155,12 +181,11 @@
mScaleOnDrop = scaleOnDrop;
// Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
- int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
- measure(ms, ms);
- mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
setElevation(getResources().getDimension(R.dimen.drag_elevation));
+ setWillNotDraw(false);
}
@Override
@@ -187,148 +212,113 @@
*/
@TargetApi(Build.VERSION_CODES.O)
public void setItemInfo(final ItemInfo info) {
- if (!Utilities.ATLEAST_OREO) {
- return;
- }
- if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
- info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
- info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+ if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+ && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+ && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
return;
}
// Load the adaptive icon on a background thread and add the view in ui thread.
- MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(new Runnable() {
- @Override
- public void run() {
- Object[] outObj = new Object[1];
- int w = mBitmap.getWidth();
- int h = mBitmap.getHeight();
- Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
+ MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
+ Object[] outObj = new Object[1];
+ int w = mWidth;
+ int h = mHeight;
+ Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
- if (dr instanceof AdaptiveIconDrawable) {
- int blurMargin = (int) mLauncher.getResources()
- .getDimension(R.dimen.blur_size_medium_outline) / 2;
+ if (dr instanceof AdaptiveIconDrawable) {
+ int blurMargin = (int) mLauncher.getResources()
+ .getDimension(R.dimen.blur_size_medium_outline) / 2;
- Rect bounds = new Rect(0, 0, w, h);
- bounds.inset(blurMargin, blurMargin);
- // Badge is applied after icon normalization so the bounds for badge should not
- // be scaled down due to icon normalization.
- Rect badgeBounds = new Rect(bounds);
- mBadge = getBadge(mLauncher, info, outObj[0]);
- mBadge.setBounds(badgeBounds);
+ Rect bounds = new Rect(0, 0, w, h);
+ bounds.inset(blurMargin, blurMargin);
+ // Badge is applied after icon normalization so the bounds for badge should not
+ // be scaled down due to icon normalization.
+ Rect badgeBounds = new Rect(bounds);
+ mBadge = getBadge(mLauncher, info, outObj[0]);
+ mBadge.setBounds(badgeBounds);
- // Do not draw the background in case of folder as its translucent
- mDrawBitmap = !(dr instanceof FolderAdaptiveIcon);
+ // Do not draw the background in case of folder as its translucent
+ final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon);
- try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) {
- Drawable nDr; // drawable to be normalized
- if (mDrawBitmap) {
- nDr = dr;
- } else {
- // Since we just want the scale, avoid heavy drawing operations
- nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
- }
- Utilities.scaleRectAboutCenter(bounds,
- li.getNormalizer().getScale(nDr, null, null, null));
+ try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) {
+ Drawable nDr; // drawable to be normalized
+ if (shouldDrawBackground) {
+ nDr = dr;
+ } else {
+ // Since we just want the scale, avoid heavy drawing operations
+ nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
}
- AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;
-
- // Shrink very tiny bit so that the clip path is smaller than the original bitmap
- // that has anti aliased edges and shadows.
- Rect shrunkBounds = new Rect(bounds);
- Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
- adaptiveIcon.setBounds(shrunkBounds);
- final Path mask = adaptiveIcon.getIconMask();
-
- mTranslateX = new SpringFloatValue(DragView.this,
- w * AdaptiveIconDrawable.getExtraInsetFraction());
- mTranslateY = new SpringFloatValue(DragView.this,
- h * AdaptiveIconDrawable.getExtraInsetFraction());
-
- bounds.inset(
- (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
- (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
- );
- mBgSpringDrawable = adaptiveIcon.getBackground();
- if (mBgSpringDrawable == null) {
- mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
- }
- mBgSpringDrawable.setBounds(bounds);
- mFgSpringDrawable = adaptiveIcon.getForeground();
- if (mFgSpringDrawable == null) {
- mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
- }
- mFgSpringDrawable.setBounds(bounds);
-
- new Handler(Looper.getMainLooper()).post(new Runnable() {
- @Override
- public void run() {
- // Assign the variable on the UI thread to avoid race conditions.
- mScaledMaskPath = mask;
-
- if (info.isDisabled()) {
- FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null);
- d.setIsDisabled(true);
- mBaseFilter = (ColorMatrixColorFilter) d.getColorFilter();
- }
- updateColorFilter();
- }
- });
+ Utilities.scaleRectAboutCenter(bounds,
+ li.getNormalizer().getScale(nDr, null, null, null));
}
- }});
+ AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;
+
+ // Shrink very tiny bit so that the clip path is smaller than the original bitmap
+ // that has anti aliased edges and shadows.
+ Rect shrunkBounds = new Rect(bounds);
+ Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
+ adaptiveIcon.setBounds(shrunkBounds);
+ final Path mask = adaptiveIcon.getIconMask();
+
+ mTranslateX = new SpringFloatValue(DragView.this,
+ w * AdaptiveIconDrawable.getExtraInsetFraction());
+ mTranslateY = new SpringFloatValue(DragView.this,
+ h * AdaptiveIconDrawable.getExtraInsetFraction());
+
+ bounds.inset(
+ (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
+ (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
+ );
+ mBgSpringDrawable = adaptiveIcon.getBackground();
+ if (mBgSpringDrawable == null) {
+ mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
+ }
+ mBgSpringDrawable.setBounds(bounds);
+ mFgSpringDrawable = adaptiveIcon.getForeground();
+ if (mFgSpringDrawable == null) {
+ mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
+ }
+ mFgSpringDrawable.setBounds(bounds);
+
+ new Handler(Looper.getMainLooper()).post(() -> mOnDragStartCallback.add(() -> {
+ // TODO: Consider fade-in animation
+ // Assign the variable on the UI thread to avoid race conditions.
+ mScaledMaskPath = mask;
+ // Avoid relayout as we do not care about children affecting layout
+ removeAllViewsInLayout();
+
+ if (info.isDisabled()) {
+ FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null);
+ d.setIsDisabled(true);
+ mBgSpringDrawable.setColorFilter(d.getColorFilter());
+ mFgSpringDrawable.setColorFilter(d.getColorFilter());
+ mBadge.setColorFilter(d.getColorFilter());
+ }
+ invalidate();
+ }));
+ }
+ });
}
- @TargetApi(Build.VERSION_CODES.O)
- private void updateColorFilter() {
- if (mCurrentFilter == null) {
- mPaint.setColorFilter(null);
+ /**
+ * Called when pre-drag finishes for an icon
+ */
+ public void onDragStart() {
+ mOnDragStartCallback.executeAllAndDestroy();
+ }
- if (mScaledMaskPath != null) {
- mBgSpringDrawable.setColorFilter(mBaseFilter);
- mFgSpringDrawable.setColorFilter(mBaseFilter);
- mBadge.setColorFilter(mBaseFilter);
- }
- } else {
- ColorMatrixColorFilter currentFilter = new ColorMatrixColorFilter(mCurrentFilter);
- mPaint.setColorFilter(currentFilter);
-
- if (mScaledMaskPath != null) {
- if (mBaseFilter != null) {
- mBaseFilter.getColorMatrix(sTempMatrix1);
- sTempMatrix2.set(mCurrentFilter);
- sTempMatrix1.postConcat(sTempMatrix2);
-
- currentFilter = new ColorMatrixColorFilter(sTempMatrix1);
- }
-
- mBgSpringDrawable.setColorFilter(currentFilter);
- mFgSpringDrawable.setColorFilter(currentFilter);
- mBadge.setColorFilter(currentFilter);
- }
+ // TODO(b/183609936): This is only for LauncherAppWidgetHostView that is rendered in a drawable.
+ // Once LauncherAppWidgetHostView is directly rendered in this view, removes this method.
+ @Override
+ public void invalidate() {
+ super.invalidate();
+ if (mContent instanceof ImageView) {
+ mContent.invalidate();
}
-
- invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight());
- }
-
- /** Sets the scale of the view over the normal workspace icon size. */
- public void setIntrinsicIconScaleFactor(float scale) {
- mIntrinsicIconScale = scale;
- }
-
- public float getIntrinsicIconScaleFactor() {
- return mIntrinsicIconScale;
- }
-
- public int getDragRegionLeft() {
- return mDragRegion.left;
- }
-
- public int getDragRegionTop() {
- return mDragRegion.top;
+ super.onMeasure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
}
public int getDragRegionWidth() {
@@ -355,33 +345,12 @@
return mDragRegion;
}
- public Bitmap getPreviewBitmap() {
- return mBitmap;
- }
-
@Override
- protected void onDraw(Canvas canvas) {
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ // Draw after the content
mHasDrawn = true;
-
- if (mDrawBitmap) {
- // Always draw the bitmap to mask anti aliasing due to clipPath
- boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null;
- if (crossFade) {
- int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255;
- mPaint.setAlpha(alpha);
- }
- canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint);
- if (crossFade) {
- mPaint.setAlpha((int) (255 * mCrossFadeProgress));
- final int saveCount = canvas.save();
- float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth();
- float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight();
- canvas.scale(sX, sY);
- canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint);
- canvas.restoreToCount(saveCount);
- }
- }
-
if (mScaledMaskPath != null) {
int cnt = canvas.save();
canvas.clipPath(mScaledMaskPath);
@@ -393,74 +362,27 @@
}
}
- public void setCrossFadeBitmap(Bitmap crossFadeBitmap) {
- mCrossFadeBitmap = crossFadeBitmap;
- }
-
- public void crossFade(int duration) {
- ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
- va.setDuration(duration);
- va.setInterpolator(Interpolators.DEACCEL_1_5);
- va.addUpdateListener(a -> {
- mCrossFadeProgress = a.getAnimatedFraction();
- invalidate();
- });
- va.start();
- }
-
- public void setColor(int color) {
- if (mPaint == null) {
- mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ public void crossFadeContent(Drawable crossFadeDrawable, int duration) {
+ if (mContent.getParent() == null) {
+ // If the content is already removed, ignore
+ return;
}
- if (color != 0) {
- ColorMatrix m1 = new ColorMatrix();
- m1.setSaturation(0);
+ View newContent = getViewFromDrawable(getContext(), crossFadeDrawable);
+ newContent.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
+ newContent.layout(0, 0, mWidth, mHeight);
+ addViewInLayout(newContent, 0, new LayoutParams(mWidth, mHeight));
- ColorMatrix m2 = new ColorMatrix();
- Themes.setColorScaleOnMatrix(color, m2);
- m1.postConcat(m2);
-
- animateFilterTo(m1.getArray());
- } else {
- if (mCurrentFilter == null) {
- updateColorFilter();
- } else {
- animateFilterTo(new ColorMatrix().getArray());
- }
- }
- }
-
- private void animateFilterTo(float[] targetFilter) {
- float[] oldFilter = mCurrentFilter == null ? new ColorMatrix().getArray() : mCurrentFilter;
- mCurrentFilter = Arrays.copyOf(oldFilter, oldFilter.length);
-
- if (mFilterAnimator != null) {
- mFilterAnimator.cancel();
- }
- mFilterAnimator = ValueAnimator.ofObject(new FloatArrayEvaluator(mCurrentFilter),
- oldFilter, targetFilter);
- mFilterAnimator.setDuration(COLOR_CHANGE_DURATION);
- mFilterAnimator.addUpdateListener(new AnimatorUpdateListener() {
-
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- updateColorFilter();
- }
- });
- mFilterAnimator.start();
+ AnimatorSet anim = new AnimatorSet();
+ anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1));
+ anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0));
+ anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5);
+ anim.start();
}
public boolean hasDrawn() {
return mHasDrawn;
}
- @Override
- public void setAlpha(float alpha) {
- super.setAlpha(alpha);
- mPaint.setAlpha((int) (255 * alpha));
- invalidate();
- }
-
/**
* Create a window containing this view and show it.
*
@@ -471,22 +393,21 @@
mDragLayer.addView(this);
// Start the pick-up animation
- DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);
- lp.width = mBitmap.getWidth();
- lp.height = mBitmap.getHeight();
+ BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(mWidth, mHeight);
lp.customPosition = true;
setLayoutParams(lp);
+
+ if (mContent != null) {
+ // At the drag start, the source view visibility is set to invisible.
+ mContent.setVisibility(VISIBLE);
+ }
+
move(touchX, touchY);
// Post the animation to skip other expensive work happening on the first frame
- post(new Runnable() {
- public void run() {
- mAnim.start();
- }
- });
+ post(mAnim::start);
}
public void cancelAnimation() {
- mAnimationCancelled = true;
if (mAnim != null && mAnim.isRunning()) {
mAnim.cancel();
}
@@ -539,6 +460,63 @@
setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY);
}
+ /**
+ * Detaches {@link #mContent}, if previously attached, from this view.
+ *
+ * <p>In the case of no change in the drop position, sets {@code reattachToPreviousParent} to
+ * {@code true} to attach the {@link #mContent} back to its previous parent.
+ */
+ public void detachContentView(boolean reattachToPreviousParent) {
+ if (mContent != null && mContentViewParent != null && mContentViewInParentViewIndex >= 0) {
+ Picture picture = new Picture();
+ mContent.draw(picture.beginRecording(mWidth, mHeight));
+ picture.endRecording();
+ View view = new View(mLauncher);
+ view.setBackground(new PictureDrawable(picture));
+ view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
+ view.layout(mContent.getLeft(), mContent.getTop(),
+ mContent.getRight(), mContent.getBottom());
+ setClipToOutline(mContent.getClipToOutline());
+ setOutlineProvider(mContent.getOutlineProvider());
+ addViewInLayout(view, indexOfChild(mContent), mContent.getLayoutParams(), true);
+
+ removeViewInLayout(mContent);
+ mContent.setVisibility(INVISIBLE);
+ mContent.setLayoutParams(mContentViewLayoutParams);
+ if (reattachToPreviousParent) {
+ mContentViewParent.addView(mContent, mContentViewInParentViewIndex);
+ }
+ mContentViewParent = null;
+ mContentViewInParentViewIndex = -1;
+ }
+ }
+
+ /**
+ * If the drag view uses color extraction, block it.
+ */
+ public void disableColorExtraction() {
+ if (mContent instanceof LauncherAppWidgetHostView) {
+ ((LauncherAppWidgetHostView) mContent).disableColorExtraction();
+ }
+ }
+
+ /**
+ * If the drag view uses color extraction, restores it.
+ */
+ public void resumeColorExtraction() {
+ if (mContent instanceof LauncherAppWidgetHostView) {
+ ((LauncherAppWidgetHostView) mContent).enableColorExtraction(/* updateColors= */ false);
+ }
+ }
+
+ /**
+ * Removes this view from the {@link DragLayer}.
+ *
+ * <p>If the drag content is a {@link #mContent}, this call doesn't reattach the
+ * {@link #mContent} back to its previous parent. To reattach to previous parent, the caller
+ * should call {@link #detachContentView} with {@code reattachToPreviousParent} sets to true
+ * before this call.
+ */
public void remove() {
if (getParent() != null) {
mDragLayer.removeView(DragView.this);
@@ -553,6 +531,25 @@
return mInitialScale;
}
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ /** Returns the current content view that is rendered in the drag view. */
+ public View getContentView() {
+ return mContent;
+ }
+
+ /**
+ * Returns the previous {@link ViewGroup} parent of the {@link #mContent} before the drag
+ * content is attached to this view.
+ */
+ @Nullable
+ public ViewGroup getContentViewParent() {
+ return mContentViewParent;
+ }
+
private static class SpringFloatValue {
private static final FloatPropertyCompat<SpringFloatValue> VALUE =
@@ -570,9 +567,9 @@
};
// Following three values are fine tuned with motion ux designer
- private final static int STIFFNESS = 4000;
- private final static float DAMPENING_RATIO = 1f;
- private final static int PARALLAX_MAX_IN_DP = 8;
+ private static final int STIFFNESS = 4000;
+ private static final float DAMPENING_RATIO = 1f;
+ private static final int PARALLAX_MAX_IN_DP = 8;
private final View mView;
private final SpringAnimation mSpring;
@@ -594,4 +591,10 @@
mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta));
}
}
+
+ private static View getViewFromDrawable(Context context, Drawable drawable) {
+ ImageView iv = new ImageView(context);
+ iv.setImageDrawable(drawable);
+ return iv;
+ }
}
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index 7788f93..336fced 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -22,6 +22,7 @@
import android.view.ViewConfiguration;
import com.android.launcher3.ButtonDropTarget;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DropTarget;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
@@ -35,15 +36,12 @@
private static final float MAX_FLING_DEGREES = 35f;
private final Launcher mLauncher;
- private final int mFlingToDeleteThresholdVelocity;
private ButtonDropTarget mDropTarget;
private VelocityTracker mVelocityTracker;
public FlingToDeleteHelper(Launcher launcher) {
mLauncher = launcher;
- mFlingToDeleteThresholdVelocity = launcher.getResources()
- .getDimensionPixelSize(R.dimen.drag_flingToDeleteMinVelocity);
}
public void recordMotionEvent(MotionEvent ev) {
@@ -65,6 +63,9 @@
}
public Runnable getFlingAnimation(DropTarget.DragObject dragObject, DragOptions options) {
+ if (options == null) {
+ return null;
+ }
PointF vel = isFlingingToDelete();
options.isFlingToDelete = vel != null;
if (!options.isFlingToDelete) {
@@ -88,12 +89,13 @@
mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity());
PointF vel = new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
float theta = MAX_FLING_DEGREES + 1;
- if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) {
+ DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
+ if (mVelocityTracker.getYVelocity() < deviceProfile.flingToDeleteThresholdVelocity) {
// Do a quick dot product test to ensure that we are flinging upwards
PointF upVec = new PointF(0f, -1f);
theta = getAngleBetweenVectors(vel, upVec);
} else if (mLauncher.getDeviceProfile().isVerticalBarLayout() &&
- mVelocityTracker.getXVelocity() < mFlingToDeleteThresholdVelocity) {
+ mVelocityTracker.getXVelocity() < deviceProfile.flingToDeleteThresholdVelocity) {
// Remove icon is on left side instead of top, so check if we are flinging to the left.
PointF leftVec = new PointF(-1f, 0f);
theta = getAngleBetweenVectors(vel, leftVec);
diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
index ea1fbdb..98c0cfc 100644
--- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
+++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
@@ -21,11 +21,11 @@
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Matrix;
+import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
@@ -129,10 +129,19 @@
canvas.restore();
});
+ Bitmap bgBitmap = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y,
+ (canvas) -> {
+ Paint p = new Paint();
+ p.setColor(bg.getBgColor());
+
+ canvas.drawCircle(dragViewSize.x / 2f, dragViewSize.y / 2f, bg.getRadius(), p);
+ });
+
ShiftedBitmapDrawable badge = new ShiftedBitmapDrawable(badgeBmp, 0, 0);
ShiftedBitmapDrawable foreground = new ShiftedBitmapDrawable(previewBitmap, 0, 0);
+ ShiftedBitmapDrawable background = new ShiftedBitmapDrawable(bgBitmap, 0, 0);
- return new FolderAdaptiveIcon(new ColorDrawable(bg.getBgColor()), foreground, badge, mask);
+ return new FolderAdaptiveIcon(background, foreground, badge, mask);
}
@Override
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
new file mode 100644
index 0000000..a98d70c
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.dragndrop;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
+import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.HapticFeedbackConstants;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.model.data.ItemInfo;
+
+/**
+ * Drag controller for Launcher activity
+ */
+public class LauncherDragController extends DragController<Launcher> {
+
+ private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
+
+ private final FlingToDeleteHelper mFlingToDeleteHelper;
+
+ public LauncherDragController(Launcher launcher) {
+ super(launcher);
+ mFlingToDeleteHelper = new FlingToDeleteHelper(launcher);
+ }
+
+ @Override
+ protected DragView startDrag(
+ @Nullable Drawable drawable,
+ @Nullable View view,
+ DraggableView originalView,
+ int dragLayerX,
+ int dragLayerY,
+ DragSource source,
+ ItemInfo dragInfo,
+ Point dragOffset,
+ Rect dragRegion,
+ float initialDragViewScale,
+ float dragViewScaleOnDrop,
+ DragOptions options) {
+ if (PROFILE_DRAWING_DURING_DRAG) {
+ android.os.Debug.startMethodTracing("Launcher");
+ }
+
+ mActivity.hideKeyboard();
+ AbstractFloatingView.closeOpenViews(mActivity, false, TYPE_DISCOVERY_BOUNCE);
+
+ mOptions = options;
+ if (mOptions.simulatedDndStartPoint != null) {
+ mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x;
+ mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y;
+ }
+
+ final int registrationX = mMotionDown.x - dragLayerX;
+ final int registrationY = mMotionDown.y - dragLayerY;
+
+ final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
+ final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
+
+ mLastDropTarget = null;
+
+ mDragObject = new DropTarget.DragObject(mActivity.getApplicationContext());
+ mDragObject.originalView = originalView;
+
+ mIsInPreDrag = mOptions.preDragCondition != null
+ && !mOptions.preDragCondition.shouldStartDrag(0);
+
+ final Resources res = mActivity.getResources();
+ final float scaleDps = mIsInPreDrag
+ ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
+ final DragView dragView = mDragObject.dragView = drawable != null
+ ? new DragView(
+ mActivity,
+ drawable,
+ registrationX,
+ registrationY,
+ initialDragViewScale,
+ dragViewScaleOnDrop,
+ scaleDps)
+ : new DragView(
+ mActivity,
+ view,
+ view.getMeasuredWidth(),
+ view.getMeasuredHeight(),
+ registrationX,
+ registrationY,
+ initialDragViewScale,
+ dragViewScaleOnDrop,
+ scaleDps);
+ dragView.setItemInfo(dragInfo);
+ mDragObject.dragComplete = false;
+
+ mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
+ mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
+
+ mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
+ if (!mOptions.isAccessibleDrag) {
+ mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
+ }
+
+ mDragObject.dragSource = source;
+ mDragObject.dragInfo = dragInfo;
+ mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
+
+ if (dragOffset != null) {
+ dragView.setDragVisualizeOffset(new Point(dragOffset));
+ }
+ if (dragRegion != null) {
+ dragView.setDragRegion(new Rect(dragRegion));
+ }
+
+ mActivity.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ dragView.show(mLastTouch.x, mLastTouch.y);
+ mDistanceSinceScroll = 0;
+
+ if (!mIsInPreDrag) {
+ callOnDragStart();
+ } else if (mOptions.preDragCondition != null) {
+ mOptions.preDragCondition.onPreDragStart(mDragObject);
+ }
+
+ handleMoveEvent(mLastTouch.x, mLastTouch.y);
+
+ if (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null) {
+ // If it is an internal drag and the touch is already complete, cancel immediately
+ MAIN_EXECUTOR.submit(this::cancelDrag);
+ }
+ return dragView;
+ }
+
+ @Override
+ protected void exitDrag() {
+ mActivity.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+ }
+
+ @Override
+ protected boolean endWithFlingAnimation() {
+ Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject, mOptions);
+ if (flingAnimation != null) {
+ drop(mFlingToDeleteHelper.getDropTarget(), flingAnimation);
+ return true;
+ }
+ return super.endWithFlingAnimation();
+ }
+
+ @Override
+ protected void endDrag() {
+ super.endDrag();
+ mFlingToDeleteHelper.releaseVelocityTracker();
+ }
+
+ @Override
+ protected DropTarget getDefaultDropTarget(int[] dropCoordinates) {
+ mActivity.getDragLayer().mapCoordInSelfToDescendant(mActivity.getWorkspace(),
+ dropCoordinates);
+ return mActivity.getWorkspace();
+ }
+}
diff --git a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java b/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
deleted file mode 100644
index a9389bc..0000000
--- a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.android.launcher3.dragndrop;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.RemoteViews;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.icons.BitmapRenderer;
-import com.android.launcher3.widget.WidgetCell;
-
-/**
- * Extension of {@link WidgetCell} which supports generating previews from {@link RemoteViews}
- */
-public class LivePreviewWidgetCell extends WidgetCell {
-
- private RemoteViews mPreview;
-
- public LivePreviewWidgetCell(Context context) {
- this(context, null);
- }
-
- public LivePreviewWidgetCell(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public LivePreviewWidgetCell(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- public void setPreview(RemoteViews view) {
- mPreview = view;
- }
-
- @Override
- public void ensurePreview() {
- if (mPreview != null && mActiveRequest == null) {
- Bitmap preview = generateFromRemoteViews(
- mActivity, mPreview, mItem.widgetInfo, mPresetPreviewSize, new int[1]);
- if (preview != null) {
- applyPreview(preview);
- return;
- }
- }
- super.ensurePreview();
- }
-
- /**
- * Generates a bitmap by inflating {@param views}.
- * @see com.android.launcher3.WidgetPreviewLoader#generateWidgetPreview
- *
- * TODO: Consider moving this to the background thread.
- */
- public static Bitmap generateFromRemoteViews(BaseActivity activity, RemoteViews views,
- LauncherAppWidgetProviderInfo info, int previewSize, int[] preScaledWidthOut) {
-
- DeviceProfile dp = activity.getDeviceProfile();
- int viewWidth = dp.cellWidthPx * info.spanX;
- int viewHeight = dp.cellHeightPx * info.spanY;
-
- final View v;
- try {
- v = views.apply(activity, new FrameLayout(activity));
- v.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
-
- viewWidth = v.getMeasuredWidth();
- viewHeight = v.getMeasuredHeight();
- v.layout(0, 0, viewWidth, viewHeight);
- } catch (Exception e) {
- return null;
- }
-
- preScaledWidthOut[0] = viewWidth;
- final int bitmapWidth, bitmapHeight;
- final float scale;
- if (viewWidth > previewSize) {
- scale = ((float) previewSize) / viewWidth;
- bitmapWidth = previewSize;
- bitmapHeight = (int) (viewHeight * scale);
- } else {
- scale = 1;
- bitmapWidth = viewWidth;
- bitmapHeight = viewHeight;
- }
-
- return BitmapRenderer.createSoftwareBitmap(bitmapWidth, bitmapHeight, c -> {
- c.scale(scale, scale);
- v.draw(c);
- });
- }
-}
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index bf3aa7f..2bdf8a0 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -16,7 +16,8 @@
package com.android.launcher3.dragndrop;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PIN_WIDGETS;
import android.annotation.TargetApi;
import android.appwidget.AppWidgetManager;
@@ -31,17 +32,13 @@
import com.android.launcher3.DragSource;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.PendingAddItemInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.PendingItemDragHelper;
import com.android.launcher3.widget.WidgetAddFlowHandler;
-import java.util.ArrayList;
-
/**
* {@link DragSource} for handling drop from a different window. This object is initialized
* in the source window and is passed on to the Launcher activity as an Intent extra.
@@ -89,7 +86,7 @@
mLauncher, mRequest.getAppWidgetProviderInfo(mLauncher));
final PinWidgetFlowHandler flowHandler =
new PinWidgetFlowHandler(providerInfo, mRequest);
- item = new PendingAddWidgetInfo(providerInfo) {
+ item = new PendingAddWidgetInfo(providerInfo, CONTAINER_PIN_WIDGETS) {
@Override
public WidgetAddFlowHandler getHandler() {
return flowHandler;
@@ -101,18 +98,12 @@
PendingItemDragHelper dragHelper = new PendingItemDragHelper(view);
if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_APPWIDGET) {
- dragHelper.setPreview(getPreview(mRequest));
+ dragHelper.setRemoteViewsPreview(getPreview(mRequest));
}
return dragHelper;
}
@Override
- public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
- ArrayList<LauncherLogProto.Target> parents) {
- parents.add(newContainerTarget(LauncherLogProto.ContainerType.PINITEM));
- }
-
- @Override
protected void postCleanup() {
super.postCleanup();
mCancelSignal.cancel();
diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
index 9982b39..29e7c18 100644
--- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
+++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
@@ -28,7 +28,6 @@
import android.os.Build;
import android.os.Process;
-import com.android.launcher3.FastBitmapDrawable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherAppState;
@@ -49,14 +48,14 @@
// Class name used in the target component, such that it will never represent an
// actual existing class.
- private static final String DUMMY_COMPONENT_CLASS = "pinned-shortcut";
+ private static final String STUB_COMPONENT_CLASS = "pinned-shortcut";
private final PinItemRequest mRequest;
private final ShortcutInfo mInfo;
private final Context mContext;
public PinShortcutRequestActivityInfo(PinItemRequest request, Context context) {
- super(new ComponentName(request.getShortcutInfo().getPackage(), DUMMY_COMPONENT_CLASS),
+ super(new ComponentName(request.getShortcutInfo().getPackage(), STUB_COMPONENT_CLASS),
request.getShortcutInfo().getUserHandle());
mRequest = request;
mInfo = request.getShortcutInfo();
@@ -78,7 +77,7 @@
Drawable d = mContext.getSystemService(LauncherApps.class)
.getShortcutIconDrawable(mInfo, LauncherAppState.getIDP(mContext).fillResIconDpi);
if (d == null) {
- d = new FastBitmapDrawable(cache.getDefaultIcon(Process.myUserHandle()));
+ d = cache.getDefaultIcon(Process.myUserHandle()).newIcon(mContext);
}
return d;
}
diff --git a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
index 37200a6..6325877 100644
--- a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
+++ b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
@@ -56,9 +56,8 @@
if (mScreen != null) {
// Snap to the screen that we are hovering over now
Workspace w = mLauncher.getWorkspace();
- int page = w.indexOfChild(mScreen);
- if (page != w.getCurrentPage()) {
- w.snapToPage(page);
+ if (!w.isVisible(mScreen)) {
+ w.snapToPage(w.indexOfChild(mScreen));
}
} else {
mLauncher.getDragController().cancelDrag();
diff --git a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
index 5954efa..8cd91d3 100644
--- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
@@ -5,10 +5,12 @@
public static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
private static final int MIN_NUM_ITEMS_IN_PREVIEW = 2;
- private static final float MIN_SCALE = 0.48f;
- private static final float MAX_SCALE = 0.58f;
- private static final float MAX_RADIUS_DILATION = 0.15f;
- private static final float ITEM_RADIUS_SCALE_FACTOR = 1.33f;
+ private static final float MIN_SCALE = 0.44f;
+ private static final float MAX_SCALE = 0.51f;
+ private static final float MAX_RADIUS_DILATION = 0.25f;
+ // The max amount of overlap the preview items can go outside of the background bounds.
+ public static final float ICON_OVERLAP_FACTOR = 1 + (MAX_RADIUS_DILATION / 2f);
+ private static final float ITEM_RADIUS_SCALE_FACTOR = 1.15f;
public static final int EXIT_INDEX = -2;
public static final int ENTER_INDEX = -3;
@@ -34,7 +36,6 @@
float totalScale = scaleForItem(curNumItems);
float transX;
float transY;
- float overlayAlpha = 0;
if (index == EXIT_INDEX) {
// 0 1 * <-- Exit position (row 0, col 2)
@@ -55,10 +56,9 @@
transY = mTmpPoint[1];
if (params == null) {
- params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
+ params = new PreviewItemDrawingParams(transX, transY, totalScale);
} else {
params.update(transX, transY, totalScale);
- params.overlayAlpha = overlayAlpha;
}
return params;
}
@@ -97,7 +97,7 @@
double thetaShift = 0;
if (curNumItems == 3) {
- thetaShift = Math.PI / 6;
+ thetaShift = Math.PI / 2;
} else if (curNumItems == 4) {
thetaShift = Math.PI / 4;
}
@@ -130,10 +130,8 @@
public float scaleForItem(int numItems) {
// Scale is determined by the number of items in the preview.
final float scale;
- if (numItems <= 2) {
+ if (numItems <= 3) {
scale = MAX_SCALE;
- } else if (numItems == 3) {
- scale = (MAX_SCALE + MIN_SCALE) / 2;
} else {
scale = MIN_SCALE;
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index d01e189..22bb56c 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -19,13 +19,12 @@
import static android.text.TextUtils.isEmpty;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -34,24 +33,33 @@
import android.appwidget.AppWidgetHostView;
import android.content.Context;
import android.graphics.Canvas;
+import android.graphics.Insets;
import android.graphics.Path;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.text.InputType;
import android.text.Selection;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
+import android.util.TypedValue;
import android.view.FocusFinder;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
+import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+import androidx.core.content.res.ResourcesCompat;
+
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Alarm;
import com.android.launcher3.BubbleTextView;
@@ -67,14 +75,13 @@
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace;
import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
import com.android.launcher3.accessibility.FolderAccessibilityHelper;
+import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragController.DragListener;
-import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.logger.LauncherAtom.FromState;
import com.android.launcher3.logger.LauncherAtom.ToState;
@@ -86,9 +93,10 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pageindicators.PageIndicatorDots;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.views.ClipPathView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -142,6 +150,7 @@
private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
private static final int FOLDER_NAME_ANIMATION_DURATION = 633;
+ private static final int FOLDER_COLOR_ANIMATION_DURATION = 200;
private static final int REORDER_DELAY = 250;
private static final int ON_EXIT_CLOSE_DELAY = 400;
@@ -151,22 +160,29 @@
private final Alarm mReorderAlarm = new Alarm();
private final Alarm mOnExitAlarm = new Alarm();
private final Alarm mOnScrollHintAlarm = new Alarm();
- @Thunk final Alarm mScrollPauseAlarm = new Alarm();
+ final Alarm mScrollPauseAlarm = new Alarm();
- @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
+ final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
private AnimatorSet mCurrentAnimator;
private boolean mIsAnimatingClosed = false;
- protected final Launcher mLauncher;
+ // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
+ // Anything specific to Launcher should use mLauncherDelegate, otherwise should
+ // use mActivityContext.
+ protected final LauncherDelegate mLauncherDelegate;
+ protected final ActivityContext mActivityContext;
+
protected DragController mDragController;
public FolderInfo mInfo;
private CharSequence mFromTitle;
private FromState mFromLabelState;
- @Thunk FolderIcon mFolderIcon;
+ @Thunk
+ FolderIcon mFolderIcon;
- @Thunk FolderPagedView mContent;
+ @Thunk
+ FolderPagedView mContent;
public FolderNameEditText mFolderName;
private PageIndicatorDots mPageIndicator;
@@ -174,7 +190,8 @@
private int mFooterHeight;
// Cell ranks used for drag and drop
- @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
+ @Thunk
+ int mTargetRank, mPrevTargetRank, mEmptyCellRank;
private Path mClipPath;
@@ -185,7 +202,8 @@
@ViewDebug.IntToString(from = STATE_ANIMATING, to = "STATE_ANIMATING"),
@ViewDebug.IntToString(from = STATE_OPEN, to = "STATE_OPEN"),
})
- @Thunk int mState = STATE_NONE;
+ @Thunk
+ int mState = STATE_NONE;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mRearrangeOnClose = false;
boolean mItemsInvalidated = false;
@@ -203,60 +221,83 @@
// Folder scrolling
private int mScrollAreaOffset;
- @Thunk int mScrollHintDir = SCROLL_NONE;
- @Thunk int mCurrentScrollDir = SCROLL_NONE;
+ @Thunk
+ int mScrollHintDir = SCROLL_NONE;
+ @Thunk
+ int mCurrentScrollDir = SCROLL_NONE;
private StatsLogManager mStatsLogManager;
+ @Nullable
+ private KeyboardInsetAnimationCallback mKeyboardInsetAnimationCallback;
+
+ private GradientDrawable mBackground;
+
/**
* Used to inflate the Workspace from XML.
*
* @param context The application's context.
- * @param attrs The attributes set containing the Workspace's customization values.
+ * @param attrs The attributes set containing the Workspace's customization values.
*/
public Folder(Context context, AttributeSet attrs) {
super(context, attrs);
setAlwaysDrawnWithCacheEnabled(false);
- mLauncher = Launcher.getLauncher(context);
+ mActivityContext = ActivityContext.lookupContext(context);
+ mLauncherDelegate = LauncherDelegate.from(mActivityContext);
+
mStatsLogManager = StatsLogManager.newInstance(context);
// We need this view to be focusable in touch mode so that when text editing of the folder
// name is complete, we have something to focus on, thus hiding the cursor and giving
- // reliable behavior when clicking the text field (since it will always gain focus on click).
+ // reliable behavior when clicking the text field (since it will always gain focus on
+ // click).
setFocusableInTouchMode(true);
}
@Override
+ public Drawable getBackground() {
+ return mBackground;
+ }
+
+ @Override
protected void onFinishInflate() {
super.onFinishInflate();
+ final DeviceProfile dp = mActivityContext.getDeviceProfile();
+ final int paddingLeftRight = dp.folderContentPaddingLeftRight;
+
+ mBackground = (GradientDrawable) ResourcesCompat.getDrawable(getResources(),
+ R.drawable.round_rect_folder, getContext().getTheme());
+
mContent = findViewById(R.id.folder_content);
+ mContent.setPadding(paddingLeftRight, dp.folderContentPaddingTop, paddingLeftRight, 0);
mContent.setFolder(this);
mPageIndicator = findViewById(R.id.folder_page_indicator);
mFolderName = findViewById(R.id.folder_name);
+ mFolderName.setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.folderLabelTextSizePx);
mFolderName.setOnBackKeyListener(this);
mFolderName.setOnFocusChangeListener(this);
mFolderName.setOnEditorActionListener(this);
mFolderName.setSelectAllOnFocus(true);
mFolderName.setInputType(mFolderName.getInputType()
& ~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
- & ~InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+ | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
| InputType.TYPE_TEXT_FLAG_CAP_WORDS);
- mFolderName.forceDisableSuggestions(!FeatureFlags.FOLDER_NAME_SUGGEST.get());
+ mFolderName.forceDisableSuggestions(true);
mFooter = findViewById(R.id.folder_footer);
+ mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height);
- // We find out how tall footer wants to be (it is set to wrap_content), so that
- // we can allocate the appropriate amount of space for it.
- int measureSpec = MeasureSpec.UNSPECIFIED;
- mFooter.measure(measureSpec, measureSpec);
- mFooterHeight = mFooter.getMeasuredHeight();
+ if (Utilities.ATLEAST_R) {
+ mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
+ setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
+ }
}
public boolean onLongClick(View v) {
// Return if global dragging is not enabled
- if (!mLauncher.isDraggingEnabled()) return true;
+ if (!mLauncherDelegate.isDraggingEnabled()) return true;
return startDrag(v, new DragOptions());
}
@@ -272,17 +313,17 @@
if (options.isAccessibleDrag) {
mDragController.addDragListener(new AccessibleDragListenerAdapter(
mContent, FolderAccessibilityHelper::new) {
- @Override
- protected void enableAccessibleDrag(boolean enable) {
- super.enableAccessibleDrag(enable);
- mFooter.setImportantForAccessibility(enable
- ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
- });
+ @Override
+ protected void enableAccessibleDrag(boolean enable) {
+ super.enableAccessibleDrag(enable);
+ mFooter.setImportantForAccessibility(enable
+ ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+ });
}
- mLauncher.getWorkspace().beginDragShared(v, this, options);
+ mLauncherDelegate.beginDragShared(v, this, options);
}
return true;
}
@@ -338,7 +379,7 @@
if (DEBUG) {
Log.d(TAG, "onBackKey newTitle=" + newTitle);
}
- mInfo.setTitle(newTitle, mLauncher.getModelWriter());
+ mInfo.setTitle(newTitle, mLauncherDelegate.getModelWriter());
mFolderIcon.onTitleChanged(newTitle);
if (TextUtils.isEmpty(mInfo.title)) {
@@ -373,6 +414,26 @@
return false;
}
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets windowInsets) {
+ if (Utilities.ATLEAST_R) {
+ this.setTranslationY(0);
+
+ if (windowInsets.isVisible(WindowInsets.Type.ime())) {
+ Insets keyboardInsets = windowInsets.getInsets(WindowInsets.Type.ime());
+ int folderHeightFromBottom = getHeightFromBottom();
+
+ if (keyboardInsets.bottom > folderHeightFromBottom) {
+ // Translate this folder above the keyboard, then add the folder name's padding
+ this.setTranslationY(folderHeightFromBottom - keyboardInsets.bottom
+ - mFolderName.getPaddingBottom());
+ }
+ }
+ }
+
+ return windowInsets;
+ }
+
public FolderIcon getFolderIcon() {
return mFolderIcon;
}
@@ -383,6 +444,7 @@
public void setFolderIcon(FolderIcon icon) {
mFolderIcon = icon;
+ mLauncherDelegate.init(this, icon);
}
@Override
@@ -421,9 +483,9 @@
Collections.sort(children, ITEM_POS_COMPARATOR);
updateItemLocationsInDatabaseBatch(true);
- DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+ BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
if (lp == null) {
- lp = new DragLayer.LayoutParams(0, 0);
+ lp = new BaseDragLayer.LayoutParams(0, 0);
lp.customPosition = true;
setLayoutParams(lp);
}
@@ -477,20 +539,36 @@
/**
* Creates a new UserFolder, inflated from R.layout.user_folder.
*
- * @param launcher The main activity.
- *
+ * @param activityContext The main ActivityContext in which to inflate this Folder. It must also
+ * be an instance or ContextWrapper around the Launcher activity context.
* @return A new UserFolder.
*/
@SuppressLint("InflateParams")
- static Folder fromXml(Launcher launcher) {
- return (Folder) launcher.getLayoutInflater()
+ static <T extends Context & ActivityContext> Folder fromXml(T activityContext) {
+ return (Folder) LayoutInflater.from(activityContext).cloneInContext(activityContext)
.inflate(R.layout.user_folder_icon_normalized, null);
}
private void startAnimation(final AnimatorSet a) {
- final Workspace workspace = mLauncher.getWorkspace();
- final CellLayout currentCellLayout =
- (CellLayout) workspace.getChildAt(workspace.getCurrentPage());
+ mLauncherDelegate.forEachVisibleWorkspacePage(
+ visiblePage -> addAnimatorListenerForPage(a, (CellLayout) visiblePage));
+
+ a.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mState = STATE_ANIMATING;
+ mCurrentAnimator = a;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentAnimator = null;
+ }
+ });
+ a.start();
+ }
+
+ private void addAnimatorListenerForPage(AnimatorSet a, CellLayout currentCellLayout) {
final boolean useHardware = shouldUseHardwareLayerForAnimation(currentCellLayout);
final boolean wasHardwareAccelerated = currentCellLayout.isHardwareLayerEnabled();
@@ -500,8 +578,6 @@
if (useHardware) {
currentCellLayout.enableHardwareLayer(true);
}
- mState = STATE_ANIMATING;
- mCurrentAnimator = a;
}
@Override
@@ -509,10 +585,8 @@
if (useHardware) {
currentCellLayout.enableHardwareLayer(wasHardwareAccelerated);
}
- mCurrentAnimator = null;
}
});
- a.start();
}
private boolean shouldUseHardwareLayerForAnimation(CellLayout currentCellLayout) {
@@ -561,16 +635,7 @@
* is played.
*/
private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
- animateOpen(items, pageNo, false);
- }
-
- /**
- * Opens the user folder described by the specified tag. The opening of the folder
- * is animated relative to the specified View. If the View is null, no animation
- * is played.
- */
- private void animateOpen(List<WorkspaceItemInfo> items, int pageNo, boolean skipUserEventLog) {
- Folder openFolder = getOpen(mLauncher);
+ Folder openFolder = getOpen(mActivityContext);
if (openFolder != null && openFolder != this) {
// Close any open folder before opening a folder.
openFolder.close(true);
@@ -583,7 +648,7 @@
mIsOpen = true;
- DragLayer dragLayer = mLauncher.getDragLayer();
+ BaseDragLayer dragLayer = mActivityContext.getDragLayer();
// Just verify that the folder hasn't already been added to the DragLayer.
// There was a one-off crash where the folder had a parent already.
if (getParent() == null) {
@@ -597,36 +662,28 @@
}
mContent.completePendingPageChanges();
- mContent.snapToPageImmediately(pageNo);
+ mContent.setCurrentPage(pageNo);
// This is set to true in close(), but isn't reset to false until onDropCompleted(). This
// leads to an inconsistent state if you drag out of the folder and drag back in without
// dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
mDeleteFolderOnDropCompleted = false;
- if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
- mCurrentAnimator.cancel();
- }
- AnimatorSet anim = new FolderAnimationManager(this, true /* isOpening */).getAnimator();
+ cancelRunningAnimations();
+ FolderAnimationManager fam = new FolderAnimationManager(this, true /* isOpening */);
+ AnimatorSet anim = fam.getAnimator();
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mFolderIcon.setIconVisible(false);
mFolderIcon.drawLeaveBehindIfExists();
}
+
@Override
public void onAnimationEnd(Animator animation) {
mState = STATE_OPEN;
announceAccessibilityChanges();
- if (!skipUserEventLog) {
- mLauncher.getUserEventDispatcher().logActionOnItem(
- LauncherLogProto.Action.Touch.TAP,
- LauncherLogProto.Action.Direction.NONE,
- LauncherLogProto.ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
- }
-
-
mContent.setFocusOnFirstChild();
}
});
@@ -636,7 +693,7 @@
int footerWidth = mContent.getDesiredWidth()
- mFooter.getPaddingLeft() - mFooter.getPaddingRight();
- float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
+ float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
float translation = (footerWidth - textWidth) / 2;
mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation);
mPageIndicator.prepareEntryAnimation();
@@ -650,14 +707,14 @@
@Override
public void onAnimationEnd(Animator animation) {
mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION)
- .translationX(0)
- .setInterpolator(AnimationUtils.loadInterpolator(
- mLauncher, android.R.interpolator.fast_out_slow_in));
+ .translationX(0)
+ .setInterpolator(AnimationUtils.loadInterpolator(
+ getContext(), android.R.interpolator.fast_out_slow_in));
mPageIndicator.playEntryAnimation();
if (updateAnimationFlag) {
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true,
- mLauncher.getModelWriter());
+ mLauncherDelegate.getModelWriter());
}
}
});
@@ -667,6 +724,9 @@
mPageIndicator.stopAllAnimations();
startAnimation(anim);
+ // Because t=0 has the folder match the folder icon, we can skip the
+ // first frame and have the same movement one frame earlier.
+ anim.setCurrentPlayTime(Math.min(getSingleFrameMs(getContext()), anim.getTotalDuration()));
// Make sure the folder picks up the last drag move even if the finger doesn't move.
if (mDragController.isDragging()) {
@@ -705,26 +765,40 @@
// Notify the accessibility manager that this folder "window" has disappeared and no
// longer occludes the workspace items
- mLauncher.getDragLayer().sendAccessibilityEvent(
+ mActivityContext.getDragLayer().sendAccessibilityEvent(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
+ private void cancelRunningAnimations() {
+ if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
+ mCurrentAnimator.cancel();
+ }
+ }
+
private void animateClosed() {
if (mIsAnimatingClosed) {
return;
}
- if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
- mCurrentAnimator.cancel();
- }
+
+ mContent.completePendingPageChanges();
+ mContent.snapToPageImmediately(mContent.getDestinationPage());
+
+ cancelRunningAnimations();
AnimatorSet a = new FolderAnimationManager(this, false /* isOpening */).getAnimator();
a.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
+ if (Utilities.ATLEAST_R) {
+ setWindowInsetsAnimationCallback(null);
+ }
mIsAnimatingClosed = true;
}
@Override
public void onAnimationEnd(Animator animation) {
+ if (Utilities.ATLEAST_R && mKeyboardInsetAnimationCallback != null) {
+ setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
+ }
closeComplete(true);
announceAccessibilityChanges();
mIsAnimatingClosed = false;
@@ -741,12 +815,13 @@
@Override
protected View getAccessibilityInitialFocusView() {
- return mContent.getFirstItem();
+ View firstItem = mContent.getFirstItem();
+ return firstItem != null ? firstItem : super.getAccessibilityInitialFocusView();
}
private void closeComplete(boolean wasAnimated) {
// TODO: Clear all active animations.
- DragLayer parent = (DragLayer) getParent();
+ BaseDragLayer parent = (BaseDragLayer) getParent();
if (parent != null) {
parent.removeView(this);
}
@@ -979,13 +1054,13 @@
if (getItemCount() <= mContent.itemsPerPage()) {
// Show the animation, next time something is added to the folder.
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false,
- mLauncher.getModelWriter());
+ mLauncherDelegate.getModelWriter());
}
}
private void updateItemLocationsInDatabaseBatch(boolean isBind) {
FolderGridOrganizer verifier = new FolderGridOrganizer(
- mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+ mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
ArrayList<ItemInfo> items = new ArrayList<>();
int total = mInfo.contents.size();
@@ -997,7 +1072,7 @@
}
if (!items.isEmpty()) {
- mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
+ mLauncherDelegate.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
}
if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && !isBind
&& total > 1 /* no need to update if there's one icon */) {
@@ -1022,10 +1097,8 @@
}
private void centerAboutIcon() {
- DeviceProfile grid = mLauncher.getDeviceProfile();
-
- DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
- DragLayer parent = mLauncher.getDragLayer();
+ BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
+ BaseDragLayer parent = mActivityContext.getDragLayer();
int width = getFolderWidth();
int height = getFolderHeight();
@@ -1035,38 +1108,13 @@
int centeredLeft = centerX - width / 2;
int centeredTop = centerY - height / 2;
- // We need to bound the folder to the currently visible workspace area
- if (mLauncher.getStateManager().getState().overviewUi) {
- parent.getDescendantRectRelativeToSelf(mLauncher.getOverviewPanel(), sTempRect);
- } else {
- mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
- }
- int left = Math.min(Math.max(sTempRect.left, centeredLeft),
- sTempRect.right- width);
- int top = Math.min(Math.max(sTempRect.top, centeredTop),
- sTempRect.bottom - height);
-
- int distFromEdgeOfScreen = mLauncher.getWorkspace().getPaddingLeft() + getPaddingLeft();
-
- if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) {
- // Center the folder if it is very close to being centered anyway, by virtue of
- // filling the majority of the viewport. ie. remove it from the uncanny valley
- // of centeredness.
- left = (grid.availableWidthPx - width) / 2;
- } else if (width >= sTempRect.width()) {
- // If the folder doesn't fit within the bounds, center it about the desired bounds
- left = sTempRect.left + (sTempRect.width() - width) / 2;
- }
- if (height >= sTempRect.height()) {
- // Folder height is greater than page height, center on page
- top = sTempRect.top + (sTempRect.height() - height) / 2;
- } else {
- // Folder height is less than page height, so bound it to the absolute open folder
- // bounds if necessary
- Rect folderBounds = grid.getAbsoluteOpenFolderBounds();
- left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width));
- top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height));
- }
+ sTempRect.set(mActivityContext.getFolderBoundingBox());
+ int left = Utilities.boundToRange(centeredLeft, sTempRect.left, sTempRect.right - width);
+ int top = Utilities.boundToRange(centeredTop, sTempRect.top, sTempRect.bottom - height);
+ int[] inOutPosition = new int[]{left, top};
+ mActivityContext.updateOpenFolderPosition(inOutPosition, sTempRect, width, height);
+ left = inOutPosition[0];
+ top = inOutPosition[1];
int folderPivotX = width / 2 + (centeredLeft - left);
int folderPivotY = height / 2 + (centeredTop - top);
@@ -1077,10 +1125,12 @@
lp.height = height;
lp.x = left;
lp.y = top;
+
+ mBackground.setBounds(0, 0, width, height);
}
protected int getContentAreaHeight() {
- DeviceProfile grid = mLauncher.getDeviceProfile();
+ DeviceProfile grid = mActivityContext.getDeviceProfile();
int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
- mFooterHeight;
int height = Math.min(maxContentAreaHeight,
@@ -1116,7 +1166,7 @@
if (mContent.getChildCount() > 0) {
int cellIconGap = (mContent.getPageAt(0).getCellWidth()
- - mLauncher.getDeviceProfile().iconSizePx) / 2;
+ - mActivityContext.getDeviceProfile().iconSizePx) / 2;
mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap,
mFooter.getPaddingTop(),
mContent.getPaddingRight() + cellIconGap,
@@ -1134,6 +1184,9 @@
* Rearranges the children based on their rank.
*/
public void rearrangeChildren() {
+ if (!mContent.areViewsBound()) {
+ return;
+ }
mContent.arrangeChildren(getIconsInReadingOrder());
mItemsInvalidated = true;
}
@@ -1142,54 +1195,8 @@
return mInfo.contents.size();
}
- @Thunk void replaceFolderWithFinalItem() {
- // Add the last remaining child to the workspace in place of the folder
- Runnable onCompleteRunnable = new Runnable() {
- @Override
- public void run() {
- int itemCount = getItemCount();
- if (itemCount <= 1) {
- View newIcon = null;
- WorkspaceItemInfo finalItem = null;
-
- if (itemCount == 1) {
- // Move the item from the folder to the workspace, in the position of the
- // folder
- CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container,
- mInfo.screenId);
- finalItem = mInfo.contents.remove(0);
- newIcon = mLauncher.createShortcut(cellLayout, finalItem);
- mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
- mInfo.container, mInfo.screenId, mInfo.cellX, mInfo.cellY);
- }
-
- // Remove the folder
- mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */);
- if (mFolderIcon instanceof DropTarget) {
- mDragController.removeDropTarget((DropTarget) mFolderIcon);
- }
-
- if (newIcon != null) {
- // We add the child after removing the folder to prevent both from existing
- // at the same time in the CellLayout. We need to add the new item with
- // addInScreenFromBind() to ensure that hotseat items are placed correctly.
- mLauncher.getWorkspace().addInScreenFromBind(newIcon, mInfo);
-
- // Focus the newly created child
- newIcon.requestFocus();
- }
- if (finalItem != null) {
- mLauncher.folderConvertedToItem(mFolderIcon.getFolder(), finalItem);
- }
- }
- }
- };
- View finalChild = mContent.getLastItem();
- if (finalChild != null) {
- mFolderIcon.performDestroyAnimation(onCompleteRunnable);
- } else {
- onCompleteRunnable.run();
- }
+ void replaceFolderWithFinalItem() {
+ mLauncherDelegate.replaceFolderWithFinalItem(this);
mDestroyed = true;
}
@@ -1248,6 +1255,10 @@
mScrollPauseAlarm.cancelAlarm();
}
mContent.completePendingPageChanges();
+ Launcher launcher = mLauncherDelegate.getLauncher();
+ if (launcher == null) {
+ return;
+ }
PendingAddShortcutInfo pasi = d.dragInfo instanceof PendingAddShortcutInfo
? (PendingAddShortcutInfo) d.dragInfo : null;
@@ -1257,7 +1268,7 @@
pasi.container = mInfo.id;
pasi.rank = mEmptyCellRank;
- mLauncher.addPendingItem(pasi, pasi.container, pasi.screenId, null, pasi.spanX,
+ launcher.addPendingItem(pasi, pasi.container, pasi.screenId, null, pasi.spanX,
pasi.spanY);
d.deferDragViewCleanupPostAnimation = false;
mRearrangeOnClose = true;
@@ -1279,7 +1290,7 @@
// Actually move the item in the database if it was an external drag. Call this
// before creating the view, so that WorkspaceItemInfo is updated appropriately.
- mLauncher.getModelWriter().addOrMoveItemInDatabase(
+ mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase(
si, mInfo.id, 0, si.cellX, si.cellY);
mIsExternalDrag = false;
} else {
@@ -1294,7 +1305,7 @@
float scaleY = getScaleY();
setScaleX(1.0f);
setScaleY(1.0f);
- mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView, null);
+ launcher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView, null);
setScaleX(scaleX);
setScaleY(scaleY);
} else {
@@ -1322,10 +1333,11 @@
if (mContent.getPageCount() > 1) {
// The animation has already been shown while opening the folder.
- mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher.getModelWriter());
+ mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true,
+ mLauncherDelegate.getModelWriter());
}
- mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+ launcher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
if (d.stateAnnouncer != null) {
d.stateAnnouncer.completeAction(R.string.item_moved);
}
@@ -1342,6 +1354,7 @@
v.setVisibility(INVISIBLE);
}
}
+
public void showItem(WorkspaceItemInfo info) {
View v = getViewForInfo(info);
if (v != null) {
@@ -1352,9 +1365,9 @@
@Override
public void onAdd(WorkspaceItemInfo item, int rank) {
FolderGridOrganizer verifier = new FolderGridOrganizer(
- mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+ mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
verifier.updateRankAndPos(item, rank);
- mLauncher.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
+ mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
item.cellY);
updateItemLocationsInDatabaseBatch(false);
@@ -1364,10 +1377,10 @@
mItemsInvalidated = true;
}
- public void onRemove(WorkspaceItemInfo item) {
+ @Override
+ public void onRemove(List<WorkspaceItemInfo> items) {
mItemsInvalidated = true;
- View v = getViewForInfo(item);
- mContent.removeItem(v);
+ items.stream().map(this::getViewForInfo).forEach(mContent::removeItem);
if (mState == STATE_ANIMATING) {
mRearrangeOnClose = true;
} else {
@@ -1464,7 +1477,6 @@
}
statsLogger.log(LAUNCHER_FOLDER_LABEL_UPDATED);
- logFolderLabelState(mFromLabelState, toLabelState);
mFolderName.dispatchBackKey();
}
}
@@ -1477,27 +1489,6 @@
outRect.right += mScrollAreaOffset;
}
- @Override
- public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
- ArrayList<LauncherLogProto.Target> targets) {
- child.gridX = childInfo.cellX;
- child.gridY = childInfo.cellY;
- child.pageIndex = mContent.getCurrentPage();
-
- LauncherLogProto.Target target = newContainerTarget(LauncherLogProto.ContainerType.FOLDER);
- target.pageIndex = mInfo.screenId;
- target.gridX = mInfo.cellX;
- target.gridY = mInfo.cellY;
- targets.add(target);
-
- // continue to parent
- if (mInfo.container == CONTAINER_HOTSEAT) {
- mLauncher.getHotseat().fillInLogContainerData(mInfo, target, targets);
- } else {
- mLauncher.getWorkspace().fillInLogContainerData(mInfo, target, targets);
- }
- }
-
private class OnScrollHintListener implements OnAlarmListener {
private final DragObject mDragObject;
@@ -1581,19 +1572,8 @@
/**
* Returns a folder which is already open or null
*/
- public static Folder getOpen(Launcher launcher) {
- return getOpenView(launcher, TYPE_FOLDER);
- }
-
- @Override
- public void logActionCommand(int command) {
- mLauncher.getUserEventDispatcher().logActionCommand(
- command, getFolderIcon(), getLogContainerType());
- }
-
- @Override
- public int getLogContainerType() {
- return LauncherLogProto.ContainerType.FOLDER;
+ public static Folder getOpen(ActivityContext activityContext) {
+ return getOpenView(activityContext, TYPE_FOLDER);
}
/**
@@ -1612,7 +1592,7 @@
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- DragLayer dl = mLauncher.getDragLayer();
+ BaseDragLayer dl = (BaseDragLayer) getParent();
if (isEditingName()) {
if (!dl.isEventOverView(mFolderName, ev)) {
@@ -1620,23 +1600,19 @@
return true;
}
return false;
- } else if (!dl.isEventOverView(this, ev)) {
- if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
- // Do not close the container if in drag and drop.
- if (!dl.isEventOverView(mLauncher.getDropTargetBar(), ev)) {
- return true;
- }
- } else {
- mLauncher.getUserEventDispatcher().logActionTapOutside(
- newContainerTarget(LauncherLogProto.ContainerType.FOLDER));
- close(true);
- return true;
- }
+ } else if (!dl.isEventOverView(this, ev)
+ && mLauncherDelegate.interceptOutsideTouch(ev, dl, this)) {
+ return true;
}
}
return false;
}
+ @Override
+ public boolean canInterceptEventsInSystemGestureRegion() {
+ return true;
+ }
+
/**
* Alternative to using {@link #getClipToOutline()} as it only works with derivatives of
* rounded rect.
@@ -1648,14 +1624,16 @@
}
@Override
- public void draw(Canvas canvas) {
+ protected void dispatchDraw(Canvas canvas) {
if (mClipPath != null) {
int count = canvas.save();
canvas.clipPath(mClipPath);
- super.draw(canvas);
+ mBackground.draw(canvas);
canvas.restoreToCount(count);
+ super.dispatchDraw(canvas);
} else {
- super.draw(canvas);
+ mBackground.draw(canvas);
+ super.dispatchDraw(canvas);
}
}
@@ -1663,14 +1641,12 @@
return mContent;
}
- /**
- * Logs current folder label info.
- *
- * @deprecated This method is only used for log validation and soon will be removed.
- */
- @Deprecated
- public void logFolderLabelState(FromState fromState, ToState toState) {
- mLauncher.getUserEventDispatcher()
- .logLauncherEvent(mInfo.getFolderLabelStateLauncherEvent(fromState, toState));
+ /** Returns the height of the current folder's bottom edge from the bottom of the screen. */
+ private int getHeightFromBottom() {
+ BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
+ int folderBottomPx = layoutParams.y + layoutParams.height;
+ int windowBottomPx = mActivityContext.getDeviceProfile().heightPx;
+
+ return windowBottomPx - folderBottomPx;
}
}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 3d72b49..cb3884d 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -16,6 +16,8 @@
package com.android.launcher3.folder;
+import static android.view.View.ALPHA;
+
import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
@@ -35,18 +37,15 @@
import android.view.View;
import android.view.animation.AnimationUtils;
-import androidx.core.graphics.ColorUtils;
-
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.ResourceUtils;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.BaseDragLayer;
import java.util.List;
@@ -60,6 +59,7 @@
public class FolderAnimationManager {
private static final int FOLDER_NAME_ALPHA_DURATION = 32;
+ private static final int LARGE_FOLDER_FOOTER_DURATION = 128;
private Folder mFolder;
private FolderPagedView mContent;
@@ -69,7 +69,6 @@
private PreviewBackground mPreviewBackground;
private Context mContext;
- private Launcher mLauncher;
private final boolean mIsOpening;
@@ -80,9 +79,13 @@
private final TimeInterpolator mLargeFolderPreviewItemOpenInterpolator;
private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator;
- private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+ private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0);
private final FolderGridOrganizer mPreviewVerifier;
+ private ObjectAnimator mBgColorAnimator;
+
+ private DeviceProfile mDeviceProfile;
+
public FolderAnimationManager(Folder folder, boolean isOpening) {
mFolder = folder;
mContent = folder.mContent;
@@ -92,8 +95,8 @@
mPreviewBackground = mFolderIcon.mBackground;
mContext = folder.getContext();
- mLauncher = folder.mLauncher;
- mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
+ mDeviceProfile = folder.mActivityContext.getDeviceProfile();
+ mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile.inv);
mIsOpening = isOpening;
@@ -109,19 +112,26 @@
R.interpolator.large_folder_preview_item_close_interpolator);
}
+ /**
+ * Returns the animator that changes the background color.
+ */
+ public ObjectAnimator getBgColorAnimator() {
+ return mBgColorAnimator;
+ }
/**
* Prepares the Folder for animating between open / closed states.
*/
public AnimatorSet getAnimator() {
- final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
+ final BaseDragLayer.LayoutParams lp =
+ (BaseDragLayer.LayoutParams) mFolder.getLayoutParams();
mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams();
ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0);
// Match position of the FolderIcon
final Rect folderIconPos = new Rect();
- float scaleRelativeToDragLayer = mLauncher.getDragLayer()
+ float scaleRelativeToDragLayer = mFolder.mActivityContext.getDragLayer()
.getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos);
int scaledRadius = mPreviewBackground.getScaledRadius();
float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer;
@@ -165,10 +175,15 @@
final float yDistance = initialY - lp.y;
// Set up the Folder background.
- final int finalColor = ColorUtils.setAlphaComponent(
- Themes.getAttrColor(mContext, R.attr.folderFillColor), 255);
+ final int finalColor;
+ int folderFillColor = Themes.getAttrColor(mContext, R.attr.folderFillColor);
+ if (mIsOpening) {
+ finalColor = folderFillColor;
+ } else {
+ finalColor = mFolderBackground.getColor().getDefaultColor();
+ }
final int initialColor = setColorAlphaBound(
- finalColor, mPreviewBackground.getBackgroundAlpha());
+ folderFillColor, mPreviewBackground.getBackgroundAlpha());
mFolderBackground.mutate();
mFolderBackground.setColor(mIsOpening ? initialColor : finalColor);
@@ -179,7 +194,7 @@
Math.round((totalOffsetX + initialSize)),
Math.round((paddingOffsetY + initialSize)));
Rect endRect = new Rect(0, 0, lp.width, lp.height);
- float finalRadius = ResourceUtils.pxFromDp(2, mContext.getResources().getDisplayMetrics());
+ float finalRadius = mFolderBackground.getCornerRadius();
// Create the animators.
AnimatorSet a = new AnimatorSet();
@@ -196,14 +211,46 @@
play(a, anim);
}
+ mBgColorAnimator = getAnimator(mFolderBackground, "color", initialColor, finalColor);
+ play(a, mBgColorAnimator);
play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f));
play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f));
play(a, getAnimator(mFolder.mContent, SCALE_PROPERTY, initialScale, finalScale));
play(a, getAnimator(mFolder.mFooter, SCALE_PROPERTY, initialScale, finalScale));
- play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor));
- play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening));
+
+ final int footerAlphaDuration;
+ final int footerStartDelay;
+ if (isLargeFolder()) {
+ if (mIsOpening) {
+ footerAlphaDuration = LARGE_FOLDER_FOOTER_DURATION;
+ footerStartDelay = mDuration - footerAlphaDuration;
+ } else {
+ footerAlphaDuration = 0;
+ footerStartDelay = 0;
+ }
+ } else {
+ footerStartDelay = 0;
+ footerAlphaDuration = mDuration;
+ }
+ play(a, getAnimator(mFolder.mFooter, ALPHA, 0, 1f), footerStartDelay, footerAlphaDuration);
+
+ // Create reveal animator for the folder background
play(a, getShape().createRevealAnimator(
mFolder, startRect, endRect, finalRadius, !mIsOpening));
+
+ // Create reveal animator for the folder content (capture the top 4 icons 2x2)
+ int width = mDeviceProfile.folderCellLayoutBorderSpacingPx
+ + mDeviceProfile.folderCellWidthPx * 2;
+ int height = mDeviceProfile.folderCellLayoutBorderSpacingPx
+ + mDeviceProfile.folderCellHeightPx * 2;
+ int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage();
+ int left = mContent.getPaddingLeft() + page * lp.width;
+ Rect contentStart = new Rect(left, 0, left + width, height);
+ Rect contentEnd = new Rect(left, 0, left + lp.width, lp.height);
+ play(a, getShape().createRevealAnimator(
+ mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
+
+
// Fade in the folder name, as the text can overlap the icons when grid size is small.
mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
@@ -326,7 +373,9 @@
final int previewPosX =
(int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
- final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale);
+ final float paddingTop = btv.getPaddingTop() * iconScale;
+ final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop)
+ / folderScale);
final float xDistance = previewPosX - btvLp.x;
final float yDistance = previewPosY - btvLp.y;
@@ -392,8 +441,12 @@
as.play(a);
}
+ private boolean isLargeFolder() {
+ return mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW;
+ }
+
private TimeInterpolator getPreviewItemInterpolator() {
- if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
+ if (isLargeFolder()) {
// With larger folders, we want the preview items to reach their final positions faster
// (when opening) and later (when closing) so that they appear aligned with the rest of
// the folder items when they are both visible.
@@ -410,7 +463,7 @@
: ObjectAnimator.ofFloat(view, property, v2, v1);
}
- private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
+ private ObjectAnimator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
return mIsOpening
? ObjectAnimator.ofArgb(drawable, property, v1, v2)
: ObjectAnimator.ofArgb(drawable, property, v2, v1);
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 75275b2..96030f9 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -16,6 +16,7 @@
package com.android.launcher3.folder;
+import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
@@ -113,7 +114,7 @@
FolderGridOrganizer mPreviewVerifier;
ClippedFolderIconLayoutRule mPreviewLayoutRule;
private PreviewItemManager mPreviewItemManager;
- private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+ private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0);
private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>();
boolean mAnimating = false;
@@ -129,6 +130,8 @@
private float mDotScale;
private Animator mDotScaleAnim;
+ private Rect mTouchArea = new Rect();
+
private final PointF mTranslationForReorderBounce = new PointF(0, 0);
private final PointF mTranslationForReorderPreview = new PointF(0, 0);
private float mScaleForReorderBounce = 1f;
@@ -164,17 +167,14 @@
mDotParams = new DotRenderer.DrawParams();
}
- public static FolderIcon inflateFolderAndIcon(int resId, Launcher launcher, ViewGroup group,
- FolderInfo folderInfo) {
- Folder folder = Folder.fromXml(launcher);
- folder.setDragController(launcher.getDragController());
+ public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
+ T activityContext, ViewGroup group, FolderInfo folderInfo) {
+ Folder folder = Folder.fromXml(activityContext);
- FolderIcon icon = inflateIcon(resId, launcher, group, folderInfo);
+ FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
folder.setFolderIcon(icon);
folder.bind(folderInfo);
icon.setFolder(folder);
-
- icon.setOnFocusChangeListener(launcher.getFocusHandler());
return icon;
}
@@ -237,6 +237,8 @@
public void getPreviewBounds(Rect outBounds) {
mPreviewItemManager.recomputePreviewDrawingParams();
mBackground.getBounds(outBounds);
+ // The preview items go outside of the bounds of the background.
+ Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR);
}
public float getBackgroundStrokeWidth() {
@@ -389,7 +391,7 @@
to.offset(center[0] - animateView.getMeasuredWidth() / 2,
center[1] - animateView.getMeasuredHeight() / 2);
- float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f;
+ float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f;
float finalScale = scale * scaleRelativeToDragLayer;
@@ -400,15 +402,18 @@
finalScale *= containerScale;
}
+ final int finalIndex = index;
dragLayer.animateView(animateView, from, to, finalAlpha,
1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
Interpolators.DEACCEL_2, Interpolators.ACCEL_2,
- null, DragLayer.ANIMATION_END_DISAPPEAR, null);
+ () -> {
+ mPreviewItemManager.hidePreviewItem(finalIndex, false);
+ mFolder.showItem(item);
+ }, DragLayer.ANIMATION_END_DISAPPEAR, null);
mFolder.hideItem(item);
if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
- final int finalIndex = index;
FolderNameInfos nameInfos = new FolderNameInfos();
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
@@ -428,8 +433,6 @@
private void showFinalView(int finalIndex, final WorkspaceItemInfo item,
FolderNameInfos nameInfos, InstanceId instanceId) {
postDelayed(() -> {
- mPreviewItemManager.hidePreviewItem(finalIndex, false);
- mFolder.showItem(item);
setLabelSuggestion(nameInfos, instanceId);
invalidate();
}, DROP_IN_ANIMATION_DURATION);
@@ -462,7 +465,7 @@
CharSequence newTitle = nameInfos.getLabels()[0];
FromState fromState = mInfo.getFromLabelState();
- mInfo.setTitle(newTitle, mFolder.mLauncher.getModelWriter());
+ mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
onTitleChanged(mInfo.title);
mFolder.mFolderName.setText(mInfo.title);
@@ -476,7 +479,6 @@
// event is assumed to be folder creation on the server side.
.withEditText(newTitle.toString())
.log(LAUNCHER_FOLDER_AUTO_LABELED);
- mFolder.logFolderLabelState(fromState, ToState.TO_SUGGESTION0);
}
@@ -612,10 +614,7 @@
if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
- final int saveCount = canvas.save();
- canvas.clipPath(mBackground.getClipPath());
mPreviewItemManager.draw(canvas);
- canvas.restoreToCount(saveCount);
if (!mBackground.drawingDelegated()) {
mBackground.drawBackgroundStroke(canvas);
@@ -694,9 +693,9 @@
}
@Override
- public void onRemove(WorkspaceItemInfo item) {
+ public void onRemove(List<WorkspaceItemInfo> items) {
boolean wasDotted = mDotInfo.hasDot();
- mDotInfo.subtractDotInfo(mActivity.getDotInfoForItem(item));
+ items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo);
boolean isDotted = mDotInfo.hasDot();
updateDotScale(wasDotted, isDotted);
setContentDescription(getAccessiblityTitle(mInfo.title));
@@ -711,6 +710,11 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN
+ && shouldIgnoreTouchDown(event.getX(), event.getY())) {
+ return false;
+ }
+
// Call the superclass onTouchEvent first, because sometimes it changes the state to
// isPressed() on an ACTION_UP
super.onTouchEvent(event);
@@ -719,6 +723,15 @@
return true;
}
+ /**
+ * Returns true if the touch down at the provided position be ignored
+ */
+ protected boolean shouldIgnoreTouchDown(float x, float y) {
+ mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
+ getHeight() - getPaddingBottom());
+ return !mTouchArea.contains((int) x, (int) y);
+ }
+
@Override
public void cancelLongPress() {
super.cancelLongPress();
@@ -730,21 +743,19 @@
mInfo.removeListener(mFolder);
}
+ private boolean isInHotseat() {
+ return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+ }
+
public void clearLeaveBehindIfExists() {
- ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
- if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
- CellLayout cl = (CellLayout) getParent().getParent();
- cl.clearFolderLeaveBehind();
+ if (getParent() instanceof FolderIconParent) {
+ ((FolderIconParent) getParent()).clearFolderLeaveBehind(this);
}
}
public void drawLeaveBehindIfExists() {
- CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
- // While the folder is open, the position of the icon cannot change.
- lp.canReorder = false;
- if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
- CellLayout cl = (CellLayout) getParent().getParent();
- cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
+ if (getParent() instanceof FolderIconParent) {
+ ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this);
}
}
@@ -813,4 +824,19 @@
MAX_NUM_ITEMS_IN_PREVIEW);
}
}
+
+ /**
+ * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
+ */
+ public interface FolderIconParent {
+ /**
+ * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a
+ * gap where the FolderIcon would be when the Folder is closed.
+ */
+ void drawFolderLeaveBehindForIcon(FolderIcon child);
+ /**
+ * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed.
+ */
+ void clearFolderLeaveBehind(FolderIcon child);
+ }
}
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index d166e27..9c1b24d 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -48,7 +48,7 @@
public class FolderNameProvider implements ResourceBasedOverride {
private static final String TAG = "FolderNameProvider";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
/**
* IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 32531c0..3d2884a 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -22,6 +22,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
+import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.util.ArrayMap;
import android.util.AttributeSet;
@@ -31,19 +32,16 @@
import android.view.ViewDebug;
import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseActivity;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace.ItemOperator;
-import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -51,6 +49,8 @@
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.ClipPathView;
import java.util.ArrayList;
import java.util.Iterator;
@@ -59,7 +59,7 @@
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
-public class FolderPagedView extends PagedView<PageIndicatorDots> {
+public class FolderPagedView extends PagedView<PageIndicatorDots> implements ClipPathView {
private static final String TAG = "FolderPagedView";
@@ -91,6 +91,8 @@
private Folder mFolder;
+ private Path mClipPath;
+
// If the views are attached to the folder or not. A folder should be bound when its
// animating or is open.
private boolean mViewsBound = false;
@@ -104,7 +106,7 @@
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
- mViewCache = BaseActivity.fromContext(context).getViewCache();
+ mViewCache = ActivityContext.lookupContext(context).getViewCache();
}
public void setFolder(Folder folder) {
@@ -130,8 +132,16 @@
@Override
protected void dispatchDraw(Canvas canvas) {
- mFocusIndicatorHelper.draw(canvas);
- super.dispatchDraw(canvas);
+ if (mClipPath != null) {
+ int count = canvas.save();
+ canvas.clipPath(mClipPath);
+ mFocusIndicatorHelper.draw(canvas);
+ super.dispatchDraw(canvas);
+ canvas.restoreToCount(count);
+ } else {
+ mFocusIndicatorHelper.draw(canvas);
+ super.dispatchDraw(canvas);
+ }
}
/**
@@ -193,7 +203,7 @@
int pageNo = rank / mOrganizer.getMaxItemsPerPage();
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
- lp.setXY(mOrganizer.getPosForRank(rank));
+ lp.setCellXY(mOrganizer.getPosForRank(rank));
getPageAt(pageNo).addViewToCellLayout(view, -1, item.getViewId(), lp, true);
}
@@ -230,7 +240,7 @@
}
private CellLayout createAndAddNewPage() {
- DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
+ DeviceProfile grid = mFolder.mActivityContext.getDeviceProfile();
CellLayout page = mViewCache.getView(R.layout.folder_page, getContext(), this);
page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
@@ -306,7 +316,7 @@
if (v != null) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
ItemInfo info = (ItemInfo) v.getTag();
- lp.setXY(mOrganizer.getPosForRank(rank));
+ lp.setCellXY(mOrganizer.getPosForRank(rank));
currentPage.addViewToCellLayout(v, -1, info.getViewId(), lp, true);
if (mOrganizer.isItemInPreview(rank) && v instanceof BubbleTextView) {
@@ -434,8 +444,7 @@
int scroll = getScrollForPage(getNextPage()) + hint;
int delta = scroll - getScrollX();
if (delta != 0) {
- mScroller.setInterpolator(Interpolators.DEACCEL);
- mScroller.startScroll(getScrollX(), delta, Folder.SCROLL_HINT_DURATION);
+ mScroller.startScroll(getScrollX(), 0, delta, 0, Folder.SCROLL_HINT_DURATION);
invalidate();
}
}
@@ -484,7 +493,7 @@
icon.verifyHighRes();
// Set the callback back to the actual icon, in case
// it was captured by the FolderIcon
- Drawable d = icon.getCompoundDrawables()[1];
+ Drawable d = icon.getIcon();
if (d != null) {
d.setCallback(icon);
}
@@ -500,6 +509,9 @@
* Reorders the items such that the {@param empty} spot moves to {@param target}
*/
public void realTimeReorder(int empty, int target) {
+ if (!mViewsBound) {
+ return;
+ }
completePendingPageChanges();
int delay = 0;
float delayAmount = START_VIEW_REORDER_DELAY;
@@ -621,11 +633,17 @@
@Override
protected boolean canScroll(float absVScroll, float absHScroll) {
- return AbstractFloatingView.getTopOpenViewWithType(mFolder.mLauncher,
+ return AbstractFloatingView.getTopOpenViewWithType(mFolder.mActivityContext,
TYPE_ALL & ~TYPE_FOLDER) == null;
}
public int itemsPerPage() {
return mOrganizer.getMaxItemsPerPage();
}
+
+ @Override
+ public void setClipPath(Path clipPath) {
+ mClipPath = clipPath;
+ invalidate();
+ }
}
diff --git a/src/com/android/launcher3/folder/FolderPreviewItemAnim.java b/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
index 22f7333..e20bafb 100644
--- a/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
+++ b/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
@@ -45,7 +45,7 @@
};
private static final PreviewItemDrawingParams sTmpParams =
- new PreviewItemDrawingParams(0, 0, 0, 0);
+ new PreviewItemDrawingParams(0, 0, 0);
private static final float[] sTempParamsArray = new float[3];
private final ObjectAnimator mAnimator;
@@ -68,6 +68,7 @@
int duration, final Runnable onCompleteRunnable) {
mItemManager = itemManager;
mParams = params;
+ mParams.index = index1;
mItemManager.computePreviewItemDrawingParams(index1, items1, sTmpParams);
finalState = new float[] {sTmpParams.scale, sTmpParams.transX, sTmpParams.transY};
diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java
new file mode 100644
index 0000000..f7d8e8c
--- /dev/null
+++ b/src/com/android/launcher3/folder/LauncherDelegate.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.folder;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.model.ModelWriter;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.views.BaseDragLayer.LayoutParams;
+import com.android.launcher3.widget.LocalColorExtractor;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Wrapper around Launcher methods to allow folders in non-launcher context
+ */
+public class LauncherDelegate {
+
+ private final Launcher mLauncher;
+ private final Rect mTempRect = new Rect();
+ private final RectF mTempRectF = new RectF();
+
+ private LauncherDelegate(Launcher launcher) {
+ mLauncher = launcher;
+ }
+
+ void init(Folder folder, FolderIcon icon) {
+ folder.setDragController(mLauncher.getDragController());
+ icon.setOnFocusChangeListener(mLauncher.getFocusHandler());
+ }
+
+ boolean isDraggingEnabled() {
+ return mLauncher.isDraggingEnabled();
+ }
+
+ void beginDragShared(View child, DragSource source, DragOptions options) {
+ mLauncher.getWorkspace().beginDragShared(child, source, options);
+ }
+
+ ModelWriter getModelWriter() {
+ return mLauncher.getModelWriter();
+ }
+
+ void forEachVisibleWorkspacePage(Consumer<View> callback) {
+ mLauncher.getWorkspace().forEachVisiblePage(callback);
+ }
+
+ @Nullable
+ Launcher getLauncher() {
+ return mLauncher;
+ }
+
+ void addRectForColorExtraction(BaseDragLayer.LayoutParams lp, LocalColorExtractor target) {
+ mTempRect.set(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height);
+ target.getExtractedRectForViewRect(mLauncher,
+ mLauncher.getWorkspace().getCurrentPage(), mTempRect, mTempRectF);
+ if (!mTempRectF.isEmpty()) {
+ target.addLocation(Arrays.asList(mTempRectF));
+ }
+ }
+
+ void replaceFolderWithFinalItem(Folder folder) {
+ // Add the last remaining child to the workspace in place of the folder
+ Runnable onCompleteRunnable = new Runnable() {
+ @Override
+ public void run() {
+ int itemCount = folder.getItemCount();
+ FolderInfo info = folder.mInfo;
+ if (itemCount <= 1) {
+ View newIcon = null;
+ WorkspaceItemInfo finalItem = null;
+
+ if (itemCount == 1) {
+ // Move the item from the folder to the workspace, in the position of the
+ // folder
+ CellLayout cellLayout = mLauncher.getCellLayout(info.container,
+ info.screenId);
+ finalItem = info.contents.remove(0);
+ newIcon = mLauncher.createShortcut(cellLayout, finalItem);
+ mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
+ info.container, info.screenId, info.cellX, info.cellY);
+ }
+
+ // Remove the folder
+ mLauncher.removeItem(folder.mFolderIcon, info, true /* deleteFromDb */);
+ if (folder.mFolderIcon instanceof DropTarget) {
+ folder.mDragController.removeDropTarget((DropTarget) folder.mFolderIcon);
+ }
+
+ if (newIcon != null) {
+ // We add the child after removing the folder to prevent both from existing
+ // at the same time in the CellLayout. We need to add the new item with
+ // addInScreenFromBind() to ensure that hotseat items are placed correctly.
+ mLauncher.getWorkspace().addInScreenFromBind(newIcon, info);
+
+ // Focus the newly created child
+ newIcon.requestFocus();
+ }
+ if (finalItem != null) {
+ StatsLogger logger = mLauncher.getStatsLogManager().logger()
+ .withItemInfo(finalItem);
+ ((Optional<InstanceId>) folder.mDragController.getLogInstanceId())
+ .map(logger::withInstanceId)
+ .orElse(logger)
+ .log(LAUNCHER_FOLDER_CONVERTED_TO_ICON);
+ }
+ }
+ }
+ };
+ View finalChild = folder.mContent.getLastItem();
+ if (finalChild != null) {
+ folder.mFolderIcon.performDestroyAnimation(onCompleteRunnable);
+ } else {
+ onCompleteRunnable.run();
+ }
+ }
+
+
+ boolean interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder) {
+ if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
+ // Do not close the container if in drag and drop.
+ if (!dl.isEventOverView(mLauncher.getDropTargetBar(), ev)) {
+ return true;
+ }
+ } else {
+ // TODO: add ww log if need to gather tap outside to close folder
+ folder.close(true);
+ return true;
+ }
+ return false;
+ }
+
+ private static class FallbackDelegate extends LauncherDelegate {
+
+ private final ActivityContext mContext;
+ private ModelWriter mWriter;
+
+ FallbackDelegate(ActivityContext context) {
+ super(null);
+ mContext = context;
+ }
+
+ @Override
+ void init(Folder folder, FolderIcon icon) {
+ folder.setDragController(mContext.getDragController());
+ }
+
+ @Override
+ boolean isDraggingEnabled() {
+ return false;
+ }
+
+ @Override
+ void beginDragShared(View child, DragSource source, DragOptions options) { }
+
+ @Override
+ ModelWriter getModelWriter() {
+ if (mWriter == null) {
+ mWriter = LauncherAppState.getInstance((Context) mContext).getModel()
+ .getWriter(false, false);
+ }
+ return mWriter;
+ }
+
+ @Override
+ void forEachVisibleWorkspacePage(Consumer<View> callback) { }
+
+ @Override
+ Launcher getLauncher() {
+ return null;
+ }
+
+ @Override
+ void replaceFolderWithFinalItem(Folder folder) { }
+
+ @Override
+ boolean interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder) {
+ folder.close(true);
+ return true;
+ }
+
+ @Override
+ void addRectForColorExtraction(LayoutParams lp, LocalColorExtractor target) { }
+ }
+
+ static LauncherDelegate from(ActivityContext context) {
+ return context instanceof Launcher
+ ? new LauncherDelegate((Launcher) context)
+ : new FallbackDelegate(context);
+ }
+}
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 27b906b..18d0b10 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -16,6 +16,7 @@
package com.android.launcher3.folder;
+import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.graphics.IconShape.getShape;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -50,6 +51,9 @@
*/
public class PreviewBackground extends CellLayout.DelegatedCellDrawing {
+ private static final boolean DRAW_SHADOW = false;
+ private static final boolean DRAW_STROKE = false;
+
private static final int CONSUMPTION_ANIMATION_DURATION = 100;
private final PorterDuffXfermode mShadowPorterDuffXfermode
@@ -86,8 +90,8 @@
private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
// Expressed on a scale from 0 to 255.
- private static final int BG_OPACITY = 160;
- private static final int MAX_BG_OPACITY = 225;
+ private static final int BG_OPACITY = 255;
+ private static final int MAX_BG_OPACITY = 255;
private static final int SHADOW_OPACITY = 40;
private ValueAnimator mScaleAnimator;
@@ -162,13 +166,15 @@
// Stroke width is 1dp
mStrokeWidth = context.getResources().getDisplayMetrics().density;
- float radius = getScaledRadius();
- float shadowRadius = radius + mStrokeWidth;
- int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
- mShadowShader = new RadialGradient(0, 0, 1,
- new int[] {shadowColor, Color.TRANSPARENT},
- new float[] {radius / shadowRadius, 1},
- Shader.TileMode.CLAMP);
+ if (DRAW_SHADOW) {
+ float radius = getScaledRadius();
+ float shadowRadius = radius + mStrokeWidth;
+ int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
+ mShadowShader = new RadialGradient(0, 0, 1,
+ new int[]{shadowColor, Color.TRANSPARENT},
+ new float[]{radius / shadowRadius, 1},
+ Shader.TileMode.CLAMP);
+ }
invalidate();
}
@@ -181,7 +187,7 @@
outBounds.set(left, top, right, bottom);
}
- int getRadius() {
+ public int getRadius() {
return previewSize / 2;
}
@@ -238,6 +244,9 @@
}
public void drawShadow(Canvas canvas) {
+ if (!DRAW_SHADOW) {
+ return;
+ }
if (mShadowShader == null) {
return;
}
@@ -276,6 +285,9 @@
}
public void fadeInBackgroundShadow() {
+ if (!DRAW_SHADOW) {
+ return;
+ }
if (mShadowAnimator != null) {
mShadowAnimator.cancel();
}
@@ -292,6 +304,10 @@
}
public void animateBackgroundStroke() {
+ if (!DRAW_STROKE) {
+ return;
+ }
+
if (mStrokeAlphaAnimator != null) {
mStrokeAlphaAnimator.cancel();
}
@@ -308,6 +324,9 @@
}
public void drawBackgroundStroke(Canvas canvas) {
+ if (!DRAW_STROKE) {
+ return;
+ }
mPaint.setColor(setColorAlphaBound(mStrokeColor, mStrokeAlpha));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
@@ -330,7 +349,12 @@
public Path getClipPath() {
mPath.reset();
- getShape().addToPath(mPath, getOffsetX(), getOffsetY(), getScaledRadius());
+ float radius = getScaledRadius() * ICON_OVERLAP_FACTOR;
+ // Find the difference in radius so that the clip path remains centered.
+ float radiusDifference = radius - getRadius();
+ float offsetX = basePreviewOffsetX - radiusDifference;
+ float offsetY = basePreviewOffsetY - radiusDifference;
+ getShape().addToPath(mPath, offsetX, offsetY, radius);
return mPath;
}
@@ -352,7 +376,7 @@
}
mDrawingDelegate = null;
- isClipping = true;
+ isClipping = false;
invalidate();
}
diff --git a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
index a14a0d8..58efdc1 100644
--- a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
+++ b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
@@ -23,20 +23,19 @@
* Manages the parameters used to draw a Folder preview item.
*/
class PreviewItemDrawingParams {
+ float index;
float transX;
float transY;
float scale;
- float overlayAlpha;
public FolderPreviewItemAnim anim;
public boolean hidden;
public Drawable drawable;
public WorkspaceItemInfo item;
- PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
+ PreviewItemDrawingParams(float transX, float transY, float scale) {
this.transX = transX;
this.transY = transY;
this.scale = scale;
- this.overlayAlpha = overlayAlpha;
}
public void update(float transX, float transY, float scale) {
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 7f8a15c..8bef6ad 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -16,7 +16,6 @@
package com.android.launcher3.folder;
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
@@ -29,16 +28,19 @@
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.view.View;
-import android.widget.TextView;
import androidx.annotation.NonNull;
+import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Utilities;
import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.views.ActivityContext;
@@ -81,6 +83,10 @@
// These hold the current page preview items. It is empty if the current page is the first page.
private ArrayList<PreviewItemDrawingParams> mCurrentPageParams = new ArrayList<>();
+ // We clip the preview items during the middle of the animation, so that it does not go outside
+ // of the visual shape. We stop clipping at this threshold, since the preview items ultimately
+ // do not get cropped in their resting state.
+ private final float mClipThreshold;
private float mCurrentPageItemsTransX = 0;
private boolean mShouldSlideInFirstPage;
@@ -96,6 +102,7 @@
mIcon = icon;
mIconSize = ActivityContext.lookupContext(
mContext).getDeviceProfile().folderChildIconSizePx;
+ mClipThreshold = Utilities.dpToPx(1f);
}
/**
@@ -112,7 +119,7 @@
}
Drawable prepareCreateAnimation(final View destView) {
- Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1];
+ Drawable animateDrawable = ((BubbleTextView) destView).getIcon();
computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
destView.getMeasuredWidth());
mReferenceDrawable = animateDrawable;
@@ -163,41 +170,60 @@
}
public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
- float transX) {
- canvas.translate(transX, 0);
+ PointF offset, boolean shouldClipPath, Path clipPath) {
// The first item should be drawn last (ie. on top of later items)
for (int i = params.size() - 1; i >= 0; i--) {
PreviewItemDrawingParams p = params.get(i);
if (!p.hidden) {
- drawPreviewItem(canvas, p);
+ // Exiting param should always be clipped.
+ boolean isExiting = p.index == EXIT_INDEX;
+ drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
}
}
- canvas.translate(-transX, 0);
}
+ /**
+ * Draws the preview items on {@param canvas}.
+ */
public void draw(Canvas canvas) {
+ int saveCount = canvas.getSaveCount();
// The items are drawn in coordinates relative to the preview offset
PreviewBackground bg = mIcon.getFolderBackground();
- canvas.translate(bg.basePreviewOffsetX, bg.basePreviewOffsetY);
-
+ Path clipPath = bg.getClipPath();
float firstPageItemsTransX = 0;
if (mShouldSlideInFirstPage) {
- drawParams(canvas, mCurrentPageParams, mCurrentPageItemsTransX);
-
+ PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + mCurrentPageItemsTransX,
+ bg.basePreviewOffsetY);
+ boolean shouldClip = mCurrentPageItemsTransX > mClipThreshold;
+ drawParams(canvas, mCurrentPageParams, firstPageOffset, shouldClip, clipPath);
firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX;
}
- drawParams(canvas, mFirstPageParams, firstPageItemsTransX);
- canvas.translate(-bg.basePreviewOffsetX, -bg.basePreviewOffsetY);
+ PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + firstPageItemsTransX,
+ bg.basePreviewOffsetY);
+ boolean shouldClipFirstPage = firstPageItemsTransX < -mClipThreshold;
+ drawParams(canvas, mFirstPageParams, firstPageOffset, shouldClipFirstPage, clipPath);
+ canvas.restoreToCount(saveCount);
}
public void onParamsChanged() {
mIcon.invalidate();
}
- private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
+ /**
+ * Draws each preview item.
+ *
+ * @param offset The offset needed to draw the preview items.
+ * @param shouldClipPath Iff true, clip path using {@param clipPath}.
+ * @param clipPath The clip path of the folder icon.
+ */
+ private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
+ boolean shouldClipPath, Path clipPath) {
canvas.save();
- canvas.translate(params.transX, params.transY);
+ if (shouldClipPath) {
+ canvas.clipPath(clipPath);
+ }
+ canvas.translate(offset.x + params.transX, offset.y + params.transY);
canvas.scale(params.scale, params.scale);
Drawable d = params.drawable;
@@ -234,7 +260,7 @@
params.remove(params.size() - 1);
}
while (items.size() > params.size()) {
- params.add(new PreviewItemDrawingParams(0, 0, 0, 0));
+ params.add(new PreviewItemDrawingParams(0, 0, 0));
}
int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
@@ -243,6 +269,9 @@
setDrawable(p, items.get(i));
if (!animate) {
+ if (p.anim != null) {
+ p.anim.cancel();
+ }
computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
if (mReferenceDrawable == null) {
mReferenceDrawable = p.drawable;
@@ -394,12 +423,13 @@
}
private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
- if (item.hasPromiseIconUi()) {
+ if (item.hasPromiseIconUi() || (item.runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
PreloadIconDrawable drawable = newPendingIcon(mContext, item);
- drawable.setLevel(item.getInstallProgress());
+ drawable.setLevel(item.getProgressLevel());
p.drawable = drawable;
} else {
- p.drawable = newIcon(mContext, item);
+ p.drawable = item.newIcon(mContext, true);
}
p.drawable.setBounds(0, 0, mIconSize, mIconSize);
p.item = item;
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index 21822a3..a549750 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -29,12 +29,15 @@
import android.graphics.drawable.Drawable;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
@@ -88,10 +91,14 @@
}
/**
- * Returns a new bitmap to show when the {@link #mView} is being dragged around.
- * Responsibility for the bitmap is transferred to the caller.
+ * Returns a new drawable to show when the {@link #mView} is being dragged around.
+ * Responsibility for the drawable is transferred to the caller.
*/
- public Bitmap createDragBitmap() {
+ public Drawable createDrawable() {
+ if (mView instanceof LauncherAppWidgetHostView) {
+ return null;
+ }
+
int width = 0;
int height = 0;
// Assume scaleX == scaleY, which is always the case for workspace items.
@@ -105,8 +112,21 @@
height = mView.getHeight();
}
- return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
- height + blurSizeOutline, (c) -> drawDragView(c, scale));
+ return new FastBitmapDrawable(
+ BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
+ height + blurSizeOutline, (c) -> drawDragView(c, scale)));
+ }
+
+ /**
+ * Returns the content view if the content should be rendered directly in
+ * {@link com.android.launcher3.dragndrop.DragView}. Otherwise, returns null.
+ */
+ @Nullable
+ public View getContentView() {
+ if (mView instanceof LauncherAppWidgetHostView) {
+ return mView;
+ }
+ return null;
}
public final void generateDragOutline(Bitmap preview) {
@@ -129,7 +149,7 @@
return bounds;
}
- public float getScaleAndPosition(Bitmap preview, int[] outPos) {
+ public float getScaleAndPosition(Drawable preview, int[] outPos) {
float scale = Launcher.getLauncher(mView.getContext())
.getDragLayer().getLocationInDragLayer(mView, outPos);
if (mView instanceof LauncherAppWidgetHostView) {
@@ -139,12 +159,28 @@
}
outPos[0] = Math.round(outPos[0] -
- (preview.getWidth() - scale * mView.getWidth() * mView.getScaleX()) / 2);
- outPos[1] = Math.round(outPos[1] - (1 - scale) * preview.getHeight() / 2
+ (preview.getIntrinsicWidth() - scale * mView.getWidth() * mView.getScaleX()) / 2);
+ outPos[1] = Math.round(outPos[1] - (1 - scale) * preview.getIntrinsicHeight() / 2
- previewPadding / 2);
return scale;
}
+ /** Returns the scale and position of a given view for drag-n-drop. */
+ public float getScaleAndPosition(View view, int[] outPos) {
+ float scale = Launcher.getLauncher(mView.getContext())
+ .getDragLayer().getLocationInDragLayer(mView, outPos);
+ if (mView instanceof LauncherAppWidgetHostView) {
+ // App widgets are technically scaled, but are drawn at their expected size -- so the
+ // app widget scale should not affect the scale of the preview.
+ scale /= ((LauncherAppWidgetHostView) mView).getScaleToFit();
+ }
+
+ outPos[0] = Math.round(outPos[0]
+ - (view.getWidth() - scale * mView.getWidth() * mView.getScaleX()) / 2);
+ outPos[1] = Math.round(outPos[1] - (1 - scale) * view.getHeight() / 2 - previewPadding / 2);
+ return scale;
+ }
+
protected Bitmap convertPreviewToAlphaBitmap(Bitmap preview) {
return preview.copy(Bitmap.Config.ALPHA_8, true);
}
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
new file mode 100644
index 0000000..e4f5539
--- /dev/null
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -0,0 +1,268 @@
+package com.android.launcher3.graphics;
+
+import static com.android.launcher3.Utilities.getPrefs;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS;
+import static com.android.launcher3.util.Themes.isThemedIconEnabled;
+
+import android.annotation.TargetApi;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.content.res.XmlResourceParser;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile.GridOption;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Executors;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Exposes various launcher grid options and allows the caller to change them.
+ * APIs:
+ * /list_options: List the various available grip options, has following columns
+ * name: name of the grid
+ * rows: number of rows in the grid
+ * cols: number of columns in the grid
+ * preview_count: number of previews available for this grid option. The preview uri
+ * looks like /preview/<grid-name>/<preview index starting with 0>
+ * is_default: true if this grid is currently active
+ *
+ * /preview: Opens a file stream for the grid preview
+ *
+ * /default_grid: Call update to set the current grid, with values
+ * name: name of the grid to apply
+ */
+public class GridCustomizationsProvider extends ContentProvider {
+
+ private static final String TAG = "GridCustomizationsProvider";
+
+ private static final String KEY_NAME = "name";
+ private static final String KEY_ROWS = "rows";
+ private static final String KEY_COLS = "cols";
+ private static final String KEY_PREVIEW_COUNT = "preview_count";
+ private static final String KEY_IS_DEFAULT = "is_default";
+
+ private static final String KEY_LIST_OPTIONS = "/list_options";
+ private static final String KEY_DEFAULT_GRID = "/default_grid";
+
+ private static final String METHOD_GET_PREVIEW = "get_preview";
+
+ private static final String GET_ICON_THEMED = "/get_icon_themed";
+ private static final String SET_ICON_THEMED = "/set_icon_themed";
+ private static final String ICON_THEMED = "/icon_themed";
+ private static final String BOOLEAN_VALUE = "boolean_value";
+
+ private static final String KEY_SURFACE_PACKAGE = "surface_package";
+ private static final String KEY_CALLBACK = "callback";
+
+ private final ArrayMap<IBinder, PreviewLifecycleObserver> mActivePreviews = new ArrayMap<>();
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ switch (uri.getPath()) {
+ case KEY_LIST_OPTIONS: {
+ MatrixCursor cursor = new MatrixCursor(new String[] {
+ KEY_NAME, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT, KEY_IS_DEFAULT});
+ InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
+ for (GridOption gridOption : parseAllGridOptions()) {
+ cursor.newRow()
+ .add(KEY_NAME, gridOption.name)
+ .add(KEY_ROWS, gridOption.numRows)
+ .add(KEY_COLS, gridOption.numColumns)
+ .add(KEY_PREVIEW_COUNT, 1)
+ .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
+ && idp.numRows == gridOption.numRows);
+ }
+ return cursor;
+ }
+ case GET_ICON_THEMED:
+ case ICON_THEMED: {
+ MatrixCursor cursor = new MatrixCursor(new String[] {BOOLEAN_VALUE});
+ cursor.newRow().add(BOOLEAN_VALUE, isThemedIconEnabled(getContext()) ? 1 : 0);
+ return cursor;
+ }
+ default:
+ return null;
+ }
+ }
+
+ private List<GridOption> parseAllGridOptions() {
+ List<GridOption> result = new ArrayList<>();
+ try (XmlResourceParser parser = getContext().getResources().getXml(R.xml.device_profiles)) {
+ final int depth = parser.getDepth();
+ int type;
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+ if ((type == XmlPullParser.START_TAG)
+ && GridOption.TAG_NAME.equals(parser.getName())) {
+ result.add(new GridOption(getContext(), Xml.asAttributeSet(parser)));
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ Log.e(TAG, "Error parsing device profile", e);
+ return Collections.emptyList();
+ }
+ return result;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "vnd.android.cursor.dir/launcher_grid";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues initialValues) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ switch (uri.getPath()) {
+ case KEY_DEFAULT_GRID: {
+ String gridName = values.getAsString(KEY_NAME);
+ // Verify that this is a valid grid option
+ GridOption match = null;
+ for (GridOption option : parseAllGridOptions()) {
+ if (option.name.equals(gridName)) {
+ match = option;
+ break;
+ }
+ }
+ if (match == null) {
+ return 0;
+ }
+
+ InvariantDeviceProfile.INSTANCE.get(getContext())
+ .setCurrentGrid(getContext(), gridName);
+ return 1;
+ }
+ case ICON_THEMED:
+ case SET_ICON_THEMED: {
+ if (FeatureFlags.ENABLE_THEMED_ICONS.get()) {
+ getPrefs(getContext()).edit()
+ .putBoolean(KEY_THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE))
+ .apply();
+ }
+ return 1;
+ }
+ default:
+ return 0;
+ }
+ }
+
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ if (getContext().checkPermission("android.permission.BIND_WALLPAPER",
+ Binder.getCallingPid(), Binder.getCallingUid())
+ != PackageManager.PERMISSION_GRANTED) {
+ return null;
+ }
+
+ if (!Utilities.ATLEAST_R || !METHOD_GET_PREVIEW.equals(method)) {
+ return null;
+ }
+ return getPreview(extras);
+ }
+
+ @TargetApi(Build.VERSION_CODES.R)
+ private synchronized Bundle getPreview(Bundle request) {
+ PreviewLifecycleObserver observer = null;
+ try {
+ PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(getContext(), request);
+
+ // Destroy previous
+ destroyObserver(mActivePreviews.get(renderer.getHostToken()));
+
+ observer = new PreviewLifecycleObserver(renderer);
+ mActivePreviews.put(renderer.getHostToken(), observer);
+
+ renderer.loadAsync();
+ renderer.getHostToken().linkToDeath(observer, 0);
+
+ Bundle result = new Bundle();
+ result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getSurfacePackage());
+
+ Messenger messenger =
+ new Messenger(new Handler(UI_HELPER_EXECUTOR.getLooper(), observer));
+ Message msg = Message.obtain();
+ msg.replyTo = messenger;
+ result.putParcelable(KEY_CALLBACK, msg);
+ return result;
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to generate preview", e);
+ if (observer != null) {
+ destroyObserver(observer);
+ }
+ return null;
+ }
+ }
+
+ private synchronized void destroyObserver(PreviewLifecycleObserver observer) {
+ if (observer == null || observer.destroyed) {
+ return;
+ }
+ observer.destroyed = true;
+ observer.renderer.getHostToken().unlinkToDeath(observer, 0);
+ Executors.MAIN_EXECUTOR.execute(observer.renderer::destroy);
+ PreviewLifecycleObserver cached = mActivePreviews.get(observer.renderer.getHostToken());
+ if (cached == observer) {
+ mActivePreviews.remove(observer.renderer.getHostToken());
+ }
+ }
+
+ private class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient {
+
+ public final PreviewSurfaceRenderer renderer;
+ public boolean destroyed = false;
+
+ PreviewLifecycleObserver(PreviewSurfaceRenderer renderer) {
+ this.renderer = renderer;
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ destroyObserver(this);
+ return true;
+ }
+
+ @Override
+ public void binderDied() {
+ destroyObserver(this);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/graphics/GridOptionsProvider.java b/src/com/android/launcher3/graphics/GridOptionsProvider.java
deleted file mode 100644
index 08d7e4c..0000000
--- a/src/com/android/launcher3/graphics/GridOptionsProvider.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package com.android.launcher3.graphics;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.pm.PackageManager;
-import android.content.res.XmlResourceParser;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.Bundle;
-import android.util.Log;
-import android.util.Xml;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile.GridOption;
-import com.android.launcher3.R;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Exposes various launcher grid options and allows the caller to change them.
- * APIs:
- * /list_options: List the various available grip options, has following columns
- * name: name of the grid
- * rows: number of rows in the grid
- * cols: number of columns in the grid
- * preview_count: number of previews available for this grid option. The preview uri
- * looks like /preview/<grid-name>/<preview index starting with 0>
- * is_default: true if this grid is currently active
- *
- * /preview: Opens a file stream for the grid preview
- *
- * /default_grid: Call update to set the current grid, with values
- * name: name of the grid to apply
- */
-public class GridOptionsProvider extends ContentProvider {
-
- private static final String TAG = "GridOptionsProvider";
-
- private static final String KEY_NAME = "name";
- private static final String KEY_ROWS = "rows";
- private static final String KEY_COLS = "cols";
- private static final String KEY_PREVIEW_COUNT = "preview_count";
- private static final String KEY_IS_DEFAULT = "is_default";
-
- private static final String KEY_LIST_OPTIONS = "/list_options";
- private static final String KEY_DEFAULT_GRID = "/default_grid";
-
- private static final String METHOD_GET_PREVIEW = "get_preview";
-
- @Override
- public boolean onCreate() {
- return true;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- if (!KEY_LIST_OPTIONS.equals(uri.getPath())) {
- return null;
- }
- MatrixCursor cursor = new MatrixCursor(new String[] {
- KEY_NAME, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT, KEY_IS_DEFAULT});
- InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
- for (GridOption gridOption : parseAllGridOptions()) {
- cursor.newRow()
- .add(KEY_NAME, gridOption.name)
- .add(KEY_ROWS, gridOption.numRows)
- .add(KEY_COLS, gridOption.numColumns)
- .add(KEY_PREVIEW_COUNT, 1)
- .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
- && idp.numRows == gridOption.numRows);
- }
- return cursor;
- }
-
- private List<GridOption> parseAllGridOptions() {
- List<GridOption> result = new ArrayList<>();
- try (XmlResourceParser parser = getContext().getResources().getXml(R.xml.device_profiles)) {
- final int depth = parser.getDepth();
- int type;
- while (((type = parser.next()) != XmlPullParser.END_TAG ||
- parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
- if ((type == XmlPullParser.START_TAG)
- && GridOption.TAG_NAME.equals(parser.getName())) {
- result.add(new GridOption(getContext(), Xml.asAttributeSet(parser)));
- }
- }
- } catch (IOException | XmlPullParserException e) {
- Log.e(TAG, "Error parsing device profile", e);
- return Collections.emptyList();
- }
- return result;
- }
-
- @Override
- public String getType(Uri uri) {
- return "vnd.android.cursor.dir/launcher_grid";
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues initialValues) {
- return null;
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- return 0;
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- if (!KEY_DEFAULT_GRID.equals(uri.getPath())) {
- return 0;
- }
-
- String gridName = values.getAsString(KEY_NAME);
- // Verify that this is a valid grid option
- GridOption match = null;
- for (GridOption option : parseAllGridOptions()) {
- if (option.name.equals(gridName)) {
- match = option;
- break;
- }
- }
- if (match == null) {
- return 0;
- }
-
- InvariantDeviceProfile.INSTANCE.get(getContext()).setCurrentGrid(getContext(), gridName);
- return 1;
- }
-
- @Override
- public Bundle call(String method, String arg, Bundle extras) {
- if (getContext().checkPermission("android.permission.BIND_WALLPAPER",
- Binder.getCallingPid(), Binder.getCallingUid())
- != PackageManager.PERMISSION_GRANTED) {
- return null;
- }
-
- if (!METHOD_GET_PREVIEW.equals(method)) {
- return null;
- }
-
- return new PreviewSurfaceRenderer(getContext(), extras).render();
- }
-}
diff --git a/src/com/android/launcher3/graphics/IconShape.java b/src/com/android/launcher3/graphics/IconShape.java
index 4369385..f82b07e 100644
--- a/src/com/android/launcher3/graphics/IconShape.java
+++ b/src/com/android/launcher3/graphics/IconShape.java
@@ -37,19 +37,14 @@
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.util.AttributeSet;
-import android.util.SparseArray;
-import android.util.TypedValue;
import android.util.Xml;
import android.view.View;
import android.view.ViewOutlineProvider;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ClipPathView;
import org.xmlpull.v1.XmlPullParser;
@@ -59,38 +54,22 @@
import java.util.ArrayList;
import java.util.List;
-import androidx.annotation.Nullable;
-
/**
* Abstract representation of the shape of an icon shape
*/
public abstract class IconShape {
private static IconShape sInstance = new Circle();
- private static Path sShapePath;
private static float sNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
- public static final int DEFAULT_PATH_SIZE = 100;
-
public static IconShape getShape() {
return sInstance;
}
- public static Path getShapePath() {
- if (sShapePath == null) {
- Path p = new Path();
- getShape().addToPath(p, 0, 0, DEFAULT_PATH_SIZE * 0.5f);
- sShapePath = p;
- }
- return sShapePath;
- }
-
public static float getNormalizationScale() {
return sNormalizationScale;
}
- private SparseArray<TypedValue> mAttrs;
-
public boolean enableShapeDetection(){
return false;
};
@@ -103,11 +82,6 @@
public abstract <T extends View & ClipPathView> Animator createRevealAnimator(T target,
Rect startRect, Rect endRect, float endRadius, boolean isReversed);
- @Nullable
- public TypedValue getAttrValue(int attr) {
- return mAttrs == null ? null : mAttrs.get(attr);
- }
-
/**
* Abstract shape where the reveal animation is a derivative of a round rect animation
*/
@@ -182,19 +156,43 @@
}
}
- public static final class Circle extends SimpleRectShape {
+ public static final class Circle extends PathShape {
- @Override
- public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
- canvas.drawCircle(radius + offsetX, radius + offsetY, radius, p);
+ private final float[] mTempRadii = new float[8];
+
+ protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
+ float endRadius, Path outPath) {
+ float r1 = getStartRadius(startRect);
+
+ float[] startValues = new float[] {
+ startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r1};
+ float[] endValues = new float[] {
+ endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius};
+
+ FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]);
+
+ return (anim) -> {
+ float progress = (Float) anim.getAnimatedValue();
+ float[] values = evaluator.evaluate(progress, startValues, endValues);
+ outPath.addRoundRect(
+ values[0], values[1], values[2], values[3],
+ getRadiiArray(values[4], values[5]), Path.Direction.CW);
+ };
}
+ private float[] getRadiiArray(float r1, float r2) {
+ mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] =
+ mTempRadii[6] = mTempRadii[7] = r1;
+ mTempRadii[4] = mTempRadii[5] = r2;
+ return mTempRadii;
+ }
+
+
@Override
public void addToPath(Path path, float offsetX, float offsetY, float radius) {
path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW);
}
- @Override
protected float getStartRadius(Rect startRect) {
return startRect.width() / 2f;
}
@@ -381,9 +379,6 @@
* Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
*/
public static void init(Context context) {
- if (!Utilities.ATLEAST_OREO) {
- return;
- }
pickBestShape(context);
}
@@ -414,7 +409,6 @@
final int depth = parser.getDepth();
int[] radiusAttr = new int[] {R.attr.folderIconRadius};
- IntArray keysToIgnore = new IntArray(0);
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
@@ -425,7 +419,6 @@
IconShape shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
a.recycle();
- shape.mAttrs = Themes.createValueMap(context, attrs, keysToIgnore);
result.add(shape);
}
}
@@ -471,8 +464,6 @@
}
// Initialize shape properties
- drawable.setBounds(0, 0, DEFAULT_PATH_SIZE, DEFAULT_PATH_SIZE);
- sShapePath = new Path(drawable.getIconMask());
sNormalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null);
}
}
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 885fb66..7a31ea0 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -15,23 +15,25 @@
*/
package com.android.launcher3.graphics;
+import static android.app.WallpaperManager.FLAG_SYSTEM;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static android.view.View.VISIBLE;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.annotation.TargetApi;
import android.app.Fragment;
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
-import android.content.pm.ShortcutInfo;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
@@ -42,11 +44,14 @@
import android.os.Looper;
import android.os.Process;
import android.util.AttributeSet;
-import android.util.Log;
+import android.util.SparseIntArray;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.view.WindowManager;
import android.widget.TextClock;
import com.android.launcher3.BubbleTextView;
@@ -56,25 +61,19 @@
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
import com.android.launcher3.WorkspaceLayoutManager;
-import com.android.launcher3.allapps.SearchUiManager;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.model.LoaderResults;
-import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.WidgetsModel;
-import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -83,26 +82,24 @@
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.uioverrides.PredictedAppIconInflater;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.LocalColorExtractor;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
/**
* Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
@@ -113,9 +110,8 @@
* 4) Measure and draw the view on a canvas
*/
@TargetApi(Build.VERSION_CODES.O)
-public class LauncherPreviewRenderer {
-
- private static final String TAG = "LauncherPreviewRenderer";
+public class LauncherPreviewRenderer extends ContextWrapper
+ implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {
/**
* Context used just for preview. It also provides a few objects (e.g. UserCache) just for
@@ -123,7 +119,7 @@
*/
public static class PreviewContext extends ContextWrapper {
- private static final Set<MainThreadInitializedObject> WHITELIST = new HashSet<>(
+ private final Set<MainThreadInitializedObject> mAllowedObjects = new HashSet<>(
Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE));
@@ -133,9 +129,15 @@
private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
new ConcurrentLinkedQueue<>();
+ private boolean mDestroyed = false;
+
public PreviewContext(Context base, InvariantDeviceProfile idp) {
super(base);
mIdp = idp;
+ mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp);
+ mObjectMap.put(LauncherAppState.INSTANCE,
+ new LauncherAppState(this, null /* iconCacheFileName */));
+
}
@Override
@@ -144,11 +146,9 @@
}
public void onDestroy() {
- CustomWidgetManager customWidgetManager = (CustomWidgetManager) mObjectMap.get(
- CustomWidgetManager.INSTANCE);
- if (customWidgetManager != null) {
- customWidgetManager.onDestroy();
- }
+ CustomWidgetManager.INSTANCE.get(this).onDestroy();
+ LauncherAppState.INSTANCE.get(this).onTerminate();
+ mDestroyed = true;
}
/**
@@ -157,17 +157,12 @@
*/
public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
MainThreadInitializedObject.ObjectProvider<T> provider) {
- if (!WHITELIST.contains(mainThreadInitializedObject)) {
+ if (FeatureFlags.IS_STUDIO_BUILD && mDestroyed) {
+ throw new RuntimeException("Context already destroyed");
+ }
+ if (!mAllowedObjects.contains(mainThreadInitializedObject)) {
throw new IllegalStateException("Leaking unknown objects");
}
- if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
- throw new IllegalStateException(
- "Should not use MainThreadInitializedObject to initialize this with "
- + "PreviewContext");
- }
- if (mainThreadInitializedObject == InvariantDeviceProfile.INSTANCE) {
- return (T) mIdp;
- }
if (mObjectMap.containsKey(mainThreadInitializedObject)) {
return (T) mObjectMap.get(mainThreadInitializedObject);
}
@@ -205,22 +200,37 @@
private final Context mContext;
private final InvariantDeviceProfile mIdp;
private final DeviceProfile mDp;
- private final boolean mMigrated;
private final Rect mInsets;
-
private final WorkspaceItemInfo mWorkspaceItemInfo;
+ private final LayoutInflater mHomeElementInflater;
+ private final InsettableFrameLayout mRootView;
+ private final Hotseat mHotseat;
+ private final CellLayout mWorkspace;
+ private final SparseIntArray mWallpaperColorResources;
- public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, boolean migrated) {
+ public LauncherPreviewRenderer(Context context,
+ InvariantDeviceProfile idp,
+ WallpaperColors wallpaperColorsOverride) {
+
+ super(context);
mUiHandler = new Handler(Looper.getMainLooper());
mContext = context;
mIdp = idp;
- mDp = idp.portraitProfile.copy(context);
- mMigrated = migrated;
+ mDp = idp.getDeviceProfile(context).copy(context);
- // TODO: get correct insets once display cutout API is available.
- mInsets = new Rect();
- mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2;
- mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2;
+ if (Utilities.ATLEAST_R) {
+ WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class)
+ .getCurrentWindowMetrics().getWindowInsets();
+ mInsets = new Rect(
+ currentWindowInsets.getSystemWindowInsetLeft(),
+ currentWindowInsets.getSystemWindowInsetTop(),
+ currentWindowInsets.getSystemWindowInsetRight(),
+ currentWindowInsets.getSystemWindowInsetBottom());
+ } else {
+ mInsets = new Rect();
+ mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2;
+ mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2;
+ }
mDp.updateInsets(mInsets);
BaseIconFactory iconFactory =
@@ -235,267 +245,247 @@
mWorkspaceItemInfo.intent = new Intent();
mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
context.getString(R.string.label_application);
+
+ mHomeElementInflater = LayoutInflater.from(
+ new ContextThemeWrapper(this, R.style.HomeScreenElementTheme));
+ mHomeElementInflater.setFactory2(this);
+
+ mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate(
+ R.layout.launcher_preview_layout, null, false);
+ mRootView.setInsets(mInsets);
+ measureView(mRootView, mDp.widthPx, mDp.heightPx);
+
+ mHotseat = mRootView.findViewById(R.id.hotseat);
+ mHotseat.resetLayout(false);
+
+ mWorkspace = mRootView.findViewById(R.id.workspace);
+ mWorkspace.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
+ mDp.workspacePadding.top,
+ mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
+ mDp.workspacePadding.bottom);
+
+ if (Utilities.ATLEAST_S) {
+ WallpaperColors wallpaperColors = wallpaperColorsOverride != null
+ ? wallpaperColorsOverride
+ : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);
+ mWallpaperColorResources = wallpaperColors != null ? LocalColorExtractor.newInstance(
+ context).generateColorsOverride(wallpaperColors) : null;
+ } else {
+ mWallpaperColorResources = null;
+ }
}
/** Populate preview and render it. */
- public View getRenderedView() {
- MainThreadRenderer renderer = new MainThreadRenderer(mContext);
- renderer.populate();
- return renderer.mRootView;
+ public View getRenderedView(BgDataModel dataModel,
+ Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
+ populate(dataModel, widgetProviderInfoMap);
+ return mRootView;
}
- private class MainThreadRenderer extends ContextThemeWrapper
- implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {
+ @Override
+ public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+ if ("TextClock".equals(name)) {
+ // Workaround for TextClock accessing handler for unregistering ticker.
+ return new TextClock(context, attrs) {
- private final LayoutInflater mHomeElementInflater;
- private final InsettableFrameLayout mRootView;
-
- private final Hotseat mHotseat;
- private final CellLayout mWorkspace;
-
- MainThreadRenderer(Context context) {
- super(context, R.style.AppTheme);
-
- mHomeElementInflater = LayoutInflater.from(
- new ContextThemeWrapper(this, R.style.HomeScreenElementTheme));
- mHomeElementInflater.setFactory2(this);
-
- mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate(
- R.layout.launcher_preview_layout, null, false);
- mRootView.setInsets(mInsets);
- measureView(mRootView, mDp.widthPx, mDp.heightPx);
-
- mHotseat = mRootView.findViewById(R.id.hotseat);
- mHotseat.resetLayout(false);
-
- mWorkspace = mRootView.findViewById(R.id.workspace);
- mWorkspace.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
- mDp.workspacePadding.top,
- mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
- mDp.workspacePadding.bottom);
+ @Override
+ public Handler getHandler() {
+ return mUiHandler;
+ }
+ };
+ } else if (!"fragment".equals(name)) {
+ return null;
}
- @Override
- public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
- if ("TextClock".equals(name)) {
- // Workaround for TextClock accessing handler for unregistering ticker.
- return new TextClock(context, attrs) {
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PreviewFragment);
+ FragmentWithPreview f = (FragmentWithPreview) Fragment.instantiate(
+ context, ta.getString(R.styleable.PreviewFragment_android_name));
+ f.enterPreviewMode(context);
+ f.onInit(null);
- @Override
- public Handler getHandler() {
- return mUiHandler;
- }
- };
- } else if (!"fragment".equals(name)) {
- return null;
- }
+ View view = f.onCreateView(LayoutInflater.from(context), (ViewGroup) parent, null);
+ view.setId(ta.getInt(R.styleable.PreviewFragment_android_id, View.NO_ID));
+ return view;
+ }
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PreviewFragment);
- FragmentWithPreview f = (FragmentWithPreview) Fragment.instantiate(
- context, ta.getString(R.styleable.PreviewFragment_android_name));
- f.enterPreviewMode(context);
- f.onInit(null);
+ @Override
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ return onCreateView(null, name, context, attrs);
+ }
- View view = f.onCreateView(LayoutInflater.from(context), (ViewGroup) parent, null);
- view.setId(ta.getInt(R.styleable.PreviewFragment_android_id, View.NO_ID));
- return view;
+ @Override
+ public BaseDragLayer getDragLayer() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DeviceProfile getDeviceProfile() {
+ return mDp;
+ }
+
+ @Override
+ public Hotseat getHotseat() {
+ return mHotseat;
+ }
+
+ @Override
+ public CellLayout getScreenWithId(int screenId) {
+ return mWorkspace;
+ }
+
+ private void inflateAndAddIcon(WorkspaceItemInfo info) {
+ BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate(
+ R.layout.app_icon, mWorkspace, false);
+ icon.applyFromWorkspaceItem(info);
+ addInScreenFromBind(icon, info);
+ }
+
+ private void inflateAndAddFolder(FolderInfo info) {
+ FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
+ info);
+ addInScreenFromBind(folderIcon, info);
+ }
+
+ private void inflateAndAddWidgets(
+ LauncherAppWidgetInfo info,
+ Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
+ if (widgetProviderInfoMap == null) {
+ return;
+ }
+ AppWidgetProviderInfo providerInfo = widgetProviderInfoMap.get(
+ new ComponentKey(info.providerName, info.user));
+ if (providerInfo == null) {
+ return;
+ }
+ inflateAndAddWidgets(info, LauncherAppWidgetProviderInfo.fromProviderInfo(
+ getApplicationContext(), providerInfo));
+ }
+
+ private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
+ WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
+ info.providerName);
+ if (widgetItem == null) {
+ return;
+ }
+ inflateAndAddWidgets(info, widgetItem.widgetInfo);
+ }
+
+ private void inflateAndAddWidgets(
+ LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
+ AppWidgetHostView view = new AppWidgetHostView(mContext);
+ view.setAppWidget(-1, providerInfo);
+ view.updateAppWidget(null);
+ view.setTag(info);
+
+ if (mWallpaperColorResources != null) {
+ view.setColorResources(mWallpaperColorResources);
}
- @Override
- public View onCreateView(String name, Context context, AttributeSet attrs) {
- return onCreateView(null, name, context, attrs);
- }
+ addInScreenFromBind(view, info);
+ }
- @Override
- public BaseDragLayer getDragLayer() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public DeviceProfile getDeviceProfile() {
- return mDp;
- }
-
- @Override
- public Hotseat getHotseat() {
- return mHotseat;
- }
-
- @Override
- public CellLayout getScreenWithId(int screenId) {
- return mWorkspace;
- }
-
- private void inflateAndAddIcon(WorkspaceItemInfo info) {
- BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate(
- R.layout.app_icon, mWorkspace, false);
- icon.applyFromWorkspaceItem(info);
- addInScreenFromBind(icon, info);
- }
-
- private void inflateAndAddFolder(FolderInfo info) {
- FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
- info);
- addInScreenFromBind(folderIcon, info);
- }
-
- private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
- WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
- info.providerName);
- if (widgetItem == null) {
- return;
- }
- AppWidgetHostView view = new AppWidgetHostView(mContext);
- view.setAppWidget(-1, widgetItem.widgetInfo);
- view.updateAppWidget(null);
- view.setTag(info);
+ private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) {
+ View view = PredictedAppIconInflater.inflate(mHomeElementInflater, mWorkspace, info);
+ if (view != null) {
addInScreenFromBind(view, info);
}
+ }
- private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) {
- View view = PredictedAppIconInflater.inflate(mHomeElementInflater, mWorkspace, info);
- if (view != null) {
- addInScreenFromBind(view, info);
- }
+ private void dispatchVisibilityAggregated(View view, boolean isVisible) {
+ // Similar to View.dispatchVisibilityAggregated implementation.
+ final boolean thisVisible = view.getVisibility() == VISIBLE;
+ if (thisVisible || !isVisible) {
+ view.onVisibilityAggregated(isVisible);
}
- private void dispatchVisibilityAggregated(View view, boolean isVisible) {
- // Similar to View.dispatchVisibilityAggregated implementation.
- final boolean thisVisible = view.getVisibility() == VISIBLE;
- if (thisVisible || !isVisible) {
- view.onVisibilityAggregated(isVisible);
- }
+ if (view instanceof ViewGroup) {
+ isVisible = thisVisible && isVisible;
+ ViewGroup vg = (ViewGroup) view;
+ int count = vg.getChildCount();
- if (view instanceof ViewGroup) {
- isVisible = thisVisible && isVisible;
- ViewGroup vg = (ViewGroup) view;
- int count = vg.getChildCount();
-
- for (int i = 0; i < count; i++) {
- dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
- }
+ for (int i = 0; i < count; i++) {
+ dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
}
}
+ }
- private void populate() {
- if (ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER.get()) {
- WorkspaceFetcher fetcher;
- PreviewContext previewContext = null;
- if (mMigrated) {
- previewContext = new PreviewContext(mContext, mIdp);
- LauncherAppState appForPreview = new LauncherAppState(
- previewContext, null /* iconCacheFileName */);
- fetcher = new WorkspaceItemsInfoFromPreviewFetcher(appForPreview);
- MODEL_EXECUTOR.execute(fetcher);
- } else {
- fetcher = new WorkspaceItemsInfoFetcher();
- LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
- (LauncherModel.ModelUpdateTask) fetcher);
- }
- WorkspaceResult workspaceResult = fetcher.get();
- if (previewContext != null) {
- previewContext.onDestroy();
- }
-
- if (workspaceResult == null) {
- return;
- }
-
- // Separate the items that are on the current screen, and all the other remaining
- // items
- ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
- ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
- ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
- ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
-
- filterCurrentWorkspaceItems(0 /* currentScreenId */,
- workspaceResult.mWorkspaceItems, currentWorkspaceItems,
- otherWorkspaceItems);
- filterCurrentWorkspaceItems(0 /* currentScreenId */, workspaceResult.mAppWidgets,
- currentAppWidgets, otherAppWidgets);
- sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
-
- for (ItemInfo itemInfo : currentWorkspaceItems) {
- switch (itemInfo.itemType) {
- case Favorites.ITEM_TYPE_APPLICATION:
- case Favorites.ITEM_TYPE_SHORTCUT:
- case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
- inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
- break;
- case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
- inflateAndAddFolder((FolderInfo) itemInfo);
- break;
- default:
- break;
+ private void populate(BgDataModel dataModel,
+ Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
+ // Separate the items that are on the current screen, and the other remaining items.
+ ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
+ ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+ ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+ ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+ filterCurrentWorkspaceItems(0 /* currentScreenId */,
+ dataModel.workspaceItems, currentWorkspaceItems,
+ otherWorkspaceItems);
+ filterCurrentWorkspaceItems(0 /* currentScreenId */, dataModel.appWidgets,
+ currentAppWidgets, otherAppWidgets);
+ sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
+ for (ItemInfo itemInfo : currentWorkspaceItems) {
+ switch (itemInfo.itemType) {
+ case Favorites.ITEM_TYPE_APPLICATION:
+ case Favorites.ITEM_TYPE_SHORTCUT:
+ case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+ inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
+ break;
+ case Favorites.ITEM_TYPE_FOLDER:
+ inflateAndAddFolder((FolderInfo) itemInfo);
+ break;
+ default:
+ break;
+ }
+ }
+ for (ItemInfo itemInfo : currentAppWidgets) {
+ switch (itemInfo.itemType) {
+ case Favorites.ITEM_TYPE_APPWIDGET:
+ case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
+ if (widgetProviderInfoMap != null) {
+ inflateAndAddWidgets(
+ (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap);
+ } else {
+ inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
+ dataModel.widgetsModel);
}
- }
- for (ItemInfo itemInfo : currentAppWidgets) {
- switch (itemInfo.itemType) {
- case Favorites.ITEM_TYPE_APPWIDGET:
- case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
- inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
- workspaceResult.mWidgetsModel);
- break;
- default:
- break;
- }
- }
-
- IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
- mIdp.numHotseatIcons);
- int count = Math.min(ranks.size(), workspaceResult.mCachedPredictedItems.size());
- for (int i = 0; i < count; i++) {
- AppInfo appInfo = workspaceResult.mCachedPredictedItems.get(i);
- int rank = ranks.get(i);
- WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(appInfo);
- itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
- itemInfo.rank = rank;
- itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
- itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
- itemInfo.screenId = rank;
- inflateAndAddPredictedIcon(itemInfo);
- }
- } else {
- // Add hotseat icons
- for (int i = 0; i < mIdp.numHotseatIcons; i++) {
- WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
- info.container = Favorites.CONTAINER_HOTSEAT;
- info.screenId = i;
- inflateAndAddIcon(info);
- }
- // Add workspace icons
- for (int i = 0; i < mIdp.numColumns; i++) {
- WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
- info.container = Favorites.CONTAINER_DESKTOP;
- info.screenId = 0;
- info.cellX = i;
- info.cellY = mIdp.numRows - 1;
- inflateAndAddIcon(info);
- }
+ break;
+ default:
+ break;
}
-
- // Add first page QSB
- if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
- View qsb = mHomeElementInflater.inflate(
- R.layout.search_container_workspace, mWorkspace, false);
- CellLayout.LayoutParams lp =
- new CellLayout.LayoutParams(0, 0, mWorkspace.getCountX(), 1);
- lp.canReorder = false;
- mWorkspace.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
- }
-
- // Setup search view
- SearchUiManager searchUiManager =
- mRootView.findViewById(R.id.search_container_all_apps);
- mRootView.findViewById(R.id.apps_view).setTranslationY(
- mDp.heightPx - searchUiManager.getScrollRangeDelta(mInsets));
-
- measureView(mRootView, mDp.widthPx, mDp.heightPx);
- dispatchVisibilityAggregated(mRootView, true);
- measureView(mRootView, mDp.widthPx, mDp.heightPx);
- // Additional measure for views which use auto text size API
- measureView(mRootView, mDp.widthPx, mDp.heightPx);
}
+ IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
+ mDp.numShownHotseatIcons);
+ FixedContainerItems hotseatpredictions =
+ dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
+ List<ItemInfo> predictions = hotseatpredictions == null
+ ? Collections.emptyList() : hotseatpredictions.items;
+ int count = Math.min(ranks.size(), predictions.size());
+ for (int i = 0; i < count; i++) {
+ int rank = ranks.get(i);
+ WorkspaceItemInfo itemInfo =
+ new WorkspaceItemInfo((WorkspaceItemInfo) predictions.get(i));
+ itemInfo.container = CONTAINER_HOTSEAT_PREDICTION;
+ itemInfo.rank = rank;
+ itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
+ itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
+ itemInfo.screenId = rank;
+ inflateAndAddPredictedIcon(itemInfo);
+ }
+
+ // Add first page QSB
+ if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
+ View qsb = mHomeElementInflater.inflate(
+ R.layout.search_container_workspace, mWorkspace, false);
+ CellLayout.LayoutParams lp =
+ new CellLayout.LayoutParams(0, 0, mWorkspace.getCountX(), 1);
+ lp.canReorder = false;
+ mWorkspace.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
+ }
+
+ measureView(mRootView, mDp.widthPx, mDp.heightPx);
+ dispatchVisibilityAggregated(mRootView, true);
+ measureView(mRootView, mDp.widthPx, mDp.heightPx);
+ // Additional measure for views which use auto text size API
+ measureView(mRootView, mDp.widthPx, mDp.heightPx);
}
private static void measureView(View view, int width, int height) {
@@ -503,104 +493,15 @@
view.layout(0, 0, width, height);
}
- private static class WorkspaceItemsInfoFetcher implements LauncherModel.ModelUpdateTask,
- WorkspaceFetcher {
-
- private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
-
- private LauncherAppState mApp;
- private LauncherModel mModel;
- private BgDataModel mBgDataModel;
- private AllAppsList mAllAppsList;
-
- @Override
- public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
- AllAppsList allAppsList, Executor uiExecutor) {
- mApp = app;
- mModel = model;
- mBgDataModel = dataModel;
- mAllAppsList = allAppsList;
+ /** Root layout for launcher preview that intercepts all touch events. */
+ public static class LauncherPreviewLayout extends InsettableFrameLayout {
+ public LauncherPreviewLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
}
@Override
- public FutureTask<WorkspaceResult> getTask() {
- return mTask;
- }
-
- @Override
- public void run() {
- mTask.run();
- }
-
- @Override
- public WorkspaceResult call() throws Exception {
- if (!mModel.isModelLoaded()) {
- Log.d(TAG, "Workspace not loaded, loading now");
- mModel.startLoaderForResults(
- new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
- return null;
- }
-
- return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
- mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel);
- }
- }
-
- private static class WorkspaceItemsInfoFromPreviewFetcher extends LoaderTask implements
- WorkspaceFetcher {
-
- private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
-
- WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
- super(app, null, new BgDataModel(), null);
- }
-
- @Override
- public FutureTask<WorkspaceResult> getTask() {
- return mTask;
- }
-
- @Override
- public void run() {
- mTask.run();
- }
-
- @Override
- public WorkspaceResult call() throws Exception {
- List<ShortcutInfo> allShortcuts = new ArrayList<>();
- loadWorkspace(allShortcuts, LauncherSettings.Favorites.PREVIEW_CONTENT_URI);
- mBgDataModel.widgetsModel.update(mApp, null);
- return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
- mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel);
- }
- }
-
- private interface WorkspaceFetcher extends Runnable, Callable<WorkspaceResult> {
- FutureTask<WorkspaceResult> getTask();
-
- default WorkspaceResult get() {
- try {
- return getTask().get(5, TimeUnit.SECONDS);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- Log.d(TAG, "Error fetching workspace items info", e);
- return null;
- }
- }
- }
-
- private static class WorkspaceResult {
- private final ArrayList<ItemInfo> mWorkspaceItems;
- private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
- private final ArrayList<AppInfo> mCachedPredictedItems;
- private final WidgetsModel mWidgetsModel;
-
- private WorkspaceResult(ArrayList<ItemInfo> workspaceItems,
- ArrayList<LauncherAppWidgetInfo> appWidgets,
- ArrayList<AppInfo> cachedPredictedItems, WidgetsModel widgetsModel) {
- mWorkspaceItems = workspaceItems;
- mAppWidgets = appWidgets;
- mCachedPredictedItems = cachedPredictedItems;
- mWidgetsModel = widgetsModel;
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return true;
}
}
}
diff --git a/src/com/android/launcher3/graphics/NinePatchDrawHelper.java b/src/com/android/launcher3/graphics/NinePatchDrawHelper.java
deleted file mode 100644
index 5872689..0000000
--- a/src/com/android/launcher3/graphics/NinePatchDrawHelper.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.graphics;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-
-/**
- * Utility class which draws a bitmap by dissecting it into 3 segments and stretching
- * the middle segment.
- */
-public class NinePatchDrawHelper {
-
- // The extra width used for the bitmap. This portion of the bitmap is stretched to match the
- // width of the draw region. Randomly chosen, any value > 4 will be sufficient.
- public static final int EXTENSION_PX = 20;
-
- private final Rect mSrc = new Rect();
- private final RectF mDst = new RectF();
- // Enable filtering to always get a nice edge. This avoids jagged line, when bitmap is
- // translated by half pixel.
- public final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
-
- /**
- * Draws the bitmap split into three parts horizontally, with the middle part having width
- * as {@link #EXTENSION_PX} in the center of the bitmap.
- */
- public void draw(Bitmap bitmap, Canvas canvas, float left, float top, float right) {
- int height = bitmap.getHeight();
-
- mSrc.top = 0;
- mSrc.bottom = height;
- mDst.top = top;
- mDst.bottom = top + height;
- draw3Patch(bitmap, canvas, left, right);
- }
-
-
- /**
- * Draws the bitmap split horizontally into 3 parts (same as {@link #draw}) and split
- * vertically into two parts, bottom part of size {@link #EXTENSION_PX} / 2 which is
- * stretched vertically.
- */
- public void drawVerticallyStretched(Bitmap bitmap, Canvas canvas, float left, float top,
- float right, float bottom) {
- draw(bitmap, canvas, left, top, right);
-
- // Draw bottom stretched region.
- int height = bitmap.getHeight();
- mSrc.top = height - EXTENSION_PX / 4;
- mSrc.bottom = height;
- mDst.top = top + height;
- mDst.bottom = bottom;
- draw3Patch(bitmap, canvas, left, right);
- }
-
-
-
- private void draw3Patch(Bitmap bitmap, Canvas canvas, float left, float right) {
- int width = bitmap.getWidth();
- int halfWidth = width / 2;
-
- // Draw left edge
- drawRegion(bitmap, canvas, 0, halfWidth, left, left + halfWidth);
-
- // Draw right edge
- drawRegion(bitmap, canvas, halfWidth, width, right - halfWidth, right);
-
- // Draw middle stretched region
- int halfExt = EXTENSION_PX / 4;
- drawRegion(bitmap, canvas, halfWidth - halfExt, halfWidth + halfExt,
- left + halfWidth, right - halfWidth);
- }
-
- private void drawRegion(Bitmap bitmap, Canvas c,
- int srcLeft, int srcRight, float dstLeft, float dstRight) {
- mSrc.left = srcLeft;
- mSrc.right = srcRight;
-
- mDst.left = dstLeft;
- mDst.right = dstRight;
- c.drawBitmap(bitmap, mSrc, mDst, paint);
- }
-}
diff --git a/src/com/android/launcher3/graphics/OverviewScrim.java b/src/com/android/launcher3/graphics/OverviewScrim.java
deleted file mode 100644
index 94acbfd..0000000
--- a/src/com/android/launcher3/graphics/OverviewScrim.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.graphics;
-
-import static android.view.View.VISIBLE;
-
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
-import android.graphics.Rect;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * View scrim which draws behind overview (recent apps).
- */
-public class OverviewScrim extends Scrim {
-
- private @NonNull View mStableScrimmedView;
- // Might be higher up if mStableScrimmedView is invisible.
- private @Nullable View mCurrentScrimmedView;
-
- public OverviewScrim(View view) {
- super(view);
- mStableScrimmedView = mCurrentScrimmedView = mLauncher.getOverviewPanel();
-
- onExtractedColorsChanged(mWallpaperColorInfo);
- }
-
- public void onInsetsChanged(Rect insets) {
- mStableScrimmedView = (OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0
- ? mLauncher.getHotseat()
- : mLauncher.getOverviewPanel();
- }
-
- public void updateCurrentScrimmedView(ViewGroup root) {
- // Find the lowest view that is at or above the view we want to show the scrim behind.
- mCurrentScrimmedView = mStableScrimmedView;
- int currentIndex = root.indexOfChild(mCurrentScrimmedView);
- final int childCount = root.getChildCount();
- while (mCurrentScrimmedView != null && mCurrentScrimmedView.getVisibility() != VISIBLE
- && currentIndex < childCount) {
- currentIndex++;
- mCurrentScrimmedView = root.getChildAt(currentIndex);
- }
- }
-
- /**
- * @return The view to draw the scrim behind, or null if all visible views should be scrimmed.
- */
- public @Nullable View getScrimmedView() {
- return mCurrentScrimmedView;
- }
-}
diff --git a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
deleted file mode 100644
index d347e8f..0000000
--- a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.graphics;
-
-import static androidx.core.graphics.ColorUtils.compositeColors;
-
-import static com.android.launcher3.graphics.IconShape.getShapePath;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Path;
-import android.graphics.Rect;
-
-import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.util.Themes;
-
-/**
- * Subclass which draws a placeholder icon when the actual icon is not yet loaded
- */
-public class PlaceHolderIconDrawable extends FastBitmapDrawable {
-
- // Path in [0, 100] bounds.
- private final Path mProgressPath;
-
- public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
- super(info);
-
- mProgressPath = getShapePath();
- mPaint.setColor(compositeColors(
- Themes.getAttrColor(context, R.attr.loadingIconColor), info.color));
- }
-
- @Override
- protected void drawInternal(Canvas canvas, Rect bounds) {
- int saveCount = canvas.save();
- canvas.translate(bounds.left, bounds.top);
- canvas.scale(bounds.width() / 100f, bounds.height() / 100f);
- canvas.drawPath(mProgressPath, mPaint);
- canvas.restoreToCount(saveCount);
- }
-}
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index e85b056..e45b8f7 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -17,9 +17,6 @@
package com.android.launcher3.graphics;
-import static com.android.launcher3.graphics.IconShape.DEFAULT_PATH_SIZE;
-import static com.android.launcher3.graphics.IconShape.getShapePath;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
@@ -34,10 +31,14 @@
import android.util.Pair;
import android.util.Property;
import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
-import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.icons.FastBitmapDrawable;
+import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.util.Themes;
import java.lang.ref.WeakReference;
@@ -59,6 +60,7 @@
}
};
+ private static final int DEFAULT_PATH_SIZE = 100;
private static final float PROGRESS_WIDTH = 7;
private static final float PROGRESS_GAP = 2;
private static final int MAX_PAINT_ALPHA = 255;
@@ -77,6 +79,9 @@
private static final SparseArray<WeakReference<Pair<Path, Bitmap>>> sShadowCache =
new SparseArray<>();
+ private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
+ private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
+
private final Matrix mTmpMatrix = new Matrix();
private final PathMeasure mPathMeasure = new PathMeasure();
@@ -91,6 +96,9 @@
private Bitmap mShadowBitmap;
private final int mIndicatorColor;
+ private final int mSystemAccentColor;
+ private final int mSystemBackgroundColor;
+ private final boolean mIsDarkMode;
private int mTrackAlpha;
private float mTrackLength;
@@ -104,19 +112,38 @@
private ObjectAnimator mCurrentAnim;
+ private boolean mIsStartable;
+
public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
+ this(
+ info,
+ IconPalette.getPreloadProgressColor(context, info.bitmap.color),
+ getPreloadColors(context),
+ Utilities.isDarkTheme(context));
+ }
+
+ public PreloadIconDrawable(
+ ItemInfoWithIcon info,
+ int indicatorColor,
+ int[] preloadColors,
+ boolean isDarkMode) {
super(info.bitmap);
mItem = info;
- mShapePath = getShapePath();
+ mShapePath = GraphicsUtils.getShapePath(DEFAULT_PATH_SIZE);
mScaledTrackPath = new Path();
mScaledProgressPath = new Path();
mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
- mIndicatorColor = IconPalette.getPreloadProgressColor(context, mIconColor);
+ mIndicatorColor = indicatorColor;
- setInternalProgress(0);
+ mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX];
+ mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX];
+ mIsDarkMode = isDarkMode;
+
+ setInternalProgress(info.getProgressLevel());
+ setIsStartable(info.isAppStartable());
}
@Override
@@ -142,7 +169,7 @@
}
private Bitmap getShadowBitmap(int width, int height, float shadowRadius) {
- int key = (width << 16) | height;
+ int key = ((width << 16) | height) * (mIsDarkMode ? -1 : 1);
WeakReference<Pair<Path, Bitmap>> shadowRef = sShadowCache.get(key);
Pair<Path, Bitmap> cache = shadowRef != null ? shadowRef.get() : null;
Bitmap shadow = cache != null && cache.first.equals(mShapePath) ? cache.second : null;
@@ -151,8 +178,9 @@
}
shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(shadow);
- mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW);
- mProgressPaint.setColor(COLOR_TRACK);
+ mProgressPaint.setShadowLayer(shadowRadius, 0, 0, mIsStartable
+ ? COLOR_SHADOW : mSystemAccentColor);
+ mProgressPaint.setColor(mIsStartable ? COLOR_TRACK : mSystemBackgroundColor);
mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
c.drawPath(mScaledTrackPath, mProgressPaint);
mProgressPaint.clearShadowLayer();
@@ -170,7 +198,7 @@
}
// Draw track.
- mProgressPaint.setColor(mIndicatorColor);
+ mProgressPaint.setColor(mIsStartable ? mIndicatorColor : mSystemAccentColor);
mProgressPaint.setAlpha(mTrackAlpha);
if (mShadowBitmap != null) {
canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint);
@@ -209,6 +237,14 @@
return !mRanFinishAnimation;
}
+ /** Sets whether this icon should display the startable app UI. */
+ public void setIsStartable(boolean isStartable) {
+ if (mIsStartable != isStartable) {
+ mIsStartable = isStartable;
+ setIsDisabled(!isStartable);
+ }
+ }
+
private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
if (mCurrentAnim != null) {
mCurrentAnim.cancel();
@@ -266,14 +302,12 @@
mIconScale = SMALL_SCALE;
mScaledTrackPath.reset();
mTrackAlpha = MAX_PAINT_ALPHA;
- setIsDisabled(true);
}
if (progress < 1 && progress > 0) {
mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
mIconScale = SMALL_SCALE;
mTrackAlpha = MAX_PAINT_ALPHA;
- setIsDisabled(true);
} else if (progress >= 1) {
setIsDisabled(mItem.isDisabled());
mScaledTrackPath.set(mScaledProgressPath);
@@ -291,10 +325,73 @@
invalidateSelf();
}
+ private static int[] getPreloadColors(Context context) {
+ Context dayNightThemeContext = new ContextThemeWrapper(
+ context, android.R.style.Theme_DeviceDefault_DayNight);
+ int[] preloadColors = new int[2];
+
+ preloadColors[PRELOAD_ACCENT_COLOR_INDEX] = Themes.getColorAccent(dayNightThemeContext);
+ preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX] = Themes.getColorBackgroundFloating(
+ dayNightThemeContext);
+
+ return preloadColors;
+ }
+
/**
* Returns a FastBitmapDrawable with the icon.
*/
public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
return new PreloadIconDrawable(info, context);
}
+
+ @Override
+ public ConstantState getConstantState() {
+ return new PreloadIconConstantState(
+ mBitmap,
+ mIconColor,
+ !mItem.isAppStartable(),
+ mItem,
+ mIndicatorColor,
+ new int[] {mSystemAccentColor, mSystemBackgroundColor},
+ mIsDarkMode);
+ }
+
+ protected static class PreloadIconConstantState extends FastBitmapConstantState {
+
+ protected final ItemInfoWithIcon mInfo;
+ protected final int mIndicatorColor;
+ protected final int[] mPreloadColors;
+ protected final boolean mIsDarkMode;
+ protected final int mLevel;
+
+ public PreloadIconConstantState(
+ Bitmap bitmap,
+ int iconColor,
+ boolean isDisabled,
+ ItemInfoWithIcon info,
+ int indicatorColor,
+ int[] preloadColors,
+ boolean isDarkMode) {
+ super(bitmap, iconColor, isDisabled);
+ mInfo = info;
+ mIndicatorColor = indicatorColor;
+ mPreloadColors = preloadColors;
+ mIsDarkMode = isDarkMode;
+ mLevel = info.getProgressLevel();
+ }
+
+ @Override
+ public PreloadIconDrawable newDrawable() {
+ return new PreloadIconDrawable(
+ mInfo,
+ mIndicatorColor,
+ mPreloadColors,
+ mIsDarkMode);
+ }
+
+ @Override
+ public int getChangingConfigurations() {
+ return 0;
+ }
+ }
}
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index fdc3a94..3b140a0 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -20,27 +20,49 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import android.app.WallpaperColors;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
-import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Messenger;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
import android.view.Display;
import android.view.SurfaceControlViewHost;
+import android.view.SurfaceControlViewHost.SurfacePackage;
import android.view.View;
+import android.view.WindowManager.LayoutParams;
import android.view.animation.AccelerateDecelerateInterpolator;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext;
+import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.GridSizeMigrationTask;
import com.android.launcher3.model.GridSizeMigrationTaskV2;
+import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDelegate;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.LocalColorExtractor;
+import java.util.ArrayList;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
/** Render preview using surface view. */
-public class PreviewSurfaceRenderer implements IBinder.DeathRecipient {
+@SuppressWarnings("NewApi")
+public class PreviewSurfaceRenderer {
+
+ private static final String TAG = "PreviewSurfaceRenderer";
private static final int FADE_IN_ANIMATION_DURATION = 200;
@@ -48,8 +70,7 @@
private static final String KEY_VIEW_WIDTH = "width";
private static final String KEY_VIEW_HEIGHT = "height";
private static final String KEY_DISPLAY_ID = "display_id";
- private static final String KEY_SURFACE_PACKAGE = "surface_package";
- private static final String KEY_CALLBACK = "callback";
+ private static final String KEY_COLORS = "wallpaper_colors";
private final Context mContext;
private final InvariantDeviceProfile mIdp;
@@ -57,10 +78,14 @@
private final int mWidth;
private final int mHeight;
private final Display mDisplay;
+ private final WallpaperColors mWallpaperColors;
+ private final RunnableList mOnDestroyCallbacks = new RunnableList();
- private SurfaceControlViewHost mSurfaceControlViewHost;
+ private final SurfaceControlViewHost mSurfaceControlViewHost;
- PreviewSurfaceRenderer(Context context, Bundle bundle) {
+ private boolean mDestroyed = false;
+
+ public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
mContext = context;
String gridName = bundle.getString("name");
@@ -68,91 +93,97 @@
if (gridName == null) {
gridName = InvariantDeviceProfile.getCurrentGridName(context);
}
+ mWallpaperColors = bundle.getParcelable(KEY_COLORS);
mIdp = new InvariantDeviceProfile(context, gridName);
mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
mWidth = bundle.getInt(KEY_VIEW_WIDTH);
mHeight = bundle.getInt(KEY_VIEW_HEIGHT);
+ mDisplay = context.getSystemService(DisplayManager.class)
+ .getDisplay(bundle.getInt(KEY_DISPLAY_ID));
- final DisplayManager displayManager = (DisplayManager) context.getSystemService(
- Context.DISPLAY_SERVICE);
- mDisplay = displayManager.getDisplay(bundle.getInt(KEY_DISPLAY_ID));
+ mSurfaceControlViewHost = MAIN_EXECUTOR
+ .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))
+ .get(5, TimeUnit.SECONDS);
+ mOnDestroyCallbacks.add(mSurfaceControlViewHost::release);
}
- /** Handle a received surface view request. */
- Bundle render() {
- if (mSurfaceControlViewHost != null) {
- binderDied();
+ public IBinder getHostToken() {
+ return mHostToken;
+ }
+
+ public SurfacePackage getSurfacePackage() {
+ return mSurfaceControlViewHost.getSurfacePackage();
+ }
+
+ /**
+ * Destroys the preview and all associated data
+ */
+ @UiThread
+ public void destroy() {
+ mDestroyed = true;
+ mOnDestroyCallbacks.executeAllAndDestroy();
+ }
+
+ /**
+ * Generates the preview in background
+ */
+ public void loadAsync() {
+ MODEL_EXECUTOR.execute(this::loadModelData);
+ }
+
+ @WorkerThread
+ private void loadModelData() {
+ final boolean migrated = doGridMigrationIfNecessary();
+
+ final Context inflationContext;
+ if (mWallpaperColors != null) {
+ // Create a themed context, without affecting the main application context
+ Context context = mContext.createDisplayContext(mDisplay);
+ if (Utilities.ATLEAST_R) {
+ context = context.createWindowContext(
+ LayoutParams.TYPE_APPLICATION_OVERLAY, null);
+ }
+ LocalColorExtractor.newInstance(mContext)
+ .applyColorsOverride(context, mWallpaperColors);
+ inflationContext = new ContextThemeWrapper(context,
+ Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
+ } else {
+ inflationContext = new ContextThemeWrapper(mContext, R.style.AppTheme);
}
- SurfaceControlViewHost.SurfacePackage surfacePackage;
- try {
- mSurfaceControlViewHost = MAIN_EXECUTOR
- .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))
- .get(5, TimeUnit.SECONDS);
- surfacePackage = mSurfaceControlViewHost.getSurfacePackage();
- mHostToken.linkToDeath(this, 0);
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
+ if (migrated) {
+ PreviewContext previewContext = new PreviewContext(inflationContext, mIdp);
+ new LoaderTask(
+ LauncherAppState.getInstance(previewContext),
+ null,
+ new BgDataModel(),
+ new ModelDelegate(), null) {
- MODEL_EXECUTOR.post(() -> {
- final boolean success = doGridMigrationIfNecessary();
-
- MAIN_EXECUTOR.post(() -> {
- // If mSurfaceControlViewHost is null due to any reason (e.g. binder died,
- // happening when user leaves the preview screen before preview rendering finishes),
- // we should return here.
- SurfaceControlViewHost host = mSurfaceControlViewHost;
- if (host == null) {
- return;
+ @Override
+ public void run() {
+ loadWorkspace(new ArrayList<>(), LauncherSettings.Favorites.PREVIEW_CONTENT_URI,
+ LauncherSettings.Favorites.SCREEN + " = 0 or "
+ + LauncherSettings.Favorites.CONTAINER + " = "
+ + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
+ MAIN_EXECUTOR.execute(() -> {
+ renderView(previewContext, mBgDataModel, mWidgetProvidersMap);
+ mOnDestroyCallbacks.add(previewContext::onDestroy);
+ });
}
-
- View view = new LauncherPreviewRenderer(mContext, mIdp, success).getRenderedView();
- // This aspect scales the view to fit in the surface and centers it
- final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
- mHeight / (float) view.getMeasuredHeight());
- view.setScaleX(scale);
- view.setScaleY(scale);
- view.setPivotX(0);
- view.setPivotY(0);
- view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
- view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
- view.setAlpha(0);
- view.animate().alpha(1)
- .setInterpolator(new AccelerateDecelerateInterpolator())
- .setDuration(FADE_IN_ANIMATION_DURATION)
- .start();
- host.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());
- });
- });
-
- Bundle result = new Bundle();
- result.putParcelable(KEY_SURFACE_PACKAGE, surfacePackage);
-
- Handler handler = new Handler(Looper.getMainLooper(), message -> {
- binderDied();
- return true;
- });
- Messenger messenger = new Messenger(handler);
- Message msg = Message.obtain();
- msg.replyTo = messenger;
- result.putParcelable(KEY_CALLBACK, msg);
- return result;
- }
-
- @Override
- public void binderDied() {
- if (mSurfaceControlViewHost != null) {
- MAIN_EXECUTOR.execute(() -> {
- mSurfaceControlViewHost.release();
- mSurfaceControlViewHost = null;
+ }.run();
+ } else {
+ LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
+ if (dataModel != null) {
+ MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel, null));
+ } else {
+ Log.e(TAG, "Model loading failed");
+ }
});
}
- mHostToken.unlinkToDeath(this, 0);
}
+ @WorkerThread
private boolean doGridMigrationIfNecessary() {
boolean needsToMigrate =
MULTI_DB_GRID_MIRATION_ALGO.get()
@@ -165,4 +196,29 @@
? GridSizeMigrationTaskV2.migrateGridIfNeeded(mContext, mIdp)
: GridSizeMigrationTask.migrateGridIfNeeded(mContext, mIdp);
}
+
+ @UiThread
+ private void renderView(Context inflationContext, BgDataModel dataModel,
+ Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
+ if (mDestroyed) {
+ return;
+ }
+ View view = new LauncherPreviewRenderer(inflationContext, mIdp, mWallpaperColors)
+ .getRenderedView(dataModel, widgetProviderInfoMap);
+ // This aspect scales the view to fit in the surface and centers it
+ final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
+ mHeight / (float) view.getMeasuredHeight());
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ view.setPivotX(0);
+ view.setPivotY(0);
+ view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
+ view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+ view.setAlpha(0);
+ view.animate().alpha(1)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .setDuration(FADE_IN_ANIMATION_DURATION)
+ .start();
+ mSurfaceControlViewHost.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());
+ }
}
diff --git a/src/com/android/launcher3/graphics/Scrim.java b/src/com/android/launcher3/graphics/Scrim.java
index f90962d..a77e058 100644
--- a/src/com/android/launcher3/graphics/Scrim.java
+++ b/src/com/android/launcher3/graphics/Scrim.java
@@ -22,14 +22,12 @@
import android.util.FloatProperty;
import android.view.View;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.uioverrides.WallpaperColorInfo;
+import com.android.launcher3.R;
/**
* Contains general scrim properties such as wallpaper-extracted color that subclasses can use.
*/
-public class Scrim implements View.OnAttachStateChangeListener,
- WallpaperColorInfo.OnChangeListener {
+public class Scrim {
public static final FloatProperty<Scrim> SCRIM_PROGRESS =
new FloatProperty<Scrim>("scrimProgress") {
@@ -44,8 +42,6 @@
}
};
- protected final Launcher mLauncher;
- protected final WallpaperColorInfo mWallpaperColorInfo;
protected final View mRoot;
protected float mScrimProgress;
@@ -54,10 +50,7 @@
public Scrim(View view) {
mRoot = view;
- mLauncher = Launcher.getLauncher(view.getContext());
- mWallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(mLauncher);
-
- view.addOnAttachStateChangeListener(this);
+ mScrimColor = mRoot.getContext().getColor(R.color.wallpaper_popup_scrim);
}
public void draw(Canvas canvas) {
@@ -68,30 +61,7 @@
if (mScrimProgress != progress) {
mScrimProgress = progress;
mScrimAlpha = Math.round(255 * mScrimProgress);
- invalidate();
+ mRoot.invalidate();
}
}
-
- @Override
- public void onViewAttachedToWindow(View view) {
- mWallpaperColorInfo.addOnChangeListener(this);
- onExtractedColorsChanged(mWallpaperColorInfo);
- }
-
- @Override
- public void onViewDetachedFromWindow(View view) {
- mWallpaperColorInfo.removeOnChangeListener(this);
- }
-
- @Override
- public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
- mScrimColor = wallpaperColorInfo.getMainColor();
- if (mScrimAlpha > 0) {
- invalidate();
- }
- }
-
- public void invalidate() {
- mRoot.invalidate();
- }
}
diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
new file mode 100644
index 0000000..f0766c5
--- /dev/null
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -0,0 +1,278 @@
+/*
+ * 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.graphics;
+
+import static android.content.Intent.ACTION_SCREEN_OFF;
+import static android.content.Intent.ACTION_USER_PRESENT;
+
+import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
+import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+
+import android.animation.ObjectAnimator;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.util.DisplayMetrics;
+import android.util.FloatProperty;
+import android.view.View;
+import android.view.WindowInsets;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.Themes;
+import com.android.systemui.plugins.ResourceProvider;
+
+/**
+ * View scrim which draws behind hotseat and workspace
+ */
+public class SysUiScrim implements View.OnAttachStateChangeListener {
+
+ public static final FloatProperty<SysUiScrim> SYSUI_PROGRESS =
+ new FloatProperty<SysUiScrim>("sysUiProgress") {
+ @Override
+ public Float get(SysUiScrim scrim) {
+ return scrim.mSysUiProgress;
+ }
+
+ @Override
+ public void setValue(SysUiScrim scrim, float value) {
+ scrim.setSysUiProgress(value);
+ }
+ };
+
+ private static final FloatProperty<SysUiScrim> SYSUI_ANIM_MULTIPLIER =
+ new FloatProperty<SysUiScrim>("sysUiAnimMultiplier") {
+ @Override
+ public Float get(SysUiScrim scrim) {
+ return scrim.mSysUiAnimMultiplier;
+ }
+
+ @Override
+ public void setValue(SysUiScrim scrim, float value) {
+ scrim.mSysUiAnimMultiplier = value;
+ scrim.reapplySysUiAlpha();
+ }
+ };
+
+ /**
+ * Receiver used to get a signal that the user unlocked their device.
+ */
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (ACTION_SCREEN_OFF.equals(action)) {
+ mAnimateScrimOnNextDraw = true;
+ } else if (ACTION_USER_PRESENT.equals(action)) {
+ // ACTION_USER_PRESENT is sent after onStart/onResume. This covers the case where
+ // the user unlocked and the Launcher is not in the foreground.
+ mAnimateScrimOnNextDraw = false;
+ }
+ }
+ };
+
+ private static final int MAX_HOTSEAT_SCRIM_ALPHA = 100;
+ private static final int ALPHA_MASK_HEIGHT_DP = 500;
+ private static final int ALPHA_MASK_BITMAP_DP = 200;
+ private static final int ALPHA_MASK_WIDTH_DP = 2;
+
+ private boolean mDrawTopScrim, mDrawBottomScrim, mDrawWallpaperScrim;
+
+ private final RectF mWallpaperScrimRect = new RectF();
+ private final Paint mWallpaperScrimPaint = new Paint();
+ private final RectF mFinalMaskRect = new RectF();
+ private final Paint mBottomMaskPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ private final Bitmap mBottomMask;
+ private final int mMaskHeight;
+
+ private final View mRoot;
+ private final BaseDraggingActivity mActivity;
+ private final Drawable mTopScrim;
+
+ private float mSysUiProgress = 1;
+ private boolean mHideSysUiScrim;
+
+ private boolean mAnimateScrimOnNextDraw = false;
+ private float mSysUiAnimMultiplier = 1;
+ private int mWallpaperScrimMaxAlpha;
+
+ public SysUiScrim(View view) {
+ mRoot = view;
+ mActivity = BaseDraggingActivity.fromContext(view.getContext());
+ mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
+ view.getResources().getDisplayMetrics());
+ mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
+ mBottomMask = mTopScrim == null ? null : createDitheredAlphaMask();
+ mHideSysUiScrim = mTopScrim == null;
+
+ mDrawWallpaperScrim = FeatureFlags.ENABLE_WALLPAPER_SCRIM.get()
+ && !Themes.getAttrBoolean(view.getContext(), R.attr.isMainColorDark)
+ && !Themes.getAttrBoolean(view.getContext(), R.attr.isWorkspaceDarkText);
+ ResourceProvider rp = DynamicResource.provider(view.getContext());
+ int wallpaperScrimColor = rp.getColor(R.color.wallpaper_scrim_color);
+ mWallpaperScrimMaxAlpha = Color.alpha(wallpaperScrimColor);
+ mWallpaperScrimPaint.setColor(wallpaperScrimColor);
+
+ view.addOnAttachStateChangeListener(this);
+ }
+
+ /**
+ * Draw the top and bottom scrims
+ */
+ public void draw(Canvas canvas) {
+ if (!mHideSysUiScrim) {
+ if (mSysUiProgress <= 0) {
+ mAnimateScrimOnNextDraw = false;
+ return;
+ }
+
+ if (mAnimateScrimOnNextDraw) {
+ mSysUiAnimMultiplier = 0;
+ reapplySysUiAlphaNoInvalidate();
+
+ ObjectAnimator oa = createSysuiMultiplierAnim(1);
+ oa.setDuration(600);
+ oa.setStartDelay(mActivity.getWindow().getTransitionBackgroundFadeDuration());
+ oa.start();
+ mAnimateScrimOnNextDraw = false;
+ }
+
+ if (mDrawWallpaperScrim) {
+ canvas.drawRect(mWallpaperScrimRect, mWallpaperScrimPaint);
+ }
+ if (mDrawTopScrim) {
+ mTopScrim.draw(canvas);
+ }
+ if (mDrawBottomScrim) {
+ canvas.drawBitmap(mBottomMask, null, mFinalMaskRect, mBottomMaskPaint);
+ }
+ }
+ }
+
+ /**
+ * @return an ObjectAnimator that controls the fade in/out of the sys ui scrim.
+ */
+ public ObjectAnimator createSysuiMultiplierAnim(float... values) {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, values);
+ anim.setAutoCancel(true);
+ return anim;
+ }
+
+ /**
+ * Determines whether to draw the top and/or bottom scrim based on new insets.
+ */
+ public void onInsetsChanged(Rect insets) {
+ mDrawTopScrim = mTopScrim != null && insets.top > 0;
+ mDrawBottomScrim = mBottomMask != null
+ && !mActivity.getDeviceProfile().isVerticalBarLayout()
+ && hasBottomNavButtons();
+ }
+
+ private boolean hasBottomNavButtons() {
+ if (Utilities.ATLEAST_Q && mActivity.getRootView() != null
+ && mActivity.getRootView().getRootWindowInsets() != null) {
+ WindowInsets windowInsets = mActivity.getRootView().getRootWindowInsets();
+ return windowInsets.getTappableElementInsets().bottom > 0;
+ }
+ return true;
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
+ IntentFilter filter = new IntentFilter(ACTION_SCREEN_OFF);
+ filter.addAction(ACTION_USER_PRESENT); // When the device wakes up + keyguard is gone
+ mRoot.getContext().registerReceiver(mReceiver, filter);
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
+ mRoot.getContext().unregisterReceiver(mReceiver);
+ }
+ }
+
+ /**
+ * Set the width and height of the view being scrimmed
+ * @param w
+ * @param h
+ */
+ public void setSize(int w, int h) {
+ if (mTopScrim != null) {
+ mTopScrim.setBounds(0, 0, w, h);
+ mFinalMaskRect.set(0, h - mMaskHeight, w, h);
+ }
+ mWallpaperScrimRect.set(0, 0, w, h);
+ }
+
+ private void setSysUiProgress(float progress) {
+ if (progress != mSysUiProgress) {
+ mSysUiProgress = progress;
+ reapplySysUiAlpha();
+ }
+ }
+
+ private void reapplySysUiAlpha() {
+ reapplySysUiAlphaNoInvalidate();
+ if (!mHideSysUiScrim) {
+ mRoot.invalidate();
+ }
+ }
+
+ private void reapplySysUiAlphaNoInvalidate() {
+ float factor = mSysUiProgress * mSysUiAnimMultiplier;
+ mBottomMaskPaint.setAlpha(Math.round(MAX_HOTSEAT_SCRIM_ALPHA * factor));
+ if (mTopScrim != null) {
+ mTopScrim.setAlpha(Math.round(255 * factor));
+ }
+ mWallpaperScrimPaint.setAlpha(Math.round(mWallpaperScrimMaxAlpha * factor));
+ }
+
+ private Bitmap createDitheredAlphaMask() {
+ DisplayMetrics dm = mActivity.getResources().getDisplayMetrics();
+ int width = ResourceUtils.pxFromDp(ALPHA_MASK_WIDTH_DP, dm);
+ int gradientHeight = ResourceUtils.pxFromDp(ALPHA_MASK_HEIGHT_DP, dm);
+ Bitmap dst = Bitmap.createBitmap(width, mMaskHeight, Bitmap.Config.ALPHA_8);
+ Canvas c = new Canvas(dst);
+ Paint paint = new Paint(Paint.DITHER_FLAG);
+ LinearGradient lg = new LinearGradient(0, 0, 0, gradientHeight,
+ new int[]{
+ 0x00FFFFFF,
+ setColorAlphaBound(Color.WHITE, (int) (0xFF * 0.95)),
+ 0xFFFFFFFF},
+ new float[]{0f, 0.8f, 1f},
+ Shader.TileMode.CLAMP);
+ paint.setShader(lg);
+ c.drawRect(0, 0, width, gradientHeight, paint);
+ return dst;
+ }
+}
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
deleted file mode 100644
index 7b7ab5e..0000000
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ /dev/null
@@ -1,297 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.graphics;
-
-import static android.content.Intent.ACTION_SCREEN_OFF;
-import static android.content.Intent.ACTION_USER_PRESENT;
-
-import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-
-import android.animation.ObjectAnimator;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Region;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
-import android.util.DisplayMetrics;
-import android.util.FloatProperty;
-import android.view.View;
-import android.view.WindowInsets;
-
-import androidx.core.graphics.ColorUtils;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.R;
-import com.android.launcher3.ResourceUtils;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.uioverrides.WallpaperColorInfo;
-import com.android.launcher3.util.Themes;
-
-/**
- * View scrim which draws behind hotseat and workspace
- */
-public class WorkspaceAndHotseatScrim extends Scrim {
-
- public static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_PROGRESS =
- new FloatProperty<WorkspaceAndHotseatScrim>("sysUiProgress") {
- @Override
- public Float get(WorkspaceAndHotseatScrim scrim) {
- return scrim.mSysUiProgress;
- }
-
- @Override
- public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
- scrim.setSysUiProgress(value);
- }
- };
-
- private static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_ANIM_MULTIPLIER =
- new FloatProperty<WorkspaceAndHotseatScrim>("sysUiAnimMultiplier") {
- @Override
- public Float get(WorkspaceAndHotseatScrim scrim) {
- return scrim.mSysUiAnimMultiplier;
- }
-
- @Override
- public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
- scrim.mSysUiAnimMultiplier = value;
- scrim.reapplySysUiAlpha();
- }
- };
-
- /**
- * Receiver used to get a signal that the user unlocked their device.
- * @see KEYGUARD_ANIMATION For proper signal.
- */
- private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- if (ACTION_SCREEN_OFF.equals(action)) {
- mAnimateScrimOnNextDraw = true;
- } else if (ACTION_USER_PRESENT.equals(action)) {
- // ACTION_USER_PRESENT is sent after onStart/onResume. This covers the case where
- // the user unlocked and the Launcher is not in the foreground.
- mAnimateScrimOnNextDraw = false;
- }
- }
- };
-
- private static final int DARK_SCRIM_COLOR = 0x55000000;
- private static final int MAX_HOTSEAT_SCRIM_ALPHA = 100;
- private static final int ALPHA_MASK_HEIGHT_DP = 500;
- private static final int ALPHA_MASK_BITMAP_DP = 200;
- private static final int ALPHA_MASK_WIDTH_DP = 2;
-
- private final Rect mHighlightRect = new Rect();
-
- private Workspace mWorkspace;
-
- private boolean mDrawTopScrim, mDrawBottomScrim;
-
- private final RectF mFinalMaskRect = new RectF();
- private final Paint mBottomMaskPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
- private final Bitmap mBottomMask;
- private final int mMaskHeight;
-
- private final Drawable mTopScrim;
-
- private float mSysUiProgress = 1;
- private boolean mHideSysUiScrim;
-
- private boolean mAnimateScrimOnNextDraw = false;
- private float mSysUiAnimMultiplier = 1;
-
- public WorkspaceAndHotseatScrim(View view) {
- super(view);
-
- mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
- view.getResources().getDisplayMetrics());
- mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
- mBottomMask = mTopScrim == null ? null : createDitheredAlphaMask();
- mHideSysUiScrim = mTopScrim == null;
-
- onExtractedColorsChanged(mWallpaperColorInfo);
- }
-
- public void setWorkspace(Workspace workspace) {
- mWorkspace = workspace;
- }
-
- public void draw(Canvas canvas) {
- // Draw the background below children.
- if (mScrimAlpha > 0) {
- // Update the scroll position first to ensure scrim cutout is in the right place.
- mWorkspace.computeScrollWithoutInvalidation();
- CellLayout currCellLayout = mWorkspace.getCurrentDragOverlappingLayout();
- canvas.save();
- if (currCellLayout != null && currCellLayout != mLauncher.getHotseat()) {
- // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
- mLauncher.getDragLayer()
- .getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
- canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
- }
-
- super.draw(canvas);
- canvas.restore();
- }
-
- if (!mHideSysUiScrim) {
- if (mSysUiProgress <= 0) {
- mAnimateScrimOnNextDraw = false;
- return;
- }
-
- if (mAnimateScrimOnNextDraw) {
- mSysUiAnimMultiplier = 0;
- reapplySysUiAlphaNoInvalidate();
-
- ObjectAnimator oa = createSysuiMultiplierAnim(1);
- oa.setDuration(600);
- oa.setStartDelay(mLauncher.getWindow().getTransitionBackgroundFadeDuration());
- oa.start();
- mAnimateScrimOnNextDraw = false;
- }
-
- if (mDrawTopScrim) {
- mTopScrim.draw(canvas);
- }
- if (mDrawBottomScrim) {
- canvas.drawBitmap(mBottomMask, null, mFinalMaskRect, mBottomMaskPaint);
- }
- }
- }
-
- /**
- * @return an ObjectAnimator that controls the fade in/out of the sys ui scrim.
- */
- public ObjectAnimator createSysuiMultiplierAnim(float... values) {
- ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, values);
- anim.setAutoCancel(true);
- return anim;
- }
-
- /**
- * Determines whether to draw the top and/or bottom scrim based on new insets.
- */
- public void onInsetsChanged(Rect insets, boolean allowSysuiScrims) {
- mDrawTopScrim = allowSysuiScrims
- && mTopScrim != null
- && insets.top > 0;
- mDrawBottomScrim = allowSysuiScrims
- && mBottomMask != null
- && !mLauncher.getDeviceProfile().isVerticalBarLayout()
- && hasBottomNavButtons();
- }
-
- private boolean hasBottomNavButtons() {
- if (Utilities.ATLEAST_Q && mLauncher.getRootView() != null
- && mLauncher.getRootView().getRootWindowInsets() != null) {
- WindowInsets windowInsets = mLauncher.getRootView().getRootWindowInsets();
- return windowInsets.getTappableElementInsets().bottom > 0;
- }
- return true;
- }
-
- @Override
- public void onViewAttachedToWindow(View view) {
- super.onViewAttachedToWindow(view);
-
- if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
- IntentFilter filter = new IntentFilter(ACTION_SCREEN_OFF);
- filter.addAction(ACTION_USER_PRESENT); // When the device wakes up + keyguard is gone
- mRoot.getContext().registerReceiver(mReceiver, filter);
- }
- }
-
- @Override
- public void onViewDetachedFromWindow(View view) {
- super.onViewDetachedFromWindow(view);
- if (!KEYGUARD_ANIMATION.get() && mTopScrim != null) {
- mRoot.getContext().unregisterReceiver(mReceiver);
- }
- }
-
- @Override
- public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
- // for super light wallpaper it needs to be darken for contrast to workspace
- // for dark wallpapers the text is white so darkening works as well
- mBottomMaskPaint.setColor(ColorUtils.compositeColors(DARK_SCRIM_COLOR,
- wallpaperColorInfo.getMainColor()));
- reapplySysUiAlpha();
- super.onExtractedColorsChanged(wallpaperColorInfo);
- }
-
- public void setSize(int w, int h) {
- if (mTopScrim != null) {
- mTopScrim.setBounds(0, 0, w, h);
- mFinalMaskRect.set(0, h - mMaskHeight, w, h);
- }
- }
-
- private void setSysUiProgress(float progress) {
- if (progress != mSysUiProgress) {
- mSysUiProgress = progress;
- reapplySysUiAlpha();
- }
- }
-
- private void reapplySysUiAlpha() {
- reapplySysUiAlphaNoInvalidate();
- if (!mHideSysUiScrim) {
- invalidate();
- }
- }
-
- private void reapplySysUiAlphaNoInvalidate() {
- float factor = mSysUiProgress * mSysUiAnimMultiplier;
- mBottomMaskPaint.setAlpha(Math.round(MAX_HOTSEAT_SCRIM_ALPHA * factor));
- if (mTopScrim != null) {
- mTopScrim.setAlpha(Math.round(255 * factor));
- }
- }
-
- private Bitmap createDitheredAlphaMask() {
- DisplayMetrics dm = mLauncher.getResources().getDisplayMetrics();
- int width = ResourceUtils.pxFromDp(ALPHA_MASK_WIDTH_DP, dm);
- int gradientHeight = ResourceUtils.pxFromDp(ALPHA_MASK_HEIGHT_DP, dm);
- Bitmap dst = Bitmap.createBitmap(width, mMaskHeight, Bitmap.Config.ALPHA_8);
- Canvas c = new Canvas(dst);
- Paint paint = new Paint(Paint.DITHER_FLAG);
- LinearGradient lg = new LinearGradient(0, 0, 0, gradientHeight,
- new int[]{
- 0x00FFFFFF,
- setColorAlphaBound(Color.WHITE, (int) (0xFF * 0.95)),
- 0xFFFFFFFF},
- new float[]{0f, 0.8f, 1f},
- Shader.TileMode.CLAMP);
- paint.setShader(lg);
- c.drawRect(0, 0, width, gradientHeight, paint);
- return dst;
- }
-}
diff --git a/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/src/com/android/launcher3/icons/ClockDrawableWrapper.java
deleted file mode 100644
index b7dd092..0000000
--- a/src/com/android/launcher3/icons/ClockDrawableWrapper.java
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.icons;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.launcher3.FastBitmapDrawable;
-
-import java.util.Calendar;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
- * clock icons
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
-
- private static final String TAG = "ClockDrawableWrapper";
-
- private static final boolean DISABLE_SECONDS = true;
-
- // Time after which the clock icon should check for an update. The actual invalidate
- // will only happen in case of any change.
- public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
-
- private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
- private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
- + ".LEVEL_PER_TICK_ICON_ROUND";
- private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
- private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
- + ".MINUTE_LAYER_INDEX";
- private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
- + ".SECOND_LAYER_INDEX";
- private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
- + ".DEFAULT_HOUR";
- private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
- + ".DEFAULT_MINUTE";
- private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
- + ".DEFAULT_SECOND";
-
- /* Number of levels to jump per second for the second hand */
- private static final int LEVELS_PER_SECOND = 10;
-
- public static final int INVALID_VALUE = -1;
-
- private final AnimationInfo mAnimationInfo = new AnimationInfo();
- private int mTargetSdkVersion;
-
- public ClockDrawableWrapper(AdaptiveIconDrawable base) {
- super(base.getBackground(), base.getForeground());
- }
-
- /**
- * Loads and returns the wrapper from the provided package, or returns null
- * if it is unable to load.
- */
- public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) {
- try {
- PackageManager pm = context.getPackageManager();
- ApplicationInfo appInfo = pm.getApplicationInfo(pkg,
- PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
- final Bundle metadata = appInfo.metaData;
- if (metadata == null) {
- return null;
- }
- int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
- if (drawableId == 0) {
- return null;
- }
-
- Drawable drawable = pm.getResourcesForApplication(appInfo).getDrawableForDensity(
- drawableId, iconDpi).mutate();
- if (!(drawable instanceof AdaptiveIconDrawable)) {
- return null;
- }
-
- ClockDrawableWrapper wrapper =
- new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
- wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
- AnimationInfo info = wrapper.mAnimationInfo;
-
- info.baseDrawableState = drawable.getConstantState();
-
- info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
- info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
- info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
-
- info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
- info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
- info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
-
- LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
- int layerCount = foreground.getNumberOfLayers();
- if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
- info.hourLayerIndex = INVALID_VALUE;
- }
- if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
- info.minuteLayerIndex = INVALID_VALUE;
- }
- if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
- info.secondLayerIndex = INVALID_VALUE;
- } else if (DISABLE_SECONDS) {
- foreground.setDrawable(info.secondLayerIndex, null);
- info.secondLayerIndex = INVALID_VALUE;
- }
- return wrapper;
- } catch (Exception e) {
- Log.d(TAG, "Unable to load clock drawable info", e);
- }
- return null;
- }
-
- @Override
- public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
- iconFactory.disableColorExtraction();
- float [] scale = new float[1];
- AdaptiveIconDrawable background = new AdaptiveIconDrawable(
- getBackground().getConstantState().newDrawable(), null);
- BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background,
- Process.myUserHandle(), mTargetSdkVersion, false, scale);
-
- return new ClockBitmapInfo(bitmap, color, scale[0], mAnimationInfo, bitmapInfo.icon);
- }
-
- @Override
- public void prepareToDrawOnUi() {
- mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
- }
-
- private static class AnimationInfo {
-
- public ConstantState baseDrawableState;
-
- public int hourLayerIndex;
- public int minuteLayerIndex;
- public int secondLayerIndex;
- public int defaultHour;
- public int defaultMinute;
- public int defaultSecond;
-
- boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
- time.setTimeInMillis(System.currentTimeMillis());
-
- // We need to rotate by the difference from the default time if one is specified.
- int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
- int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
- int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
-
- boolean invalidate = false;
- if (hourLayerIndex != INVALID_VALUE) {
- final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
- if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
- invalidate = true;
- }
- }
-
- if (minuteLayerIndex != INVALID_VALUE) {
- final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
- if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
- invalidate = true;
- }
- }
-
- if (secondLayerIndex != INVALID_VALUE) {
- final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
- if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
- invalidate = true;
- }
- }
-
- return invalidate;
- }
- }
-
- private static class ClockBitmapInfo extends BitmapInfo implements FastBitmapDrawable.Factory {
-
- public final float scale;
- public final int offset;
- public final AnimationInfo animInfo;
- public final Bitmap mFlattenedBackground;
-
- ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
- Bitmap background) {
- super(icon, color);
- this.scale = scale;
- this.animInfo = animInfo;
- this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
- this.mFlattenedBackground = background;
- }
-
- @Override
- public FastBitmapDrawable newDrawable() {
- return new ClockIconDrawable(this);
- }
- }
-
- private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
-
- private final Calendar mTime = Calendar.getInstance();
-
- private final ClockBitmapInfo mInfo;
-
- private final AdaptiveIconDrawable mFullDrawable;
- private final LayerDrawable mForeground;
-
- ClockIconDrawable(ClockBitmapInfo clockInfo) {
- super(clockInfo);
-
- mInfo = clockInfo;
-
- mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState.newDrawable();
- mForeground = (LayerDrawable) mFullDrawable.getForeground();
- }
-
- @Override
- protected void onBoundsChange(Rect bounds) {
- super.onBoundsChange(bounds);
- mFullDrawable.setBounds(bounds);
- }
-
- @Override
- public void drawInternal(Canvas canvas, Rect bounds) {
- if (mInfo == null) {
- super.drawInternal(canvas, bounds);
- return;
- }
- // draw the background that is already flattened to a bitmap
- canvas.drawBitmap(mInfo.mFlattenedBackground, null, bounds, mPaint);
-
- // prepare and draw the foreground
- mInfo.animInfo.applyTime(mTime, mForeground);
-
- canvas.scale(mInfo.scale, mInfo.scale,
- bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset);
- canvas.clipPath(mFullDrawable.getIconMask());
- mForeground.draw(canvas);
-
- reschedule();
- }
-
- @Override
- protected void updateFilter() {
- super.updateFilter();
- mFullDrawable.setColorFilter(mPaint.getColorFilter());
- }
-
- @Override
- public void run() {
- if (mInfo.animInfo.applyTime(mTime, mForeground)) {
- invalidateSelf();
- } else {
- reschedule();
- }
- }
-
- @Override
- public boolean setVisible(boolean visible, boolean restart) {
- boolean result = super.setVisible(visible, restart);
- if (visible) {
- reschedule();
- } else {
- unscheduleSelf(this);
- }
- return result;
- }
-
- private void reschedule() {
- if (!isVisible()) {
- return;
- }
-
- unscheduleSelf(this);
- final long upTime = SystemClock.uptimeMillis();
- final long step = TICK_MS; /* tick every 200 ms */
- scheduleSelf(this, upTime - ((upTime % step)) + step);
- }
-
- @Override
- public ConstantState getConstantState() {
- return new ClockConstantState(mInfo, isDisabled());
- }
-
- private static class ClockConstantState extends MyConstantState {
-
- private final ClockBitmapInfo mInfo;
-
- ClockConstantState(ClockBitmapInfo info, boolean isDisabled) {
- super(info.icon, info.color, isDisabled);
- mInfo = info;
- }
-
- @Override
- public FastBitmapDrawable newDrawable() {
- ClockIconDrawable drawable = new ClockIconDrawable(mInfo);
- drawable.setIsDisabled(mIsDisabled);
- return drawable;
- }
- }
- }
-}
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index ff0f773..cd13cd0 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -31,7 +31,6 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ShortcutInfo;
import android.graphics.drawable.Drawable;
-import android.os.Handler;
import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
@@ -40,6 +39,7 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
@@ -52,7 +52,6 @@
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.InstantAppResolver;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
@@ -67,8 +66,8 @@
private static final String TAG = "Launcher.IconCache";
- private final Predicate<ItemInfoWithIcon> mIsUsingFallbackIconCheck = w -> w.bitmap != null
- && w.bitmap.isNullOrLowRes() && !isDefaultIcon(w.bitmap, w.user);
+ private final Predicate<ItemInfoWithIcon> mIsUsingFallbackOrNonDefaultIconCheck = w ->
+ w.bitmap != null && (w.bitmap.isNullOrLowRes() || !isDefaultIcon(w.bitmap, w.user));
private final CachingLogic<ComponentWithLabel> mComponentWithLabelCachingLogic;
private final CachingLogic<LauncherActivityInfo> mLauncherActivityInfoCachingLogic;
@@ -82,10 +81,11 @@
private int mPendingIconRequestCount = 0;
public IconCache(Context context, InvariantDeviceProfile idp) {
- this(context, idp, LauncherFiles.APP_ICONS_DB);
+ this(context, idp, LauncherFiles.APP_ICONS_DB, new IconProvider(context));
}
- public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName) {
+ public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName,
+ IconProvider iconProvider) {
super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */);
mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false);
@@ -94,7 +94,7 @@
mLauncherApps = mContext.getSystemService(LauncherApps.class);
mUserManager = UserCache.INSTANCE.get(mContext);
mInstantAppResolver = InstantAppResolver.newInstance(mContext);
- mIconProvider = new IconProvider(context);
+ mIconProvider = iconProvider;
}
@Override
@@ -108,7 +108,7 @@
}
@Override
- protected BaseIconFactory getIconFactory() {
+ public BaseIconFactory getIconFactory() {
return LauncherIcons.obtain(mContext);
}
@@ -131,37 +131,43 @@
}
/**
+ * Closes the cache DB. This will clear any in-memory cache.
+ */
+ public void close() {
+ mIconDb.close();
+ }
+
+ /**
* Fetches high-res icon for the provided ItemInfo and updates the caller when done.
+ *
* @return a request ID that can be used to cancel the request.
*/
- public IconLoadRequest updateIconInBackground(final ItemInfoUpdateReceiver caller,
+ public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller,
final ItemInfoWithIcon info) {
Preconditions.assertUIThread();
if (mPendingIconRequestCount <= 0) {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
}
- mPendingIconRequestCount ++;
+ mPendingIconRequestCount++;
- IconLoadRequest request = new IconLoadRequest(mWorkerHandler, this::onIconRequestEnd) {
- @Override
- public void run() {
- if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
- getTitleAndIcon(info, false);
- } else if (info instanceof PackageItemInfo) {
- getTitleAndIconForApp((PackageItemInfo) info, false);
- }
- MAIN_EXECUTOR.execute(() -> {
- caller.reapplyItemInfo(info);
- onEnd();
- });
- }
- };
+ HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler,
+ () -> {
+ if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
+ getTitleAndIcon(info, false);
+ } else if (info instanceof PackageItemInfo) {
+ getTitleAndIconForApp((PackageItemInfo) info, false);
+ }
+ return info;
+ },
+ MAIN_EXECUTOR,
+ caller::reapplyItemInfo,
+ this::onIconRequestEnd);
Utilities.postAsyncCallback(mWorkerHandler, request);
return request;
}
private void onIconRequestEnd() {
- mPendingIconRequestCount --;
+ mPendingIconRequestCount--;
if (mPendingIconRequestCount <= 0) {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
@@ -192,14 +198,14 @@
* Fill in {@param info} with the icon for {@param si}
*/
public void getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
- getShortcutIcon(info, si, true, mIsUsingFallbackIconCheck);
+ getShortcutIcon(info, si, true, mIsUsingFallbackOrNonDefaultIconCheck);
}
/**
* Fill in {@param info} with an unbadged icon for {@param si}
*/
public void getUnbadgedShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
- getShortcutIcon(info, si, false, mIsUsingFallbackIconCheck);
+ getShortcutIcon(info, si, false, mIsUsingFallbackOrNonDefaultIconCheck);
}
/**
@@ -260,14 +266,6 @@
}
/**
- * Fill in info with the icon and label for deep shortcut.
- */
- public synchronized CacheEntry getDeepShortcutTitleAndIcon(ShortcutInfo info) {
- return cacheLocked(ShortcutKey.fromInfo(info).componentName, info.getUserHandle(),
- () -> info, mShortcutCachingLogic, false, false);
- }
-
- /**
* Fill in {@param info} with the icon and label. If the
* corresponding activity is not found, it reverts to the package icon.
*/
@@ -295,12 +293,13 @@
/**
* Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info}
*/
- private synchronized void getTitleAndIcon(
+ public synchronized void getTitleAndIcon(
@NonNull ItemInfoWithIcon infoInOut,
@NonNull Supplier<LauncherActivityInfo> activityInfoProvider,
boolean usePkgIcon, boolean useLowResIcon) {
CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user,
- activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon, useLowResIcon);
+ activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon,
+ useLowResIcon);
applyCacheEntry(entry, infoInOut);
}
@@ -313,6 +312,11 @@
CacheEntry entry = getEntryForPackageLocked(
infoInOut.packageName, infoInOut.user, useLowResIcon);
applyCacheEntry(entry, infoInOut);
+ if (infoInOut.category == PackageItemInfo.CONVERSATIONS) {
+ infoInOut.title = mContext.getString(R.string.widget_category_conversations);
+ infoInOut.contentDescription = mPackageManager.getUserBadgedLabel(
+ infoInOut.title, infoInOut.user);
+ }
}
protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) {
@@ -326,28 +330,13 @@
}
public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) {
- cachePackageInstallInfo(key.mPackageName, key.mUser, info.getAppIcon(), info.getAppLabel());
+ cachePackageInstallInfo(key.mPackageName, key.mUser, info.getAppIcon(),
+ info.getAppLabel());
}
@Override
protected String getIconSystemState(String packageName) {
- return mIconProvider.getSystemStateForPackage(mSystemState, packageName)
- + ",flags_asi:" + FeatureFlags.APP_SEARCH_IMPROVEMENTS.get();
- }
-
- @Override
- protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
- if (mIconProvider.isClockIcon(cacheKey)) {
- // For clock icon, we always load the dynamic icon
- return false;
- }
- return super.getEntryFromDB(cacheKey, entry, lowRes);
- }
-
- public static abstract class IconLoadRequest extends HandlerRunnable {
- IconLoadRequest(Handler handler, Runnable endRunnable) {
- super(handler, endRunnable);
- }
+ return mIconProvider.getSystemStateForPackage(mSystemState, packageName);
}
/**
diff --git a/src/com/android/launcher3/icons/IconProvider.java b/src/com/android/launcher3/icons/IconProvider.java
deleted file mode 100644
index 1468b27..0000000
--- a/src/com/android/launcher3/icons/IconProvider.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.icons;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Process;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BitmapInfo.Extender;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.SafeCloseable;
-
-import java.util.Calendar;
-import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
-
-/**
- * Class to handle icon loading from different packages
- */
-public class IconProvider {
-
- private static final String TAG = "IconProvider";
- private static final boolean DEBUG = false;
-
- private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
-
- private static final String SYSTEM_STATE_SEPARATOR = " ";
-
- // Default value returned if there are problems getting resources.
- private static final int NO_ID = 0;
-
- private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER =
- LauncherActivityInfo::getIcon;
-
- private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER =
- ActivityInfo::loadUnbadgedIcon;
-
-
- private final Context mContext;
- private final ComponentName mCalendar;
- private final ComponentName mClock;
-
- public IconProvider(Context context) {
- mContext = context;
- mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
- mClock = parseComponentOrNull(context, R.string.clock_component_name);
- }
-
- /**
- * Adds any modification to the provided systemState for dynamic icons. This system state
- * is used by caches to check for icon invalidation.
- */
- public String getSystemStateForPackage(String systemState, String packageName) {
- if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
- return systemState + SYSTEM_STATE_SEPARATOR + getDay();
- } else {
- return systemState;
- }
- }
-
- /**
- * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly
- * on the UI
- */
- public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) {
- Drawable icon = getIcon(info, iconDpi);
- if (icon instanceof BitmapInfo.Extender) {
- ((Extender) icon).prepareToDrawOnUi();
- }
- return icon;
- }
-
- /**
- * Loads the icon for the provided LauncherActivityInfo
- */
- public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
- return getIcon(info.getApplicationInfo().packageName, info.getUser(),
- info, iconDpi, LAI_LOADER);
- }
-
- /**
- * Loads the icon for the provided activity info
- */
- public Drawable getIcon(ActivityInfo info, UserHandle user) {
- return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(),
- AI_LOADER);
- }
-
- private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param,
- BiFunction<T, P, Drawable> loader) {
- Drawable icon = null;
- if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
- icon = loadCalendarDrawable(0);
- } else if (mClock != null
- && mClock.getPackageName().equals(packageName)
- && Process.myUserHandle().equals(user)) {
- icon = loadClockDrawable(0);
- }
- return icon == null ? loader.apply(obj, param) : icon;
- }
-
- private Drawable loadCalendarDrawable(int iconDpi) {
- PackageManager pm = mContext.getPackageManager();
- try {
- final Bundle metadata = pm.getActivityInfo(
- mCalendar,
- PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
- .metaData;
- final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
- final int id = getDynamicIconId(metadata, resources);
- if (id != NO_ID) {
- if (DEBUG) Log.d(TAG, "Got icon #" + id);
- return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
- }
- } catch (PackageManager.NameNotFoundException e) {
- if (DEBUG) {
- Log.d(TAG, "Could not get activityinfo or resources for package: "
- + mCalendar.getPackageName());
- }
- }
- return null;
- }
-
- private Drawable loadClockDrawable(int iconDpi) {
- return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
- }
-
- protected boolean isClockIcon(ComponentKey key) {
- return mClock != null && mClock.equals(key.componentName)
- && Process.myUserHandle().equals(key.user);
- }
-
- /**
- * @param metadata metadata of the default activity of Calendar
- * @param resources from the Calendar package
- * @return the resource id for today's Calendar icon; 0 if resources cannot be found.
- */
- private int getDynamicIconId(Bundle metadata, Resources resources) {
- if (metadata == null) {
- return NO_ID;
- }
- String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
- final int arrayId = metadata.getInt(key, NO_ID);
- if (arrayId == NO_ID) {
- return NO_ID;
- }
- try {
- return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
- } catch (Resources.NotFoundException e) {
- if (DEBUG) {
- Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
- }
- return NO_ID;
- }
- }
-
- /**
- * @return Today's day of the month, zero-indexed.
- */
- private int getDay() {
- return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
- }
-
-
- /**
- * Registers a callback to listen for calendar icon changes.
- * The callback receives the packageName for the calendar icon
- */
- public static SafeCloseable registerIconChangeListener(Context context,
- BiConsumer<String, UserHandle> callback, Handler handler) {
- ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name);
- ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
-
- if (calendar == null && clock == null) {
- return () -> { };
- }
-
- BroadcastReceiver receiver = new DateTimeChangeReceiver(callback);
- final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
- if (calendar != null) {
- filter.addAction(Intent.ACTION_TIME_CHANGED);
- filter.addAction(Intent.ACTION_DATE_CHANGED);
- }
- context.registerReceiver(receiver, filter, null, handler);
-
- return () -> context.unregisterReceiver(receiver);
- }
-
- private static class DateTimeChangeReceiver extends BroadcastReceiver {
-
- private final BiConsumer<String, UserHandle> mCallback;
-
- DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) {
- mCallback = callback;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
- ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
- if (clock != null) {
- mCallback.accept(clock.getPackageName(), Process.myUserHandle());
- }
- }
-
- ComponentName calendar =
- parseComponentOrNull(context, R.string.calendar_component_name);
- if (calendar != null) {
- for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
- mCallback.accept(calendar.getPackageName(), user);
- }
- }
-
- }
- }
-
- private static ComponentName parseComponentOrNull(Context context, int resId) {
- String cn = context.getString(resId);
- return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
-
- }
-}
diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
index 93de35a..e820ac4 100644
--- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
+++ b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
@@ -20,6 +20,7 @@
import android.content.pm.LauncherActivityInfo;
import android.os.UserHandle;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.icons.cache.CachingLogic;
import com.android.launcher3.util.ResourceBasedOverride;
@@ -56,8 +57,8 @@
@Override
public BitmapInfo loadIcon(Context context, LauncherActivityInfo object) {
try (LauncherIcons li = LauncherIcons.obtain(context)) {
- return li.createBadgedIconBitmap(new IconProvider(context)
- .getIcon(object, li.mFillResIconDpi),
+ return li.createBadgedIconBitmap(LauncherAppState.getInstance(context)
+ .getIconProvider().getIcon(object, li.mFillResIconDpi),
object.getUser(), object.getApplicationInfo().targetSdkVersion);
}
}
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
deleted file mode 100644
index 800598e..0000000
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.keyboard;
-
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import android.widget.PopupMenu;
-import android.widget.PopupMenu.OnMenuItemClickListener;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.popup.PopupContainerWithArrow;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Handles showing a popup menu with available custom actions for a launcher icon.
- * This allows exposing various custom actions using keyboard shortcuts.
- */
-public class CustomActionsPopup implements OnMenuItemClickListener {
-
- private final Launcher mLauncher;
- private final LauncherAccessibilityDelegate mDelegate;
- private final View mIcon;
-
- public CustomActionsPopup(Launcher launcher, View icon) {
- mLauncher = launcher;
- mIcon = icon;
- PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
- if (container != null) {
- mDelegate = container.getAccessibilityDelegate();
- } else {
- mDelegate = launcher.getAccessibilityDelegate();
- }
- }
-
- private List<AccessibilityAction> getActionList() {
- if (mIcon == null || !(mIcon.getTag() instanceof ItemInfo)) {
- return Collections.EMPTY_LIST;
- }
-
- AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
- mDelegate.addSupportedActions(mIcon, info, true);
- List<AccessibilityAction> result = new ArrayList<>(info.getActionList());
- info.recycle();
- return result;
- }
-
- public boolean canShow() {
- return !getActionList().isEmpty();
- }
-
- public boolean show() {
- List<AccessibilityAction> actions = getActionList();
- if (actions.isEmpty()) {
- return false;
- }
-
- PopupMenu popup = new PopupMenu(mLauncher, mIcon);
- popup.setOnMenuItemClickListener(this);
- Menu menu = popup.getMenu();
- for (AccessibilityAction action : actions) {
- menu.add(Menu.NONE, action.getId(), Menu.NONE, action.getLabel());
- }
- popup.show();
- return true;
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem menuItem) {
- return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId());
- }
-}
diff --git a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
index c50189c..83003ff 100644
--- a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
@@ -16,18 +16,7 @@
package com.android.launcher3.keyboard;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.RectEvaluator;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
import android.graphics.Rect;
-import android.util.Property;
import android.view.View;
import android.view.View.OnFocusChangeListener;
@@ -36,205 +25,21 @@
/**
* A helper class to draw background of a focused view.
*/
-public abstract class FocusIndicatorHelper implements
- OnFocusChangeListener, AnimatorUpdateListener {
-
- private static final float MIN_VISIBLE_ALPHA = 0.2f;
- private static final long ANIM_DURATION = 150;
-
- public static final Property<FocusIndicatorHelper, Float> ALPHA =
- new Property<FocusIndicatorHelper, Float>(Float.TYPE, "alpha") {
- @Override
- public void set(FocusIndicatorHelper object, Float value) {
- object.setAlpha(value);
- }
-
- @Override
- public Float get(FocusIndicatorHelper object) {
- return object.mAlpha;
- }
- };
-
- public static final Property<FocusIndicatorHelper, Float> SHIFT =
- new Property<FocusIndicatorHelper, Float>(
- Float.TYPE, "shift") {
-
- @Override
- public void set(FocusIndicatorHelper object, Float value) {
- object.mShift = value;
- }
-
- @Override
- public Float get(FocusIndicatorHelper object) {
- return object.mShift;
- }
- };
-
- private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
- private static final Rect sTempRect1 = new Rect();
- private static final Rect sTempRect2 = new Rect();
-
- private final View mContainer;
- private final Paint mPaint;
- private final int mMaxAlpha;
-
- private final Rect mDirtyRect = new Rect();
- private boolean mIsDirty = false;
-
- private View mLastFocusedView;
-
- private View mCurrentView;
- private View mTargetView;
- /**
- * The fraction indicating the position of the focusRect between {@link #mCurrentView}
- * & {@link #mTargetView}
- */
- private float mShift;
-
- private ObjectAnimator mCurrentAnimation;
- private float mAlpha;
+public abstract class FocusIndicatorHelper extends ItemFocusIndicatorHelper<View>
+ implements OnFocusChangeListener {
public FocusIndicatorHelper(View container) {
- mContainer = container;
-
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- int color = container.getResources().getColor(R.color.focused_background);
- mMaxAlpha = Color.alpha(color);
- mPaint.setColor(0xFF000000 | color);
-
- setAlpha(0);
- mShift = 0;
- }
-
- protected void setAlpha(float alpha) {
- mAlpha = alpha;
- mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
- }
-
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- invalidateDirty();
- }
-
- protected void invalidateDirty() {
- if (mIsDirty) {
- mContainer.invalidate(mDirtyRect);
- mIsDirty = false;
- }
-
- Rect newRect = getDrawRect();
- if (newRect != null) {
- mContainer.invalidate(newRect);
- }
- }
-
- public void draw(Canvas c) {
- if (mAlpha > 0) {
- Rect newRect = getDrawRect();
- if (newRect != null) {
- mDirtyRect.set(newRect);
- c.drawRect(mDirtyRect, mPaint);
- mIsDirty = true;
- }
- }
- }
-
- private Rect getDrawRect() {
- if (mCurrentView != null && mCurrentView.isAttachedToWindow()) {
- viewToRect(mCurrentView, sTempRect1);
-
- if (mShift > 0 && mTargetView != null) {
- viewToRect(mTargetView, sTempRect2);
- return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
- } else {
- return sTempRect1;
- }
- }
- return null;
+ super(container, container.getResources().getColor(R.color.focused_background));
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
- if (hasFocus) {
- endCurrentAnimation();
-
- if (mAlpha > MIN_VISIBLE_ALPHA) {
- mTargetView = v;
-
- mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
- PropertyValuesHolder.ofFloat(ALPHA, 1),
- PropertyValuesHolder.ofFloat(SHIFT, 1));
- mCurrentAnimation.addListener(new ViewSetListener(v, true));
- } else {
- setCurrentView(v);
-
- mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
- PropertyValuesHolder.ofFloat(ALPHA, 1));
- }
-
- mLastFocusedView = v;
- } else {
- if (mLastFocusedView == v) {
- mLastFocusedView = null;
- endCurrentAnimation();
- mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
- PropertyValuesHolder.ofFloat(ALPHA, 0));
- mCurrentAnimation.addListener(new ViewSetListener(null, false));
- }
- }
-
- // invalidate once
- invalidateDirty();
-
- mLastFocusedView = hasFocus ? v : null;
- if (mCurrentAnimation != null) {
- mCurrentAnimation.addUpdateListener(this);
- mCurrentAnimation.setDuration(ANIM_DURATION).start();
- }
+ changeFocus(v, hasFocus);
}
- protected void endCurrentAnimation() {
- if (mCurrentAnimation != null) {
- mCurrentAnimation.cancel();
- mCurrentAnimation = null;
- }
- }
-
- protected void setCurrentView(View v) {
- mCurrentView = v;
- mShift = 0;
- mTargetView = null;
- }
-
- /**
- * Gets the position of {@param v} relative to {@link #mContainer}.
- */
- public abstract void viewToRect(View v, Rect outRect);
-
- private class ViewSetListener extends AnimatorListenerAdapter {
- private final View mViewToSet;
- private final boolean mCallOnCancel;
- private boolean mCalled = false;
-
- public ViewSetListener(View v, boolean callOnCancel) {
- mViewToSet = v;
- mCallOnCancel = callOnCancel;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- if (!mCallOnCancel) {
- mCalled = true;
- }
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (!mCalled) {
- setCurrentView(mViewToSet);
- mCalled = true;
- }
- }
+ @Override
+ protected boolean shouldDraw(View item) {
+ return item.isAttachedToWindow();
}
/**
diff --git a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
new file mode 100644
index 0000000..57fab2d
--- /dev/null
+++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.keyboard;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Themes;
+
+/**
+ * A helper class to draw background of a focused item.
+ * @param <T> Item type
+ */
+public abstract class ItemFocusIndicatorHelper<T> implements AnimatorUpdateListener {
+
+ private static final float MIN_VISIBLE_ALPHA = 0.2f;
+ private static final long ANIM_DURATION = 150;
+
+ public static final FloatProperty<ItemFocusIndicatorHelper> ALPHA =
+ new FloatProperty<ItemFocusIndicatorHelper>("alpha") {
+
+ @Override
+ public void setValue(ItemFocusIndicatorHelper object, float value) {
+ object.setAlpha(value);
+ }
+
+ @Override
+ public Float get(ItemFocusIndicatorHelper object) {
+ return object.mAlpha;
+ }
+ };
+
+ public static final FloatProperty<ItemFocusIndicatorHelper> SHIFT =
+ new FloatProperty<ItemFocusIndicatorHelper>("shift") {
+
+ @Override
+ public void setValue(ItemFocusIndicatorHelper object, float value) {
+ object.mShift = value;
+ }
+
+ @Override
+ public Float get(ItemFocusIndicatorHelper object) {
+ return object.mShift;
+ }
+ };
+
+ private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
+ private static final Rect sTempRect1 = new Rect();
+ private static final Rect sTempRect2 = new Rect();
+
+ private final View mContainer;
+ protected final Paint mPaint;
+ private final int mMaxAlpha;
+
+ private final Rect mDirtyRect = new Rect();
+ private boolean mIsDirty = false;
+
+ private T mLastFocusedItem;
+
+ private T mCurrentItem;
+ private T mTargetItem;
+ /**
+ * The fraction indicating the position of the focusRect between {@link #mCurrentItem}
+ * & {@link #mTargetItem}
+ */
+ private float mShift;
+
+ private ObjectAnimator mCurrentAnimation;
+ private float mAlpha;
+ private float mRadius;
+
+ public ItemFocusIndicatorHelper(View container, int color) {
+ mContainer = container;
+
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mMaxAlpha = Color.alpha(color);
+ mPaint.setColor(0xFF000000 | color);
+
+ setAlpha(0);
+ mShift = 0;
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ mRadius = Themes.getDialogCornerRadius(container.getContext());
+ }
+ }
+
+ protected void setAlpha(float alpha) {
+ mAlpha = alpha;
+ mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ invalidateDirty();
+ }
+
+ protected void invalidateDirty() {
+ if (mIsDirty) {
+ mContainer.invalidate(mDirtyRect);
+ mIsDirty = false;
+ }
+
+ Rect newRect = getDrawRect();
+ if (newRect != null) {
+ mContainer.invalidate(newRect);
+ }
+ }
+
+ /**
+ * Draws the indicator on the canvas
+ */
+ public void draw(Canvas c) {
+ if (mAlpha <= 0) return;
+
+ Rect newRect = getDrawRect();
+ if (newRect != null) {
+ mDirtyRect.set(newRect);
+ c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top,
+ (float) mDirtyRect.right, (float) mDirtyRect.bottom,
+ mRadius, mRadius, mPaint);
+ mIsDirty = true;
+ }
+ }
+
+ private Rect getDrawRect() {
+ if (mCurrentItem != null && shouldDraw(mCurrentItem)) {
+ viewToRect(mCurrentItem, sTempRect1);
+
+ if (mShift > 0 && mTargetItem != null) {
+ viewToRect(mTargetItem, sTempRect2);
+ return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
+ } else {
+ return sTempRect1;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the provided item is valid
+ */
+ protected boolean shouldDraw(T item) {
+ return true;
+ }
+
+ protected void changeFocus(T item, boolean hasFocus) {
+ if (hasFocus) {
+ endCurrentAnimation();
+
+ if (mAlpha > MIN_VISIBLE_ALPHA) {
+ mTargetItem = item;
+
+ mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+ PropertyValuesHolder.ofFloat(ALPHA, 1),
+ PropertyValuesHolder.ofFloat(SHIFT, 1));
+ mCurrentAnimation.addListener(new ViewSetListener(item, true));
+ } else {
+ setCurrentItem(item);
+
+ mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+ PropertyValuesHolder.ofFloat(ALPHA, 1));
+ }
+
+ mLastFocusedItem = item;
+ } else {
+ if (mLastFocusedItem == item) {
+ mLastFocusedItem = null;
+ endCurrentAnimation();
+ mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+ PropertyValuesHolder.ofFloat(ALPHA, 0));
+ mCurrentAnimation.addListener(new ViewSetListener(null, false));
+ }
+ }
+
+ // invalidate once
+ invalidateDirty();
+
+ mLastFocusedItem = hasFocus ? item : null;
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.addUpdateListener(this);
+ mCurrentAnimation.setDuration(ANIM_DURATION).start();
+ }
+ }
+
+ protected void endCurrentAnimation() {
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.cancel();
+ mCurrentAnimation = null;
+ }
+ }
+
+ protected void setCurrentItem(T item) {
+ mCurrentItem = item;
+ mShift = 0;
+ mTargetItem = null;
+ }
+
+ /**
+ * Gets the position of the item relative to {@link #mContainer}.
+ */
+ public abstract void viewToRect(T item, Rect outRect);
+
+ private class ViewSetListener extends AnimatorListenerAdapter {
+ private final T mItemToSet;
+ private final boolean mCallOnCancel;
+ private boolean mCalled = false;
+
+ ViewSetListener(T item, boolean callOnCancel) {
+ mItemToSet = item;
+ mCallOnCancel = callOnCancel;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (!mCallOnCancel) {
+ mCalled = true;
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCalled) {
+ setCurrentItem(mItemToSet);
+ mCalled = true;
+ }
+ }
+ }
+}
diff --git a/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java
new file mode 100644
index 0000000..a6c897f
--- /dev/null
+++ b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.keyboard;
+
+import static android.app.Activity.DEFAULT_KEYS_SEARCH_LOCAL;
+
+import static com.android.launcher3.LauncherState.SPRING_LOADED;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.TextView;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.util.Themes;
+
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.function.ToIntBiFunction;
+import java.util.function.ToIntFunction;
+
+/**
+ * A floating view to allow keyboard navigation across virtual nodes
+ */
+public class KeyboardDragAndDropView extends AbstractFloatingView
+ implements Insettable, StateListener<LauncherState> {
+
+ private static final long MINOR_AXIS_WEIGHT = 13;
+
+ private final ArrayList<Integer> mIntList = new ArrayList<>();
+ private final ArrayList<DragAndDropAccessibilityDelegate> mDelegates = new ArrayList<>();
+ private final ArrayList<VirtualNodeInfo> mNodes = new ArrayList<>();
+
+ private final Rect mTempRect = new Rect();
+ private final Rect mTempRect2 = new Rect();
+ private final AccessibilityNodeInfoCompat mTempNodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+ private final RectFocusIndicator mFocusIndicator;
+
+ private final Launcher mLauncher;
+ private VirtualNodeInfo mCurrentSelection;
+
+
+ public KeyboardDragAndDropView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+ mFocusIndicator = new RectFocusIndicator(this);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ mLauncher.getDragLayer().removeView(this);
+ mLauncher.getStateManager().removeStateListener(this);
+ mLauncher.setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+ mIsOpen = false;
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_DRAG_DROP_POPUP) != 0;
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ // Consume all touch
+ return true;
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ setPadding(insets.left, insets.top, insets.right, insets.bottom);
+ }
+
+ @Override
+ public void onStateTransitionStart(LauncherState toState) {
+ if (toState != SPRING_LOADED) {
+ close(false);
+ }
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if (mCurrentSelection != null) {
+ setCurrentSelection(mCurrentSelection);
+ }
+ }
+
+ private void setCurrentSelection(VirtualNodeInfo nodeInfo) {
+ mCurrentSelection = nodeInfo;
+ ((TextView) findViewById(R.id.label))
+ .setText(nodeInfo.populate(mTempNodeInfo).getContentDescription());
+
+ Rect bounds = new Rect();
+ mTempNodeInfo.getBoundsInParent(bounds);
+ View host = nodeInfo.delegate.getHost();
+ ViewParent parent = host.getParent();
+ if (parent instanceof PagedView) {
+ PagedView pv = (PagedView) parent;
+ int pageIndex = pv.indexOfChild(host);
+
+ pv.setCurrentPage(pageIndex);
+ bounds.offset(pv.getScrollX() - pv.getScrollForPage(pageIndex), 0);
+ }
+ float[] pos = new float[] {bounds.left, bounds.top, bounds.right, bounds.bottom};
+ Utilities.getDescendantCoordRelativeToAncestor(host, mLauncher.getDragLayer(), pos, true);
+
+ new RectF(pos[0], pos[1], pos[2], pos[3]).roundOut(bounds);
+ mFocusIndicator.changeFocus(bounds, true);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ mFocusIndicator.draw(canvas);
+ }
+
+ @Override
+ public boolean dispatchUnhandledMove(View focused, int direction) {
+ VirtualNodeInfo nodeInfo = getNextSelection(direction);
+ if (nodeInfo == null) {
+ return false;
+ }
+ setCurrentSelection(nodeInfo);
+ return true;
+ }
+
+ /**
+ * Focus finding logic:
+ * Collect all virtual nodes in reading order (used for forward and backwards).
+ * Then find the closest view by comparing the distances spatially. Since it is a move
+ * operation. consider all cell sizes to be approximately of the same size.
+ */
+ private VirtualNodeInfo getNextSelection(int direction) {
+ // Collect all virtual nodes
+ mDelegates.clear();
+ mNodes.clear();
+
+ Folder openFolder = Folder.getOpen(mLauncher);
+ PagedView pv = openFolder == null ? mLauncher.getWorkspace() : openFolder.getContent();
+ int count = pv.getPageCount();
+ for (int i = 0; i < count; i++) {
+ mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate());
+ }
+ if (openFolder == null) {
+ mDelegates.add(pv.getNextPage() + 1,
+ mLauncher.getHotseat().getDragAndDropAccessibilityDelegate());
+ }
+ mDelegates.forEach(delegate -> {
+ mIntList.clear();
+ delegate.getVisibleVirtualViews(mIntList);
+ mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id)));
+ });
+
+ if (mNodes.isEmpty()) {
+ return null;
+ }
+ int index = mNodes.indexOf(mCurrentSelection);
+ if (mCurrentSelection == null || index < 0) {
+ return null;
+ }
+ int totalNodes = mNodes.size();
+
+ final ToIntBiFunction<Rect, Rect> majorAxis;
+ final ToIntFunction<Rect> minorAxis;
+
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ majorAxis = (source, dest) -> dest.left - source.left;
+ minorAxis = Rect::centerY;
+ break;
+ case View.FOCUS_LEFT:
+ majorAxis = (source, dest) -> source.left - dest.left;
+ minorAxis = Rect::centerY;
+ break;
+ case View.FOCUS_UP:
+ majorAxis = (source, dest) -> source.top - dest.top;
+ minorAxis = Rect::centerX;
+ break;
+ case View.FOCUS_DOWN:
+ majorAxis = (source, dest) -> dest.top - source.top;
+ minorAxis = Rect::centerX;
+ break;
+ case View.FOCUS_FORWARD:
+ return mNodes.get((index + 1) % totalNodes);
+ case View.FOCUS_BACKWARD:
+ return mNodes.get((index + totalNodes - 1) % totalNodes);
+ default:
+ // Unknown direction
+ return null;
+ }
+ mCurrentSelection.populate(mTempNodeInfo).getBoundsInScreen(mTempRect);
+
+ float minWeight = Float.MAX_VALUE;
+ VirtualNodeInfo match = null;
+ for (int i = 0; i < totalNodes; i++) {
+ VirtualNodeInfo node = mNodes.get(i);
+ node.populate(mTempNodeInfo).getBoundsInScreen(mTempRect2);
+
+ int majorAxisWeight = majorAxis.applyAsInt(mTempRect, mTempRect2);
+ if (majorAxisWeight <= 0) {
+ continue;
+ }
+ int minorAxisWeight = minorAxis.applyAsInt(mTempRect2)
+ - minorAxis.applyAsInt(mTempRect);
+
+ float weight = majorAxisWeight * majorAxisWeight
+ + minorAxisWeight * minorAxisWeight * MINOR_AXIS_WEIGHT;
+ if (weight < minWeight) {
+ minWeight = weight;
+ match = node;
+ }
+ }
+ return match;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER && mCurrentSelection != null) {
+ mCurrentSelection.delegate.onPerformActionForVirtualView(
+ mCurrentSelection.id, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ /**
+ * Shows the keyboard drag popup for the provided view
+ */
+ public void showForIcon(View icon, ItemInfo item, DragOptions dragOptions) {
+ mIsOpen = true;
+ mLauncher.getDragLayer().addView(this);
+ mLauncher.getStateManager().addStateListener(this);
+
+ // Find current selection
+ CellLayout currentParent = (CellLayout) icon.getParent().getParent();
+ float[] iconPos = new float[] {currentParent.getCellWidth() / 2,
+ currentParent.getCellHeight() / 2};
+ Utilities.getDescendantCoordRelativeToAncestor(icon, currentParent, iconPos, false);
+
+ ItemLongClickListener.beginDrag(icon, mLauncher, item, dragOptions);
+
+ DragAndDropAccessibilityDelegate dndDelegate =
+ currentParent.getDragAndDropAccessibilityDelegate();
+ setCurrentSelection(new VirtualNodeInfo(
+ dndDelegate, dndDelegate.getVirtualViewAt(iconPos[0], iconPos[1])));
+
+ mLauncher.setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE);
+ requestFocus();
+ }
+
+ private static class VirtualNodeInfo {
+ public final DragAndDropAccessibilityDelegate delegate;
+ public final int id;
+
+ VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id) {
+ this.id = id;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof VirtualNodeInfo)) {
+ return false;
+ }
+ VirtualNodeInfo that = (VirtualNodeInfo) o;
+ return id == that.id && delegate.equals(that.delegate);
+ }
+
+ public AccessibilityNodeInfoCompat populate(AccessibilityNodeInfoCompat nodeInfo) {
+ delegate.onPopulateNodeForVirtualView(id, nodeInfo);
+ return nodeInfo;
+ }
+
+ public void getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out) {
+ delegate.onPopulateNodeForVirtualView(id, nodeInfo);
+ nodeInfo.getBoundsInScreen(out);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, delegate);
+ }
+ }
+
+ private static class RectFocusIndicator extends ItemFocusIndicatorHelper<Rect> {
+
+ RectFocusIndicator(View container) {
+ super(container, Themes.getColorAccent(container.getContext()));
+ mPaint.setStrokeWidth(container.getResources()
+ .getDimension(R.dimen.keyboard_drag_stroke_width));
+ mPaint.setStyle(Style.STROKE);
+ }
+
+ @Override
+ public void viewToRect(Rect item, Rect outRect) {
+ outRect.set(item);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index 6bc1ecb..cdd0bda 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -10,7 +10,6 @@
import androidx.annotation.VisibleForTesting;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.IOUtils;
import java.io.BufferedReader;
@@ -43,7 +42,7 @@
private static Handler sHandler = null;
private static File sLogsDirectory = null;
- public static final int LOG_DAYS = FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() ? 10 : 4;
+ public static final int LOG_DAYS = 4;
public static void setDir(File logsDir) {
if (ENABLED) {
diff --git a/src/com/android/launcher3/logging/LoggerUtils.java b/src/com/android/launcher3/logging/LoggerUtils.java
deleted file mode 100644
index cd4f034..0000000
--- a/src/com/android/launcher3/logging/LoggerUtils.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.logging;
-
-import android.util.ArrayMap;
-import android.util.SparseArray;
-import android.view.View;
-
-import com.android.launcher3.ButtonDropTarget;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogExtensions.TargetExtension;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.util.InstantAppResolver;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-
-/**
- * Helper methods for logging.
- */
-public class LoggerUtils {
- private static final ArrayMap<Class, SparseArray<String>> sNameCache = new ArrayMap<>();
- private static final String UNKNOWN = "UNKNOWN";
- private static final int DEFAULT_PREDICTED_RANK = 10000;
- private static final String DELIMITER_DOT = "\\.";
-
- public static String getFieldName(int value, Class c) {
- SparseArray<String> cache;
- synchronized (sNameCache) {
- cache = sNameCache.get(c);
- if (cache == null) {
- cache = new SparseArray<>();
- for (Field f : c.getDeclaredFields()) {
- if (f.getType() == int.class && Modifier.isStatic(f.getModifiers())) {
- try {
- f.setAccessible(true);
- cache.put(f.getInt(null), f.getName());
- } catch (IllegalAccessException e) {
- // Ignore
- }
- }
- }
- sNameCache.put(c, cache);
- }
- }
- String result = cache.get(value);
- return result != null ? result : UNKNOWN;
- }
-
- public static Target newItemTarget(int itemType) {
- Target t = newTarget(Target.Type.ITEM);
- t.itemType = itemType;
- return t;
- }
-
- public static Target newItemTarget(View v, InstantAppResolver instantAppResolver) {
- return (v != null) && (v.getTag() instanceof ItemInfo)
- ? newItemTarget((ItemInfo) v.getTag(), instantAppResolver)
- : newTarget(Target.Type.ITEM);
- }
-
- public static Target newItemTarget(ItemInfo info, InstantAppResolver instantAppResolver) {
- Target t = newTarget(Target.Type.ITEM);
- switch (info.itemType) {
- case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
- t.itemType = (instantAppResolver != null && info instanceof AppInfo
- && instantAppResolver.isInstantApp(((AppInfo) info)))
- ? ItemType.WEB_APP
- : ItemType.APP_ICON;
- t.predictedRank = DEFAULT_PREDICTED_RANK;
- break;
- case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
- t.itemType = ItemType.SHORTCUT;
- t.predictedRank = DEFAULT_PREDICTED_RANK;
- break;
- case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
- t.itemType = ItemType.FOLDER_ICON;
- break;
- case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
- t.itemType = ItemType.WIDGET;
- break;
- case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
- t.itemType = ItemType.DEEPSHORTCUT;
- t.predictedRank = DEFAULT_PREDICTED_RANK;
- break;
- }
- return t;
- }
-
- public static Target newDropTarget(View v) {
- if (!(v instanceof ButtonDropTarget)) {
- return newTarget(Target.Type.CONTAINER);
- }
- if (v instanceof ButtonDropTarget) {
- return ((ButtonDropTarget) v).getDropTargetForLogging();
- }
- return newTarget(Target.Type.CONTROL);
- }
-
- public static Target newTarget(int targetType, TargetExtension extension) {
- Target t = new Target();
- t.type = targetType;
- t.extension = extension;
- return t;
- }
-
- public static Target newTarget(int targetType) {
- Target t = new Target();
- t.type = targetType;
- return t;
- }
-
- public static Target newControlTarget(int controlType) {
- Target t = newTarget(Target.Type.CONTROL);
- t.controlType = controlType;
- return t;
- }
-
- public static Target newContainerTarget(int containerType) {
- Target t = newTarget(Target.Type.CONTAINER);
- t.containerType = containerType;
- return t;
- }
-
- public static Action newAction(int type) {
- Action a = new Action();
- a.type = type;
- return a;
- }
-
- public static Action newCommandAction(int command) {
- Action a = newAction(Action.Type.COMMAND);
- a.command = command;
- return a;
- }
-
- public static Action newTouchAction(int touch) {
- Action a = newAction(Action.Type.TOUCH);
- a.touch = touch;
- return a;
- }
-
- public static LauncherEvent newLauncherEvent(Action action, Target... srcTargets) {
- LauncherEvent event = new LauncherEvent();
- event.srcTarget = srcTargets;
- event.action = action;
- return event;
- }
-
- /**
- * Creates LauncherEvent using Action and ArrayList of Targets
- */
- public static LauncherEvent newLauncherEvent(Action action, ArrayList<Target> targets) {
- Target[] targetsArray = new Target[targets.size()];
- targets.toArray(targetsArray);
- return newLauncherEvent(action, targetsArray);
- }
-
- /**
- * String conversion for only the helpful parts of {@link Object#toString()} method
- * @param stringToExtract "foo.bar.baz.MyObject@1234"
- * @return "MyObject@1234"
- */
- public static String extractObjectNameAndAddress(String stringToExtract) {
- String[] superStringParts = stringToExtract.split(DELIMITER_DOT);
- if (superStringParts.length == 0) {
- return "";
- }
- return superStringParts[superStringParts.length - 1];
- }
-}
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 8e23b65..79e5b5d 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -15,29 +15,33 @@
*/
package com.android.launcher3.logging;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_DOWN;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_OPEN_UP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
import android.content.Context;
+import android.view.View;
import androidx.annotation.Nullable;
+import androidx.slice.SliceItem;
import com.android.launcher3.R;
+import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
import com.android.launcher3.logger.LauncherAtom.FromState;
import com.android.launcher3.logger.LauncherAtom.ToState;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.LauncherLogProto;
import com.android.launcher3.util.ResourceBasedOverride;
/**
* Handles the user event logging in R+.
+ *
+ * <pre>
* All of the event ids are defined here.
- * Most of the methods are dummy methods for Launcher3
+ * Most of the methods are placeholder methods for Launcher3
* Actual call happens only for Launcher variant that implements QuickStep.
+ * </pre>
*/
public class StatsLogManager implements ResourceBasedOverride {
@@ -48,42 +52,24 @@
public static final int LAUNCHER_STATE_ALLAPPS = 4;
public static final int LAUNCHER_STATE_UNCHANGED = 5;
+ private InstanceId mInstanceId;
/**
- * Returns proper launcher state enum for {@link StatsLogManager}
- * (to be removed during UserEventDispatcher cleanup)
+ * Returns event enum based on the two state transition information when swipe
+ * gesture happens(to be removed during UserEventDispatcher cleanup).
*/
- public static int containerTypeToAtomState(int containerType) {
- switch (containerType) {
- case LauncherLogProto.ContainerType.ALLAPPS_VALUE:
- return LAUNCHER_STATE_ALLAPPS;
- case LauncherLogProto.ContainerType.OVERVIEW_VALUE:
- return LAUNCHER_STATE_OVERVIEW;
- case LauncherLogProto.ContainerType.WORKSPACE_VALUE:
- return LAUNCHER_STATE_HOME;
- case LauncherLogProto.ContainerType.APP_VALUE:
- return LAUNCHER_STATE_BACKGROUND;
- }
- return LAUNCHER_STATE_UNSPECIFIED;
- }
-
- /**
- * Returns event enum based on the two {@link ContainerType} transition information when
- * swipe gesture happens.
- * (to be removed during UserEventDispatcher cleanup)
- */
- public static EventEnum getLauncherAtomEvent(int startContainerType,
- int targetContainerType, EventEnum fallbackEvent) {
- if (startContainerType == LauncherLogProto.ContainerType.WORKSPACE.getNumber()
- && targetContainerType == LauncherLogProto.ContainerType.WORKSPACE.getNumber()) {
+ public static EventEnum getLauncherAtomEvent(int startState,
+ int targetState, EventEnum fallbackEvent) {
+ if (startState == LAUNCHER_STATE_HOME
+ && targetState == LAUNCHER_STATE_HOME) {
return LAUNCHER_HOME_GESTURE;
- } else if (startContainerType != LauncherLogProto.ContainerType.TASKSWITCHER.getNumber()
- && targetContainerType == LauncherLogProto.ContainerType.TASKSWITCHER.getNumber()) {
+ } else if (startState != LAUNCHER_STATE_OVERVIEW
+ && targetState == LAUNCHER_STATE_OVERVIEW) {
return LAUNCHER_OVERVIEW_GESTURE;
- } else if (startContainerType != LauncherLogProto.ContainerType.ALLAPPS.getNumber()
- && targetContainerType == LauncherLogProto.ContainerType.ALLAPPS.getNumber()) {
+ } else if (startState != LAUNCHER_STATE_ALLAPPS
+ && targetState == LAUNCHER_STATE_ALLAPPS) {
return LAUNCHER_ALLAPPS_OPEN_UP;
- } else if (startContainerType == LauncherLogProto.ContainerType.ALLAPPS.getNumber()
- && targetContainerType != LauncherLogProto.ContainerType.ALLAPPS.getNumber()) {
+ } else if (startState == LAUNCHER_STATE_ALLAPPS
+ && targetState != LAUNCHER_STATE_ALLAPPS) {
return LAUNCHER_ALLAPPS_CLOSE_DOWN;
}
return fallbackEvent; // TODO fix
@@ -115,9 +101,13 @@
@UiEvent(doc = "User dragged a launcher item")
LAUNCHER_ITEM_DRAG_STARTED(383),
- @UiEvent(doc = "A dragged launcher item is successfully dropped")
+ @UiEvent(doc = "A dragged launcher item is successfully dropped onto workspace, hotseat "
+ + "open folder etc")
LAUNCHER_ITEM_DROP_COMPLETED(385),
+ @UiEvent(doc = "A dragged launcher item is successfully dropped onto a folder icon.")
+ LAUNCHER_ITEM_DROP_COMPLETED_ON_FOLDER_ICON(697),
+
@UiEvent(doc = "A dragged launcher item is successfully dropped on another item "
+ "resulting in a new folder creation")
LAUNCHER_ITEM_DROP_FOLDER_CREATED(386),
@@ -146,6 +136,12 @@
@UiEvent(doc = "User tapped or long pressed on widget tray icon inside launcher settings.")
LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS(464),
+ @UiEvent(doc = "User expanded the list of widgets for a single app in the widget picker.")
+ LAUNCHER_WIDGETSTRAY_APP_EXPANDED(818),
+
+ @UiEvent(doc = "User searched for a widget in the widget picker.")
+ LAUNCHER_WIDGETSTRAY_SEARCHED(819),
+
@UiEvent(doc = "A dragged item is dropped on 'Remove' button in the target bar")
LAUNCHER_ITEM_DROPPED_ON_REMOVE(465),
@@ -273,7 +269,225 @@
LAUNCHER_SELECT_MODE_CLOSE(583),
@UiEvent(doc = "User tapped on the highlight items in select mode")
- LAUNCHER_SELECT_MODE_ITEM(584);
+ LAUNCHER_SELECT_MODE_ITEM(584),
+
+ @UiEvent(doc = "Notification dot on app icon enabled.")
+ LAUNCHER_NOTIFICATION_DOT_ENABLED(611),
+
+ @UiEvent(doc = "Notification dot on app icon disabled.")
+ LAUNCHER_NOTIFICATION_DOT_DISABLED(612),
+
+ @UiEvent(doc = "For new apps, add app icons to home screen enabled.")
+ LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED(613),
+
+ @UiEvent(doc = "For new apps, add app icons to home screen disabled.")
+ LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_DISABLED(614),
+
+ @UiEvent(doc = "Home screen rotation is enabled when phone is rotated.")
+ LAUNCHER_HOME_SCREEN_ROTATION_ENABLED(615),
+
+ @UiEvent(doc = "Home screen rotation is disabled when phone is rotated.")
+ LAUNCHER_HOME_SCREEN_ROTATION_DISABLED(616),
+
+ @UiEvent(doc = "Suggestions in all apps list enabled.")
+ LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED(619),
+
+ @UiEvent(doc = "Suggestions in all apps list disabled.")
+ LAUNCHER_ALL_APPS_SUGGESTIONS_DISABLED(620),
+
+ @UiEvent(doc = "Suggestions on home screen is enabled.")
+ LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED(621),
+
+ @UiEvent(doc = "Suggestions on home screen is disabled.")
+ LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED(622),
+
+ @UiEvent(doc = "System navigation is 3 button mode.")
+ LAUNCHER_NAVIGATION_MODE_3_BUTTON(623),
+
+ @UiEvent(doc = "System navigation mode is 2 button mode.")
+ LAUNCHER_NAVIGATION_MODE_2_BUTTON(624),
+
+ @UiEvent(doc = "System navigation mode is 0 button mode/gesture navigation mode .")
+ LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON(625),
+
+ @UiEvent(doc = "User tapped on image content in Overview Select mode.")
+ LAUNCHER_SELECT_MODE_IMAGE(627),
+
+ @UiEvent(doc = "Activity to add external item was started")
+ LAUNCHER_ADD_EXTERNAL_ITEM_START(641),
+
+ @UiEvent(doc = "Activity to add external item was cancelled")
+ LAUNCHER_ADD_EXTERNAL_ITEM_CANCELLED(642),
+
+ @UiEvent(doc = "Activity to add external item was backed out")
+ LAUNCHER_ADD_EXTERNAL_ITEM_BACK(643),
+
+ @UiEvent(doc = "Item was placed automatically in external item addition flow")
+ LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY(644),
+
+ @UiEvent(doc = "Item was dragged in external item addition flow")
+ LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED(645),
+
+ @UiEvent(doc = "A folder was replaced by a single item")
+ LAUNCHER_FOLDER_CONVERTED_TO_ICON(646),
+
+ @UiEvent(doc = "A hotseat prediction item was pinned")
+ LAUNCHER_HOTSEAT_PREDICTION_PINNED(647),
+
+ @UiEvent(doc = "Undo event was tapped.")
+ LAUNCHER_UNDO(648),
+
+ @UiEvent(doc = "Task switcher clear all target was tapped.")
+ LAUNCHER_TASK_CLEAR_ALL(649),
+
+ @UiEvent(doc = "Task preview was long pressed.")
+ LAUNCHER_TASK_PREVIEW_LONGPRESS(650),
+
+ @UiEvent(doc = "User swiped down on workspace (triggering noti shade to open).")
+ LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN(651),
+
+ @UiEvent(doc = "Notification dismissed by swiping right.")
+ LAUNCHER_NOTIFICATION_DISMISSED(652),
+
+ @UiEvent(doc = "Current grid size is changed to 5.")
+ LAUNCHER_GRID_SIZE_5(662),
+
+ @UiEvent(doc = "Current grid size is changed to 4.")
+ LAUNCHER_GRID_SIZE_4(663),
+
+ @UiEvent(doc = "Current grid size is changed to 3.")
+ LAUNCHER_GRID_SIZE_3(664),
+
+ @UiEvent(doc = "Current grid size is changed to 2.")
+ LAUNCHER_GRID_SIZE_2(665),
+
+ @UiEvent(doc = "Launcher entered into AllApps state.")
+ LAUNCHER_ALLAPPS_ENTRY(692),
+
+ @UiEvent(doc = "Launcher exited from AllApps state.")
+ LAUNCHER_ALLAPPS_EXIT(693),
+
+ @UiEvent(doc = "User closed the AllApps keyboard.")
+ LAUNCHER_ALLAPPS_KEYBOARD_CLOSED(694),
+
+ @UiEvent(doc = "User switched to AllApps Main/Personal tab by swiping left.")
+ LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB(695),
+
+ @UiEvent(doc = "User switched to AllApps Work tab by swiping right.")
+ LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB(696),
+
+ @UiEvent(doc = "Default event when dedicated UI event is not available for the user action"
+ + " on slice .")
+ LAUNCHER_SLICE_DEFAULT_ACTION(700),
+
+ @UiEvent(doc = "User toggled-on a Slice item.")
+ LAUNCHER_SLICE_TOGGLE_ON(701),
+
+ @UiEvent(doc = "User toggled-off a Slice item.")
+ LAUNCHER_SLICE_TOGGLE_OFF(702),
+
+ @UiEvent(doc = "User acted on a Slice item with a button.")
+ LAUNCHER_SLICE_BUTTON_ACTION(703),
+
+ @UiEvent(doc = "User acted on a Slice item with a slider.")
+ LAUNCHER_SLICE_SLIDER_ACTION(704),
+
+ @UiEvent(doc = "User tapped on the entire row of a Slice.")
+ LAUNCHER_SLICE_CONTENT_ACTION(705),
+
+ @UiEvent(doc = "User tapped on the see more button of a Slice.")
+ LAUNCHER_SLICE_SEE_MORE_ACTION(706),
+
+ @UiEvent(doc = "User selected from a selection row of Slice.")
+ LAUNCHER_SLICE_SELECTION_ACTION(707),
+
+ @UiEvent(doc = "IME is used for selecting the focused item on the AllApps screen.")
+ LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME(718),
+
+ @UiEvent(doc = "User long-pressed on an AllApps item.")
+ LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED(719),
+
+ @UiEvent(doc = "Launcher entered into AllApps state with device search enabled.")
+ LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH(720),
+
+ @UiEvent(doc = "User switched to AllApps Main/Personal tab by tapping on it.")
+ LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB(721),
+
+ @UiEvent(doc = "User switched to AllApps Work tab by tapping on it.")
+ LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB(722),
+
+ @UiEvent(doc = "All apps vertical fling started.")
+ LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN(724),
+
+ @UiEvent(doc = "All apps vertical fling ended.")
+ LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END(725),
+
+ @UiEvent(doc = "Show URL indicator for Overview Sharing")
+ LAUNCHER_OVERVIEW_SHARING_SHOW_URL_INDICATOR(764),
+
+ @UiEvent(doc = "Show image indicator for Overview Sharing")
+ LAUNCHER_OVERVIEW_SHARING_SHOW_IMAGE_INDICATOR(765),
+
+ @UiEvent(doc = "User taps URL indicator in Overview")
+ LAUNCHER_OVERVIEW_SHARING_URL_INDICATOR_TAP(766),
+
+ @UiEvent(doc = "User taps image indicator in Overview")
+ LAUNCHER_OVERVIEW_SHARING_IMAGE_INDICATOR_TAP(767),
+
+ @UiEvent(doc = "User long presses an image in Overview")
+ LAUNCHER_OVERVIEW_SHARING_IMAGE_LONG_PRESS(768),
+
+ @UiEvent(doc = "User drags a URL in Overview")
+ LAUNCHER_OVERVIEW_SHARING_URL_DRAG(769),
+
+ @UiEvent(doc = "User drags an image in Overview")
+ LAUNCHER_OVERVIEW_SHARING_IMAGE_DRAG(770),
+
+ @UiEvent(doc = "User drops URL to a direct share target")
+ LAUNCHER_OVERVIEW_SHARING_DROP_URL_TO_TARGET(771),
+
+ @UiEvent(doc = "User drops an image to a direct share target")
+ LAUNCHER_OVERVIEW_SHARING_DROP_IMAGE_TO_TARGET(772),
+
+ @UiEvent(doc = "User drops URL to the More button")
+ LAUNCHER_OVERVIEW_SHARING_DROP_URL_TO_MORE(773),
+
+ @UiEvent(doc = "User drops an image to the More button")
+ LAUNCHER_OVERVIEW_SHARING_DROP_IMAGE_TO_MORE(774),
+
+ @UiEvent(doc = "User taps a share target to share URL")
+ LAUNCHER_OVERVIEW_SHARING_TAP_TARGET_TO_SHARE_URL(775),
+
+ @UiEvent(doc = "User taps a share target to share an image")
+ LAUNCHER_OVERVIEW_SHARING_TAP_TARGET_TO_SHARE_IMAGE(776),
+
+ @UiEvent(doc = "User taps the More button to share URL")
+ LAUNCHER_OVERVIEW_SHARING_TAP_MORE_TO_SHARE_URL(777),
+
+ @UiEvent(doc = "User taps the More button to share an image")
+ LAUNCHER_OVERVIEW_SHARING_TAP_MORE_TO_SHARE_IMAGE(778),
+
+ @UiEvent(doc = "User started resizing a widget on their home screen.")
+ LAUNCHER_WIDGET_RESIZE_STARTED(820),
+
+ @UiEvent(doc = "User finished resizing a widget on their home screen.")
+ LAUNCHER_WIDGET_RESIZE_COMPLETED(824),
+
+ @UiEvent(doc = "User reconfigured a widget on their home screen.")
+ LAUNCHER_WIDGET_RECONFIGURED(821),
+
+ @UiEvent(doc = "User enabled themed icons option in wallpaper & style settings.")
+ LAUNCHER_THEMED_ICON_ENABLED(836),
+
+ @UiEvent(doc = "User disabled themed icons option in wallpaper & style settings.")
+ LAUNCHER_THEMED_ICON_DISABLED(837),
+
+ @UiEvent(doc = "User tapped on 'Turn on work apps' button in all apps window.")
+ LAUNCHER_TURN_ON_WORK_APPS_TAP(838),
+
+ @UiEvent(doc = "User tapped on 'Turn off work apps' button in all apps window.")
+ LAUNCHER_TURN_OFF_WORK_APPS_TAP(839)
+ ;
// ADD MORE
@@ -380,45 +594,63 @@
}
/**
+ * Sets logging fields from provided {@link SliceItem}.
+ */
+ default StatsLogger withSliceItem(SliceItem sliceItem) {
+ return this;
+ }
+
+ /**
+ * Sets logging fields from provided {@link LauncherAtom.Slice}.
+ */
+ default StatsLogger withSlice(LauncherAtom.Slice slice) {
+ return this;
+ }
+
+ /**
* Builds the final message and logs it as {@link EventEnum}.
*/
default void log(EventEnum event) {
}
+
+ /**
+ * Builds the final message and logs it to two different atoms, one for
+ * event tracking and the other for jank tracking.
+ */
+ default void sendToInteractionJankMonitor(EventEnum event, View v) {
+ }
}
/**
* Returns new logger object.
*/
public StatsLogger logger() {
+ StatsLogger logger = createLogger();
+ if (mInstanceId != null) {
+ return logger.withInstanceId(mInstanceId);
+ }
+ return logger;
+ }
+
+ protected StatsLogger createLogger() {
return new StatsLogger() {
};
}
/**
+ * Sets InstanceId to every new {@link StatsLogger} object returned by {@link #logger()} when
+ * not-null.
+ */
+ public StatsLogManager withDefaultInstanceId(@Nullable InstanceId instanceId) {
+ this.mInstanceId = instanceId;
+ return this;
+ }
+
+ /**
* Creates a new instance of {@link StatsLogManager} based on provided context.
*/
public static StatsLogManager newInstance(Context context) {
- StatsLogManager mgr = Overrides.getObject(StatsLogManager.class,
+ return Overrides.getObject(StatsLogManager.class,
context.getApplicationContext(), R.string.stats_log_manager_class);
- return mgr;
- }
-
- /**
- * Log an event with ranked-choice information along with package. Does nothing if event.getId()
- * <= 0.
- *
- * @param rankingEvent an enum implementing EventEnum interface.
- * @param instanceId An identifier obtained from an InstanceIdSequence.
- * @param packageName the package name of the relevant app, if known (null otherwise).
- * @param position the position picked.
- */
- public void log(EventEnum rankingEvent, InstanceId instanceId, @Nullable String packageName,
- int position) {
- }
-
- /**
- * Logs snapshot, or impression of the current workspace.
- */
- public void logSnapshot() {
}
}
diff --git a/src/com/android/launcher3/logging/StatsLogUtils.java b/src/com/android/launcher3/logging/StatsLogUtils.java
deleted file mode 100644
index a5cc7ea..0000000
--- a/src/com/android/launcher3/logging/StatsLogUtils.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.android.launcher3.logging;
-
-import android.view.View;
-import android.view.ViewParent;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-
-import java.util.ArrayList;
-
-public class StatsLogUtils {
- private final static int MAXIMUM_VIEW_HIERARCHY_LEVEL = 5;
-
- /**
- * Implemented by containers to provide a container source for a given child.
- */
- public interface LogContainerProvider {
-
- /**
- * Populates parent container targets for an item
- */
- void fillInLogContainerData(ItemInfo childInfo, Target child, ArrayList<Target> parents);
- }
-
- /**
- * Recursively finds the parent of the given child which implements IconLogInfoProvider
- */
- public static LogContainerProvider getLaunchProviderRecursive(@Nullable View v) {
- ViewParent parent;
- if (v != null) {
- parent = v.getParent();
- } else {
- return null;
- }
-
- // Optimization to only check up to 5 parents.
- int count = MAXIMUM_VIEW_HIERARCHY_LEVEL;
- while (parent != null && count-- > 0) {
- if (parent instanceof LogContainerProvider) {
- return (LogContainerProvider) parent;
- } else {
- parent = parent.getParent();
- }
- }
- return null;
- }
-}
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
deleted file mode 100644
index e094cab..0000000
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ /dev/null
@@ -1,494 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.logging;
-
-import static com.android.launcher3.logging.LoggerUtils.newAction;
-import static com.android.launcher3.logging.LoggerUtils.newCommandAction;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.logging.LoggerUtils.newControlTarget;
-import static com.android.launcher3.logging.LoggerUtils.newDropTarget;
-import static com.android.launcher3.logging.LoggerUtils.newItemTarget;
-import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
-import static com.android.launcher3.logging.LoggerUtils.newTarget;
-import static com.android.launcher3.logging.LoggerUtils.newTouchAction;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.TipType;
-
-import static java.util.Optional.ofNullable;
-
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Process;
-import android.os.SystemClock;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DropTarget;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.LauncherLogProto;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.InstantAppResolver;
-import com.android.launcher3.util.LogConfig;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
-import com.google.protobuf.nano.MessageNano;
-
-import java.util.ArrayList;
-import java.util.UUID;
-
-/**
- * Manages the creation of {@link LauncherEvent}.
- * To debug this class, execute following command before side loading a new apk.
- * <p>
- * $ adb shell setprop log.tag.UserEvent VERBOSE
- */
-public class UserEventDispatcher implements ResourceBasedOverride {
-
- private static final String TAG = "UserEvent";
- private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.USEREVENT);
- private static final String UUID_STORAGE = "uuid";
-
- /**
- * A factory method for UserEventDispatcher
- */
- public static UserEventDispatcher newInstance(Context context) {
- SharedPreferences sharedPrefs = Utilities.getDevicePrefs(context);
- String uuidStr = sharedPrefs.getString(UUID_STORAGE, null);
- if (uuidStr == null) {
- uuidStr = UUID.randomUUID().toString();
- sharedPrefs.edit().putString(UUID_STORAGE, uuidStr).apply();
- }
- UserEventDispatcher ued = Overrides.getObject(UserEventDispatcher.class,
- context.getApplicationContext(), R.string.user_event_dispatcher_class);
- ued.mUuidStr = uuidStr;
- ued.mInstantAppResolver = InstantAppResolver.newInstance(context);
- return ued;
- }
-
-
- /**
- * Fills in the container data on the given event if the given view is not null.
- *
- * @return whether container data was added.
- */
- public boolean fillLogContainer(@Nullable View v, Target child,
- @Nullable ArrayList<Target> targets) {
- LogContainerProvider firstParent = StatsLogUtils.getLaunchProviderRecursive(v);
- if (v == null || !(v.getTag() instanceof ItemInfo) || firstParent == null) {
- return false;
- }
- final ItemInfo itemInfo = (ItemInfo) v.getTag();
- firstParent.fillInLogContainerData(itemInfo, child, targets);
- return true;
- }
-
- protected void onFillInLogContainerData(@NonNull ItemInfo itemInfo, @NonNull Target target,
- @NonNull ArrayList<Target> targets) {
- }
-
- private boolean mSessionStarted;
- private long mElapsedContainerMillis;
- private long mElapsedSessionMillis;
- private long mActionDurationMillis;
- private String mUuidStr;
- protected InstantAppResolver mInstantAppResolver;
- private boolean mAppOrTaskLaunch;
- private boolean mPreviousHomeGesture;
-
- // APP_ICON SHORTCUT WIDGET
- // --------------------------------------------------------------
- // packageNameHash required optional required
- // componentNameHash required required
- // intentHash required
- // --------------------------------------------------------------
-
- @Deprecated
- public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) {
- Target itemTarget = newItemTarget(v, mInstantAppResolver);
- Action action = newTouchAction(Action.Touch.TAP);
- ArrayList<Target> targets = makeTargetsList(itemTarget);
- if (fillLogContainer(v, itemTarget, targets)) {
- onFillInLogContainerData((ItemInfo) v.getTag(), itemTarget, targets);
- fillIntentInfo(itemTarget, intent, userHandle);
- }
- LauncherEvent event = newLauncherEvent(action, targets);
- dispatchUserEvent(event, intent);
- mAppOrTaskLaunch = true;
- }
-
- /**
- * Dummy method.
- */
- public void logActionTip(int actionType, int viewType) {
- }
-
- @Deprecated
- public void logTaskLaunchOrDismiss(int action, int direction, int taskIndex,
- ComponentKey componentKey) {
- LauncherEvent event = newLauncherEvent(newTouchAction(action), // TAP or SWIPE or FLING
- newTarget(Target.Type.ITEM));
- if (action == Action.Touch.SWIPE || action == Action.Touch.FLING) {
- // Direction DOWN means the task was launched, UP means it was dismissed.
- event.action.dir = direction;
- }
- event.srcTarget[0].itemType = ItemType.TASK;
- event.srcTarget[0].pageIndex = taskIndex;
- fillComponentInfo(event.srcTarget[0], componentKey.componentName);
- dispatchUserEvent(event, null);
- mAppOrTaskLaunch = true;
- }
-
- protected void fillIntentInfo(Target target, Intent intent, @Nullable UserHandle userHandle) {
- target.intentHash = intent.hashCode();
- target.isWorkApp = userHandle != null && !userHandle.equals(Process.myUserHandle());
- fillComponentInfo(target, intent.getComponent());
- }
-
- private void fillComponentInfo(Target target, ComponentName cn) {
- if (cn != null) {
- target.packageNameHash = (mUuidStr + cn.getPackageName()).hashCode();
- target.componentHash = (mUuidStr + cn.flattenToString()).hashCode();
- }
- }
-
- public void logNotificationLaunch(View v, PendingIntent intent) {
- LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP),
- newItemTarget(v, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
- Target itemTarget = newItemTarget(v, mInstantAppResolver);
- ArrayList<Target> targets = makeTargetsList(itemTarget);
-
- if (fillLogContainer(v, itemTarget, targets)) {
- itemTarget.packageNameHash = (mUuidStr + intent.getCreatorPackage()).hashCode();
- }
- dispatchUserEvent(event, null);
- }
-
- public void logActionCommand(int command, Target srcTarget) {
- logActionCommand(command, srcTarget, null);
- }
-
- public void logActionCommand(int command, int srcContainerType, int dstContainerType) {
- logActionCommand(command, newContainerTarget(srcContainerType),
- dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null);
- }
-
- public void logActionCommand(int command, int srcContainerType, int dstContainerType,
- int pageIndex) {
- Target srcTarget = newContainerTarget(srcContainerType);
- srcTarget.pageIndex = pageIndex;
- logActionCommand(command, srcTarget,
- dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null);
- }
-
- public void logActionCommand(int command, Target srcTarget, Target dstTarget) {
- LauncherEvent event = newLauncherEvent(newCommandAction(command), srcTarget);
- if (command == Action.Command.STOP) {
- if (mAppOrTaskLaunch || !mSessionStarted) {
- mSessionStarted = false;
- return;
- }
- }
-
- if (dstTarget != null) {
- event.destTarget = new Target[1];
- event.destTarget[0] = dstTarget;
- event.action.isStateChange = true;
- }
- dispatchUserEvent(event, null);
- }
-
- /**
- * TODO: Make this function work when a container view is passed as the 2nd param.
- */
- public void logActionCommand(int command, View itemView, int srcContainerType) {
- LauncherEvent event = newLauncherEvent(newCommandAction(command),
- newItemTarget(itemView, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
-
- Target itemTarget = newItemTarget(itemView, mInstantAppResolver);
- ArrayList<Target> targets = makeTargetsList(itemTarget);
-
- if (fillLogContainer(itemView, itemTarget, targets)) {
- // TODO: Remove the following two lines once fillInLogContainerData can take in a
- // container view.
- itemTarget.type = Target.Type.CONTAINER;
- itemTarget.containerType = srcContainerType;
- }
- dispatchUserEvent(event, null);
- }
-
- public void logActionOnControl(int action, int controlType) {
- logActionOnControl(action, controlType, null);
- }
-
- public void logActionOnControl(int action, int controlType, int parentContainerType) {
- logActionOnControl(action, controlType, null, parentContainerType);
- }
-
- /**
- * Logs control action with proper parent hierarchy
- */
- public void logActionOnControl(int actionType, int controlType,
- @Nullable View controlInContainer, int... parentTypes) {
- Target control = newTarget(Target.Type.CONTROL);
- control.controlType = controlType;
- Action action = newAction(actionType);
-
- ArrayList<Target> targets = makeTargetsList(control);
- if (controlInContainer != null) {
- fillLogContainer(controlInContainer, control, targets);
- }
- for (int parentContainerType : parentTypes) {
- if (parentContainerType < 0) continue;
- targets.add(newContainerTarget(parentContainerType));
- }
- LauncherEvent event = newLauncherEvent(action, targets);
- if (actionType == Action.Touch.DRAGDROP) {
- event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis;
- }
- dispatchUserEvent(event, null);
- }
-
- public void logActionTapOutside(Target target) {
- LauncherEvent event = newLauncherEvent(newTouchAction(Action.Type.TOUCH),
- target);
- event.action.isOutside = true;
- dispatchUserEvent(event, null);
- }
-
- public void logActionBounceTip(int containerType) {
- LauncherEvent event = newLauncherEvent(newAction(Action.Type.TIP),
- newContainerTarget(containerType));
- event.srcTarget[0].tipType = TipType.BOUNCE;
- dispatchUserEvent(event, null);
- }
-
- public void logActionOnContainer(int action, int dir, int containerType) {
- logActionOnContainer(action, dir, containerType, 0);
- }
-
- public void logActionOnContainer(int action, int dir, int containerType, int pageIndex) {
- LauncherEvent event = newLauncherEvent(newTouchAction(action),
- newContainerTarget(containerType));
- event.action.dir = dir;
- event.srcTarget[0].pageIndex = pageIndex;
- dispatchUserEvent(event, null);
- }
-
- /**
- * Used primarily for swipe up and down when state changes when swipe up happens from the
- * navbar bezel, the {@param srcChildContainerType} is NAVBAR and
- * {@param srcParentContainerType} is either one of the two
- * (1) WORKSPACE: if the launcher is the foreground activity
- * (2) APP: if another app was the foreground activity
- */
- public void logStateChangeAction(int action, int dir, int downX, int downY,
- int srcChildTargetType, int srcParentContainerType, int dstContainerType,
- int pageIndex) {
- LauncherEvent event;
- if (srcChildTargetType == ItemType.TASK) {
- event = newLauncherEvent(newTouchAction(action),
- newItemTarget(srcChildTargetType),
- newContainerTarget(srcParentContainerType));
- } else {
- event = newLauncherEvent(newTouchAction(action),
- newContainerTarget(srcChildTargetType),
- newContainerTarget(srcParentContainerType));
- }
- event.destTarget = new Target[1];
- event.destTarget[0] = newContainerTarget(dstContainerType);
- event.action.dir = dir;
- event.action.isStateChange = true;
- event.srcTarget[0].pageIndex = pageIndex;
- event.srcTarget[0].spanX = downX;
- event.srcTarget[0].spanY = downY;
- dispatchUserEvent(event, null);
- resetElapsedContainerMillis("state changed");
- }
-
- public void logActionOnItem(int action, int dir, int itemType) {
- logActionOnItem(action, dir, itemType, null, null);
- }
-
- /**
- * Creates new {@link LauncherEvent} of ITEM target type with input arguments and dispatches it.
- *
- * @param touchAction ENUM value of {@link LauncherLogProto.Action.Touch} Action
- * @param dir ENUM value of {@link LauncherLogProto.Action.Direction} Action
- * @param itemType ENUM value of {@link LauncherLogProto.ItemType}
- * @param gridX Nullable X coordinate of item's position on the workspace grid
- * @param gridY Nullable Y coordinate of item's position on the workspace grid
- */
- public void logActionOnItem(int touchAction, int dir, int itemType,
- @Nullable Integer gridX, @Nullable Integer gridY) {
- Target itemTarget = newTarget(Target.Type.ITEM);
- itemTarget.itemType = itemType;
- ofNullable(gridX).ifPresent(value -> itemTarget.gridX = value);
- ofNullable(gridY).ifPresent(value -> itemTarget.gridY = value);
- LauncherEvent event = newLauncherEvent(newTouchAction(touchAction), itemTarget);
- event.action.dir = dir;
- dispatchUserEvent(event, null);
- }
-
- /**
- * Logs proto lite version of LauncherEvent object to clearcut.
- */
- public void logLauncherEvent(
- com.android.launcher3.userevent.LauncherLogProto.LauncherEvent launcherEvent) {
-
- if (mPreviousHomeGesture) {
- mPreviousHomeGesture = false;
- }
- mAppOrTaskLaunch = false;
- launcherEvent.toBuilder()
- .setElapsedContainerMillis(SystemClock.uptimeMillis() - mElapsedContainerMillis)
- .setElapsedSessionMillis(
- SystemClock.uptimeMillis() - mElapsedSessionMillis).build();
- try {
- dispatchUserEvent(LauncherEvent.parseFrom(launcherEvent.toByteArray()), null);
- } catch (InvalidProtocolBufferNanoException e) {
- throw new RuntimeException("Cannot convert LauncherEvent from Lite to Nano version.");
- }
- }
-
- public void logDeepShortcutsOpen(View icon) {
- ItemInfo info = (ItemInfo) icon.getTag();
- Target child = newItemTarget(info, mInstantAppResolver);
- ArrayList<Target> targets = makeTargetsList(child);
- fillLogContainer(icon, child, targets);
- dispatchUserEvent(newLauncherEvent(newTouchAction(Action.Touch.TAP), targets), null);
- resetElapsedContainerMillis("deep shortcut open");
- }
-
- public void logDragNDrop(DropTarget.DragObject dragObj, View dropTargetAsView) {
- Target srcChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver);
- ArrayList<Target> srcTargets = makeTargetsList(srcChild);
-
-
- Target destChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver);
- ArrayList<Target> destTargets = makeTargetsList(destChild);
-
- dragObj.dragSource.fillInLogContainerData(dragObj.originalDragInfo, srcChild, srcTargets);
- if (dropTargetAsView instanceof LogContainerProvider) {
- ((LogContainerProvider) dropTargetAsView).fillInLogContainerData(dragObj.dragInfo,
- destChild, destTargets);
- }
- else {
- destTargets.add(newDropTarget(dropTargetAsView));
- }
- LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP), srcTargets);
- Target[] destTargetsArray = new Target[destTargets.size()];
- destTargets.toArray(destTargetsArray);
- event.destTarget = destTargetsArray;
-
- event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis;
- dispatchUserEvent(event, null);
- }
-
- public void logActionBack(boolean completed, int downX, int downY, boolean isButton,
- boolean gestureSwipeLeft, int containerType) {
- int actionTouch = isButton ? Action.Touch.TAP : Action.Touch.SWIPE;
- Action action = newCommandAction(actionTouch);
- action.command = Action.Command.BACK;
- action.dir = isButton ? Action.Direction.NONE :
- gestureSwipeLeft ? Action.Direction.LEFT : Action.Direction.RIGHT;
- Target target = newControlTarget(isButton ? ControlType.BACK_BUTTON :
- ControlType.BACK_GESTURE);
- target.spanX = downX;
- target.spanY = downY;
- target.cardinality = completed ? 1 : 0;
- LauncherEvent event = newLauncherEvent(action, target, newContainerTarget(containerType));
-
- dispatchUserEvent(event, null);
- }
-
- /**
- * Currently logs following containers: workspace, allapps, widget tray.
- */
- public final void resetElapsedContainerMillis(String reason) {
- mElapsedContainerMillis = SystemClock.uptimeMillis();
- if (!IS_VERBOSE) {
- return;
- }
- Log.d(TAG, "resetElapsedContainerMillis reason=" + reason);
-
- }
-
- public final void startSession() {
- mSessionStarted = true;
- mElapsedSessionMillis = SystemClock.uptimeMillis();
- mElapsedContainerMillis = SystemClock.uptimeMillis();
- }
-
- public final void setPreviousHomeGesture(boolean homeGesture) {
- mPreviousHomeGesture = homeGesture;
- }
-
- public final boolean isPreviousHomeGesture() {
- return mPreviousHomeGesture;
- }
-
- public final void resetActionDurationMillis() {
- mActionDurationMillis = SystemClock.uptimeMillis();
- }
-
- public void dispatchUserEvent(LauncherEvent ev, Intent intent) {
- if (mPreviousHomeGesture) {
- mPreviousHomeGesture = false;
- }
- mAppOrTaskLaunch = false;
- ev.elapsedContainerMillis = SystemClock.uptimeMillis() - mElapsedContainerMillis;
- ev.elapsedSessionMillis = SystemClock.uptimeMillis() - mElapsedSessionMillis;
- if (!IS_VERBOSE) {
- return;
- }
- LauncherLogProto.LauncherEvent liteLauncherEvent;
- try {
- liteLauncherEvent =
- LauncherLogProto.LauncherEvent.parseFrom(MessageNano.toByteArray(ev));
- } catch (InvalidProtocolBufferException e) {
- throw new RuntimeException("Cannot parse LauncherEvent from Nano to Lite version");
- }
- Log.d(TAG, liteLauncherEvent.toString());
- }
-
- /**
- * Constructs an ArrayList with targets
- */
- public static ArrayList<Target> makeTargetsList(Target... targets) {
- ArrayList<Target> result = new ArrayList<>();
- for (Target target : targets) {
- result.add(target);
- }
- return result;
- }
-}
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index c236fa6..01b3e6e 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -27,6 +27,7 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
@@ -34,6 +35,7 @@
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.PackageManagerHelper;
@@ -46,6 +48,8 @@
*/
public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
+ private static final String LOG = "AddWorkspaceItemsTask";
+
private final List<Pair<ItemInfo, Object>> mItemList;
/**
@@ -122,6 +126,13 @@
}
SessionInfo sessionInfo = packageInstaller.getActiveSessionInfo(item.user,
packageName);
+
+ if (!packageInstaller.verifySessionInfo(sessionInfo)) {
+ FileLog.d(LOG, "Item info failed session info verification. "
+ + "Skipping : " + workspaceInfo);
+ continue;
+ }
+
List<LauncherActivityInfo> activities = launcherApps
.getActivityList(packageName, item.user);
boolean hasActivity = activities != null && !activities.isEmpty();
@@ -132,7 +143,9 @@
continue;
}
} else {
- workspaceInfo.setInstallProgress((int) sessionInfo.getProgress());
+ workspaceInfo.setProgressLevel(
+ (int) (sessionInfo.getProgress() * 100),
+ PackageInstallInfo.STATUS_INSTALLING);
}
if (hasActivity) {
@@ -164,6 +177,9 @@
// Save the WorkspaceItemInfo for binding in the workspace
addedItemsFinal.add(itemInfo);
+
+ // log bitmap and label
+ FileLog.d(LOG, "Adding item info to workspace: " + itemInfo);
}
}
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index eb5d106..92b5885 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -21,11 +21,10 @@
import android.content.ComponentName;
import android.content.Context;
-import android.content.pm.ApplicationInfo;
+import android.content.Intent;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.os.LocaleList;
-import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
@@ -37,7 +36,6 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.ItemInfoMatcher;
@@ -138,7 +136,7 @@
if (findAppInfo(info.componentName, info.user) != null) {
return;
}
- mIconCache.getTitleAndIcon(info, activityInfo, true /* useLowResIcon */);
+ mIconCache.getTitleAndIcon(info, activityInfo, false /* useLowResIcon */);
info.sectionName = mIndex.computeSectionName(info.title);
data.add(info);
@@ -146,11 +144,10 @@
}
public void addPromiseApp(Context context, PackageInstallInfo installInfo) {
- ApplicationInfo applicationInfo = new PackageManagerHelper(context)
- .getApplicationInfo(installInfo.packageName, installInfo.user, 0);
// only if not yet installed
- if (applicationInfo == null) {
- PromiseAppInfo info = new PromiseAppInfo(installInfo);
+ if (!new PackageManagerHelper(context)
+ .isAppInstalled(installInfo.packageName, installInfo.user)) {
+ AppInfo info = new AppInfo(installInfo);
mIconCache.getTitleAndIcon(info, info.usingLowResIcon());
info.sectionName = mIndex.computeSectionName(info.title);
@@ -159,24 +156,31 @@
}
}
- public PromiseAppInfo updatePromiseInstallInfo(PackageInstallInfo installInfo) {
- UserHandle user = Process.myUserHandle();
- for (int i=0; i < data.size(); i++) {
+ /** Updates the given PackageInstallInfo's associated AppInfo's installation info. */
+ public List<AppInfo> updatePromiseInstallInfo(PackageInstallInfo installInfo) {
+ List<AppInfo> updatedAppInfos = new ArrayList<>();
+ UserHandle user = installInfo.user;
+ for (int i = data.size() - 1; i >= 0; i--) {
final AppInfo appInfo = data.get(i);
final ComponentName tgtComp = appInfo.getTargetComponent();
if (tgtComp != null && tgtComp.getPackageName().equals(installInfo.packageName)
- && appInfo.user.equals(user)
- && appInfo instanceof PromiseAppInfo) {
- final PromiseAppInfo promiseAppInfo = (PromiseAppInfo) appInfo;
- if (installInfo.state == PackageInstallInfo.STATUS_INSTALLING) {
- promiseAppInfo.level = installInfo.progress;
- return promiseAppInfo;
- } else if (installInfo.state == PackageInstallInfo.STATUS_FAILED) {
+ && appInfo.user.equals(user)) {
+ if (installInfo.state == PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING
+ || installInfo.state == PackageInstallInfo.STATUS_INSTALLING) {
+ if (appInfo.isAppStartable()
+ && installInfo.state == PackageInstallInfo.STATUS_INSTALLING) {
+ continue;
+ }
+ appInfo.setProgressLevel(installInfo);
+
+ updatedAppInfos.add(appInfo);
+ } else if (installInfo.state == PackageInstallInfo.STATUS_FAILED
+ && !appInfo.isAppStartable()) {
removeApp(i);
}
}
}
- return null;
+ return updatedAppInfos;
}
private void removeApp(int index) {
@@ -197,11 +201,16 @@
/**
* Add the icons for the supplied apk called packageName.
*/
- public void addPackage(Context context, String packageName, UserHandle user) {
- for (LauncherActivityInfo info : context.getSystemService(LauncherApps.class)
- .getActivityList(packageName, user)) {
+ public List<LauncherActivityInfo> addPackage(
+ Context context, String packageName, UserHandle user) {
+ List<LauncherActivityInfo> activities = context.getSystemService(LauncherApps.class)
+ .getActivityList(packageName, user);
+
+ for (LauncherActivityInfo info : activities) {
add(new AppInfo(context, info, user), info);
}
+
+ return activities;
}
/**
@@ -244,7 +253,8 @@
/**
* Add and remove icons for this package which has been updated.
*/
- public void updatePackage(Context context, String packageName, UserHandle user) {
+ public List<LauncherActivityInfo> updatePackage(
+ Context context, String packageName, UserHandle user) {
final List<LauncherActivityInfo> matches = context.getSystemService(LauncherApps.class)
.getActivityList(packageName, user);
if (matches.size() > 0) {
@@ -268,8 +278,14 @@
if (applicationInfo == null) {
add(new AppInfo(context, info, user), info);
} else {
- mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */);
+ Intent launchIntent = AppInfo.makeLaunchIntent(info);
+
+ mIconCache.getTitleAndIcon(applicationInfo, info, false /* useLowResIcon */);
applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title);
+ applicationInfo.setProgressLevel(
+ PackageManagerHelper.getLoadingProgress(info),
+ PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
+ applicationInfo.intent = launchIntent;
mDataChanged = true;
}
@@ -285,6 +301,8 @@
}
}
}
+
+ return matches;
}
/**
@@ -305,7 +323,7 @@
*
* @return the corresponding AppInfo or null
*/
- private @Nullable AppInfo findAppInfo(@NonNull ComponentName componentName,
+ public @Nullable AppInfo findAppInfo(@NonNull ComponentName componentName,
@NonNull UserHandle user) {
for (AppInfo info: data) {
if (componentName.equals(info.componentName) && user.equals(info.user)) {
diff --git a/src/com/android/launcher3/model/AppLaunchTracker.java b/src/com/android/launcher3/model/AppLaunchTracker.java
deleted file mode 100644
index 629a0ee..0000000
--- a/src/com/android/launcher3/model/AppLaunchTracker.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.model;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import android.content.ComponentName;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Callback for receiving various app launch events
- */
-public class AppLaunchTracker implements ResourceBasedOverride {
-
- /**
- * Derived from LauncherEvent proto.
- * TODO: Use proper descriptive constants
- */
- public static final String CONTAINER_DEFAULT = Integer.toString(ContainerType.WORKSPACE);
- public static final String CONTAINER_ALL_APPS = Integer.toString(ContainerType.ALLAPPS);
- public static final String CONTAINER_PREDICTIONS = Integer.toString(ContainerType.PREDICTION);
- public static final String CONTAINER_SEARCH = Integer.toString(ContainerType.SEARCHRESULT);
- public static final String CONTAINER_OVERVIEW = Integer.toString(ContainerType.OVERVIEW);
-
-
- public static final MainThreadInitializedObject<AppLaunchTracker> INSTANCE =
- forOverride(AppLaunchTracker.class, R.string.app_launch_tracker_class);
-
- public void onStartShortcut(String packageName, String shortcutId, UserHandle user,
- @Nullable String container) { }
-
- public void onStartApp(ComponentName componentName, UserHandle user,
- @Nullable String container) { }
-
- public void onDismissApp(ComponentName componentName, UserHandle user,
- @Nullable String container){}
-
- public void onReturnedToHome() { }
-}
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 8b0ef7b..5c85bab 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -17,7 +17,6 @@
package com.android.launcher3.model;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
-import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
import android.util.Log;
@@ -27,6 +26,7 @@
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.PagedView;
import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -76,18 +76,20 @@
ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
final IntArray orderedScreenIds = new IntArray();
+ ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
synchronized (mBgDataModel) {
workspaceItems.addAll(mBgDataModel.workspaceItems);
appWidgets.addAll(mBgDataModel.appWidgets);
orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
+ mBgDataModel.extraItems.forEach(extraItems::add);
mBgDataModel.lastBindId++;
mMyBindingId = mBgDataModel.lastBindId;
}
for (Callbacks cb : mCallbacksList) {
new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
- workspaceItems, appWidgets, orderedScreenIds).bind();
+ workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
}
}
@@ -135,7 +137,7 @@
private final ArrayList<ItemInfo> mWorkspaceItems;
private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
private final IntArray mOrderedScreenIds;
-
+ private final ArrayList<FixedContainerItems> mExtraItems;
WorkspaceBinder(Callbacks callbacks,
Executor uiExecutor,
@@ -144,6 +146,7 @@
int myBindingId,
ArrayList<ItemInfo> workspaceItems,
ArrayList<LauncherAppWidgetInfo> appWidgets,
+ ArrayList<FixedContainerItems> extraItems,
IntArray orderedScreenIds) {
mCallbacks = callbacks;
mUiExecutor = uiExecutor;
@@ -152,6 +155,7 @@
mMyBindingId = myBindingId;
mWorkspaceItems = workspaceItems;
mAppWidgets = appWidgets;
+ mExtraItems = extraItems;
mOrderedScreenIds = orderedScreenIds;
}
@@ -198,15 +202,15 @@
// Load items on the current page.
bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
bindAppWidgets(currentAppWidgets, mainExecutor);
+ mExtraItems.forEach(item ->
+ executeCallbacksTask(c -> c.bindExtraContainerItems(item), mainExecutor));
- // Locate available spots for prediction using currentWorkspaceItems
- IntArray gaps = getMissingHotseatRanks(currentWorkspaceItems, idp.numHotseatIcons);
- bindPredictedItems(gaps, mainExecutor);
// In case of validFirstPage, only bind the first screen, and defer binding the
// remaining screens after first onDraw (and an optional the fade animation whichever
// happens later).
// This ensures that the first screen is immediately visible (eg. during rotation)
// In case of !validFirstPage, bind all pages one after other.
+
final Executor deferredExecutor =
validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
@@ -253,11 +257,6 @@
}
}
- private void bindPredictedItems(IntArray ranks, final Executor executor) {
- ArrayList<AppInfo> items = new ArrayList<>(mBgDataModel.cachedPredictedItems);
- executeCallbacksTask(c -> c.bindPredictedItems(items, ranks), executor);
- }
-
protected void executeCallbacksTask(CallbackTask task, Executor executor) {
executor.execute(() -> {
if (mMyBindingId != mBgDataModel.lastBindId) {
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 9013cba..ad553d5 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -22,15 +22,20 @@
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
/**
* Extension of {@link ModelUpdateTask} with some utility methods
@@ -88,11 +93,27 @@
return mModel.getWriter(false /* hasVerticalHotseat */, false /* verifyChanges */);
}
-
- public void bindUpdatedWorkspaceItems(final ArrayList<WorkspaceItemInfo> updatedShortcuts) {
- if (!updatedShortcuts.isEmpty()) {
- scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(updatedShortcuts));
+ public void bindUpdatedWorkspaceItems(List<WorkspaceItemInfo> allUpdates) {
+ // Bind workspace items
+ List<WorkspaceItemInfo> workspaceUpdates = allUpdates.stream()
+ .filter(info -> info.id != ItemInfo.NO_ID)
+ .collect(Collectors.toList());
+ if (!workspaceUpdates.isEmpty()) {
+ scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(workspaceUpdates));
}
+
+ // Bind extra items if any
+ allUpdates.stream()
+ .mapToInt(info -> info.container)
+ .distinct()
+ .mapToObj(mDataModel.extraItems::get)
+ .filter(Objects::nonNull)
+ .forEach(this::bindExtraContainerItems);
+ }
+
+ public void bindExtraContainerItems(FixedContainerItems item) {
+ FixedContainerItems copy = item.clone();
+ scheduleCallbackTask(c -> c.bindExtraContainerItems(copy));
}
public void bindDeepShortcuts(BgDataModel dataModel) {
@@ -102,8 +123,8 @@
}
public void bindUpdatedWidgets(BgDataModel dataModel) {
- final ArrayList<WidgetListRowEntry> widgets =
- dataModel.widgetsModel.getWidgetsList(mApp.getContext());
+ final ArrayList<WidgetsListBaseEntry> widgets =
+ dataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
scheduleCallbackTask(c -> c.bindAllWidgets(widgets));
}
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 9bef847..1d7d1a2 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -15,48 +15,57 @@
*/
package com.android.launcher3.model;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY;
+
import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.os.UserHandle;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
-import android.util.MutableInt;
-import com.android.launcher3.InstallShortcutReceiver;
import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.ViewOnDrawExecutor;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.function.BiConsumer;
+import java.util.Set;
+import java.util.function.Consumer;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* All the data stored in-memory and managed by the LauncherModel
@@ -88,21 +97,9 @@
public final IntSparseArrayMap<FolderInfo> folders = new IntSparseArrayMap<>();
/**
- * Map of ShortcutKey to the number of times it is pinned.
+ * Extra container based items
*/
- public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
-
- /**
- * List of all cached predicted items visible on home screen
- */
- public final ArrayList<AppInfo> cachedPredictedItems = new ArrayList<>();
-
- /**
- * @see Callbacks#FLAG_HAS_SHORTCUT_PERMISSION
- * @see Callbacks#FLAG_QUIET_MODE_ENABLED
- * @see Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION
- */
- public int flags;
+ public final IntSparseArrayMap<FixedContainerItems> extraItems = new IntSparseArrayMap<>();
/**
* Maps all launcher activities to counts of their shortcuts.
@@ -127,8 +124,8 @@
appWidgets.clear();
folders.clear();
itemsIdMap.clear();
- pinnedShortcutCounts.clear();
deepShortcutMap.clear();
+ extraItems.clear();
}
/**
@@ -181,6 +178,7 @@
}
public synchronized void removeItem(Context context, Iterable<? extends ItemInfo> items) {
+ ArraySet<UserHandle> updatedDeepShortcuts = new ArraySet<>();
for (ItemInfo item : items) {
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
@@ -199,14 +197,7 @@
workspaceItems.remove(item);
break;
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
- // Decrement pinned shortcut count
- ShortcutKey pinnedShortcut = ShortcutKey.fromItemInfo(item);
- MutableInt count = pinnedShortcutCounts.get(pinnedShortcut);
- if ((count == null || --count.value == 0)
- && !InstallShortcutReceiver.getPendingShortcuts(context)
- .contains(pinnedShortcut)) {
- unpinShortcut(context, pinnedShortcut);
- }
+ updatedDeepShortcuts.add(item.user);
// Fall through.
}
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
@@ -220,6 +211,7 @@
}
itemsIdMap.remove(item.id);
}
+ updatedDeepShortcuts.forEach(user -> updateShortcutPinnedState(context, user));
}
public synchronized void addItem(Context context, ItemInfo item, boolean newItem) {
@@ -229,23 +221,7 @@
folders.put(item.id, (FolderInfo) item);
workspaceItems.add(item);
break;
- case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
- // Increment the count for the given shortcut
- ShortcutKey pinnedShortcut = ShortcutKey.fromItemInfo(item);
- MutableInt count = pinnedShortcutCounts.get(pinnedShortcut);
- if (count == null) {
- count = new MutableInt(1);
- pinnedShortcutCounts.put(pinnedShortcut, count);
- } else {
- count.value++;
- }
-
- // Since this is a new item, pin the shortcut in the system server.
- if (newItem && count.value == 1) {
- updatePinnedShortcuts(context, pinnedShortcut, List::add);
- }
- // Fall through
- }
+ case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
@@ -270,36 +246,86 @@
appWidgets.add((LauncherAppWidgetInfo) item);
break;
}
+ if (newItem && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ updateShortcutPinnedState(context, item.user);
+ }
}
/**
- * Removes the given shortcut from the current list of pinned shortcuts.
- * (Runs on background thread)
+ * Updates the deep shortucts state in system to match out internal model, pinning any missing
+ * shortcuts and unpinning any extra shortcuts.
*/
- public void unpinShortcut(Context context, ShortcutKey key) {
- updatePinnedShortcuts(context, key, List::remove);
+ public void updateShortcutPinnedState(Context context) {
+ for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
+ updateShortcutPinnedState(context, user);
+ }
}
- private void updatePinnedShortcuts(Context context, ShortcutKey key,
- BiConsumer<List<String>, String> idOp) {
+ /**
+ * Updates the deep shortucts state in system to match out internal model, pinning any missing
+ * shortcuts and unpinning any extra shortcuts.
+ */
+ public synchronized void updateShortcutPinnedState(Context context, UserHandle user) {
if (GO_DISABLE_WIDGETS) {
return;
}
- String packageName = key.componentName.getPackageName();
- String id = key.getId();
- UserHandle user = key.user;
- List<String> pinnedIds = new ShortcutRequest(context, user)
- .forPackage(packageName)
- .query(PINNED)
- .stream()
- .map(ShortcutInfo::getId)
- .collect(Collectors.toCollection(ArrayList::new));
- idOp.accept(pinnedIds, id);
- try {
- context.getSystemService(LauncherApps.class).pinShortcuts(packageName, pinnedIds, user);
- } catch (SecurityException | IllegalStateException e) {
- Log.w(TAG, "Failed to pin shortcut", e);
+
+ // Collect all system shortcuts
+ QueryResult result = new ShortcutRequest(context, user)
+ .query(PINNED | FLAG_GET_KEY_FIELDS_ONLY);
+ if (!result.wasSuccess()) {
+ return;
}
+ // Map of packageName to shortcutIds that are currently in the system
+ Map<String, Set<String>> systemMap = result.stream()
+ .collect(groupingBy(ShortcutInfo::getPackage,
+ mapping(ShortcutInfo::getId, Collectors.toSet())));
+
+ // Collect all model shortcuts
+ Stream.Builder<WorkspaceItemInfo> itemStream = Stream.builder();
+ forAllWorkspaceItemInfos(user, itemStream::accept);
+ // Map of packageName to shortcutIds that are currently in our model
+ Map<String, Set<String>> modelMap = Stream.concat(
+ // Model shortcuts
+ itemStream.build()
+ .filter(wi -> wi.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+ .map(ShortcutKey::fromItemInfo),
+ // Pending shortcuts
+ ItemInstallQueue.INSTANCE.get(context).getPendingShortcuts(user))
+ .collect(groupingBy(ShortcutKey::getPackageName,
+ mapping(ShortcutKey::getId, Collectors.toSet())));
+
+ // Check for diff
+ for (Map.Entry<String, Set<String>> entry : modelMap.entrySet()) {
+ Set<String> modelShortcuts = entry.getValue();
+ Set<String> systemShortcuts = systemMap.remove(entry.getKey());
+ if (systemShortcuts == null) {
+ systemShortcuts = Collections.emptySet();
+ }
+
+ // Do not use .equals as it can vary based on the type of set
+ if (systemShortcuts.size() != modelShortcuts.size()
+ || !systemShortcuts.containsAll(modelShortcuts)) {
+ // Update system state for this package
+ try {
+ context.getSystemService(LauncherApps.class).pinShortcuts(
+ entry.getKey(), new ArrayList<>(modelShortcuts), user);
+ } catch (SecurityException | IllegalStateException e) {
+ Log.w(TAG, "Failed to pin shortcut", e);
+ }
+ }
+ }
+
+ // If there are any extra pinned shortcuts, remove them
+ systemMap.keySet().forEach(packageName -> {
+ // Update system state
+ try {
+ context.getSystemService(LauncherApps.class).pinShortcuts(
+ packageName, Collections.emptyList(), user);
+ } catch (SecurityException | IllegalStateException e) {
+ Log.w(TAG, "Failed to unpin shortcut", e);
+ }
+ });
}
/**
@@ -348,6 +374,69 @@
}
}
+ /**
+ * Returns a list containing all workspace items including widgets.
+ */
+ public synchronized ArrayList<ItemInfo> getAllWorkspaceItems() {
+ ArrayList<ItemInfo> items = new ArrayList<>(workspaceItems.size() + appWidgets.size());
+ items.addAll(workspaceItems);
+ items.addAll(appWidgets);
+ return items;
+ }
+
+ /**
+ * Calls the provided {@code op} for all workspaceItems in the in-memory model (both persisted
+ * items and dynamic/predicted items for the provided {@code userHandle}.
+ * Note the call is not synchronized over the model, that should be handled by the called.
+ */
+ public void forAllWorkspaceItemInfos(UserHandle userHandle, Consumer<WorkspaceItemInfo> op) {
+ for (ItemInfo info : itemsIdMap) {
+ if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
+ op.accept((WorkspaceItemInfo) info);
+ }
+ }
+
+ for (int i = extraItems.size() - 1; i >= 0; i--) {
+ for (ItemInfo info : extraItems.valueAt(i).items) {
+ if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
+ op.accept((WorkspaceItemInfo) info);
+ }
+ }
+ }
+ }
+
+ /**
+ * An object containing items corresponding to a fixed container
+ */
+ public static class FixedContainerItems {
+
+ public final int containerId;
+ public final List<ItemInfo> items;
+
+ public FixedContainerItems(int containerId) {
+ this(containerId, new ArrayList<>());
+ }
+
+ public FixedContainerItems(int containerId, List<ItemInfo> items) {
+ this.containerId = containerId;
+ this.items = items;
+ }
+
+ @Override
+ public FixedContainerItems clone() {
+ return new FixedContainerItems(containerId, new ArrayList<>(items));
+ }
+
+ public void setItems(List<ItemInfo> newItems) {
+ items.clear();
+ newItems.forEach(item -> {
+ item.container = containerId;
+ items.add(item);
+ });
+ }
+ }
+
+
public interface Callbacks {
// If the launcher has permission to access deep shortcuts.
int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0;
@@ -369,21 +458,25 @@
void preAddApps();
void bindAppsAdded(IntArray newScreens,
ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated);
- void bindPromiseAppProgressUpdated(PromiseAppInfo app);
- void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated);
+
+ /**
+ * Binds updated incremental download progress
+ */
+ void bindIncrementalDownloadProgressUpdated(AppInfo app);
+ void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated);
void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
void bindRestoreItemsChange(HashSet<ItemInfo> updates);
void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
- void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets);
+ void bindAllWidgets(List<WidgetsListBaseEntry> widgets);
void onPageBoundSynchronously(int page);
void executeOnNextDraw(ViewOnDrawExecutor executor);
void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
- void bindAllApplications(AppInfo[] apps, int flags);
-
/**
- * Binds predicted appInfos at at available prediction slots.
+ * Binds extra item provided any external source
*/
- void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks);
+ default void bindExtraContainerItems(FixedContainerItems item) { }
+
+ void bindAllApplications(AppInfo[] apps, int flags);
}
}
diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
index 8e6b064..f644d49 100644
--- a/src/com/android/launcher3/model/CacheDataUpdatedTask.java
+++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
@@ -21,7 +21,6 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import java.util.ArrayList;
@@ -48,23 +47,18 @@
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
IconCache iconCache = app.getIconCache();
-
-
ArrayList<WorkspaceItemInfo> updatedShortcuts = new ArrayList<>();
synchronized (dataModel) {
- for (ItemInfo info : dataModel.itemsIdMap) {
- if (info instanceof WorkspaceItemInfo && mUser.equals(info.user)) {
- WorkspaceItemInfo si = (WorkspaceItemInfo) info;
- ComponentName cn = si.getTargetComponent();
- if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
- && isValidShortcut(si) && cn != null
- && mPackages.contains(cn.getPackageName())) {
- iconCache.getTitleAndIcon(si, si.usingLowResIcon());
- updatedShortcuts.add(si);
- }
+ dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+ ComponentName cn = si.getTargetComponent();
+ if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+ && isValidShortcut(si) && cn != null
+ && mPackages.contains(cn.getPackageName())) {
+ iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+ updatedShortcuts.add(si);
}
- }
+ });
apps.updateIconsAndLabels(mPackages, mUser);
}
bindUpdatedWorkspaceItems(updatedShortcuts);
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java
index 5112304..e391d37 100644
--- a/src/com/android/launcher3/model/FirstScreenBroadcast.java
+++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java
@@ -15,27 +15,34 @@
*/
package com.android.launcher3.model;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.os.Process.myUserHandle;
+import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle;
+
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller.SessionInfo;
+import android.os.UserHandle;
import android.util.Log;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.util.MultiHashMap;
import com.android.launcher3.util.PackageUserKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Helper class to send broadcasts to package installers that have:
@@ -61,26 +68,10 @@
private static final String VERIFICATION_TOKEN_EXTRA = "verificationToken";
- private final MultiHashMap<String, String> mPackagesForInstaller;
+ private final HashMap<PackageUserKey, SessionInfo> mSessionInfoForPackage;
public FirstScreenBroadcast(HashMap<PackageUserKey, SessionInfo> sessionInfoForPackage) {
- mPackagesForInstaller = getPackagesForInstaller(sessionInfoForPackage);
- }
-
- /**
- * @return Map where the key is the package name of the installer, and the value is a list
- * of packages with active sessions for that installer.
- */
- private MultiHashMap<String, String> getPackagesForInstaller(
- HashMap<PackageUserKey, SessionInfo> sessionInfoForPackage) {
- MultiHashMap<String, String> packagesForInstaller = new MultiHashMap<>();
- for (Map.Entry<PackageUserKey, SessionInfo> entry : sessionInfoForPackage.entrySet()) {
- if (myUserHandle().equals(entry.getKey().mUser)) {
- packagesForInstaller.addToList(entry.getValue().getInstallerPackageName(),
- entry.getKey().mPackageName);
- }
- }
- return packagesForInstaller;
+ mSessionInfoForPackage = sessionInfoForPackage;
}
/**
@@ -88,9 +79,15 @@
* first screen.
*/
public void sendBroadcasts(Context context, List<ItemInfo> firstScreenItems) {
- for (Map.Entry<String, ArrayList<String>> entry : mPackagesForInstaller.entrySet()) {
- sendBroadcastToInstaller(context, entry.getKey(), entry.getValue(), firstScreenItems);
- }
+ UserHandle myUser = myUserHandle();
+ mSessionInfoForPackage
+ .values()
+ .stream()
+ .filter(info -> myUser.equals(getUserHandle(info)))
+ .collect(groupingBy(SessionInfo::getInstallerPackageName,
+ mapping(SessionInfo::getAppPackageName, Collectors.toSet())))
+ .forEach((installer, packages) ->
+ sendBroadcastToInstaller(context, installer, packages, firstScreenItems));
}
/**
@@ -99,7 +96,7 @@
* @param firstScreenItems List of items on the first screen.
*/
private void sendBroadcastToInstaller(Context context, String installerPackageName,
- List<String> packages, List<ItemInfo> firstScreenItems) {
+ Set<String> packages, List<ItemInfo> firstScreenItems) {
Set<String> folderItems = new HashSet<>();
Set<String> workspaceItems = new HashSet<>();
Set<String> hotseatItems = new HashSet<>();
@@ -145,7 +142,7 @@
.putStringArrayListExtra(HOTSEAT_ITEM_EXTRA, new ArrayList<>(hotseatItems))
.putStringArrayListExtra(WIDGET_ITEM_EXTRA, new ArrayList<>(widgetItems))
.putExtra(VERIFICATION_TOKEN_EXTRA, PendingIntent.getActivity(context, 0,
- new Intent(), PendingIntent.FLAG_ONE_SHOT)));
+ new Intent(), FLAG_ONE_SHOT | FLAG_IMMUTABLE)));
}
private static String getPackageName(ItemInfo info) {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index e8a52bd..7b3e509 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -25,7 +25,6 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherSettings.Settings;
@@ -40,6 +39,7 @@
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import java.util.ArrayList;
@@ -53,7 +53,7 @@
public class GridSizeMigrationTask {
private static final String TAG = "GridSizeMigrationTask";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
// These are carefully selected weights for various item types (Math.random?), to allow for
// the least absurd migration experience.
@@ -893,7 +893,7 @@
String gridSizeString = getPointString(idp.numColumns, idp.numRows);
return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
- || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
+ || idp.numDatabaseHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
}
/** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
@@ -928,7 +928,7 @@
.getBinder(Settings.EXTRA_VALUE)) {
int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
- idp.numHotseatIcons);
+ idp.numDatabaseHotseatIcons);
Point sourceSize = parsePoint(prefs.getString(
KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
@@ -948,10 +948,10 @@
HashSet<String> validPackages = getValidPackages(context);
// Hotseat.
- if (srcHotseatCount != idp.numHotseatIcons
+ if (srcHotseatCount != idp.numDatabaseHotseatIcons
&& new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
migrateForPreview, srcHotseatCount,
- idp.numHotseatIcons).migrateHotseat()) {
+ idp.numDatabaseHotseatIcons).migrateHotseat()) {
dbChanged = true;
}
@@ -991,7 +991,7 @@
// Save current configuration, so that the migration does not run again.
prefs.edit()
.putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
- .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
+ .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numDatabaseHotseatIcons)
.apply();
}
}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index ebdfa8c..8a1d73e 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -39,7 +39,6 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.Utilities;
import com.android.launcher3.graphics.LauncherPreviewRenderer;
@@ -48,6 +47,7 @@
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import java.util.ArrayList;
@@ -67,7 +67,7 @@
public class GridSizeMigrationTaskV2 {
private static final String TAG = "GridSizeMigrationTaskV2";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private final Context mContext;
private final SQLiteDatabase mDb;
@@ -110,7 +110,7 @@
String gridSizeString = getPointString(idp.numColumns, idp.numRows);
return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
- || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
+ || idp.numDatabaseHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
}
/** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
@@ -148,7 +148,8 @@
SharedPreferences prefs = Utilities.getPrefs(context);
String gridSizeString = getPointString(idp.numColumns, idp.numRows);
HashSet<String> validPackages = getValidPackages(context);
- int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
+ int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+ idp.numDatabaseHotseatIcons);
if (migrateForPreview) {
if (!LauncherSettings.Settings.call(
@@ -177,11 +178,11 @@
DbReader destReader = new DbReader(t.getDb(),
migrateForPreview ? LauncherSettings.Favorites.PREVIEW_TABLE_NAME
: LauncherSettings.Favorites.TABLE_NAME,
- context, validPackages, idp.numHotseatIcons);
+ context, validPackages, idp.numDatabaseHotseatIcons);
Point targetSize = new Point(idp.numColumns, idp.numRows);
GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(context, t.getDb(),
- srcReader, destReader, idp.numHotseatIcons, targetSize);
+ srcReader, destReader, idp.numDatabaseHotseatIcons, targetSize);
task.migrate();
if (!migrateForPreview) {
@@ -202,7 +203,7 @@
// Save current configuration, so that the migration does not run again.
prefs.edit()
.putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
- .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
+ .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numDatabaseHotseatIcons)
.apply();
}
}
@@ -406,7 +407,7 @@
* to speed up the search.
*/
private boolean findPlacement(DbEntry entry) {
- for (int y = mNextStartY; y > 0; y--) {
+ for (int y = mNextStartY; y >= (mScreenId == 0 ? 1 /* smartspace */ : 0); y--) {
for (int x = mNextStartX; x < mTrgX; x++) {
boolean fits = mOccupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
boolean minFits = mOccupied.isRegionVacant(x, y, entry.minSpanX,
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
new file mode 100644
index 0000000..217f523
--- /dev/null
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
+
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.PersistedItemArray;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Class to maintain a queue of pending items to be added to the workspace.
+ */
+public class ItemInstallQueue {
+
+ private static final String LOG = "ItemInstallQueue";
+
+ public static final int FLAG_ACTIVITY_PAUSED = 1;
+ public static final int FLAG_LOADER_RUNNING = 2;
+ public static final int FLAG_DRAG_AND_DROP = 4;
+
+ private static final String TAG = "InstallShortcutReceiver";
+
+ // The set of shortcuts that are pending install
+ private static final String APPS_PENDING_INSTALL = "apps_to_install";
+
+ public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
+ public static final int NEW_SHORTCUT_STAGGER_DELAY = 85;
+
+ public static MainThreadInitializedObject<ItemInstallQueue> INSTANCE =
+ new MainThreadInitializedObject<>(ItemInstallQueue::new);
+
+ private final PersistedItemArray<PendingInstallShortcutInfo> mStorage =
+ new PersistedItemArray<>(APPS_PENDING_INSTALL);
+ private final Context mContext;
+
+ // Determines whether to defer installing shortcuts immediately until
+ // processAllPendingInstalls() is called.
+ private int mInstallQueueDisabledFlags = 0;
+
+ // Only accessed on worker thread
+ private List<PendingInstallShortcutInfo> mItems;
+
+ private ItemInstallQueue(Context context) {
+ mContext = context;
+ }
+
+ @WorkerThread
+ private void ensureQueueLoaded() {
+ Preconditions.assertWorkerThread();
+ if (mItems == null) {
+ mItems = mStorage.read(mContext, this::decode);
+ }
+ }
+
+ @WorkerThread
+ private void addToQueue(PendingInstallShortcutInfo info) {
+ ensureQueueLoaded();
+ if (!mItems.contains(info)) {
+ mItems.add(info);
+ mStorage.write(mContext, mItems);
+ }
+ }
+
+ @WorkerThread
+ private void flushQueueInBackground() {
+ Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+ if (launcher == null) {
+ // Launcher not loaded
+ return;
+ }
+ ensureQueueLoaded();
+ if (mItems.isEmpty()) {
+ return;
+ }
+
+ List<Pair<ItemInfo, Object>> installQueue = mItems.stream()
+ .map(info -> info.getItemInfo(mContext))
+ .collect(Collectors.toList());
+
+ // Add the items and clear queue
+ if (!installQueue.isEmpty()) {
+ // add log
+ launcher.getModel().addAndBindAddedWorkspaceItems(installQueue);
+ }
+ mItems.clear();
+ mStorage.getFile(mContext).delete();
+ }
+
+ /**
+ * Removes previously added items from the queue.
+ */
+ @WorkerThread
+ public void removeFromInstallQueue(HashSet<String> packageNames, UserHandle user) {
+ if (packageNames.isEmpty()) {
+ return;
+ }
+ ensureQueueLoaded();
+ if (mItems.removeIf(item ->
+ item.user.equals(user) && packageNames.contains(getIntentPackage(item.intent)))) {
+ mStorage.write(mContext, mItems);
+ }
+ }
+
+ /**
+ * Adds an item to the install queue
+ */
+ public void queueItem(ShortcutInfo info) {
+ queuePendingShortcutInfo(new PendingInstallShortcutInfo(info));
+ }
+
+ /**
+ * Adds an item to the install queue
+ */
+ public void queueItem(AppWidgetProviderInfo info, int widgetId) {
+ queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, widgetId));
+ }
+
+ /**
+ * Adds an item to the install queue
+ */
+ public void queueItem(String packageName, UserHandle userHandle) {
+ queuePendingShortcutInfo(new PendingInstallShortcutInfo(packageName, userHandle));
+ }
+
+ /**
+ * Returns a stream of all pending shortcuts in the queue
+ */
+ @WorkerThread
+ public Stream<ShortcutKey> getPendingShortcuts(UserHandle user) {
+ ensureQueueLoaded();
+ return mItems.stream()
+ .filter(item -> item.itemType == ITEM_TYPE_DEEP_SHORTCUT && user.equals(item.user))
+ .map(item -> ShortcutKey.fromIntent(item.intent, user));
+ }
+
+ private void queuePendingShortcutInfo(PendingInstallShortcutInfo info) {
+ final Exception stackTrace = new Exception();
+
+ // Queue the item up for adding if launcher has not loaded properly yet
+ MODEL_EXECUTOR.post(() -> {
+ Pair<ItemInfo, Object> itemInfo = info.getItemInfo(mContext);
+ if (itemInfo == null) {
+ FileLog.d(LOG,
+ "Adding PendingInstallShortcutInfo with no attached info to queue.",
+ stackTrace);
+ } else {
+ FileLog.d(LOG,
+ "Adding PendingInstallShortcutInfo to queue. Attached info: "
+ + itemInfo.first,
+ stackTrace);
+ }
+
+ addToQueue(info);
+ });
+ flushInstallQueue();
+ }
+
+ /**
+ * Pauses the push-to-model flow until unpaused. All items are held in the queue and
+ * not added to the model.
+ */
+ public void pauseModelPush(int flag) {
+ mInstallQueueDisabledFlags |= flag;
+ }
+
+ /**
+ * Adds all the queue items to the model if the use is completely resumed.
+ */
+ public void resumeModelPush(int flag) {
+ mInstallQueueDisabledFlags &= ~flag;
+ flushInstallQueue();
+ }
+
+ private void flushInstallQueue() {
+ if (mInstallQueueDisabledFlags != 0) {
+ return;
+ }
+ MODEL_EXECUTOR.post(this::flushQueueInBackground);
+ }
+
+ private static class PendingInstallShortcutInfo extends ItemInfo {
+
+ final Intent intent;
+
+ @Nullable ShortcutInfo shortcutInfo;
+ @Nullable AppWidgetProviderInfo providerInfo;
+
+ /**
+ * Initializes a PendingInstallShortcutInfo to represent a pending launcher target.
+ */
+ public PendingInstallShortcutInfo(String packageName, UserHandle userHandle) {
+ itemType = Favorites.ITEM_TYPE_APPLICATION;
+ intent = new Intent().setPackage(packageName);
+ user = userHandle;
+ }
+
+ /**
+ * Initializes a PendingInstallShortcutInfo to represent a deep shortcut.
+ */
+ public PendingInstallShortcutInfo(ShortcutInfo info) {
+ itemType = Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+ intent = ShortcutKey.makeIntent(info);
+ user = info.getUserHandle();
+
+ shortcutInfo = info;
+ }
+
+ /**
+ * Initializes a PendingInstallShortcutInfo to represent an app widget.
+ */
+ public PendingInstallShortcutInfo(AppWidgetProviderInfo info, int widgetId) {
+ itemType = Favorites.ITEM_TYPE_APPWIDGET;
+ intent = new Intent()
+ .setComponent(info.provider)
+ .putExtra(EXTRA_APPWIDGET_ID, widgetId);
+ user = info.getProfile();
+
+ providerInfo = info;
+ }
+
+ @Override
+ public Intent getIntent() {
+ return intent;
+ }
+
+ public Pair<ItemInfo, Object> getItemInfo(Context context) {
+ switch (itemType) {
+ case ITEM_TYPE_APPLICATION: {
+ String packageName = intent.getPackage();
+ List<LauncherActivityInfo> laiList =
+ context.getSystemService(LauncherApps.class)
+ .getActivityList(packageName, user);
+
+ final WorkspaceItemInfo si = new WorkspaceItemInfo();
+ si.user = user;
+ si.itemType = ITEM_TYPE_APPLICATION;
+
+ LauncherActivityInfo lai;
+ boolean usePackageIcon = laiList.isEmpty();
+ if (usePackageIcon) {
+ lai = null;
+ si.intent = makeLaunchIntent(new ComponentName(packageName, ""))
+ .setPackage(packageName);
+ si.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
+ } else {
+ lai = laiList.get(0);
+ si.intent = makeLaunchIntent(lai);
+ }
+ LauncherAppState.getInstance(context).getIconCache()
+ .getTitleAndIcon(si, () -> lai, usePackageIcon, false);
+ return Pair.create(si, null);
+ }
+ case ITEM_TYPE_DEEP_SHORTCUT: {
+ WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(shortcutInfo, context);
+ LauncherAppState.getInstance(context).getIconCache()
+ .getShortcutIcon(itemInfo, shortcutInfo);
+ return Pair.create(itemInfo, shortcutInfo);
+ }
+ case ITEM_TYPE_APPWIDGET: {
+ LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo
+ .fromProviderInfo(context, providerInfo);
+ LauncherAppWidgetInfo widgetInfo = new LauncherAppWidgetInfo(
+ intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0),
+ info.provider);
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ widgetInfo.minSpanX = info.minSpanX;
+ widgetInfo.minSpanY = info.minSpanY;
+ widgetInfo.spanX = Math.min(info.spanX, idp.numColumns);
+ widgetInfo.spanY = Math.min(info.spanY, idp.numRows);
+ widgetInfo.user = user;
+ return Pair.create(widgetInfo, providerInfo);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof PendingInstallShortcutInfo) {
+ PendingInstallShortcutInfo other = (PendingInstallShortcutInfo) obj;
+
+ boolean userMatches = user.equals(other.user);
+ boolean itemTypeMatches = itemType == other.itemType;
+ boolean intentMatches = intent.toUri(0).equals(other.intent.toUri(0));
+ boolean shortcutInfoMatches = shortcutInfo == null
+ ? other.shortcutInfo == null
+ : other.shortcutInfo != null
+ && shortcutInfo.getId().equals(other.shortcutInfo.getId())
+ && shortcutInfo.getPackage().equals(other.shortcutInfo.getPackage());
+ boolean providerInfoMatches = providerInfo == null
+ ? other.providerInfo == null
+ : other.providerInfo != null
+ && providerInfo.provider.equals(other.providerInfo.provider);
+
+ return userMatches
+ && itemTypeMatches
+ && intentMatches
+ && shortcutInfoMatches
+ && providerInfoMatches;
+ }
+ return false;
+ }
+ }
+
+ private static String getIntentPackage(Intent intent) {
+ return intent.getComponent() == null
+ ? intent.getPackage() : intent.getComponent().getPackageName();
+ }
+
+ private PendingInstallShortcutInfo decode(int itemType, UserHandle user, Intent intent) {
+ switch (itemType) {
+ case Favorites.ITEM_TYPE_APPLICATION:
+ return new PendingInstallShortcutInfo(intent.getPackage(), user);
+ case Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
+ List<ShortcutInfo> si = ShortcutKey.fromIntent(intent, user)
+ .buildRequest(mContext)
+ .query(ShortcutRequest.ALL);
+ if (si.isEmpty()) {
+ return null;
+ } else {
+ return new PendingInstallShortcutInfo(si.get(0));
+ }
+ }
+ case Favorites.ITEM_TYPE_APPWIDGET: {
+ int widgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0);
+ AppWidgetProviderInfo info =
+ AppWidgetManager.getInstance(mContext).getAppWidgetInfo(widgetId);
+ if (info == null || !info.provider.equals(intent.getComponent())
+ || !info.getProfile().equals(user)) {
+ return null;
+ }
+ return new PendingInstallShortcutInfo(info, widgetId);
+ }
+ default:
+ Log.e(TAG, "Unknown item type");
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 165d1ea..7e3bcee 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -35,11 +35,13 @@
import android.util.Log;
import android.util.LongSparseArray;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
@@ -50,6 +52,7 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.ContentWriter;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
@@ -65,7 +68,7 @@
private static final String TAG = "LoaderCursor";
- public final LongSparseArray<UserHandle> allUsers;
+ private final LongSparseArray<UserHandle> allUsers;
private final Uri mContentUri;
private final Context mContext;
@@ -92,6 +95,9 @@
private final int restoredIndex;
private final int intentIndex;
+ @Nullable
+ private LauncherActivityInfo mActivityInfo;
+
// Properties loaded per iteration
public long serialNumber;
public UserHandle user;
@@ -132,6 +138,8 @@
public boolean moveToNext() {
boolean result = super.moveToNext();
if (result) {
+ mActivityInfo = null;
+
// Load common properties.
itemType = getInt(itemTypeIndex);
container = getInt(containerIndex);
@@ -245,6 +253,10 @@
return info;
}
+ public LauncherActivityInfo getLauncherActivityInfo() {
+ return mActivityInfo;
+ }
+
/**
* Make an WorkspaceItemInfo object for a shortcut that is an application.
*/
@@ -264,25 +276,25 @@
Intent newIntent = new Intent(Intent.ACTION_MAIN, null);
newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
newIntent.setComponent(componentName);
- LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+ mActivityInfo = mContext.getSystemService(LauncherApps.class)
.resolveActivity(newIntent, user);
- if ((lai == null) && !allowMissingTarget) {
+ if ((mActivityInfo == null) && !allowMissingTarget) {
Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
return null;
}
final WorkspaceItemInfo info = new WorkspaceItemInfo();
- info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ info.itemType = Favorites.ITEM_TYPE_APPLICATION;
info.user = user;
info.intent = newIntent;
- mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
+ mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon);
if (mIconCache.isDefaultIcon(info.bitmap, user)) {
loadIcon(info);
}
- if (lai != null) {
- AppInfo.updateRuntimeFlagsForActivityTarget(info, lai);
+ if (mActivityInfo != null) {
+ AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo);
}
// from the db
@@ -383,6 +395,11 @@
* otherwise marks it for deletion.
*/
public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
+ if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ // Ensure that it is a valid intent. An exception here will
+ // cause the item loading to get skipped
+ ShortcutKey.fromItemInfo(info);
+ }
if (checkItemPlacement(info)) {
dataModel.addItem(mContext, info, false);
} else {
@@ -399,10 +416,10 @@
final GridOccupancy hotseatOccupancy =
occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT);
- if (item.screenId >= mIDP.numHotseatIcons) {
+ if (item.screenId >= mIDP.numDatabaseHotseatIcons) {
Log.e(TAG, "Error loading shortcut " + item
+ " into hotseat position " + item.screenId
- + ", position out of bounds: (0 to " + (mIDP.numHotseatIcons - 1)
+ + ", position out of bounds: (0 to " + (mIDP.numDatabaseHotseatIcons - 1)
+ ")");
return false;
}
@@ -418,7 +435,7 @@
return true;
}
} else {
- final GridOccupancy occupancy = new GridOccupancy(mIDP.numHotseatIcons, 1);
+ final GridOccupancy occupancy = new GridOccupancy(mIDP.numDatabaseHotseatIcons, 1);
occupancy.cells[item.screenId][0] = true;
occupied.put(LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy);
return true;
@@ -445,7 +462,8 @@
if (item.screenId == Workspace.FIRST_SCREEN_ID) {
// Mark the first row as occupied (if the feature is enabled)
// in order to account for the QSB.
- screen.markCells(0, 0, countX + 1, 1, FeatureFlags.QSB_ON_FIRST_SCREEN);
+ int spanY = FeatureFlags.EXPANDED_SMARTSPACE.get() ? 2 : 1;
+ screen.markCells(0, 0, countX + 1, spanY, FeatureFlags.QSB_ON_FIRST_SCREEN);
}
occupied.put(item.screenId, screen);
}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 102ec31..f6b0b4d 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -28,6 +28,7 @@
import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
+import android.annotation.SuppressLint;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -40,18 +41,19 @@
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
+import android.graphics.Point;
import android.net.Uri;
+import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
-import android.util.MutableInt;
import android.util.TimingLogger;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.InstallShortcutReceiver;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
@@ -71,8 +73,8 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.PackageInstallInfo;
@@ -85,10 +87,10 @@
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IOUtils;
import com.android.launcher3.util.LooperIdleLock;
-import com.android.launcher3.util.MultiHashMap;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import java.util.ArrayList;
@@ -97,6 +99,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CancellationException;
/**
@@ -109,9 +112,12 @@
public class LoaderTask implements Runnable {
private static final String TAG = "LoaderTask";
+ private static final boolean DEBUG = true;
+
protected final LauncherAppState mApp;
private final AllAppsList mBgAllAppsList;
protected final BgDataModel mBgDataModel;
+ private final ModelDelegate mModelDelegate;
private FirstScreenBroadcast mFirstScreenBroadcast;
@@ -126,13 +132,20 @@
private final UserManagerState mUserManagerState = new UserManagerState();
+ protected final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap = new ArrayMap<>();
+
private boolean mStopped;
+ private final Set<PackageUserKey> mPendingPackages = new HashSet<>();
+ private boolean mItemsDeleted = false;
+ private String mDbName;
+
public LoaderTask(LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel dataModel,
- LoaderResults results) {
+ ModelDelegate modelDelegate, LoaderResults results) {
mApp = app;
mBgAllAppsList = bgAllAppsList;
mBgDataModel = dataModel;
+ mModelDelegate = modelDelegate;
mResults = results;
mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
@@ -160,12 +173,7 @@
private void sendFirstScreenActiveInstallsBroadcast() {
ArrayList<ItemInfo> firstScreenItems = new ArrayList<>();
-
- ArrayList<ItemInfo> allItems = new ArrayList<>();
- synchronized (mBgDataModel) {
- allItems.addAll(mBgDataModel.workspaceItems);
- allItems.addAll(mBgDataModel.appWidgets);
- }
+ ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
// Screen set is never empty
final int firstScreen = mBgDataModel.collectWorkspaceScreens().get(0);
@@ -187,29 +195,39 @@
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
loadWorkspace(allShortcuts);
- loadCachedPredictions();
- logger.addSplit("loadWorkspace");
+ logASplit(logger, "loadWorkspace");
+
+ // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
+ // sanitizeData should not be invoked if the workspace is loaded from a db different
+ // from the main db as defined in the invariant device profile.
+ // (e.g. both grid preview and minimal device mode uses a different db)
+ if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
+ verifyNotStopped();
+ sanitizeData();
+ logASplit(logger, "sanitizeData");
+ }
verifyNotStopped();
mResults.bindWorkspace();
- logger.addSplit("bindWorkspace");
+ logASplit(logger, "bindWorkspace");
+ mModelDelegate.workspaceLoadComplete();
// Notify the installer packages of packages with active installs on the first screen.
sendFirstScreenActiveInstallsBroadcast();
- logger.addSplit("sendFirstScreenActiveInstallsBroadcast");
+ logASplit(logger, "sendFirstScreenActiveInstallsBroadcast");
// Take a break
waitForIdle();
- logger.addSplit("step 1 complete");
+ logASplit(logger, "step 1 complete");
verifyNotStopped();
// second step
List<LauncherActivityInfo> allActivityList = loadAllApps();
- logger.addSplit("loadAllApps");
+ logASplit(logger, "loadAllApps");
verifyNotStopped();
mResults.bindAllApps();
- logger.addSplit("bindAllApps");
+ logASplit(logger, "bindAllApps");
verifyNotStopped();
IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
@@ -217,54 +235,54 @@
updateHandler.updateIcons(allActivityList,
LauncherActivityCachingLogic.newInstance(mApp.getContext()),
mApp.getModel()::onPackageIconsUpdated);
- logger.addSplit("update icon cache");
+ logASplit(logger, "update icon cache");
if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
verifyNotStopped();
- logger.addSplit("save shortcuts in icon cache");
+ logASplit(logger, "save shortcuts in icon cache");
updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
mApp.getModel()::onPackageIconsUpdated);
}
// Take a break
waitForIdle();
- logger.addSplit("step 2 complete");
+ logASplit(logger, "step 2 complete");
verifyNotStopped();
// third step
List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
- logger.addSplit("loadDeepShortcuts");
+ logASplit(logger, "loadDeepShortcuts");
verifyNotStopped();
mResults.bindDeepShortcuts();
- logger.addSplit("bindDeepShortcuts");
+ logASplit(logger, "bindDeepShortcuts");
if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
verifyNotStopped();
- logger.addSplit("save deep shortcuts in icon cache");
+ logASplit(logger, "save deep shortcuts in icon cache");
updateHandler.updateIcons(allDeepShortcuts,
new ShortcutCachingLogic(), (pkgs, user) -> { });
}
// Take a break
waitForIdle();
- logger.addSplit("step 3 complete");
+ logASplit(logger, "step 3 complete");
verifyNotStopped();
// fourth step
List<ComponentWithLabelAndIcon> allWidgetsList =
mBgDataModel.widgetsModel.update(mApp, null);
- logger.addSplit("load widgets");
+ logASplit(logger, "load widgets");
verifyNotStopped();
mResults.bindWidgets();
- logger.addSplit("bindWidgets");
+ logASplit(logger, "bindWidgets");
verifyNotStopped();
updateHandler.updateIcons(allWidgetsList,
new ComponentWithIconCachingLogic(mApp.getContext(), true),
mApp.getModel()::onWidgetLabelsUpdated);
- logger.addSplit("save widgets in icon cache");
+ logASplit(logger, "save widgets in icon cache");
// fifth step
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
@@ -273,12 +291,13 @@
verifyNotStopped();
updateHandler.finish();
- logger.addSplit("finish icon update");
+ logASplit(logger, "finish icon update");
+ mModelDelegate.modelLoadComplete();
transaction.commit();
} catch (CancellationException e) {
// Loader stopped, ignore
- logger.addSplit("Cancelled");
+ logASplit(logger, "Cancelled");
} finally {
logger.dumpToLog();
}
@@ -291,16 +310,18 @@
}
private void loadWorkspace(List<ShortcutInfo> allDeepShortcuts) {
- loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI);
+ loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI,
+ null /* selection */);
}
- protected void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, Uri contentUri) {
+ protected void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, Uri contentUri,
+ String selection) {
final Context context = mApp.getContext();
final ContentResolver contentResolver = context.getContentResolver();
final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
final boolean isSafeMode = pmHelper.isSafeMode();
final boolean isSdCardReady = Utilities.isBootCompleted();
- final MultiHashMap<UserHandle, String> pendingPackages = new MultiHashMap<>();
+ final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
boolean clearDb = false;
try {
@@ -329,6 +350,7 @@
synchronized (mBgDataModel) {
mBgDataModel.clear();
+ mPendingPackages.clear();
final HashMap<PackageUserKey, SessionInfo> installingPkgs =
mSessionHelper.getActiveSessions();
@@ -339,11 +361,11 @@
Map<ShortcutKey, ShortcutInfo> shortcutKeyToPinnedShortcuts = new HashMap<>();
final LoaderCursor c = new LoaderCursor(
- contentResolver.query(contentUri, null, null, null, null), contentUri, mApp,
- mUserManagerState);
-
- Map<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
-
+ contentResolver.query(contentUri, null, selection, null, null), contentUri,
+ mApp, mUserManagerState);
+ final Bundle extras = c.getExtras();
+ mDbName = extras == null
+ ? null : extras.getString(LauncherSettings.Settings.EXTRA_DB_NAME);
try {
final int appWidgetIdIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.APPWIDGET_ID);
@@ -357,16 +379,15 @@
LauncherSettings.Favorites.RANK);
final int optionsIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.OPTIONS);
+ final int sourceContainerIndex = c.getColumnIndexOrThrow(
+ LauncherSettings.Favorites.APPWIDGET_SOURCE);
- final LongSparseArray<UserHandle> allUsers = c.allUsers;
final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
mUserManagerState.init(mUserCache, mUserManager);
for (UserHandle user : mUserCache.getUserProfiles()) {
long serialNo = mUserCache.getSerialNumberForUser(user);
- allUsers.put(serialNo, user);
-
boolean userUnlocked = mUserManager.isUserUnlocked(user);
// We can only query for shortcuts when the user is unlocked.
@@ -390,6 +411,7 @@
WorkspaceItemInfo info;
LauncherAppWidgetInfo appWidgetInfo;
+ LauncherAppWidgetProviderInfo widgetProviderInfo;
Intent intent;
String targetPkg;
@@ -417,16 +439,6 @@
ComponentName cn = intent.getComponent();
targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
- if (allUsers.indexOfValue(c.user) < 0) {
- if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
- c.markDeleted("Legacy shortcuts are only allowed for current users");
- continue;
- } else if (c.restoreFlag != 0) {
- // Don't restore items for other profiles.
- c.markDeleted("Restore from other profiles not supported");
- continue;
- }
- }
if (TextUtils.isEmpty(targetPkg) &&
c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
c.markDeleted("Only legacy shortcuts can have null package");
@@ -496,7 +508,7 @@
// SdCard is not ready yet. Package might get available,
// once it is ready.
Log.d(TAG, "Missing pkg, will check later: " + targetPkg);
- pendingPackages.addToList(c.user, targetPkg);
+ mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
// Add the icon on the workspace anyway.
allowMissingTarget = true;
} else {
@@ -586,15 +598,27 @@
if (isSafeMode && !isSystemApp(context, intent)) {
info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
}
+ LauncherActivityInfo activityInfo = c.getLauncherActivityInfo();
+ if (activityInfo != null) {
+ info.setProgressLevel(
+ PackageManagerHelper
+ .getLoadingProgress(activityInfo),
+ PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
+ }
if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) {
tempPackageKey.update(targetPkg, c.user);
SessionInfo si = installingPkgs.get(tempPackageKey);
- if (si == null) {
- info.status &= ~WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE;
- } else {
- info.setInstallProgress((int) (si.getProgress() * 100));
- }
+ if (si == null) {
+ info.runtimeStatusFlags &=
+ ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
+ } else if (activityInfo == null) {
+ int installProgress = (int) (si.getProgress() * 100);
+
+ info.setProgressLevel(
+ installProgress,
+ PackageInstallInfo.STATUS_INSTALLING);
+ }
}
c.checkAndAddItem(info, mBgDataModel);
@@ -650,11 +674,13 @@
final boolean wasProviderReady = !c.hasRestoreFlag(
LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
- if (widgetProvidersMap == null) {
- widgetProvidersMap = WidgetManagerHelper.getAllProvidersMap(context);
+ ComponentKey providerKey = new ComponentKey(component, c.user);
+ if (!mWidgetProvidersMap.containsKey(providerKey)) {
+ mWidgetProvidersMap.put(providerKey,
+ widgetHelper.findProvider(component, c.user));
}
- final AppWidgetProviderInfo provider = widgetProvidersMap.get(
- new ComponentKey(component, c.user));
+ final AppWidgetProviderInfo provider =
+ mWidgetProvidersMap.get(providerKey);
final boolean isProviderReady = isValidProvider(provider);
if (!isSafeMode && !customWidget &&
@@ -722,12 +748,26 @@
appWidgetInfo.spanY = c.getInt(spanYIndex);
appWidgetInfo.options = c.getInt(optionsIndex);
appWidgetInfo.user = c.user;
+ appWidgetInfo.sourceContainer = c.getInt(sourceContainerIndex);
if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
c.markDeleted("Widget has invalid size: "
+ appWidgetInfo.spanX + "x" + appWidgetInfo.spanY);
continue;
}
+ widgetProviderInfo =
+ widgetHelper.getLauncherAppWidgetInfo(appWidgetId);
+ if (widgetProviderInfo != null
+ && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
+ || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
+ FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent()
+ + " minSizes not meet: span=" + appWidgetInfo.spanX
+ + "x" + appWidgetInfo.spanY + " minSpan="
+ + widgetProviderInfo.minSpanX + "x"
+ + widgetProviderInfo.minSpanY);
+ logWidgetInfo(mApp.getInvariantDeviceProfile(),
+ widgetProviderInfo);
+ }
if (!c.isOnWorkspaceOrHotseat()) {
c.markDeleted("Widget found where container != " +
"CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!");
@@ -750,8 +790,8 @@
if (appWidgetInfo.restoreStatus !=
LauncherAppWidgetInfo.RESTORE_COMPLETED) {
- String pkg = appWidgetInfo.providerName.getPackageName();
- appWidgetInfo.pendingItemInfo = new PackageItemInfo(pkg);
+ appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo(
+ appWidgetInfo.providerName);
appWidgetInfo.pendingItemInfo.user = appWidgetInfo.user;
mIconCache.getTitleAndIconForApp(
appWidgetInfo.pendingItemInfo, false);
@@ -769,6 +809,9 @@
IOUtils.closeSilently(c);
}
+ // Load delegate items
+ mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts);
+
// Break early if we've stopped loading
if (mStopped) {
mBgDataModel.clear();
@@ -776,34 +819,7 @@
}
// Remove dead items
- if (c.commitDeleted()) {
- // Remove any empty folder
- int[] deletedFolderIds = LauncherSettings.Settings
- .call(contentResolver,
- LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS)
- .getIntArray(LauncherSettings.Settings.EXTRA_VALUE);
- for (int folderId : deletedFolderIds) {
- mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId));
- mBgDataModel.folders.remove(folderId);
- mBgDataModel.itemsIdMap.remove(folderId);
- }
-
- // Remove any ghost widgets
- LauncherSettings.Settings.call(contentResolver,
- LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
- }
-
- // Unpin shortcuts that don't exist on the workspace.
- HashSet<ShortcutKey> pendingShortcuts =
- InstallShortcutReceiver.getPendingShortcuts(context);
- for (ShortcutKey key : shortcutKeyToPinnedShortcuts.keySet()) {
- MutableInt numTimesPinned = mBgDataModel.pinnedShortcutCounts.get(key);
- if ((numTimesPinned == null || numTimesPinned.value == 0)
- && !pendingShortcuts.contains(key)) {
- // Shortcut is pinned but doesn't exist on the workspace; unpin it.
- mBgDataModel.unpinShortcut(context, key);
- }
- }
+ mItemsDeleted = c.commitDeleted();
// Sort the folder items, update ranks, and make sure all preview items are high res.
FolderGridOrganizer verifier =
@@ -829,13 +845,6 @@
}
c.commitRestoredItems();
- if (!isSdCardReady && !pendingPackages.isEmpty()) {
- context.registerReceiver(
- new SdCardAvailableReceiver(mApp, pendingPackages),
- new IntentFilter(Intent.ACTION_BOOT_COMPLETED),
- null,
- MODEL_EXECUTOR.getHandler());
- }
}
}
@@ -860,21 +869,37 @@
}
}
- @WorkerThread
- private void loadCachedPredictions() {
- synchronized (mBgDataModel) {
- List<ComponentKey> componentKeys =
- mApp.getPredictionModel().getPredictionComponentKeys();
- List<LauncherActivityInfo> l;
- mBgDataModel.cachedPredictedItems.clear();
- for (ComponentKey key : componentKeys) {
- l = mLauncherApps.getActivityList(key.componentName.getPackageName(), key.user);
- if (l.size() == 0) continue;
- AppInfo info = new AppInfo(l.get(0), key.user,
- mUserManagerState.isUserQuiet(key.user));
- mBgDataModel.cachedPredictedItems.add(info);
- mIconCache.getTitleAndIcon(info, false);
+ private void sanitizeData() {
+ Context context = mApp.getContext();
+ ContentResolver contentResolver = context.getContentResolver();
+ if (mItemsDeleted) {
+ // Remove any empty folder
+ int[] deletedFolderIds = LauncherSettings.Settings
+ .call(contentResolver,
+ LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS)
+ .getIntArray(LauncherSettings.Settings.EXTRA_VALUE);
+ synchronized (mBgDataModel) {
+ for (int folderId : deletedFolderIds) {
+ mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId));
+ mBgDataModel.folders.remove(folderId);
+ mBgDataModel.itemsIdMap.remove(folderId);
+ }
}
+
+ }
+ // Remove any ghost widgets
+ LauncherSettings.Settings.call(contentResolver,
+ LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
+
+ // Update pinned state of model shortcuts
+ mBgDataModel.updateShortcutPinnedState(context);
+
+ if (!Utilities.isBootCompleted() && !mPendingPackages.isEmpty()) {
+ context.registerReceiver(
+ new SdCardAvailableReceiver(mApp, mPendingPackages),
+ new IntentFilter(Intent.ACTION_BOOT_COMPLETED),
+ null,
+ MODEL_EXECUTOR.getHandler());
}
}
@@ -909,14 +934,6 @@
PackageInstallInfo.fromInstallingState(info));
}
}
- for (AppInfo item : mBgDataModel.cachedPredictedItems) {
- List<LauncherActivityInfo> l = mLauncherApps.getActivityList(
- item.componentName.getPackageName(), item.user);
- for (LauncherActivityInfo info : l) {
- boolean quietMode = mUserManagerState.isUserQuiet(item.user);
- mBgAllAppsList.add(new AppInfo(info, item.user, quietMode), info);
- }
- }
mBgAllAppsList.setFlags(FLAG_QUIET_MODE_ENABLED,
mUserManagerState.isAnyProfileQuietModeEnabled());
@@ -968,4 +985,54 @@
return (provider != null) && (provider.provider != null)
&& (provider.provider.getPackageName() != null);
}
+
+ @SuppressLint("NewApi") // Already added API check.
+ private static void logWidgetInfo(InvariantDeviceProfile idp,
+ LauncherAppWidgetProviderInfo widgetProviderInfo) {
+ Point cellSize = new Point();
+ for (DeviceProfile deviceProfile : idp.supportedProfiles) {
+ deviceProfile.getCellSize(cellSize);
+ FileLog.d(TAG, "DeviceProfile available width: " + deviceProfile.availableWidthPx
+ + ", available height: " + deviceProfile.availableHeightPx
+ + ", cellLayoutBorderSpacingPx: " + deviceProfile.cellLayoutBorderSpacingPx
+ + ", cellSize: " + cellSize);
+ }
+
+ StringBuilder widgetDimension = new StringBuilder();
+ widgetDimension.append("Widget dimensions:\n")
+ .append("minResizeWidth: ")
+ .append(widgetProviderInfo.minResizeWidth)
+ .append("\n")
+ .append("minResizeHeight: ")
+ .append(widgetProviderInfo.minResizeHeight)
+ .append("\n")
+ .append("defaultWidth: ")
+ .append(widgetProviderInfo.minWidth)
+ .append("\n")
+ .append("defaultHeight: ")
+ .append(widgetProviderInfo.minHeight)
+ .append("\n");
+ if (Utilities.ATLEAST_S) {
+ widgetDimension.append("targetCellWidth: ")
+ .append(widgetProviderInfo.targetCellWidth)
+ .append("\n")
+ .append("targetCellHeight: ")
+ .append(widgetProviderInfo.targetCellHeight)
+ .append("\n")
+ .append("maxResizeWidth: ")
+ .append(widgetProviderInfo.maxResizeWidth)
+ .append("\n")
+ .append("maxResizeHeight: ")
+ .append(widgetProviderInfo.maxResizeHeight)
+ .append("\n");
+ }
+ FileLog.d(TAG, widgetDimension.toString());
+ }
+
+ private static void logASplit(final TimingLogger logger, final String label) {
+ logger.addSplit(label);
+ if (DEBUG) {
+ Log.d(TAG, label);
+ }
+ }
}
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
new file mode 100644
index 0000000..13ec1ec
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
+import android.content.Context;
+import android.content.pm.ShortcutInfo;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Map;
+
+/**
+ * Class to extend LauncherModel functionality to provide extra data
+ */
+public class ModelDelegate implements ResourceBasedOverride {
+
+ /**
+ * Creates and initializes a new instance of the delegate
+ */
+ public static ModelDelegate newInstance(
+ Context context, LauncherAppState app, AllAppsList appsList, BgDataModel dataModel) {
+ ModelDelegate delegate = Overrides.getObject(
+ ModelDelegate.class, context, R.string.model_delegate_class);
+
+ delegate.mApp = app;
+ delegate.mAppsList = appsList;
+ delegate.mDataModel = dataModel;
+ return delegate;
+ }
+
+ protected LauncherAppState mApp;
+ protected AllAppsList mAppsList;
+ protected BgDataModel mDataModel;
+
+ public ModelDelegate() { }
+
+ /**
+ * Called periodically to validate and update any data
+ */
+ @WorkerThread
+ public void validateData() {
+ if (hasShortcutsPermission(mApp.getContext())
+ != mAppsList.hasShortcutHostPermission()) {
+ mApp.getModel().forceReload();
+ }
+ }
+
+ /**
+ * Load delegate items if any in the data model
+ */
+ @WorkerThread
+ public void loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) { }
+
+ /**
+ * Called during loader after workspace loading is complete
+ */
+ @WorkerThread
+ public void workspaceLoadComplete() { }
+
+ /**
+ * Called at the end of model load task
+ */
+ @WorkerThread
+ public void modelLoadComplete() { }
+
+ /**
+ * Called when the delegate is no loner needed
+ */
+ @WorkerThread
+ public void destroy() { }
+
+ /**
+ * Add data to a dumpsys request for Launcher (e.g. for bug reports).
+ *
+ * @see com.android.launcher3.Launcher#dump(java.lang.String, java.io.FileDescriptor,
+ * java.io.PrintWriter, java.lang.String[])
+ **/
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { }
+}
diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java
deleted file mode 100644
index 713492b..0000000
--- a/src/com/android/launcher3/model/ModelPreload.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.model;
-
-import android.content.Context;
-import android.util.Log;
-
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-
-import java.util.concurrent.Executor;
-
-/**
- * Utility class to preload LauncherModel
- */
-public class ModelPreload implements ModelUpdateTask {
-
- private static final String TAG = "ModelPreload";
-
- private LauncherAppState mApp;
- private LauncherModel mModel;
- private BgDataModel mBgDataModel;
- private AllAppsList mAllAppsList;
-
- @Override
- public final void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
- AllAppsList allAppsList, Executor uiExecutor) {
- mApp = app;
- mModel = model;
- mBgDataModel = dataModel;
- mAllAppsList = allAppsList;
- }
-
- @Override
- public final void run() {
- mModel.startLoaderForResultsIfNotLoaded(
- new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
- Log.d(TAG, "Preload completed : " + mModel.isModelLoaded());
- onComplete(mModel.isModelLoaded());
- }
-
- /**
- * Called when the task is complete
- */
- @WorkerThread
- public void onComplete(boolean isSuccess) { }
-
- public void start(Context context) {
- LauncherAppState.getInstance(context).getModel().enqueueModelUpdateTask(this);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
index 4efeba5..9b5fac8 100644
--- a/src/com/android/launcher3/model/ModelUtils.java
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -15,17 +15,29 @@
*/
package com.android.launcher3.model;
+import static com.android.launcher3.Utilities.isValidExtraType;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Process;
+import android.util.Log;
+
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
import java.util.stream.IntStream;
/**
@@ -33,6 +45,8 @@
*/
public class ModelUtils {
+ private static final String TAG = "ModelUtils";
+
/**
* Filters the set of items who are directly or indirectly (via another container) on the
* specified screen.
@@ -42,13 +56,7 @@
ArrayList<T> currentScreenItems,
ArrayList<T> otherScreenItems) {
// Purge any null ItemInfos
- Iterator<T> iter = allWorkspaceItems.iterator();
- while (iter.hasNext()) {
- ItemInfo i = iter.next();
- if (i == null) {
- iter.remove();
- }
- }
+ allWorkspaceItems.removeIf(Objects::isNull);
// Order the set of items by their containers first, this allows use to walk through the
// list sequentially, build up a list of containers that are in the specified screen,
// as well as all items in those containers.
@@ -125,4 +133,52 @@
IntStream.range(0, len).filter(i -> !seen.contains(i)).forEach(result::add);
return result;
}
+
+
+ /**
+ * Creates a workspace item info for the legacy shortcut intent
+ */
+ @SuppressWarnings("deprecation")
+ public static WorkspaceItemInfo fromLegacyShortcutIntent(Context context, Intent data) {
+ if (!isValidExtraType(data, Intent.EXTRA_SHORTCUT_INTENT, Intent.class)
+ || !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+ Intent.ShortcutIconResource.class))
+ || !(isValidExtraType(data, Intent.EXTRA_SHORTCUT_ICON, Bitmap.class))) {
+
+ Log.e(TAG, "Invalid install shortcut intent");
+ return null;
+ }
+
+ Intent launchIntent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
+ String label = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
+ if (launchIntent == null || label == null) {
+ Log.e(TAG, "Invalid install shortcut intent");
+ return null;
+ }
+
+ final WorkspaceItemInfo info = new WorkspaceItemInfo();
+ info.user = Process.myUserHandle();
+
+ BitmapInfo iconInfo = null;
+ try (LauncherIcons li = LauncherIcons.obtain(context)) {
+ Bitmap bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+ if (bitmap != null) {
+ iconInfo = li.createIconBitmap(bitmap);
+ } else {
+ info.iconResource = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
+ if (info.iconResource != null) {
+ iconInfo = li.createIconBitmap(info.iconResource);
+ }
+ }
+ }
+
+ if (iconInfo == null) {
+ Log.e(TAG, "Invalid icon by the app");
+ return null;
+ }
+ info.bitmap = iconInfo;
+ info.contentDescription = info.title = Utilities.trim(label);
+ info.intent = launchIntent;
+ return info;
+ }
}
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index 2c99df7..080ce20 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -28,7 +28,6 @@
import android.util.Log;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
@@ -43,6 +42,7 @@
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.ContentWriter;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
import java.util.ArrayList;
import java.util.Arrays;
@@ -51,6 +51,7 @@
import java.util.concurrent.Executor;
import java.util.function.Supplier;
import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
/**
* Class for handling model updates.
@@ -90,7 +91,7 @@
// in the hotseat
if (container == Favorites.CONTAINER_HOTSEAT) {
item.screenId = mHasVerticalHotseat
- ? LauncherAppState.getIDP(mContext).numHotseatIcons - cellY - 1 : cellX;
+ ? LauncherAppState.getIDP(mContext).numDatabaseHotseatIcons - cellY - 1 : cellX;
} else {
item.screenId = screenId;
}
@@ -259,7 +260,9 @@
* Removes all the items from the database matching {@param matcher}.
*/
public void deleteItemsFromDatabase(ItemInfoMatcher matcher) {
- deleteItemsFromDatabase(matcher.filterItemInfos(mBgDataModel.itemsIdMap));
+ deleteItemsFromDatabase(StreamSupport.stream(mBgDataModel.itemsIdMap.spliterator(), false)
+ .filter(matcher::matchesInfo)
+ .collect(Collectors.toList()));
}
/**
diff --git a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
new file mode 100644
index 0000000..c0dc34a
--- /dev/null
+++ b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.os.UserHandle;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.PackageInstallInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles updates due to incremental download progress updates.
+ */
+public class PackageIncrementalDownloadUpdatedTask extends BaseModelUpdateTask {
+
+ private final UserHandle mUser;
+ private final int mProgress;
+ private final String mPackageName;
+
+ public PackageIncrementalDownloadUpdatedTask(
+ String packageName, UserHandle user, float progress) {
+ mUser = user;
+ mProgress = 1 - progress > 0.001 ? (int) (100 * progress) : 100;
+ mPackageName = packageName;
+ }
+
+ @Override
+ public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) {
+ PackageInstallInfo downloadInfo = new PackageInstallInfo(
+ mPackageName,
+ PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING,
+ mProgress,
+ mUser);
+
+ synchronized (appsList) {
+ List<AppInfo> updatedAppInfos = appsList.updatePromiseInstallInfo(downloadInfo);
+ if (!updatedAppInfos.isEmpty()) {
+ for (AppInfo appInfo : updatedAppInfos) {
+ appInfo.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
+ scheduleCallbackTask(
+ c -> c.bindIncrementalDownloadProgressUpdated(appInfo));
+ }
+ }
+ bindApplicationsIfNeeded();
+ }
+
+ final ArrayList<WorkspaceItemInfo> updatedWorkspaceItems = new ArrayList<>();
+ synchronized (dataModel) {
+ dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+ if (mPackageName.equals(si.getTargetPackage())) {
+ si.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
+ si.setProgressLevel(downloadInfo);
+ updatedWorkspaceItems.add(si);
+ }
+ });
+ }
+ bindUpdatedWorkspaceItems(updatedWorkspaceItems);
+ }
+}
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 203f1ca..9889a80 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -15,21 +15,19 @@
*/
package com.android.launcher3.model;
-import android.content.ComponentName;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.util.InstantAppResolver;
import java.util.HashSet;
+import java.util.List;
/**
* Handles changes due to a sessions updates for a currently installing app.
@@ -61,30 +59,30 @@
}
synchronized (apps) {
- PromiseAppInfo updated = apps.updatePromiseInstallInfo(mInstallInfo);
- if (updated != null) {
- scheduleCallbackTask(c -> c.bindPromiseAppProgressUpdated(updated));
+ List<AppInfo> updatedAppInfos = apps.updatePromiseInstallInfo(mInstallInfo);
+ if (!updatedAppInfos.isEmpty()) {
+ for (AppInfo appInfo : updatedAppInfos) {
+ scheduleCallbackTask(c -> c.bindIncrementalDownloadProgressUpdated(appInfo));
+ }
}
bindApplicationsIfNeeded();
}
synchronized (dataModel) {
final HashSet<ItemInfo> updates = new HashSet<>();
- for (ItemInfo info : dataModel.itemsIdMap) {
- if (info instanceof WorkspaceItemInfo) {
- WorkspaceItemInfo si = (WorkspaceItemInfo) info;
- ComponentName cn = si.getTargetComponent();
- if (si.hasPromiseIconUi() && (cn != null)
- && mInstallInfo.packageName.equals(cn.getPackageName())) {
- si.setInstallProgress(mInstallInfo.progress);
- if (mInstallInfo.state == PackageInstallInfo.STATUS_FAILED) {
- // Mark this info as broken.
- si.status &= ~WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE;
- }
- updates.add(si);
+ dataModel.forAllWorkspaceItemInfos(mInstallInfo.user, si -> {
+ if (si.hasPromiseIconUi()
+ && mInstallInfo.packageName.equals(si.getTargetPackage())) {
+ int installProgress = mInstallInfo.progress;
+
+ si.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING);
+ if (mInstallInfo.state == PackageInstallInfo.STATUS_FAILED) {
+ // Mark this info as broken.
+ si.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
}
+ updates.add(si);
}
- }
+ });
for (LauncherAppWidgetInfo widget : dataModel.appWidgets) {
if (widget.providerName.getPackageName().equals(mInstallInfo.packageName)) {
@@ -94,12 +92,7 @@
}
if (!updates.isEmpty()) {
- scheduleCallbackTask(new CallbackTask() {
- @Override
- public void execute(Callbacks callbacks) {
- callbacks.bindRestoreItemsChange(updates);
- }
- });
+ scheduleCallbackTask(callbacks -> callbacks.bindRestoreItemsChange(updates));
}
}
}
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 7cd467e..82b0f7c 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -22,18 +22,17 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
-import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
-import com.android.launcher3.InstallShortcutReceiver;
+import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.SessionCommitReceiver;
-import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.IconCache;
@@ -42,10 +41,11 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.FlagOp;
-import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
@@ -54,6 +54,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -92,10 +93,13 @@
final String[] packages = mPackages;
final int N = packages.length;
- FlagOp flagOp = FlagOp.NO_OP;
+ final FlagOp flagOp;
final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
- ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packageSet, mUser);
+ final ItemInfoMatcher matcher = mOp == OP_USER_AVAILABILITY_CHANGE
+ ? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user
+ : ItemInfoMatcher.ofPackages(packageSet, mUser);
final HashSet<ComponentName> removedComponents = new HashSet<>();
+ final HashMap<String, List<LauncherActivityInfo>> activitiesLists = new HashMap<>();
switch (mOp) {
case OP_ADD: {
@@ -105,12 +109,8 @@
if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
appsList.removePackage(packages[i], mUser);
}
- appsList.addPackage(context, packages[i], mUser);
-
- // Automatically add homescreen icon for work profile apps for below O device.
- if (!Utilities.ATLEAST_OREO && !Process.myUserHandle().equals(mUser)) {
- SessionCommitReceiver.queueAppIconAddition(context, packages[i], mUser);
- }
+ activitiesLists.put(
+ packages[i], appsList.addPackage(context, packages[i], mUser));
}
flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;
@@ -121,8 +121,17 @@
for (int i = 0; i < N; i++) {
if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
iconCache.updateIconsForPkg(packages[i], mUser);
- appsList.updatePackage(context, packages[i], mUser);
+ activitiesLists.put(
+ packages[i], appsList.updatePackage(context, packages[i], mUser));
app.getWidgetCache().removePackage(packages[i], mUser);
+
+ // The update may have changed which shortcuts/widgets are available.
+ // Refresh the widgets for the package if we have an activity running.
+ Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+ if (launcher != null) {
+ launcher.refreshAndBindWidgetsForPackageUser(
+ new PackageUserKey(packages[i], mUser));
+ }
}
}
// Since package was just updated, the target must be available now.
@@ -158,19 +167,22 @@
flagOp = ums.isUserQuiet(mUser)
? FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER)
: FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER);
- // We want to update all packages for this user.
- matcher = ItemInfoMatcher.ofUser(mUser);
appsList.updateDisabledFlags(matcher, flagOp);
// We are not synchronizing here, as int operations are atomic
appsList.setFlags(FLAG_QUIET_MODE_ENABLED, ums.isAnyProfileQuietModeEnabled());
break;
}
+ default:
+ flagOp = FlagOp.NO_OP;
+ break;
}
bindApplicationsIfNeeded();
- final IntSparseArrayMap<Boolean> removedShortcuts = new IntSparseArrayMap<>();
+ final IntSet removedShortcuts = new IntSet();
+ // Shortcuts to keep even if the corresponding app was removed
+ final IntSet forceKeepShortcuts = new IntSet();
// Update shortcut infos
if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
@@ -180,118 +192,129 @@
// For system apps, package manager send OP_UPDATE when an app is enabled.
final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE;
synchronized (dataModel) {
- for (ItemInfo info : dataModel.itemsIdMap) {
- if (info instanceof WorkspaceItemInfo && mUser.equals(info.user)) {
- WorkspaceItemInfo si = (WorkspaceItemInfo) info;
- boolean infoUpdated = false;
- boolean shortcutUpdated = false;
+ dataModel.forAllWorkspaceItemInfos(mUser, si -> {
- // Update shortcuts which use iconResource.
- if ((si.iconResource != null)
- && packageSet.contains(si.iconResource.packageName)) {
- LauncherIcons li = LauncherIcons.obtain(context);
- BitmapInfo iconInfo = li.createIconBitmap(si.iconResource);
- li.recycle();
- if (iconInfo != null) {
- si.bitmap = iconInfo;
+ boolean infoUpdated = false;
+ boolean shortcutUpdated = false;
+
+ // Update shortcuts which use iconResource.
+ if ((si.iconResource != null)
+ && packageSet.contains(si.iconResource.packageName)) {
+ LauncherIcons li = LauncherIcons.obtain(context);
+ BitmapInfo iconInfo = li.createIconBitmap(si.iconResource);
+ li.recycle();
+ if (iconInfo != null) {
+ si.bitmap = iconInfo;
+ infoUpdated = true;
+ }
+ }
+
+ ComponentName cn = si.getTargetComponent();
+ if (cn != null && matcher.matches(si, cn)) {
+ String packageName = cn.getPackageName();
+
+ if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
+ forceKeepShortcuts.add(si.id);
+ if (mOp == OP_REMOVE) {
+ return;
+ }
+ }
+
+ if (si.isPromise() && isNewApkAvailable) {
+ boolean isTargetValid = !cn.getClassName().equals(
+ IconCache.EMPTY_CLASS_NAME);
+ if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ List<ShortcutInfo> shortcut =
+ new ShortcutRequest(context, mUser)
+ .forPackage(cn.getPackageName(),
+ si.getDeepShortcutId())
+ .query(ShortcutRequest.PINNED);
+ if (shortcut.isEmpty()) {
+ isTargetValid = false;
+ } else {
+ si.updateFromDeepShortcutInfo(shortcut.get(0), context);
+ infoUpdated = true;
+ }
+ } else if (isTargetValid) {
+ isTargetValid = context.getSystemService(LauncherApps.class)
+ .isActivityEnabled(cn, mUser);
+ }
+ if (!isTargetValid && si.hasStatusFlag(
+ FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
+ if (updateWorkspaceItemIntent(context, si, packageName)) {
+ infoUpdated = true;
+ } else if (si.hasPromiseIconUi()) {
+ removedShortcuts.add(si.id);
+ return;
+ }
+ } else if (!isTargetValid) {
+ removedShortcuts.add(si.id);
+ FileLog.e(TAG, "Restored shortcut no longer valid "
+ + si.getIntent());
+ return;
+ } else {
+ si.status = WorkspaceItemInfo.DEFAULT;
+ infoUpdated = true;
+ }
+ } else if (isNewApkAvailable && removedComponents.contains(cn)) {
+ if (updateWorkspaceItemIntent(context, si, packageName)) {
infoUpdated = true;
}
}
- ComponentName cn = si.getTargetComponent();
- if (cn != null && matcher.matches(si, cn)) {
- String packageName = cn.getPackageName();
-
- if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
- removedShortcuts.put(si.id, false);
- if (mOp == OP_REMOVE) {
- continue;
- }
- }
-
- if (si.isPromise() && isNewApkAvailable) {
- boolean isTargetValid = true;
- if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
- List<ShortcutInfo> shortcut =
- new ShortcutRequest(context, mUser)
- .forPackage(cn.getPackageName(),
- si.getDeepShortcutId())
- .query(ShortcutRequest.PINNED);
- if (shortcut.isEmpty()) {
- isTargetValid = false;
- } else {
- si.updateFromDeepShortcutInfo(shortcut.get(0), context);
- infoUpdated = true;
- }
- } else if (!cn.getClassName().equals(IconCache.EMPTY_CLASS_NAME)) {
- isTargetValid = context.getSystemService(LauncherApps.class)
- .isActivityEnabled(cn, mUser);
- }
- if (si.hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
- if (updateWorkspaceItemIntent(context, si, packageName)) {
- infoUpdated = true;
- } else if (si.hasPromiseIconUi()) {
- removedShortcuts.put(si.id, true);
- continue;
- }
- } else if (!isTargetValid) {
- removedShortcuts.put(si.id, true);
- FileLog.e(TAG, "Restored shortcut no longer valid "
- + si.getIntent());
- continue;
- } else {
- si.status = WorkspaceItemInfo.DEFAULT;
- infoUpdated = true;
- }
- } else if (isNewApkAvailable && removedComponents.contains(cn)) {
- if (updateWorkspaceItemIntent(context, si, packageName)) {
- infoUpdated = true;
- }
- }
-
- if (isNewApkAvailable &&
- si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+ if (isNewApkAvailable) {
+ List<LauncherActivityInfo> activities = activitiesLists.get(
+ packageName);
+ si.setProgressLevel(
+ activities == null || activities.isEmpty()
+ ? 100
+ : PackageManagerHelper.getLoadingProgress(
+ activities.get(0)),
+ PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
+ if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
iconCache.getTitleAndIcon(si, si.usingLowResIcon());
infoUpdated = true;
}
-
- int oldRuntimeFlags = si.runtimeStatusFlags;
- si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags);
- if (si.runtimeStatusFlags != oldRuntimeFlags) {
- shortcutUpdated = true;
- }
}
- if (infoUpdated || shortcutUpdated) {
- updatedWorkspaceItems.add(si);
+ int oldRuntimeFlags = si.runtimeStatusFlags;
+ si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags);
+ if (si.runtimeStatusFlags != oldRuntimeFlags) {
+ shortcutUpdated = true;
}
- if (infoUpdated) {
- getModelWriter().updateItemInDatabase(si);
- }
- } else if (info instanceof LauncherAppWidgetInfo && isNewApkAvailable) {
- LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info;
- if (mUser.equals(widgetInfo.user)
- && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
- && packageSet.contains(widgetInfo.providerName.getPackageName())) {
- widgetInfo.restoreStatus &=
- ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY &
- ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
+ }
- // adding this flag ensures that launcher shows 'click to setup'
- // if the widget has a config activity. In case there is no config
- // activity, it will be marked as 'restored' during bind.
- widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+ if (infoUpdated || shortcutUpdated) {
+ updatedWorkspaceItems.add(si);
+ }
+ if (infoUpdated && si.id != ItemInfo.NO_ID) {
+ getModelWriter().updateItemInDatabase(si);
+ }
+ });
- widgets.add(widgetInfo);
- getModelWriter().updateItemInDatabase(widgetInfo);
- }
+ for (LauncherAppWidgetInfo widgetInfo : dataModel.appWidgets) {
+ if (mUser.equals(widgetInfo.user)
+ && widgetInfo.hasRestoreFlag(
+ LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+ && packageSet.contains(widgetInfo.providerName.getPackageName())) {
+ widgetInfo.restoreStatus &=
+ ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
+ & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
+
+ // adding this flag ensures that launcher shows 'click to setup'
+ // if the widget has a config activity. In case there is no config
+ // activity, it will be marked as 'restored' during bind.
+ widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+
+ widgets.add(widgetInfo);
+ getModelWriter().updateItemInDatabase(widgetInfo);
}
}
}
bindUpdatedWorkspaceItems(updatedWorkspaceItems);
if (!removedShortcuts.isEmpty()) {
- deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedShortcuts, false));
+ deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedShortcuts));
}
if (!widgets.isEmpty()) {
@@ -319,14 +342,15 @@
if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
ItemInfoMatcher removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser)
.or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
- .and(ItemInfoMatcher.ofItemIds(removedShortcuts, true));
+ .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate());
deleteAndBindComponentsRemoved(removeMatch);
// Remove any queued items from the install queue
- InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
+ ItemInstallQueue.INSTANCE.get(context)
+ .removeFromInstallQueue(removedPackages, mUser);
}
- if (Utilities.ATLEAST_OREO && mOp == OP_ADD) {
+ if (mOp == OP_ADD) {
// Load widgets for the new package. Changes due to app updates are handled through
// AppWidgetHost events, this is just to initialize the long-press options.
for (int i = 0; i < N; i++) {
@@ -342,6 +366,11 @@
*/
private boolean updateWorkspaceItemIntent(Context context,
WorkspaceItemInfo si, String packageName) {
+ if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ // Do not update intent for deep shortcuts as they contain additional information
+ // about the shortcut.
+ return false;
+ }
// Try to find the best match activity.
Intent intent = new PackageManagerHelper(context).getAppLaunchIntent(packageName, mUser);
if (intent != null) {
diff --git a/src/com/android/launcher3/model/PredictionModel.java b/src/com/android/launcher3/model/PredictionModel.java
deleted file mode 100644
index 1429843..0000000
--- a/src/com/android/launcher3/model/PredictionModel.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.model;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.UserHandle;
-
-import androidx.annotation.AnyThread;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Model Helper for app predictions
- */
-public class PredictionModel implements ResourceBasedOverride {
-
- private static final String CACHED_ITEMS_KEY = "predicted_item_keys";
- private static final int MAX_CACHE_ITEMS = 5;
-
- protected Context mContext;
- private SharedPreferences mDevicePrefs;
- private UserCache mUserCache;
-
-
- /**
- * Retrieve instance of this object that can be overridden in runtime based on the build
- * variant of the application.
- */
- public static PredictionModel newInstance(Context context) {
- PredictionModel model = Overrides.getObject(PredictionModel.class, context,
- R.string.prediction_model_class);
- model.init(context);
- return model;
- }
-
- protected void init(Context context) {
- mContext = context;
- mDevicePrefs = Utilities.getDevicePrefs(mContext);
- mUserCache = UserCache.INSTANCE.get(mContext);
-
- }
- /**
- * Formats and stores a list of component key in device preferences.
- */
- @AnyThread
- public void cachePredictionComponentKeys(List<ComponentKey> componentKeys) {
- MODEL_EXECUTOR.execute(() -> {
- StringBuilder builder = new StringBuilder();
- int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS);
- for (int i = 0; i < count; i++) {
- builder.append(serializeComponentKeyToString(componentKeys.get(i)));
- builder.append("\n");
- }
- mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply();
- });
- }
-
- /**
- * parses and returns ComponentKeys saved by
- * {@link PredictionModel#cachePredictionComponentKeys(List)}
- */
- @WorkerThread
- public List<ComponentKey> getPredictionComponentKeys() {
- Preconditions.assertWorkerThread();
- ArrayList<ComponentKey> items = new ArrayList<>();
- String cachedBlob = mDevicePrefs.getString(CACHED_ITEMS_KEY, "");
- for (String line : cachedBlob.split("\n")) {
- ComponentKey key = getComponentKeyFromSerializedString(line);
- if (key != null) {
- items.add(key);
- }
-
- }
- return items;
- }
-
- private String serializeComponentKeyToString(ComponentKey componentKey) {
- long userSerialNumber = mUserCache.getSerialNumberForUser(componentKey.user);
- return componentKey.componentName.flattenToString() + "#" + userSerialNumber;
- }
-
- private ComponentKey getComponentKeyFromSerializedString(String str) {
- int sep = str.indexOf('#');
- if (sep < 0 || (sep + 1) >= str.length()) {
- return null;
- }
- ComponentName componentName = ComponentName.unflattenFromString(str.substring(0, sep));
- if (componentName == null) {
- return null;
- }
- try {
- long serialNumber = Long.parseLong(str.substring(sep + 1));
- UserHandle userHandle = mUserCache.getUserForSerialNumber(serialNumber);
- return userHandle != null ? new ComponentKey(componentName, userHandle) : null;
- } catch (NumberFormatException ex) {
- return null;
- }
- }
-}
diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
index eb3cb52..3798575 100644
--- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java
+++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
@@ -24,12 +24,11 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
-import com.android.launcher3.util.MultiHashMap;
import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.PackageUserKey;
import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Map.Entry;
+import java.util.Set;
/**
* Helper class to re-query app status when SD-card becomes available.
@@ -42,10 +41,9 @@
private final LauncherModel mModel;
private final Context mContext;
- private final MultiHashMap<UserHandle, String> mPackages;
+ private final Set<PackageUserKey> mPackages;
- public SdCardAvailableReceiver(LauncherAppState app,
- MultiHashMap<UserHandle, String> packages) {
+ public SdCardAvailableReceiver(LauncherAppState app, Set<PackageUserKey> packages) {
mModel = app.getModel();
mContext = app.getContext();
mPackages = packages;
@@ -55,19 +53,17 @@
public void onReceive(Context context, Intent intent) {
final LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
- for (Entry<UserHandle, ArrayList<String>> entry : mPackages.entrySet()) {
- UserHandle user = entry.getKey();
+ for (PackageUserKey puk : mPackages) {
+ UserHandle user = puk.mUser;
final ArrayList<String> packagesRemoved = new ArrayList<>();
final ArrayList<String> packagesUnavailable = new ArrayList<>();
- for (String pkg : new HashSet<>(entry.getValue())) {
- if (!launcherApps.isPackageEnabled(pkg, user)) {
- if (pmHelper.isAppOnSdcard(pkg, user)) {
- packagesUnavailable.add(pkg);
- } else {
- packagesRemoved.add(pkg);
- }
+ if (!launcherApps.isPackageEnabled(puk.mPackageName, user)) {
+ if (pmHelper.isAppOnSdcard(puk.mPackageName, user)) {
+ packagesUnavailable.add(puk.mPackageName);
+ } else {
+ packagesRemoved.add(puk.mPackageName);
}
}
if (!packagesRemoved.isEmpty()) {
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 1cbe5c2..4296d32 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -21,16 +21,17 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.MultiHashMap;
+import com.android.launcher3.util.PackageManagerHelper;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* Handles changes due to shortcut manager updates (deep shortcut changes)
@@ -54,54 +55,61 @@
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
final Context context = app.getContext();
// Find WorkspaceItemInfo's that have changed on the workspace.
- HashSet<ShortcutKey> removedKeys = new HashSet<>();
- MultiHashMap<ShortcutKey, WorkspaceItemInfo> keyToShortcutInfo = new MultiHashMap<>();
- HashSet<String> allIds = new HashSet<>();
+ ArrayList<WorkspaceItemInfo> matchingWorkspaceItems = new ArrayList<>();
- for (ItemInfo itemInfo : dataModel.itemsIdMap) {
- if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
- WorkspaceItemInfo si = (WorkspaceItemInfo) itemInfo;
- if (mPackageName.equals(si.getIntent().getPackage()) && si.user.equals(mUser)) {
- keyToShortcutInfo.addToList(ShortcutKey.fromItemInfo(si), si);
- allIds.add(si.getDeepShortcutId());
+ synchronized (dataModel) {
+ dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+ if ((si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+ && mPackageName.equals(si.getIntent().getPackage())) {
+ matchingWorkspaceItems.add(si);
}
- }
+ });
}
- final ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
- if (!keyToShortcutInfo.isEmpty()) {
+ if (!matchingWorkspaceItems.isEmpty()) {
+ if (mShortcuts.isEmpty()) {
+ // Verify that the app is indeed installed.
+ if (!new PackageManagerHelper(app.getContext())
+ .isAppInstalled(mPackageName, mUser)) {
+ // App is not installed, ignoring package events
+ return;
+ }
+ }
// Update the workspace to reflect the changes to updated shortcuts residing on it.
+ List<String> allLauncherKnownIds = matchingWorkspaceItems.stream()
+ .map(WorkspaceItemInfo::getDeepShortcutId)
+ .distinct()
+ .collect(Collectors.toList());
List<ShortcutInfo> shortcuts = new ShortcutRequest(context, mUser)
- .forPackage(mPackageName, new ArrayList<>(allIds))
+ .forPackage(mPackageName, allLauncherKnownIds)
.query(ShortcutRequest.ALL);
+
+ Set<String> nonPinnedIds = new HashSet<>(allLauncherKnownIds);
+ ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
for (ShortcutInfo fullDetails : shortcuts) {
- ShortcutKey key = ShortcutKey.fromInfo(fullDetails);
- List<WorkspaceItemInfo> workspaceItemInfos = keyToShortcutInfo.remove(key);
if (!fullDetails.isPinned()) {
- // The shortcut was previously pinned but is no longer, so remove it from
- // the workspace and our pinned shortcut counts.
- // Note that we put this check here, after querying for full details,
- // because there's a possible race condition between pinning and
- // receiving this callback.
- removedKeys.add(key);
continue;
}
- for (final WorkspaceItemInfo workspaceItemInfo : workspaceItemInfos) {
- workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context);
- app.getIconCache().getShortcutIcon(workspaceItemInfo, fullDetails);
- updatedWorkspaceItemInfos.add(workspaceItemInfo);
- }
+
+ String sid = fullDetails.getId();
+ nonPinnedIds.remove(sid);
+ matchingWorkspaceItems
+ .stream()
+ .filter(itemInfo -> sid.equals(itemInfo.getDeepShortcutId()))
+ .forEach(workspaceItemInfo -> {
+ workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context);
+ app.getIconCache().getShortcutIcon(workspaceItemInfo, fullDetails);
+ updatedWorkspaceItemInfos.add(workspaceItemInfo);
+ });
}
- }
- // If there are still entries in keyToShortcutInfo, that means that
- // the corresponding shortcuts weren't passed in onShortcutsChanged(). This
- // means they were cleared, so we remove and unpin them now.
- removedKeys.addAll(keyToShortcutInfo.keySet());
-
- bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos);
- if (!keyToShortcutInfo.isEmpty()) {
- deleteAndBindComponentsRemoved(ItemInfoMatcher.ofShortcutKeys(removedKeys));
+ bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos);
+ if (!nonPinnedIds.isEmpty()) {
+ deleteAndBindComponentsRemoved(ItemInfoMatcher.ofShortcutKeys(
+ nonPinnedIds.stream()
+ .map(id -> new ShortcutKey(mPackageName, mUser, id))
+ .collect(Collectors.toSet())));
+ }
}
if (mUpdateIdMap) {
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 7ec884f..5048e13 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -23,7 +23,6 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.shortcuts.ShortcutRequest;
@@ -73,27 +72,27 @@
ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
HashSet<ShortcutKey> removedKeys = new HashSet<>();
- for (ItemInfo itemInfo : dataModel.itemsIdMap) {
- if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
- && mUser.equals(itemInfo.user)) {
- WorkspaceItemInfo si = (WorkspaceItemInfo) itemInfo;
- if (mIsUserUnlocked) {
- ShortcutKey key = ShortcutKey.fromItemInfo(si);
- ShortcutInfo shortcut = pinnedShortcuts.get(key);
- // We couldn't verify the shortcut during loader. If its no longer available
- // (probably due to clear data), delete the workspace item as well
- if (shortcut == null) {
- removedKeys.add(key);
- continue;
+ synchronized (dataModel) {
+ dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+ if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ if (mIsUserUnlocked) {
+ ShortcutKey key = ShortcutKey.fromItemInfo(si);
+ ShortcutInfo shortcut = pinnedShortcuts.get(key);
+ // We couldn't verify the shortcut during loader. If its no longer available
+ // (probably due to clear data), delete the workspace item as well
+ if (shortcut == null) {
+ removedKeys.add(key);
+ return;
+ }
+ si.runtimeStatusFlags &= ~FLAG_DISABLED_LOCKED_USER;
+ si.updateFromDeepShortcutInfo(shortcut, context);
+ app.getIconCache().getShortcutIcon(si, shortcut);
+ } else {
+ si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
}
- si.runtimeStatusFlags &= ~FLAG_DISABLED_LOCKED_USER;
- si.updateFromDeepShortcutInfo(shortcut, context);
- app.getIconCache().getShortcutIcon(si, shortcut);
- } else {
- si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
+ updatedWorkspaceItemInfos.add(si);
}
- updatedWorkspaceItemInfos.add(si);
- }
+ });
}
bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos);
if (!removedKeys.isEmpty()) {
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index 37c089e..7198d54 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -1,14 +1,18 @@
package com.android.launcher3.model;
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
+import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.Utilities;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
/**
* An wrapper over various items displayed in a widget picker,
@@ -43,4 +47,31 @@
activityInfo = info;
spanX = spanY = 1;
}
+
+ /**
+ * Returns {@code true} if this {@link WidgetItem} has the same type as the given
+ * {@code otherItem}.
+ *
+ * For example, both items are widgets or both items are shortcuts.
+ */
+ public boolean hasSameType(WidgetItem otherItem) {
+ if (widgetInfo != null && otherItem.widgetInfo != null) {
+ return true;
+ }
+ if (activityInfo != null && otherItem.activityInfo != null) {
+ return true;
+ }
+ return false;
+ }
+
+ /** Returns whether this {@link WidgetItem} has a preview layout that can be used. */
+ @SuppressLint("NewApi") // Already added API check.
+ public boolean hasPreviewLayout() {
+ return ATLEAST_S && widgetInfo != null && widgetInfo.previewLayout != Resources.ID_NULL;
+ }
+
+ /** Returns whether this {@link WidgetItem} is for a shortcut rather than an app widget. */
+ public boolean isShortcut() {
+ return activityInfo != null;
+ }
}
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index b17b062..7f70bad 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -28,10 +28,13 @@
import android.os.UserHandle;
import android.os.UserManager;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.Utilities;
+import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageManagerHelper;
@@ -91,8 +94,6 @@
componentName = info.componentName;
title = Utilities.trim(info.title);
intent = new Intent(info.intent);
- user = info.user;
- runtimeStatusFlags = info.runtimeStatusFlags;
}
@VisibleForTesting
@@ -104,13 +105,38 @@
this.intent = intent;
}
+ public AppInfo(@NonNull PackageInstallInfo installInfo) {
+ componentName = installInfo.componentName;
+ intent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setComponent(componentName)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ setProgressLevel(installInfo);
+ user = installInfo.user;
+ }
+
@Override
protected String dumpProperties() {
return super.dumpProperties() + " componentName=" + componentName;
}
public WorkspaceItemInfo makeWorkspaceItem() {
- return new WorkspaceItemInfo(this);
+ WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo(this);
+
+ if ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+ // We need to update the component name when the apk is installed
+ workspaceItemInfo.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
+ // Since the user is manually placing it on homescreen, it should not be auto-removed
+ // later
+ workspaceItemInfo.status |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
+ workspaceItemInfo.status |= FLAG_INSTALL_SESSION_ACTIVE;
+ }
+ if ((runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
+ workspaceItemInfo.runtimeStatusFlags |= FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
+ }
+
+ return workspaceItemInfo;
}
public ComponentKey toComponentKey() {
@@ -129,6 +155,12 @@
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}
+ @Nullable
+ @Override
+ public ComponentName getTargetComponent() {
+ return componentName;
+ }
+
public static void updateRuntimeFlagsForActivityTarget(
ItemInfoWithIcon info, LauncherActivityInfo lai) {
ApplicationInfo appInfo = lai.getApplicationInfo();
@@ -138,12 +170,16 @@
info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
? FLAG_SYSTEM_NO : FLAG_SYSTEM_YES;
- if (Utilities.ATLEAST_OREO
- && appInfo.targetSdkVersion >= Build.VERSION_CODES.O
+ if (appInfo.targetSdkVersion >= Build.VERSION_CODES.O
&& Process.myUserHandle().equals(lai.getUser())) {
// The icon for a non-primary user is badged, hence it's not exactly an adaptive icon.
info.runtimeStatusFlags |= FLAG_ADAPTIVE_ICON;
}
+
+ // Sets the progress level, installation and incremental download flags.
+ info.setProgressLevel(
+ PackageManagerHelper.getLoadingProgress(lai),
+ PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
}
@Override
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index 41ccbd7..cd2ef35 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -20,15 +20,9 @@
import static androidx.core.util.Preconditions.checkNotNull;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL;
import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_CUSTOM;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_EMPTY;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_SUGGESTED;
import android.os.Process;
@@ -40,16 +34,15 @@
import com.android.launcher3.folder.FolderNameInfos;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logger.LauncherAtom.Attribute;
+import com.android.launcher3.logger.LauncherAtom.FolderIcon;
import com.android.launcher3.logger.LauncherAtom.FromState;
import com.android.launcher3.logger.LauncherAtom.ToState;
import com.android.launcher3.model.ModelWriter;
-import com.android.launcher3.userevent.LauncherLogProto;
-import com.android.launcher3.userevent.LauncherLogProto.Target;
-import com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState;
-import com.android.launcher3.userevent.LauncherLogProto.Target.ToFolderLabelState;
import com.android.launcher3.util.ContentWriter;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.OptionalInt;
import java.util.stream.IntStream;
@@ -147,9 +140,16 @@
* @param item
*/
public void remove(WorkspaceItemInfo item, boolean animate) {
- contents.remove(item);
+ removeAll(Collections.singletonList(item), animate);
+ }
+
+ /**
+ * Remove all matching app or shortcut. Does not change the DB.
+ */
+ public void removeAll(List<WorkspaceItemInfo> items, boolean animate) {
+ contents.removeAll(items);
for (int i = 0; i < mListeners.size(); i++) {
- mListeners.get(i).onRemove(item);
+ mListeners.get(i).onRemove(items);
}
itemsChanged(animate);
}
@@ -176,9 +176,9 @@
}
public interface FolderListener {
- public void onAdd(WorkspaceItemInfo item, int rank);
- public void onRemove(WorkspaceItemInfo item);
- public void onItemsChanged(boolean animate);
+ void onAdd(WorkspaceItemInfo item, int rank);
+ void onRemove(List<WorkspaceItemInfo> item);
+ void onItemsChanged(boolean animate);
}
public boolean hasOption(int optionFlag) {
@@ -209,8 +209,13 @@
@Override
public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
+ FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
+ .setCardinality(contents.size());
+ if (LabelState.SUGGESTED.equals(getLabelState())) {
+ folderIcon.setLabelInfo(title.toString());
+ }
return getDefaultItemInfoBuilder()
- .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
+ .setFolderIcon(folderIcon)
.setRank(rank)
.setAttribute(getLabelState().mLogAttribute)
.setContainerInfo(getContainerInfo())
@@ -359,113 +364,4 @@
}
return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
}
-
- /**
- * Returns {@link LauncherLogProto.LauncherEvent} to log current folder label info.
- *
- * @deprecated This method is used only for validation purpose and soon will be removed.
- */
- @Deprecated
- public LauncherLogProto.LauncherEvent getFolderLabelStateLauncherEvent(FromState fromState,
- ToState toState) {
- return LauncherLogProto.LauncherEvent.newBuilder()
- .setAction(LauncherLogProto.Action
- .newBuilder()
- .setType(LauncherLogProto.Action.Type.SOFT_KEYBOARD))
- .addSrcTarget(Target
- .newBuilder()
- .setType(Target.Type.ITEM)
- .setItemType(LauncherLogProto.ItemType.EDITTEXT)
- .setFromFolderLabelState(convertFolderLabelState(fromState))
- .setToFolderLabelState(convertFolderLabelState(toState)))
- .addSrcTarget(Target.newBuilder()
- .setType(Target.Type.CONTAINER)
- .setContainerType(LauncherLogProto.ContainerType.FOLDER)
- .setPageIndex(screenId)
- .setGridX(cellX)
- .setGridY(cellY)
- .setCardinality(contents.size()))
- .addSrcTarget(newParentContainerTarget())
- .build();
- }
-
- /**
- * @deprecated This method is used only for validation purpose and soon will be removed.
- */
- @Deprecated
- private Target.Builder newParentContainerTarget() {
- Target.Builder builder = Target.newBuilder().setType(Target.Type.CONTAINER);
- switch (container) {
- case CONTAINER_HOTSEAT:
- return builder.setContainerType(LauncherLogProto.ContainerType.HOTSEAT);
- case CONTAINER_DESKTOP:
- return builder.setContainerType(LauncherLogProto.ContainerType.WORKSPACE);
- default:
- throw new AssertionError(String
- .format("Expected container to be either %s or %s but found %s.",
- CONTAINER_HOTSEAT,
- CONTAINER_DESKTOP,
- container));
- }
- }
-
- /**
- * @deprecated This method is used only for validation purpose and soon will be removed.
- */
- @Deprecated
- private static FromFolderLabelState convertFolderLabelState(FromState fromState) {
- switch (fromState) {
- case FROM_EMPTY:
- return FROM_EMPTY;
- case FROM_SUGGESTED:
- return FROM_SUGGESTED;
- case FROM_CUSTOM:
- return FROM_CUSTOM;
- default:
- return FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
- }
- }
-
- /**
- * @deprecated This method is used only for validation purpose and soon will be removed.
- */
- @Deprecated
- private static ToFolderLabelState convertFolderLabelState(ToState toState) {
- switch (toState) {
- case UNCHANGED:
- return ToFolderLabelState.UNCHANGED;
- case TO_SUGGESTION0:
- return ToFolderLabelState.TO_SUGGESTION0_WITH_VALID_PRIMARY;
- case TO_SUGGESTION1_WITH_VALID_PRIMARY:
- return ToFolderLabelState.TO_SUGGESTION1_WITH_VALID_PRIMARY;
- case TO_SUGGESTION1_WITH_EMPTY_PRIMARY:
- return ToFolderLabelState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
- case TO_SUGGESTION2_WITH_VALID_PRIMARY:
- return ToFolderLabelState.TO_SUGGESTION2_WITH_VALID_PRIMARY;
- case TO_SUGGESTION2_WITH_EMPTY_PRIMARY:
- return ToFolderLabelState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
- case TO_SUGGESTION3_WITH_VALID_PRIMARY:
- return ToFolderLabelState.TO_SUGGESTION3_WITH_VALID_PRIMARY;
- case TO_SUGGESTION3_WITH_EMPTY_PRIMARY:
- return ToFolderLabelState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
- case TO_EMPTY_WITH_VALID_PRIMARY:
- return ToFolderLabelState.TO_EMPTY_WITH_VALID_PRIMARY;
- case TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
- return ToFolderLabelState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
- case TO_EMPTY_WITH_EMPTY_SUGGESTIONS:
- return ToFolderLabelState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
- case TO_EMPTY_WITH_SUGGESTIONS_DISABLED:
- return ToFolderLabelState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
- case TO_CUSTOM_WITH_VALID_PRIMARY:
- return ToFolderLabelState.TO_CUSTOM_WITH_VALID_PRIMARY;
- case TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
- return ToFolderLabelState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
- case TO_CUSTOM_WITH_EMPTY_SUGGESTIONS:
- return ToFolderLabelState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS;
- case TO_CUSTOM_WITH_SUGGESTIONS_DISABLED:
- return ToFolderLabelState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED;
- default:
- return ToFolderLabelState.TO_FOLDER_LABEL_STATE_UNSPECIFIED;
- }
- }
}
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 0d3ddad..7091d2b 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -26,12 +26,14 @@
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
+import static com.android.launcher3.LauncherSettings.Favorites.EXTENDED_CONTAINERS;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_TASK;
import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.CONTAINER_NOT_SET;
+import static com.android.launcher3.shortcuts.ShortcutKey.EXTRA_SHORTCUT_ID;
import android.content.ComponentName;
import android.content.ContentValues;
@@ -50,8 +52,10 @@
import com.android.launcher3.logger.LauncherAtom.PredictionContainer;
import com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
import com.android.launcher3.logger.LauncherAtom.SettingsContainer;
+import com.android.launcher3.logger.LauncherAtom.Shortcut;
import com.android.launcher3.logger.LauncherAtom.ShortcutsContainer;
import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer;
+import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers;
import com.android.launcher3.model.ModelWriter;
import com.android.launcher3.util.ContentWriter;
@@ -62,8 +66,10 @@
*/
public class ItemInfo {
- public static final boolean DEBUG = true;
+ public static final boolean DEBUG = false;
public static final int NO_ID = -1;
+ // An id that doesn't match any item, including predicted apps with have an id=NO_ID
+ public static final int NO_MATCHING_ID = Integer.MIN_VALUE;
/**
* The id in the settings database for this item
@@ -162,6 +168,8 @@
cellY = info.cellY;
spanX = info.spanX;
spanY = info.spanY;
+ minSpanX = info.minSpanX;
+ minSpanY = info.minSpanY;
rank = info.rank;
screenId = info.screenId;
itemType = info.itemType;
@@ -180,6 +188,24 @@
return Optional.ofNullable(getIntent()).map(Intent::getComponent).orElse(mComponentName);
}
+ /**
+ * Returns this item's package name.
+ *
+ * Prioritizes the component package name, then uses the intent package name as a fallback.
+ * This ensures deep shortcuts are supported.
+ */
+ @Nullable
+ public String getTargetPackage() {
+ ComponentName component = getTargetComponent();
+ Intent intent = getIntent();
+
+ return component != null
+ ? component.getPackageName()
+ : intent != null
+ ? intent.getPackage()
+ : null;
+ }
+
public void writeToValues(ContentWriter writer) {
writer.put(LauncherSettings.Favorites.ITEM_TYPE, itemType)
.put(LauncherSettings.Favorites.CONTAINER, container)
@@ -223,7 +249,7 @@
protected String dumpProperties() {
return "id=" + id
+ " type=" + LauncherSettings.Favorites.itemTypeToString(itemType)
- + " container=" + LauncherSettings.Favorites.containerToString(container)
+ + " container=" + getContainerInfo()
+ " targetComponent=" + getTargetComponent()
+ " screen=" + screenId
+ " cell(" + cellX + "," + cellY + ")"
@@ -257,12 +283,6 @@
}
/**
- * Can be overridden by inherited classes to fill in {@link LauncherAtom.ItemInfo}
- */
- public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
- }
-
- /**
* Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
*/
public LauncherAtom.ItemInfo buildProto() {
@@ -285,6 +305,18 @@
.orElse(LauncherAtom.Application.newBuilder()));
break;
case ITEM_TYPE_DEEP_SHORTCUT:
+ itemBuilder
+ .setShortcut(nullableComponent
+ .map(component -> {
+ Shortcut.Builder lsb = Shortcut.newBuilder()
+ .setShortcutName(component.flattenToShortString());
+ Optional.ofNullable(getIntent())
+ .map(i -> i.getStringExtra(EXTRA_SHORTCUT_ID))
+ .ifPresent(lsb::setShortcutId);
+ return lsb;
+ })
+ .orElse(LauncherAtom.Shortcut.newBuilder()));
+ break;
case ITEM_TYPE_SHORTCUT:
itemBuilder
.setShortcut(nullableComponent
@@ -338,13 +370,16 @@
return itemBuilder.build();
}
- LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder() {
+ protected LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder() {
LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
itemBuilder.setIsWork(user != Process.myUserHandle());
return itemBuilder;
}
- protected ContainerInfo getContainerInfo() {
+ /**
+ * Returns {@link ContainerInfo} used when logging this item.
+ */
+ public ContainerInfo getContainerInfo() {
switch (container) {
case CONTAINER_HOTSEAT:
return ContainerInfo.newBuilder()
@@ -392,12 +427,23 @@
return ContainerInfo.newBuilder()
.setTaskSwitcherContainer(TaskSwitcherContainer.getDefaultInstance())
.build();
-
+ case EXTENDED_CONTAINERS:
+ return ContainerInfo.newBuilder()
+ .setExtendedContainers(getExtendedContainer())
+ .build();
}
return ContainerInfo.getDefaultInstance();
}
/**
+ * Returns non-AOSP container wrapped by {@link ExtendedContainers} object. Should be overridden
+ * by build variants.
+ */
+ protected ExtendedContainers getExtendedContainer() {
+ return ExtendedContainers.getDefaultInstance();
+ }
+
+ /**
* Returns shallow copy of the object.
*/
public ItemInfo makeShallowCopy() {
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index d95f94f..6813b97 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -16,7 +16,16 @@
package com.android.launcher3.model.data;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
+
import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.FastBitmapDrawable;
+import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.util.PackageManagerHelper;
/**
* Represents an ItemInfo which also holds an icon.
@@ -88,17 +97,42 @@
public static final int FLAG_ICON_BADGED = 1 << 9;
/**
+ * The icon is being installed. If {@link WorkspaceItemInfo#FLAG_RESTORED_ICON} or
+ * {@link WorkspaceItemInfo#FLAG_AUTOINSTALL_ICON} is set, then the icon is either being
+ * installed or is in a broken state.
+ */
+ public static final int FLAG_INSTALL_SESSION_ACTIVE = 1 << 10;
+
+ /**
+ * This icon is still being downloaded.
+ */
+ public static final int FLAG_INCREMENTAL_DOWNLOAD_ACTIVE = 1 << 11;
+
+ public static final int FLAG_SHOW_DOWNLOAD_PROGRESS_MASK = FLAG_INSTALL_SESSION_ACTIVE
+ | FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
+
+ /**
* Status associated with the system state of the underlying item. This is calculated every
* time a new info is created and not persisted on the disk.
*/
public int runtimeStatusFlags = 0;
+ /**
+ * The download progress of the package that this shortcut represents. For legacy apps, this
+ * will always be the installation progress. For apps that support incremental downloads, this
+ * will only match be the installation progress until the app is installed, then this will the
+ * total download progress.
+ */
+ private int mProgressLevel = 100;
+
protected ItemInfoWithIcon() { }
protected ItemInfoWithIcon(ItemInfoWithIcon info) {
super(info);
bitmap = info.bitmap;
+ mProgressLevel = info.mProgressLevel;
runtimeStatusFlags = info.runtimeStatusFlags;
+ user = info.user;
}
@Override
@@ -114,7 +148,91 @@
}
/**
+ * Returns whether the app this shortcut represents is able to be started. For legacy apps,
+ * this returns whether it is fully installed. For apps that support incremental downloads,
+ * this returns whether the app is either fully downloaded or has installed and is downloading
+ * incrementally.
+ */
+ public boolean isAppStartable() {
+ return ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) == 0)
+ && (((runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0)
+ || mProgressLevel == 100);
+ }
+
+ /**
+ * Returns the download progress for the app this shortcut represents. If this app is not yet
+ * installed or does not support incremental downloads, this will return the installation
+ * progress.
+ */
+ public int getProgressLevel() {
+ if ((runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
+ return mProgressLevel;
+ }
+ return 100;
+ }
+
+ /**
+ * Sets the download progress for the app this shortcut represents. If this app is not yet
+ * installed or does not support incremental downloads, this will set
+ * {@code FLAG_INSTALL_SESSION_ACTIVE}. If this app is downloading incrementally, this will
+ * set {@code FLAG_INCREMENTAL_DOWNLOAD_ACTIVE}. Otherwise, this will remove both flags.
+ */
+ public void setProgressLevel(PackageInstallInfo installInfo) {
+ setProgressLevel(installInfo.progress, installInfo.state);
+ }
+
+ /**
+ * Sets the download progress for the app this shortcut represents.
+ */
+ public void setProgressLevel(int progress, int status) {
+ if (status == PackageInstallInfo.STATUS_INSTALLING) {
+ mProgressLevel = progress;
+ runtimeStatusFlags = progress < 100
+ ? runtimeStatusFlags | FLAG_INSTALL_SESSION_ACTIVE
+ : runtimeStatusFlags & ~FLAG_INSTALL_SESSION_ACTIVE;
+ } else if (status == PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING) {
+ mProgressLevel = progress;
+ runtimeStatusFlags = runtimeStatusFlags & ~FLAG_INSTALL_SESSION_ACTIVE;
+ runtimeStatusFlags = progress < 100
+ ? runtimeStatusFlags | FLAG_INCREMENTAL_DOWNLOAD_ACTIVE
+ : runtimeStatusFlags & ~FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
+ } else {
+ mProgressLevel = status == PackageInstallInfo.STATUS_INSTALLED ? 100 : 0;
+ runtimeStatusFlags &= ~FLAG_INSTALL_SESSION_ACTIVE;
+ runtimeStatusFlags &= ~FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
+ }
+ }
+
+ /** Creates an intent to that launches the app store at this app's page. */
+ @Nullable
+ public Intent getMarketIntent(Context context) {
+ ComponentName componentName = getTargetComponent();
+
+ return componentName != null
+ ? new PackageManagerHelper(context).getMarketIntent(componentName.getPackageName())
+ : null;
+ }
+
+ /**
* @return a copy of this
*/
public abstract ItemInfoWithIcon clone();
+
+
+ /**
+ * Returns a FastBitmapDrawable with the icon.
+ */
+ public FastBitmapDrawable newIcon(Context context) {
+ return newIcon(context, false);
+ }
+
+ /**
+ * Returns a FastBitmapDrawable with the icon and context theme applied
+ */
+ public FastBitmapDrawable newIcon(Context context, boolean applyTheme) {
+ FastBitmapDrawable drawable = applyTheme
+ ? bitmap.newThemedIcon(context) : bitmap.newIcon(context);
+ drawable.setIsDisabled(isDisabled());
+ return drawable;
+ }
}
diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index b0d19a6..0283d5f 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -16,18 +16,28 @@
package com.android.launcher3.model.data;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PIN_WIDGETS;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
import android.appwidget.AppWidgetHostView;
import android.content.ComponentName;
import android.content.Intent;
+import android.content.res.Resources;
import android.os.Process;
import androidx.annotation.Nullable;
-import com.android.launcher3.AppWidgetResizeFrame;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.util.WidgetSizes;
/**
* Represents a widget (either instantiated or about to be) in the Launcher.
@@ -82,6 +92,18 @@
public static final int CUSTOM_WIDGET_ID = -100;
/**
+ * Flags for recording all the features that a widget has enabled.
+ * @see widgetFeatures
+ */
+ public static final int FEATURE_RECONFIGURABLE = 1;
+ public static final int FEATURE_OPTIONAL_CONFIGURATION = 1 << 1;
+ public static final int FEATURE_PREVIEW_LAYOUT = 1 << 2;
+ public static final int FEATURE_TARGET_CELL_SIZE = 1 << 3;
+ public static final int FEATURE_MIN_SIZE = 1 << 4;
+ public static final int FEATURE_MAX_SIZE = 1 << 5;
+ public static final int FEATURE_ROUNDED_CORNERS = 1 << 6;
+
+ /**
* Identifier for this widget when talking with
* {@link android.appwidget.AppWidgetManager} for updates.
*/
@@ -114,8 +136,19 @@
*/
public PackageItemInfo pendingItemInfo;
+ /**
+ * Contains a binary representation indicating which widget features are enabled. This value is
+ * -1 if widget features could not be identified.
+ */
+ private int widgetFeatures;
+
private boolean mHasNotifiedInitialWidgetSizeChanged;
+ /**
+ * The container from which this widget was added (e.g. widgets tray, pin widget, search)
+ */
+ public int sourceContainer = LauncherSettings.Favorites.CONTAINER_UNKNOWN;
+
public LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) {
this.appWidgetId = appWidgetId;
this.providerName = providerName;
@@ -130,11 +163,18 @@
// to indicate that they should be calculated based on the layout and minWidth/minHeight
spanX = -1;
spanY = -1;
+ widgetFeatures = -1;
// We only support app widgets on current user.
user = Process.myUserHandle();
restoreStatus = RESTORE_COMPLETED;
}
+ public LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName,
+ LauncherAppWidgetProviderInfo providerInfo, AppWidgetHostView hostView) {
+ this(appWidgetId, providerName);
+ widgetFeatures = computeWidgetFeatures(providerInfo, hostView);
+ }
+
/** Used for testing **/
public LauncherAppWidgetInfo() {
itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
@@ -157,7 +197,8 @@
.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, providerName.flattenToString())
.put(LauncherSettings.Favorites.RESTORED, restoreStatus)
.put(LauncherSettings.Favorites.OPTIONS, options)
- .put(LauncherSettings.Favorites.INTENT, bindOptions);
+ .put(LauncherSettings.Favorites.INTENT, bindOptions)
+ .put(LauncherSettings.Favorites.APPWIDGET_SOURCE, sourceContainer);
}
/**
@@ -166,7 +207,7 @@
*/
public void onBindAppWidget(Launcher launcher, AppWidgetHostView hostView) {
if (!mHasNotifiedInitialWidgetSizeChanged) {
- AppWidgetResizeFrame.updateWidgetSizeRanges(hostView, launcher, spanX, spanY);
+ WidgetSizes.updateWidgetSizeRanges(hostView, launcher, spanX, spanY);
mHasNotifiedInitialWidgetSizeChanged = true;
}
}
@@ -196,12 +237,58 @@
return (options & option) != 0;
}
+ @SuppressWarnings("NewApi")
+ private static int computeWidgetFeatures(
+ LauncherAppWidgetProviderInfo providerInfo, AppWidgetHostView hostView) {
+ int widgetFeatures = 0;
+ if (providerInfo.isReconfigurable()) {
+ widgetFeatures |= FEATURE_RECONFIGURABLE;
+ }
+ if (providerInfo.isConfigurationOptional()) {
+ widgetFeatures |= FEATURE_OPTIONAL_CONFIGURATION;
+ }
+ if (ATLEAST_S && providerInfo.previewLayout != Resources.ID_NULL) {
+ widgetFeatures |= FEATURE_PREVIEW_LAYOUT;
+ }
+ if (ATLEAST_S && providerInfo.targetCellWidth > 0 || providerInfo.targetCellHeight > 0) {
+ widgetFeatures |= FEATURE_TARGET_CELL_SIZE;
+ }
+ if (providerInfo.minResizeWidth > 0 || providerInfo.minResizeHeight > 0) {
+ widgetFeatures |= FEATURE_MIN_SIZE;
+ }
+ if (ATLEAST_S && providerInfo.maxResizeWidth > 0 || providerInfo.maxResizeHeight > 0) {
+ widgetFeatures |= FEATURE_MAX_SIZE;
+ }
+ if (hostView instanceof LauncherAppWidgetHostView &&
+ ((LauncherAppWidgetHostView) hostView).hasEnforcedCornerRadius()) {
+ widgetFeatures |= FEATURE_ROUNDED_CORNERS;
+ }
+ return widgetFeatures;
+ }
+
+ public static LauncherAtom.Attribute getAttribute(int container) {
+ switch (container) {
+ case CONTAINER_WIDGETS_TRAY:
+ return LauncherAtom.Attribute.WIDGETS;
+ case CONTAINER_BOTTOM_WIDGETS_TRAY:
+ return LauncherAtom.Attribute.WIDGETS_BOTTOM_TRAY;
+ case CONTAINER_PIN_WIDGETS:
+ return LauncherAtom.Attribute.PINITEM;
+ case CONTAINER_WIDGETS_PREDICTION:
+ return LauncherAtom.Attribute.WIDGETS_TRAY_PREDICTION;
+ case CONTAINER_ALL_APPS:
+ return LauncherAtom.Attribute.ALL_APPS_SEARCH_RESULT_WIDGETS;
+ default:
+ return LauncherAtom.Attribute.UNKNOWN;
+ }
+ }
+
@Override
- public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
- builder.setWidget(LauncherAtom.Widget.newBuilder()
- .setSpanX(spanX)
- .setSpanY(spanY)
- .setComponentName(providerName.toString())
- .setPackageName(providerName.getPackageName()));
+ public LauncherAtom.ItemInfo buildProto(FolderInfo folderInfo) {
+ LauncherAtom.ItemInfo info = super.buildProto(folderInfo);
+ return info.toBuilder()
+ .setWidget(info.getWidget().toBuilder().setWidgetFeatures(widgetFeatures))
+ .setAttribute(getAttribute(sourceContainer))
+ .build();
}
}
diff --git a/src/com/android/launcher3/model/data/PackageItemInfo.java b/src/com/android/launcher3/model/data/PackageItemInfo.java
index b70d0d4..a81fe6a 100644
--- a/src/com/android/launcher3/model/data/PackageItemInfo.java
+++ b/src/com/android/launcher3/model/data/PackageItemInfo.java
@@ -16,27 +16,47 @@
package com.android.launcher3.model.data;
+import androidx.annotation.IntDef;
+
import com.android.launcher3.LauncherSettings;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/**
* Represents a {@link Package} in the widget tray section.
*/
public class PackageItemInfo extends ItemInfoWithIcon {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({NO_CATEGORY, CONVERSATIONS})
+ public @interface Category{}
+ /** The package is not categorized in the widget tray. */
+ public static final int NO_CATEGORY = 0;
+ /** The package is categorized to conversations widget in the widget tray. */
+ public static final int CONVERSATIONS = 1;
/**
* Package name of the {@link PackageItemInfo}.
*/
- public String packageName;
+ public final String packageName;
+
+ /** Represents a widget category shown in the widget tray section. */
+ @Category public final int category;
public PackageItemInfo(String packageName) {
+ this(packageName, NO_CATEGORY);
+ }
+
+ public PackageItemInfo(String packageName, @Category int category) {
this.packageName = packageName;
+ this.category = category;
this.itemType = LauncherSettings.Favorites.ITEM_TYPE_NON_ACTIONABLE;
}
public PackageItemInfo(PackageItemInfo copy) {
this.packageName = copy.packageName;
+ this.category = copy.category;
this.itemType = LauncherSettings.Favorites.ITEM_TYPE_NON_ACTIONABLE;
}
@@ -60,6 +80,6 @@
@Override
public int hashCode() {
- return Objects.hash(packageName);
+ return Objects.hash(packageName, user);
}
}
diff --git a/src/com/android/launcher3/model/data/PromiseAppInfo.java b/src/com/android/launcher3/model/data/PromiseAppInfo.java
deleted file mode 100644
index b6231ed..0000000
--- a/src/com/android/launcher3/model/data/PromiseAppInfo.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.model.data;
-
-import android.content.Context;
-import android.content.Intent;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.pm.PackageInstallInfo;
-import com.android.launcher3.util.PackageManagerHelper;
-
-public class PromiseAppInfo extends AppInfo {
-
- public int level = 0;
-
- public PromiseAppInfo(@NonNull PackageInstallInfo installInfo) {
- componentName = installInfo.componentName;
- intent = new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_LAUNCHER)
- .setComponent(componentName)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
- }
-
- @Override
- public WorkspaceItemInfo makeWorkspaceItem() {
- WorkspaceItemInfo shortcut = new WorkspaceItemInfo(this);
- shortcut.setInstallProgress(level);
- // We need to update the component name when the apk is installed
- shortcut.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
- // Since the user is manually placing it on homescreen, it should not be auto-removed later
- shortcut.status |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
- return shortcut;
- }
-
- public Intent getMarketIntent(Context context) {
- return new PackageManagerHelper(context).getMarketIntent(componentName.getPackageName());
- }
-}
diff --git a/src/com/android/launcher3/model/data/SearchActionItemInfo.java b/src/com/android/launcher3/model/data/SearchActionItemInfo.java
new file mode 100644
index 0000000..b3057d5
--- /dev/null
+++ b/src/com/android/launcher3/model/data/SearchActionItemInfo.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model.data;
+
+import static com.android.launcher3.LauncherSettings.Favorites.EXTENDED_CONTAINERS;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.Process;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.logger.LauncherAtom.ItemInfo;
+import com.android.launcher3.logger.LauncherAtom.SearchActionItem;
+
+/**
+ * Represents a SearchAction with in launcher
+ */
+public class SearchActionItemInfo extends ItemInfoWithIcon {
+
+ public static final int FLAG_SHOULD_START = 1 << 1;
+ public static final int FLAG_SHOULD_START_FOR_RESULT = FLAG_SHOULD_START | 1 << 2;
+ public static final int FLAG_BADGE_WITH_PACKAGE = 1 << 3;
+ public static final int FLAG_PRIMARY_ICON_FROM_TITLE = 1 << 4;
+ public static final int FLAG_BADGE_WITH_COMPONENT_NAME = 1 << 5;
+
+ private final String mFallbackPackageName;
+ private int mFlags = 0;
+ private final Icon mIcon;
+
+ // If true title does not contain any personal info and eligible for logging.
+ private final boolean mIsPersonalTitle;
+ private Intent mIntent;
+
+ private PendingIntent mPendingIntent;
+
+ public SearchActionItemInfo(Icon icon, String packageName, UserHandle user,
+ CharSequence title, boolean isPersonalTitle) {
+ mIsPersonalTitle = isPersonalTitle;
+ this.user = user == null ? Process.myUserHandle() : user;
+ this.title = title;
+ this.container = EXTENDED_CONTAINERS;
+ mFallbackPackageName = packageName;
+ mIcon = icon;
+ }
+
+ public SearchActionItemInfo(SearchActionItemInfo info) {
+ super(info);
+ mIcon = info.mIcon;
+ mFallbackPackageName = info.mFallbackPackageName;
+ mFlags = info.mFlags;
+ title = info.title;
+ this.container = EXTENDED_CONTAINERS;
+ this.mIsPersonalTitle = info.mIsPersonalTitle;
+ }
+
+ /**
+ * Returns if multiple flags are all available.
+ */
+ public boolean hasFlags(int flags) {
+ return (mFlags & flags) != 0;
+ }
+
+ public void setFlags(int flags) {
+ mFlags |= flags ;
+ }
+
+ @Override
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Setter for mIntent with assertion for null value mPendingIntent
+ */
+ public void setIntent(Intent intent) {
+ if (mPendingIntent != null && intent != null) {
+ throw new RuntimeException(
+ "SearchActionItemInfo can only have either an Intent or a PendingIntent");
+ }
+ mIntent = intent;
+ }
+
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ /**
+ * Setter of mPendingIntent with assertion for null value mIntent
+ */
+ public void setPendingIntent(PendingIntent pendingIntent) {
+ if (mIntent != null && pendingIntent != null) {
+ throw new RuntimeException(
+ "SearchActionItemInfo can only have either an Intent or a PendingIntent");
+ }
+ mPendingIntent = pendingIntent;
+ }
+
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ @Override
+ public ItemInfoWithIcon clone() {
+ return new SearchActionItemInfo(this);
+ }
+
+ @Override
+ public ItemInfo buildProto(FolderInfo fInfo) {
+ SearchActionItem.Builder itemBuilder = SearchActionItem.newBuilder()
+ .setPackageName(mFallbackPackageName);
+
+ if (!mIsPersonalTitle) {
+ itemBuilder.setTitle(title.toString());
+ }
+ return getDefaultItemInfoBuilder()
+ .setSearchActionItem(itemBuilder)
+ .setContainerInfo(getContainerInfo())
+ .build();
+ }
+}
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index a7bf1f3..690e904 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -58,20 +58,14 @@
public static final int FLAG_AUTOINSTALL_ICON = 1 << 1;
/**
- * The icon is being installed. If {@link #FLAG_RESTORED_ICON} or {@link #FLAG_AUTOINSTALL_ICON}
- * is set, then the icon is either being installed or is in a broken state.
- */
- public static final int FLAG_INSTALL_SESSION_ACTIVE = 1 << 2;
-
- /**
* Indicates that the widget restore has started.
*/
- public static final int FLAG_RESTORE_STARTED = 1 << 3;
+ public static final int FLAG_RESTORE_STARTED = 1 << 2;
/**
* Web UI supported.
*/
- public static final int FLAG_SUPPORTS_WEB_UI = 1 << 4;
+ public static final int FLAG_SUPPORTS_WEB_UI = 1 << 3;
/**
* The intent used to start the application.
@@ -98,11 +92,6 @@
*/
@NonNull private String[] personKeys = Utilities.EMPTY_STRING_ARRAY;
- /**
- * The installation progress [0-100] of the package that this shortcut represents.
- */
- private int mInstallProgress;
-
public WorkspaceItemInfo() {
itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
@@ -114,7 +103,6 @@
intent = new Intent(info.intent);
iconResource = info.iconResource;
status = info.status;
- mInstallProgress = info.mInstallProgress;
personKeys = info.personKeys.clone();
}
@@ -168,15 +156,6 @@
return isPromise() && !hasStatusFlag(FLAG_SUPPORTS_WEB_UI);
}
- public int getInstallProgress() {
- return mInstallProgress;
- }
-
- public void setInstallProgress(int progress) {
- mInstallProgress = progress;
- status |= FLAG_INSTALL_SESSION_ACTIVE;
- }
-
public void updateFromDeepShortcutInfo(ShortcutInfo shortcutInfo, Context context) {
// {@link ShortcutInfo#getActivity} can change during an update. Recreate the intent
intent = ShortcutKey.makeIntent(shortcutInfo);
@@ -216,7 +195,7 @@
if (cn == null && (itemType == Favorites.ITEM_TYPE_SHORTCUT || hasStatusFlag(
FLAG_SUPPORTS_WEB_UI | FLAG_AUTOINSTALL_ICON | FLAG_RESTORED_ICON))) {
// Legacy shortcuts and promise icons with web UI may not have a componentName but just
- // a packageName. In that case create a dummy componentName instead of adding additional
+ // a packageName. In that case create a empty componentName instead of adding additional
// check everywhere.
String pkg = intent.getPackage();
return pkg == null ? null : new ComponentName(pkg, IconCache.EMPTY_CLASS_NAME);
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
deleted file mode 100644
index fd3d41a..0000000
--- a/src/com/android/launcher3/notification/NotificationFooterLayout.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.notification;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.util.Themes;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * A {@link FrameLayout} that contains only icons of notifications.
- * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "..." overflow.
- */
-public class NotificationFooterLayout extends FrameLayout {
-
- public interface IconAnimationEndListener {
- void onIconAnimationEnd(NotificationInfo animatedNotification);
- }
-
- private static final int MAX_FOOTER_NOTIFICATIONS = 5;
-
- private static final Rect sTempRect = new Rect();
-
- private final List<NotificationInfo> mNotifications = new ArrayList<>();
- private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>();
- private final boolean mRtl;
- private final int mBackgroundColor;
-
- FrameLayout.LayoutParams mIconLayoutParams;
- private View mOverflowEllipsis;
- private LinearLayout mIconRow;
- private NotificationItemView mContainer;
-
- public NotificationFooterLayout(Context context) {
- this(context, null, 0);
- }
-
- public NotificationFooterLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- Resources res = getResources();
- mRtl = Utilities.isRtl(res);
-
- int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
- mIconLayoutParams = new LayoutParams(iconSize, iconSize);
- mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
- setWidth((int) res.getDimension(R.dimen.bg_popup_item_width));
- mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
- }
-
-
- /**
- * Compute margin start for each icon such that the icons between the first one and the ellipsis
- * are evenly spaced out.
- */
- public void setWidth(int width) {
- if (getLayoutParams() != null) {
- getLayoutParams().width = width;
- }
- Resources res = getResources();
- int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
-
- int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
- int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
- + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
- int availableIconRowSpace = width - paddingEnd - ellipsisSpace
- - iconSize * MAX_FOOTER_NOTIFICATIONS;
- mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mOverflowEllipsis = findViewById(R.id.overflow);
- mIconRow = findViewById(R.id.icon_row);
- }
-
- void setContainer(NotificationItemView container) {
- mContainer = container;
- }
-
- /**
- * Keep track of the NotificationInfo, and then update the UI when
- * {@link #commitNotificationInfos()} is called.
- */
- public void addNotificationInfo(final NotificationInfo notificationInfo) {
- if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) {
- mNotifications.add(notificationInfo);
- } else {
- mOverflowNotifications.add(notificationInfo);
- }
- }
-
- /**
- * Adds icons and potentially overflow text for all of the NotificationInfo's
- * added using {@link #addNotificationInfo(NotificationInfo)}.
- */
- public void commitNotificationInfos() {
- mIconRow.removeAllViews();
-
- for (int i = 0; i < mNotifications.size(); i++) {
- NotificationInfo info = mNotifications.get(i);
- addNotificationIconForInfo(info);
- }
- updateOverflowEllipsisVisibility();
- }
-
- private void updateOverflowEllipsisVisibility() {
- mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE);
- }
-
- /**
- * Creates an icon for the given NotificationInfo, and adds it to the icon row.
- * @return the icon view that was added
- */
- private View addNotificationIconForInfo(NotificationInfo info) {
- View icon = new View(getContext());
- icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor));
- icon.setOnClickListener(info);
- icon.setTag(info);
- icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mIconRow.addView(icon, 0, mIconLayoutParams);
- return icon;
- }
-
- public void animateFirstNotificationTo(Rect toBounds,
- final IconAnimationEndListener callback) {
- AnimatorSet animation = new AnimatorSet();
- final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1);
-
- Rect fromBounds = sTempRect;
- firstNotification.getGlobalVisibleRect(fromBounds);
- float scale = (float) toBounds.height() / fromBounds.height();
- Animator moveAndScaleIcon = new PropertyListBuilder().scale(scale)
- .translationY(toBounds.top - fromBounds.top
- + (fromBounds.height() * scale - fromBounds.height()) / 2)
- .build(firstNotification);
- moveAndScaleIcon.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag());
- removeViewFromIconRow(firstNotification);
- }
- });
- animation.play(moveAndScaleIcon);
-
- // Shift all notifications (not the overflow) over to fill the gap.
- int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart();
- if (mRtl) {
- gapWidth = -gapWidth;
- }
- if (!mOverflowNotifications.isEmpty()) {
- NotificationInfo notification = mOverflowNotifications.remove(0);
- mNotifications.add(notification);
- View iconFromOverflow = addNotificationIconForInfo(notification);
- animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1));
- }
- int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving.
- // We have to reset the translation X to 0 when the new main notification
- // is removed from the footer.
- PropertyResetListener<View, Float> propertyResetListener
- = new PropertyResetListener<>(TRANSLATION_X, 0f);
- for (int i = 0; i < numIcons; i++) {
- final View child = mIconRow.getChildAt(i);
- Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth);
- shiftChild.addListener(propertyResetListener);
- animation.play(shiftChild);
- }
- animation.start();
- }
-
- private void removeViewFromIconRow(View child) {
- mIconRow.removeView(child);
- mNotifications.remove(child.getTag());
- updateOverflowEllipsisVisibility();
- if (mIconRow.getChildCount() == 0) {
- // There are no more icons in the footer, so hide it.
- if (mContainer != null) {
- mContainer.removeFooter();
- }
- }
- }
-
- public void trimNotifications(List<String> notifications) {
- if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) {
- return;
- }
- Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator();
- while (overflowIterator.hasNext()) {
- if (!notifications.contains(overflowIterator.next().notificationKey)) {
- overflowIterator.remove();
- }
- }
- for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) {
- View child = mIconRow.getChildAt(i);
- NotificationInfo childInfo = (NotificationInfo) child.getTag();
- if (!notifications.contains(childInfo.notificationKey)) {
- removeViewFromIconRow(child);
- }
- }
- }
-}
diff --git a/src/com/android/launcher3/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java
index 835f72d..d27d8c7 100644
--- a/src/com/android/launcher3/notification/NotificationInfo.java
+++ b/src/com/android/launcher3/notification/NotificationInfo.java
@@ -22,7 +22,6 @@
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
-import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -86,9 +85,8 @@
mIsIconLarge = true;
}
if (mIconDrawable == null) {
- mIconDrawable = new BitmapDrawable(context.getResources(), LauncherAppState
- .getInstance(context).getIconCache()
- .getDefaultIcon(statusBarNotification.getUser()).icon);
+ mIconDrawable = LauncherAppState.getInstance(context).getIconCache()
+ .getDefaultIcon(statusBarNotification.getUser()).newIcon(context);
}
intent = notification.contentIntent;
autoCancel = (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0;
@@ -106,7 +104,6 @@
view, 0, 0, view.getWidth(), view.getHeight()).toBundle();
try {
intent.send(null, 0, null, null, null, null, activityOptions);
- launcher.getUserEventDispatcher().logNotificationLaunch(view, intent);
launcher.getStatsLogManager().logger().withItemInfo(mItemInfo)
.log(LAUNCHER_NOTIFICATION_LAUNCH_TAP);
} catch (PendingIntent.CanceledException e) {
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 0320aa3..af943a6 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -16,23 +16,22 @@
package com.android.launcher3.notification;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
-
-import android.app.Notification;
+import android.animation.AnimatorSet;
import android.content.Context;
-import android.graphics.Color;
+import android.graphics.Outline;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewOutlineProvider;
import android.widget.TextView;
import com.android.launcher3.R;
-import com.android.launcher3.graphics.IconPalette;
import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.Themes;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -43,60 +42,51 @@
private static final Rect sTempRect = new Rect();
private final Context mContext;
- private final PopupContainerWithArrow mContainer;
+ private final PopupContainerWithArrow mPopupContainer;
+ private final ViewGroup mRootView;
- private final TextView mHeaderText;
private final TextView mHeaderCount;
private final NotificationMainView mMainView;
- private final NotificationFooterLayout mFooter;
- private final SingleAxisSwipeDetector mSwipeDetector;
- private final View mIconView;
private final View mHeader;
- private final View mDivider;
private View mGutter;
private boolean mIgnoreTouch = false;
- private boolean mAnimatingNextIcon;
- private int mNotificationHeaderTextColor = Notification.COLOR_DEFAULT;
+ private List<NotificationInfo> mNotificationInfos = new ArrayList<>();
- public NotificationItemView(PopupContainerWithArrow container) {
- mContainer = container;
+ public NotificationItemView(PopupContainerWithArrow container, ViewGroup rootView) {
+ mPopupContainer = container;
+ mRootView = rootView;
mContext = container.getContext();
- mHeaderText = container.findViewById(R.id.notification_text);
mHeaderCount = container.findViewById(R.id.notification_count);
mMainView = container.findViewById(R.id.main_view);
- mFooter = container.findViewById(R.id.footer);
- mIconView = container.findViewById(R.id.popup_item_icon);
mHeader = container.findViewById(R.id.header);
- mDivider = container.findViewById(R.id.divider);
- mSwipeDetector = new SingleAxisSwipeDetector(mContext, mMainView, HORIZONTAL);
- mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
- mMainView.setSwipeDetector(mSwipeDetector);
- mFooter.setContainer(this);
+ float radius = Themes.getDialogCornerRadius(mContext);
+ rootView.setClipToOutline(true);
+ rootView.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
+ }
+ });
+ }
+
+ /**
+ * Animates the background color to a new color.
+ * @param color The color to change to.
+ * @param animatorSetOut The AnimatorSet where we add the color animator to.
+ */
+ public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
+ mMainView.updateBackgroundColor(color, animatorSetOut);
}
public void addGutter() {
if (mGutter == null) {
- mGutter = mContainer.inflateAndAdd(R.layout.notification_gutter, mContainer);
- }
- }
-
- /**
- * Sets width for notification footer and spaces out items evenly
- */
- public void setFooterWidth(int footerWidth) {
- mFooter.setWidth(footerWidth);
- }
-
- public void removeFooter() {
- if (mContainer.indexOfChild(mFooter) >= 0) {
- mContainer.removeView(mFooter);
- mContainer.removeView(mDivider);
+ mGutter = mPopupContainer.inflateAndAdd(R.layout.notification_gutter, mRootView);
}
}
@@ -108,39 +98,39 @@
}
public void removeAllViews() {
- mContainer.removeView(mMainView);
- mContainer.removeView(mHeader);
-
- if (mContainer.indexOfChild(mFooter) >= 0) {
- mContainer.removeView(mFooter);
- mContainer.removeView(mDivider);
- }
-
+ mRootView.removeView(mMainView);
+ mRootView.removeView(mHeader);
if (mGutter != null) {
- mContainer.removeView(mGutter);
+ mRootView.removeView(mGutter);
}
}
- public void updateHeader(int notificationCount, int iconColor) {
- mHeaderCount.setText(notificationCount <= 1 ? "" : String.valueOf(notificationCount));
- if (Color.alpha(iconColor) > 0) {
- if (mNotificationHeaderTextColor == Notification.COLOR_DEFAULT) {
- mNotificationHeaderTextColor =
- IconPalette.resolveContrastColor(mContext, iconColor,
- Themes.getAttrColor(mContext, R.attr.popupColorPrimary));
- }
- mHeaderText.setTextColor(mNotificationHeaderTextColor);
- mHeaderCount.setTextColor(mNotificationHeaderTextColor);
+ /**
+ * Updates the header text.
+ * @param notificationCount The number of notifications.
+ */
+ public void updateHeader(int notificationCount) {
+ final String text;
+ final int visibility;
+ if (notificationCount <= 1) {
+ text = "";
+ visibility = View.INVISIBLE;
+ } else {
+ text = String.valueOf(notificationCount);
+ visibility = View.VISIBLE;
+
}
+ mHeaderCount.setText(text);
+ mHeaderCount.setVisibility(visibility);
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- sTempRect.set(mMainView.getLeft(), mMainView.getTop(),
- mMainView.getRight(), mMainView.getBottom());
+ sTempRect.set(mRootView.getLeft(), mRootView.getTop(),
+ mRootView.getRight(), mRootView.getBottom());
mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY());
if (!mIgnoreTouch) {
- mContainer.getParent().requestDisallowInterceptTouchEvent(true);
+ mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true);
}
}
if (mIgnoreTouch) {
@@ -151,53 +141,39 @@
return false;
}
- mSwipeDetector.onTouchEvent(ev);
- return mSwipeDetector.isDraggingOrSettling();
- }
-
- public boolean onTouchEvent(MotionEvent ev) {
- if (mIgnoreTouch) {
- return false;
- }
- if (mMainView.getNotificationInfo() == null) {
- // The notification hasn't been populated yet.
- return false;
- }
- return mSwipeDetector.onTouchEvent(ev);
+ return false;
}
public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
+ mNotificationInfos.clear();
if (notificationInfos.isEmpty()) {
return;
}
+ mNotificationInfos.addAll(notificationInfos);
NotificationInfo mainNotification = notificationInfos.get(0);
mMainView.applyNotificationInfo(mainNotification, false);
-
- for (int i = 1; i < notificationInfos.size(); i++) {
- mFooter.addNotificationInfo(notificationInfos.get(i));
- }
- mFooter.commitNotificationInfos();
}
public void trimNotifications(final List<String> notificationKeys) {
- boolean dismissedMainNotification = !notificationKeys.contains(
- mMainView.getNotificationInfo().notificationKey);
- if (dismissedMainNotification && !mAnimatingNextIcon) {
- // Animate the next icon into place as the new main notification.
- mAnimatingNextIcon = true;
- mMainView.setContentVisibility(View.INVISIBLE);
- mMainView.setContentTranslation(0);
- mIconView.getGlobalVisibleRect(sTempRect);
- mFooter.animateFirstNotificationTo(sTempRect, (newMainNotification) -> {
- if (newMainNotification != null) {
- mMainView.applyNotificationInfo(newMainNotification, true);
- mMainView.setContentVisibility(View.VISIBLE);
+ NotificationInfo currentMainNotificationInfo = mMainView.getNotificationInfo();
+ boolean shouldUpdateMainNotification = !notificationKeys.contains(
+ currentMainNotificationInfo.notificationKey);
+
+ if (shouldUpdateMainNotification) {
+ int size = notificationKeys.size();
+ NotificationInfo nextNotification = null;
+ // We get the latest notification by finding the notification after the one that was
+ // just dismissed.
+ for (int i = 0; i < size; ++i) {
+ if (currentMainNotificationInfo == mNotificationInfos.get(i) && i + 1 < size) {
+ nextNotification = mNotificationInfos.get(i + 1);
+ break;
}
- mAnimatingNextIcon = false;
- });
- } else {
- mFooter.trimNotifications(notificationKeys);
+ }
+ if (nextNotification != null) {
+ mMainView.applyNotificationInfo(nextNotification, true);
+ }
}
}
}
diff --git a/src/com/android/launcher3/notification/NotificationKeyData.java b/src/com/android/launcher3/notification/NotificationKeyData.java
index a1917ec..1dda3df 100644
--- a/src/com/android/launcher3/notification/NotificationKeyData.java
+++ b/src/com/android/launcher3/notification/NotificationKeyData.java
@@ -30,9 +30,9 @@
/**
* The key data associated with the notification, used to determine what to include
- * in dots and dummy popup views before they are populated.
+ * in dots and stub popup views before they are populated.
*
- * @see NotificationInfo for the full data used when populating the dummy views.
+ * @see NotificationInfo for the full data used when populating the stub views.
*/
public class NotificationKeyData {
public final String notificationKey;
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 059ad18..e58f5fa 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -18,7 +18,7 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
import android.annotation.TargetApi;
import android.app.Notification;
@@ -38,7 +38,7 @@
import androidx.annotation.WorkerThread;
import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.SettingsCache;
import java.util.ArrayList;
import java.util.Arrays;
@@ -81,7 +81,8 @@
/** The last notification key that was dismissed from launcher UI */
private String mLastKeyDismissedByLauncher;
- private SecureSettingsObserver mNotificationDotsObserver;
+ private SettingsCache mSettingsCache;
+ private SettingsCache.OnChangeListener mNotificationSettingsChangedListener;
public NotificationListener() {
mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
@@ -207,10 +208,12 @@
super.onListenerConnected();
sIsConnected = true;
- mNotificationDotsObserver =
- newNotificationSettingsObserver(this, this::onNotificationSettingsChanged);
- mNotificationDotsObserver.register();
- mNotificationDotsObserver.dispatchOnChange();
+ // Register an observer to rebind the notification listener when dots are re-enabled.
+ mSettingsCache = SettingsCache.INSTANCE.get(this);
+ mNotificationSettingsChangedListener = this::onNotificationSettingsChanged;
+ mSettingsCache.register(NOTIFICATION_BADGING_URI,
+ mNotificationSettingsChangedListener);
+ onNotificationSettingsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI));
onNotificationFullRefresh();
}
@@ -229,7 +232,7 @@
public void onListenerDisconnected() {
super.onListenerDisconnected();
sIsConnected = false;
- mNotificationDotsObserver.unregister();
+ mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener);
onNotificationFullRefresh();
}
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index b03aa9c..b8aa824 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -16,15 +16,15 @@
package com.android.launcher3.notification;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED;
-import android.animation.Animator;
+import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
-import android.content.res.ColorStateList;
+import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -36,20 +36,15 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.touch.BaseSwipeDetector;
-import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.Themes;
/**
* A {@link android.widget.FrameLayout} that contains a single notification,
* e.g. icon + title + text.
*/
@TargetApi(Build.VERSION_CODES.N)
-public class NotificationMainView extends FrameLayout implements SingleAxisSwipeDetector.Listener {
+public class NotificationMainView extends FrameLayout {
private static final FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
new FloatProperty<NotificationMainView>("contentTranslation") {
@@ -67,8 +62,6 @@
// This is used only to track the notification view, so that it can be properly logged.
public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo();
- private final ObjectAnimator mContentTranslateAnimator;
-
private NotificationInfo mNotificationInfo;
private ViewGroup mTextAndBackground;
private int mBackgroundColor;
@@ -78,6 +71,8 @@
private SingleAxisSwipeDetector mSwipeDetector;
+ private final ColorDrawable mColorDrawable;
+
public NotificationMainView(Context context) {
this(context, null, 0);
}
@@ -89,7 +84,7 @@
public NotificationMainView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mContentTranslateAnimator = ObjectAnimator.ofFloat(this, CONTENT_TRANSLATION, 0);
+ mColorDrawable = new ColorDrawable(Color.TRANSPARENT);
}
@Override
@@ -97,19 +92,37 @@
super.onFinishInflate();
mTextAndBackground = findViewById(R.id.text_and_background);
- ColorDrawable colorBackground = (ColorDrawable) mTextAndBackground.getBackground();
- mBackgroundColor = colorBackground.getColor();
- RippleDrawable rippleBackground = new RippleDrawable(ColorStateList.valueOf(
- Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)),
- colorBackground, null);
- mTextAndBackground.setBackground(rippleBackground);
mTitleView = mTextAndBackground.findViewById(R.id.title);
mTextView = mTextAndBackground.findViewById(R.id.text);
mIconView = findViewById(R.id.popup_item_icon);
+
+ ColorDrawable colorBackground = (ColorDrawable) mTextAndBackground.getBackground();
+ updateBackgroundColor(colorBackground.getColor());
}
- public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) {
- mSwipeDetector = swipeDetector;
+ private void updateBackgroundColor(int color) {
+ mBackgroundColor = color;
+ mColorDrawable.setColor(color);
+ mTextAndBackground.setBackground(mColorDrawable);
+ if (mNotificationInfo != null) {
+ mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
+ mBackgroundColor));
+ }
+ }
+
+ /**
+ * Animates the background color to a new color.
+ * @param color The color to change to.
+ * @param animatorSetOut The AnimatorSet where we add the color animator to.
+ */
+ public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
+ int oldColor = mBackgroundColor;
+ ValueAnimator colors = ValueAnimator.ofArgb(oldColor, color);
+ colors.addUpdateListener(valueAnimator -> {
+ int newColor = (int) valueAnimator.getAnimatedValue();
+ updateBackgroundColor(newColor);
+ });
+ animatorSetOut.play(colors);
}
/**
@@ -137,7 +150,7 @@
setOnClickListener(mNotificationInfo);
}
setContentTranslation(0);
- // Add a dummy ItemInfo so that logging populates the correct container and item types
+ // Add a stub ItemInfo so that logging populates the correct container and item types
// instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively.
setTag(NOTIFICATION_ITEM_INFO);
if (animate) {
@@ -150,11 +163,6 @@
mIconView.setTranslationX(translation);
}
- public void setContentVisibility(int visibility) {
- mTextAndBackground.setVisibility(visibility);
- mIconView.setVisibility(visibility);
- }
-
public NotificationInfo getNotificationInfo() {
return mNotificationInfo;
}
@@ -168,61 +176,6 @@
Launcher launcher = Launcher.getLauncher(getContext());
launcher.getPopupDataProvider().cancelNotification(
mNotificationInfo.notificationKey);
- launcher.getUserEventDispatcher().logActionOnItem(
- LauncherLogProto.Action.Touch.SWIPE,
- LauncherLogProto.Action.Direction.RIGHT, // Assume all swipes are right for logging.
- LauncherLogProto.ItemType.NOTIFICATION);
- }
-
- // SingleAxisSwipeDetector.Listener's
- @Override
- public void onDragStart(boolean start, float startDisplacement) { }
-
-
- @Override
- public boolean onDrag(float displacement) {
- setContentTranslation(canChildBeDismissed()
- ? displacement : OverScroll.dampedScroll(displacement, getWidth()));
- mContentTranslateAnimator.cancel();
- return true;
- }
-
- @Override
- public void onDragEnd(float velocity) {
- final boolean willExit;
- final float endTranslation;
- final float startTranslation = mTextAndBackground.getTranslationX();
-
- if (!canChildBeDismissed()) {
- willExit = false;
- endTranslation = 0;
- } else if (mSwipeDetector.isFling(velocity)) {
- willExit = true;
- endTranslation = velocity < 0 ? - getWidth() : getWidth();
- } else if (Math.abs(startTranslation) > getWidth() / 2) {
- willExit = true;
- endTranslation = (startTranslation < 0 ? -getWidth() : getWidth());
- } else {
- willExit = false;
- endTranslation = 0;
- }
-
- long duration = BaseSwipeDetector.calculateDuration(velocity,
- (endTranslation - startTranslation) / getWidth());
-
- mContentTranslateAnimator.removeAllListeners();
- mContentTranslateAnimator.setDuration(duration)
- .setInterpolator(scrollInterpolatorForVelocity(velocity));
- mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation);
- mContentTranslateAnimator.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- mSwipeDetector.finishedScrolling();
- if (willExit) {
- onChildDismissed();
- }
- }
- });
- mContentTranslateAnimator.start();
+ launcher.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED);
}
}
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
index 408796f..f73d782 100644
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
@@ -24,7 +24,7 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.uioverrides.WallpaperColorInfo;
+import com.android.launcher3.util.Themes;
/**
* A PageIndicator that briefly shows a fraction of a line when moving between pages
@@ -123,7 +123,7 @@
mLauncher = Launcher.getLauncher(context);
mLineHeight = res.getDimensionPixelSize(R.dimen.workspace_page_indicator_line_height);
- boolean darkText = WallpaperColorInfo.INSTANCE.get(context).supportsDarkText();
+ boolean darkText = Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText);
mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA;
mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE);
}
@@ -268,7 +268,9 @@
} else {
lp.leftMargin = lp.rightMargin = 0;
lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
- lp.bottomMargin = grid.hotseatBarSizePx + insets.bottom;
+ lp.bottomMargin = grid.isTaskbarPresent
+ ? grid.workspacePadding.bottom + grid.taskbarSize
+ : grid.hotseatBarSizePx + insets.bottom;
}
setLayoutParams(lp);
}
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 901d27f..ab35bd6 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -17,6 +17,7 @@
package com.android.launcher3.pm;
import static com.android.launcher3.Utilities.getPrefs;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -31,17 +32,20 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.SessionCommitReceiver;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Preconditions;
import java.util.ArrayList;
import java.util.HashMap;
@@ -53,6 +57,8 @@
*/
public class InstallSessionHelper {
+ private static final String LOG = "InstallSessionHelper";
+
// Set<String> of session ids of promise icons that have been added to the home screen
// as FLAG_PROMISE_NEW_INSTALLS.
protected static final String PROMISE_ICON_IDS = "promise_icon_ids";
@@ -64,27 +70,27 @@
private final LauncherApps mLauncherApps;
private final Context mAppContext;
- private final IntSet mPromiseIconIds;
private final PackageInstaller mInstaller;
private final HashMap<String, Boolean> mSessionVerifiedMap = new HashMap<>();
+ private IntSet mPromiseIconIds;
+
public InstallSessionHelper(Context context) {
mInstaller = context.getPackageManager().getPackageInstaller();
mAppContext = context.getApplicationContext();
mLauncherApps = context.getSystemService(LauncherApps.class);
+ }
+ @WorkerThread
+ private IntSet getPromiseIconIds() {
+ Preconditions.assertWorkerThread();
+ if (mPromiseIconIds != null) {
+ return mPromiseIconIds;
+ }
mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString(
- getPrefs(context).getString(PROMISE_ICON_IDS, "")));
+ getPrefs(mAppContext).getString(PROMISE_ICON_IDS, "")));
- cleanUpPromiseIconIds();
- }
-
- public static UserHandle getUserHandle(SessionInfo info) {
- return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle();
- }
-
- protected void cleanUpPromiseIconIds() {
IntArray existingIds = new IntArray();
for (SessionInfo info : getActiveSessions().values()) {
existingIds.add(info.getSessionId());
@@ -99,6 +105,7 @@
for (int i = idsToRemove.size() - 1; i >= 0; --i) {
mPromiseIconIds.getArray().removeValue(idsToRemove.get(i));
}
+ return mPromiseIconIds;
}
public HashMap<PackageUserKey, SessionInfo> getActiveSessions() {
@@ -125,7 +132,7 @@
private void updatePromiseIconPrefs() {
getPrefs(mAppContext).edit()
- .putString(PROMISE_ICON_IDS, mPromiseIconIds.getArray().toConcatString())
+ .putString(PROMISE_ICON_IDS, getPromiseIconIds().getArray().toConcatString())
.apply();
}
@@ -167,7 +174,7 @@
* Attempt to restore workspace layout if the session is triggered due to device restore.
*/
public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) {
- if (!Utilities.ATLEAST_OREO || !FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
+ if (!FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
return false;
}
if (isRestore(info)) {
@@ -183,13 +190,15 @@
return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE;
}
+ @WorkerThread
public boolean promiseIconAddedForId(int sessionId) {
- return mPromiseIconIds.contains(sessionId);
+ return getPromiseIconIds().contains(sessionId);
}
+ @WorkerThread
public void removePromiseIconId(int sessionId) {
- if (mPromiseIconIds.contains(sessionId)) {
- mPromiseIconIds.getArray().removeValue(sessionId);
+ if (promiseIconAddedForId(sessionId)) {
+ getPromiseIconIds().getArray().removeValue(sessionId);
updatePromiseIconPrefs();
}
}
@@ -202,30 +211,39 @@
* - The app is not already installed
* - A promise icon for the session has not already been created
*/
+ @WorkerThread
void tryQueuePromiseAppIcon(PackageInstaller.SessionInfo sessionInfo) {
- if (Utilities.ATLEAST_OREO && FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
+ if (FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
&& SessionCommitReceiver.isEnabled(mAppContext)
- && verify(sessionInfo) != null
- && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
- && sessionInfo.getAppIcon() != null
- && !TextUtils.isEmpty(sessionInfo.getAppLabel())
- && !mPromiseIconIds.contains(sessionInfo.getSessionId())
- && new PackageManagerHelper(mAppContext).getApplicationInfo(
- sessionInfo.getAppPackageName(), getUserHandle(sessionInfo), 0) == null) {
- SessionCommitReceiver.queuePromiseAppIconAddition(mAppContext, sessionInfo);
- mPromiseIconIds.add(sessionInfo.getSessionId());
+ && verifySessionInfo(sessionInfo)
+ && !promiseIconAddedForId(sessionInfo.getSessionId())) {
+ FileLog.d(LOG, "Adding package name to install queue: "
+ + sessionInfo.getAppPackageName());
+
+ ItemInstallQueue.INSTANCE.get(mAppContext)
+ .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+
+ getPromiseIconIds().add(sessionInfo.getSessionId());
updatePromiseIconPrefs();
}
}
- public InstallSessionTracker registerInstallTracker(
- InstallSessionTracker.Callback callback, LooperExecutor executor) {
+ public boolean verifySessionInfo(PackageInstaller.SessionInfo sessionInfo) {
+ return verify(sessionInfo) != null
+ && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
+ && sessionInfo.getAppIcon() != null
+ && !TextUtils.isEmpty(sessionInfo.getAppLabel())
+ && !new PackageManagerHelper(mAppContext).isAppInstalled(
+ sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+ }
+
+ public InstallSessionTracker registerInstallTracker(InstallSessionTracker.Callback callback) {
InstallSessionTracker tracker = new InstallSessionTracker(this, callback);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- mInstaller.registerSessionCallback(tracker, executor.getHandler());
+ mInstaller.registerSessionCallback(tracker, MODEL_EXECUTOR.getHandler());
} else {
- mLauncherApps.registerPackageInstallerSessionCallback(executor, tracker);
+ mLauncherApps.registerPackageInstallerSessionCallback(MODEL_EXECUTOR, tracker);
}
return tracker;
}
@@ -237,4 +255,8 @@
mLauncherApps.unregisterPackageInstallerSessionCallback(tracker);
}
}
+
+ public static UserHandle getUserHandle(SessionInfo info) {
+ return Utilities.ATLEAST_Q ? info.getUser() : Process.myUserHandle();
+ }
}
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index eb3ca73..b0b907a 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -24,8 +24,11 @@
import android.os.UserHandle;
import android.util.SparseArray;
+import androidx.annotation.WorkerThread;
+
import com.android.launcher3.util.PackageUserKey;
+@WorkerThread
public class InstallSessionTracker extends PackageInstaller.SessionCallback {
// Lazily initialized
diff --git a/src/com/android/launcher3/pm/PackageInstallInfo.java b/src/com/android/launcher3/pm/PackageInstallInfo.java
index 7997d16..fad904f 100644
--- a/src/com/android/launcher3/pm/PackageInstallInfo.java
+++ b/src/com/android/launcher3/pm/PackageInstallInfo.java
@@ -25,7 +25,8 @@
public static final int STATUS_INSTALLED = 0;
public static final int STATUS_INSTALLING = 1;
- public static final int STATUS_FAILED = 2;
+ public static final int STATUS_INSTALLED_DOWNLOADING = 2;
+ public static final int STATUS_FAILED = 3;
public final ComponentName componentName;
public final String packageName;
@@ -56,5 +57,4 @@
public static PackageInstallInfo fromState(int state, String packageName, UserHandle user) {
return new PackageInstallInfo(packageName, state, 0 /* progress */, user);
}
-
}
diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index 40d7031..7af14c6 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -27,7 +27,6 @@
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Process;
@@ -39,13 +38,13 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
import com.android.launcher3.icons.ComponentWithLabelAndIcon;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.PackageUserKey;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -175,37 +174,23 @@
List<ShortcutConfigActivityInfo> result = new ArrayList<>();
UserHandle myUser = Process.myUserHandle();
- if (Utilities.ATLEAST_OREO) {
- final List<UserHandle> users;
- final String packageName;
- if (packageUser == null) {
- users = UserCache.INSTANCE.get(context).getUserProfiles();
- packageName = null;
- } else {
- users = new ArrayList<>(1);
- users.add(packageUser.mUser);
- packageName = packageUser.mPackageName;
- }
- LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
- for (UserHandle user : users) {
- boolean ignoreTargetSdk = myUser.equals(user);
- for (LauncherActivityInfo activityInfo :
- launcherApps.getShortcutConfigActivityList(packageName, user)) {
- if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion
- >= Build.VERSION_CODES.O) {
- result.add(new ShortcutConfigActivityInfoVO(activityInfo));
- }
- }
- }
+ final List<UserHandle> users;
+ final String packageName;
+ if (packageUser == null) {
+ users = UserCache.INSTANCE.get(context).getUserProfiles();
+ packageName = null;
} else {
- if (packageUser == null || packageUser.mUser.equals(myUser)) {
- Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
- if (packageUser != null) {
- intent.setPackage(packageUser.mPackageName);
- }
- for (ResolveInfo info :
- context.getPackageManager().queryIntentActivities(intent, 0)) {
- result.add(new ShortcutConfigActivityInfoVL(info.activityInfo));
+ users = Collections.singletonList(packageUser.mUser);
+ packageName = packageUser.mPackageName;
+ }
+ LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+ for (UserHandle user : users) {
+ boolean ignoreTargetSdk = myUser.equals(user);
+ for (LauncherActivityInfo activityInfo :
+ launcherApps.getShortcutConfigActivityList(packageName, user)) {
+ if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion
+ >= Build.VERSION_CODES.O) {
+ result.add(new ShortcutConfigActivityInfoVO(activityInfo));
}
}
}
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 5aab41a..5ade22b 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -21,8 +21,10 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
+import android.util.Log;
import android.util.LongSparseArray;
+import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -58,6 +60,9 @@
private void onUsersChanged(Intent intent) {
enableAndResetCache();
mUserChangeListeners.forEach(Runnable::run);
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED, "profile changed", new Exception());
+ }
}
/**
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index d5b32fc..a534ee3 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -16,52 +16,86 @@
package com.android.launcher3.popup;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static androidx.core.content.ContextCompat.getColorStateList;
+
+import static com.android.launcher3.anim.Interpolators.ACCELERATED_EASE;
+import static com.android.launcher3.anim.Interpolators.DECELERATED_EASE;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_LOCAL_COLOR_POPUPS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
-import android.graphics.CornerPathEffect;
-import android.graphics.Outline;
-import android.graphics.Paint;
+import android.graphics.Color;
import android.graphics.Rect;
-import android.graphics.drawable.ShapeDrawable;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
import android.util.AttributeSet;
import android.util.Pair;
+import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
+import android.view.ViewTreeObserver;
+import android.view.animation.Interpolator;
import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.RevealOutlineAnimation;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.Workspace;
import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.TriangleShape;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LocalColorExtractor;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
/**
* A container for shortcuts to deep links and notifications associated with an app.
*
* @param <T> The activity on with the popup shows
*/
-public abstract class ArrowPopup<T extends BaseDraggingActivity> extends AbstractFloatingView {
+public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
+ extends AbstractFloatingView {
+
+ // Duration values (ms) for popup open and close animations.
+ private static final int OPEN_DURATION = 276;
+ private static final int OPEN_FADE_START_DELAY = 0;
+ private static final int OPEN_FADE_DURATION = 38;
+ private static final int OPEN_CHILD_FADE_START_DELAY = 38;
+ private static final int OPEN_CHILD_FADE_DURATION = 76;
+
+ private static final int CLOSE_DURATION = 200;
+ private static final int CLOSE_FADE_START_DELAY = 140;
+ private static final int CLOSE_FADE_DURATION = 50;
+ private static final int CLOSE_CHILD_FADE_START_DELAY = 0;
+ private static final int CLOSE_CHILD_FADE_DURATION = 140;
+
+ // Index used to get background color when using local wallpaper color extraction,
+ private static final int DARK_COLOR_EXTRACTION_INDEX = android.R.color.system_neutral2_800;
+ private static final int LIGHT_COLOR_EXTRACTION_INDEX = android.R.color.system_accent2_50;
private final Rect mTempRect = new Rect();
@@ -70,17 +104,40 @@
protected final T mLauncher;
protected final boolean mIsRtl;
- private final int mArrowOffset;
+ private final int mArrowOffsetVertical;
+ private final int mArrowOffsetHorizontal;
+ private final int mArrowWidth;
+ private final int mArrowHeight;
+ private final int mArrowPointRadius;
private final View mArrow;
+ private final int mMargin;
+
protected boolean mIsLeftAligned;
protected boolean mIsAboveIcon;
private int mGravity;
- protected Animator mOpenCloseAnimator;
+ protected AnimatorSet mOpenCloseAnimator;
protected boolean mDeferContainerRemoval;
- private final Rect mStartRect = new Rect();
- private final Rect mEndRect = new Rect();
+
+ private final GradientDrawable mRoundedTop;
+ private final GradientDrawable mRoundedBottom;
+
+ private Runnable mOnCloseCallback = () -> { };
+
+ // The rect string of the view that the arrow is attached to, in screen reference frame.
+ protected String mArrowColorRectString;
+ private int mArrowColor;
+ protected final HashMap<String, View> mViewForRect = new HashMap<>();
+
+ @Nullable protected LocalColorExtractor mColorExtractor;
+
+ private final float mElevation;
+ private final int mBackgroundColor;
+
+ private final String mIterateChildrenTag;
+
+ private final int[] mColors;
public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
@@ -89,21 +146,50 @@
mLauncher = BaseDraggingActivity.fromContext(context);
mIsRtl = Utilities.isRtl(getResources());
- setClipToOutline(true);
- setOutlineProvider(new ViewOutlineProvider() {
- @Override
- public void getOutline(View view, Outline outline) {
- outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
- }
- });
+ mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
+ mArrowColor = mBackgroundColor;
+ mElevation = getResources().getDimension(R.dimen.deep_shortcuts_elevation);
// Initialize arrow view
final Resources resources = getResources();
- final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
- final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
+ mMargin = resources.getDimensionPixelSize(R.dimen.popup_margin);
+ mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
+ mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
mArrow = new View(context);
- mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight));
- mArrowOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
+ mArrow.setLayoutParams(new DragLayer.LayoutParams(mArrowWidth, mArrowHeight));
+ mArrowOffsetVertical = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
+ mArrowOffsetHorizontal = resources.getDimensionPixelSize(
+ R.dimen.popup_arrow_horizontal_center_offset) - (mArrowWidth / 2);
+ mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
+
+ int smallerRadius = resources.getDimensionPixelSize(R.dimen.popup_smaller_radius);
+ mRoundedTop = new GradientDrawable();
+ mRoundedTop.setColor(mBackgroundColor);
+ mRoundedTop.setCornerRadii(new float[] { mOutlineRadius, mOutlineRadius, mOutlineRadius,
+ mOutlineRadius, smallerRadius, smallerRadius, smallerRadius, smallerRadius});
+
+ mRoundedBottom = new GradientDrawable();
+ mRoundedBottom.setColor(mBackgroundColor);
+ mRoundedBottom.setCornerRadii(new float[] { smallerRadius, smallerRadius, smallerRadius,
+ smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius});
+
+ mIterateChildrenTag = getContext().getString(R.string.popup_container_iterate_children);
+
+ boolean isAboveAnotherSurface = getTopOpenViewWithType(mLauncher, TYPE_FOLDER) != null
+ || mLauncher.getStateManager().getState() == LauncherState.ALL_APPS;
+ if (!isAboveAnotherSurface && Utilities.ATLEAST_S && ENABLE_LOCAL_COLOR_POPUPS.get()) {
+ setupColorExtraction();
+ }
+
+ if (isAboveAnotherSurface) {
+ mColors = new int[] {
+ getColorStateList(context, R.color.popup_shade_first).getDefaultColor()};
+ } else {
+ mColors = new int[] {
+ getColorStateList(context, R.color.popup_shade_first).getDefaultColor(),
+ getColorStateList(context, R.color.popup_shade_second).getDefaultColor(),
+ getColorStateList(context, R.color.popup_shade_third).getDefaultColor()};
+ }
}
public ArrowPopup(Context context, AttributeSet attrs) {
@@ -147,79 +233,294 @@
protected void onInflationComplete(boolean isReversed) { }
/**
+ * Set the margins and radius of backgrounds after views are properly ordered.
+ */
+ public void assignMarginsAndBackgrounds(ViewGroup viewGroup) {
+ assignMarginsAndBackgrounds(viewGroup, Color.TRANSPARENT);
+ }
+
+ /**
+ * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColors}.
+ * Otherwise, we will use this color for all child views.
+ */
+ private void assignMarginsAndBackgrounds(ViewGroup viewGroup, int backgroundColor) {
+ final boolean getColorFromColorArray = backgroundColor == Color.TRANSPARENT;
+
+ int count = viewGroup.getChildCount();
+ int totalVisibleShortcuts = 0;
+ for (int i = 0; i < count; i++) {
+ View view = viewGroup.getChildAt(i);
+ if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) {
+ totalVisibleShortcuts++;
+ }
+ }
+
+ int numVisibleChild = 0;
+ int numVisibleShortcut = 0;
+ View lastView = null;
+ AnimatorSet colorAnimator = new AnimatorSet();
+ for (int i = 0; i < count; i++) {
+ View view = viewGroup.getChildAt(i);
+ if (view.getVisibility() == VISIBLE) {
+ if (lastView != null) {
+ MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams();
+ mlp.bottomMargin = mMargin;
+ }
+ lastView = view;
+ MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams();
+ mlp.bottomMargin = 0;
+
+
+ if (getColorFromColorArray) {
+ backgroundColor = mColors[numVisibleChild % mColors.length];
+ }
+
+ if (view instanceof ViewGroup && mIterateChildrenTag.equals(view.getTag())) {
+ assignMarginsAndBackgrounds((ViewGroup) view, backgroundColor);
+ numVisibleChild++;
+ continue;
+ }
+
+ if (view instanceof DeepShortcutView) {
+ if (totalVisibleShortcuts == 1) {
+ view.setBackgroundResource(R.drawable.single_item_primary);
+ } else if (totalVisibleShortcuts > 1) {
+ if (numVisibleShortcut == 0) {
+ view.setBackground(mRoundedTop.getConstantState().newDrawable());
+ } else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) {
+ view.setBackground(mRoundedBottom.getConstantState().newDrawable());
+ } else {
+ view.setBackgroundResource(R.drawable.middle_item_primary);
+ }
+ numVisibleShortcut++;
+ }
+ }
+
+ if (!ENABLE_LOCAL_COLOR_POPUPS.get()) {
+ setChildColor(view, backgroundColor, colorAnimator);
+ // Arrow color matches the first child or the last child.
+ if (!mIsAboveIcon && numVisibleChild == 0) {
+ mArrowColor = backgroundColor;
+ } else if (mIsAboveIcon) {
+ mArrowColor = backgroundColor;
+ }
+ }
+
+ numVisibleChild++;
+ }
+ }
+
+ colorAnimator.setDuration(0).start();
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.S)
+ private int getExtractedColor(SparseIntArray colors) {
+ int index = Utilities.isDarkTheme(getContext())
+ ? DARK_COLOR_EXTRACTION_INDEX
+ : LIGHT_COLOR_EXTRACTION_INDEX;
+ return colors.get(index, mBackgroundColor);
+ }
+
+ @TargetApi(Build.VERSION_CODES.S)
+ private void setupColorExtraction() {
+ Workspace workspace = mLauncher.findViewById(R.id.workspace);
+ if (workspace == null) {
+ return;
+ }
+
+ mColorExtractor = LocalColorExtractor.newInstance(mLauncher);
+ mColorExtractor.setListener((rect, extractedColors) -> {
+ String rectString = rect.toShortString();
+ View v = mViewForRect.get(rectString);
+ AnimatorSet colors = new AnimatorSet();
+ if (v != null) {
+ int newColor = getExtractedColor(extractedColors);
+ setChildColor(v, newColor, colors);
+ int numChildren = v instanceof ViewGroup ? ((ViewGroup) v).getChildCount() : 0;
+ for (int i = 0; i < numChildren; ++i) {
+ View childView = ((ViewGroup) v).getChildAt(i);
+ setChildColor(childView, newColor, colors);
+
+ }
+ if (rectString.equals(mArrowColorRectString)) {
+ mArrowColor = newColor;
+ updateArrowColor();
+ }
+ }
+ colors.setDuration(150);
+ v.post(colors::start);
+ });
+ }
+
+ protected void addPreDrawForColorExtraction(Launcher launcher) {
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ initColorExtractionLocations(launcher);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Returns list of child views that will receive local color extraction treatment.
+ * Note: Order should match the view hierarchy.
+ */
+ protected List<View> getChildrenForColorExtraction() {
+ return Collections.emptyList();
+ }
+
+ private void initColorExtractionLocations(Launcher launcher) {
+ if (mColorExtractor == null) {
+ return;
+ }
+ ArrayList<RectF> locations = new ArrayList<>();
+
+ boolean firstVisibleChild = true;
+ // Order matters here, since we need the arrow to match the color of its adjacent view.
+ for (View view : getChildrenForColorExtraction()) {
+ if (view != null && view.getVisibility() == VISIBLE) {
+ RectF rf = new RectF();
+ mColorExtractor.getExtractedRectForView(launcher,
+ launcher.getWorkspace().getCurrentPage(), view, rf);
+ if (!rf.isEmpty()) {
+ locations.add(rf);
+ String rectString = rf.toShortString();
+ mViewForRect.put(rectString, view);
+ if (mIsAboveIcon) {
+ mArrowColorRectString = rectString;
+ } else {
+ if (firstVisibleChild) {
+ mArrowColorRectString = rectString;
+ }
+ }
+
+ if (firstVisibleChild) {
+ firstVisibleChild = false;
+ }
+
+ }
+ }
+ }
+ if (!locations.isEmpty()) {
+ mColorExtractor.addLocation(locations);
+ }
+ }
+
+ /**
+ * Sets the background color of the child.
+ */
+ protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) {
+ Drawable bg = view.getBackground();
+ if (bg instanceof GradientDrawable) {
+ GradientDrawable gd = (GradientDrawable) bg.mutate();
+ int oldColor = ((GradientDrawable) bg).getColor().getDefaultColor();
+ animatorSetOut.play(ObjectAnimator.ofArgb(gd, "color", oldColor, color));
+ } else if (bg instanceof ColorDrawable) {
+ ColorDrawable cd = (ColorDrawable) bg.mutate();
+ int oldColor = ((ColorDrawable) bg).getColor();
+ animatorSetOut.play(ObjectAnimator.ofArgb(cd, "color", oldColor, color));
+ }
+ }
+
+ /**
* Shows the popup at the desired location, optionally reversing the children.
* @param viewsToFlip number of views from the top to to flip in case of reverse order
*/
protected void reorderAndShow(int viewsToFlip) {
+ setupForDisplay();
+ boolean reverseOrder = mIsAboveIcon;
+ if (reverseOrder) {
+ reverseOrder(viewsToFlip);
+ }
+ onInflationComplete(reverseOrder);
+ assignMarginsAndBackgrounds(this);
+ if (shouldAddArrow()) {
+ addArrow();
+ }
+ animateOpen();
+ }
+
+ /**
+ * Shows the popup at the desired location.
+ */
+ protected void show() {
+ setupForDisplay();
+ onInflationComplete(false);
+ assignMarginsAndBackgrounds(this);
+ if (shouldAddArrow()) {
+ addArrow();
+ }
+ animateOpen();
+ }
+
+ private void setupForDisplay() {
setVisibility(View.INVISIBLE);
mIsOpen = true;
getPopupContainer().addView(this);
orientAboutObject();
+ }
- boolean reverseOrder = mIsAboveIcon;
- if (reverseOrder) {
- int count = getChildCount();
- ArrayList<View> allViews = new ArrayList<>(count);
- for (int i = 0; i < count; i++) {
- if (i == viewsToFlip) {
- Collections.reverse(allViews);
- }
- allViews.add(getChildAt(i));
+ private void reverseOrder(int viewsToFlip) {
+ int count = getChildCount();
+ ArrayList<View> allViews = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ if (i == viewsToFlip) {
+ Collections.reverse(allViews);
}
- Collections.reverse(allViews);
- removeAllViews();
- for (int i = 0; i < count; i++) {
- addView(allViews.get(i));
- }
-
- orientAboutObject();
+ allViews.add(getChildAt(i));
}
- onInflationComplete(reverseOrder);
+ Collections.reverse(allViews);
+ removeAllViews();
+ for (int i = 0; i < count; i++) {
+ addView(allViews.get(i));
+ }
+ }
- // Add the arrow.
- final Resources res = getResources();
- final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
- ? R.dimen.popup_arrow_horizontal_center_start
- : R.dimen.popup_arrow_horizontal_center_end);
- final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
- getPopupContainer().addView(mArrow);
- DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
+ private int getArrowLeft() {
if (mIsLeftAligned) {
- mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
- } else {
- mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
+ return mArrowOffsetHorizontal;
}
+ return getMeasuredWidth() - mArrowOffsetHorizontal - mArrowWidth;
+ }
+
+ private void addArrow() {
+ getPopupContainer().addView(mArrow);
+ mArrow.setX(getX() + getArrowLeft());
if (Gravity.isVertical(mGravity)) {
// This is only true if there wasn't room for the container next to the icon,
// so we centered it instead. In that case we don't want to showDefaultOptions the arrow.
mArrow.setVisibility(INVISIBLE);
} else {
- ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
- arrowLp.width, arrowLp.height, !mIsAboveIcon));
- Paint arrowPaint = arrowDrawable.getPaint();
- arrowPaint.setColor(Themes.getAttrColor(getContext(), R.attr.popupColorPrimary));
- // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
- int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
- arrowPaint.setPathEffect(new CornerPathEffect(radius));
- mArrow.setBackground(arrowDrawable);
- // Clip off the part of the arrow that is underneath the popup.
- if (mIsAboveIcon) {
- mArrow.setClipBounds(new Rect(0, -mArrowOffset, arrowLp.width, arrowLp.height));
- } else {
- mArrow.setClipBounds(new Rect(0, 0, arrowLp.width, arrowLp.height + mArrowOffset));
- }
- mArrow.setElevation(getElevation());
+ updateArrowColor();
}
- mArrow.setPivotX(arrowLp.width / 2);
- mArrow.setPivotY(mIsAboveIcon ? arrowLp.height : 0);
-
- animateOpen();
+ mArrow.setPivotX(mArrowWidth / 2.0f);
+ mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0);
}
- protected boolean isAlignedWithStart() {
- return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
+ private void updateArrowColor() {
+ if (!Gravity.isVertical(mGravity)) {
+ mArrow.setBackground(new RoundedArrowDrawable(
+ mArrowWidth, mArrowHeight, mArrowPointRadius,
+ mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(),
+ mArrowOffsetHorizontal, -mArrowOffsetVertical,
+ !mIsAboveIcon, mIsLeftAligned,
+ mArrowColor));
+ setElevation(mElevation);
+ mArrow.setElevation(mElevation);
+ }
+ }
+
+ /**
+ * Returns whether or not we should add the arrow.
+ */
+ protected boolean shouldAddArrow() {
+ return true;
}
/**
@@ -252,10 +553,19 @@
*/
private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) {
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- int width = getMeasuredWidth();
- int extraVerticalSpace = mArrow.getLayoutParams().height + mArrowOffset
+
+ int extraVerticalSpace = mArrowHeight + mArrowOffsetVertical
+ getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
- int height = getMeasuredHeight() + extraVerticalSpace;
+ // The margins are added after we call this method, so we need to account for them here.
+ int numVisibleChildren = 0;
+ for (int i = getChildCount() - 1; i >= 0; --i) {
+ if (getChildAt(i).getVisibility() == VISIBLE) {
+ numVisibleChildren++;
+ }
+ }
+ int childMargins = (numVisibleChildren - 1) * mMargin;
+ int height = getMeasuredHeight() + extraVerticalSpace + childMargins;
+ int width = getMeasuredWidth() + getPaddingLeft() + getPaddingRight();
getTargetObjectLocation(mTempRect);
InsettableFrameLayout dragLayer = getPopupContainer();
@@ -269,22 +579,7 @@
// Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
int iconWidth = mTempRect.width();
- Resources resources = getResources();
- int xOffset;
- if (isAlignedWithStart()) {
- // Aligning with the shortcut icon.
- int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
- int shortcutPaddingStart = resources.getDimensionPixelSize(
- R.dimen.popup_padding_start);
- xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
- } else {
- // Aligning with the drag handle.
- int shortcutDragHandleWidth = resources.getDimensionPixelSize(
- R.dimen.deep_shortcut_drag_handle_size);
- int shortcutPaddingEnd = resources.getDimensionPixelSize(
- R.dimen.popup_padding_end);
- xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
- }
+ int xOffset = iconWidth / 2 - mArrowOffsetHorizontal - mArrowWidth / 2;
x += mIsLeftAligned ? xOffset : -xOffset;
// Check whether we can still align as we originally wanted, now that we've calculated x.
@@ -353,12 +648,14 @@
FrameLayout.LayoutParams arrowLp = (FrameLayout.LayoutParams) mArrow.getLayoutParams();
if (mIsAboveIcon) {
arrowLp.gravity = lp.gravity = Gravity.BOTTOM;
- lp.bottomMargin = getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
- arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrowOffset - insets.bottom;
+ lp.bottomMargin =
+ getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
+ arrowLp.bottomMargin =
+ lp.bottomMargin - arrowLp.height - mArrowOffsetVertical - insets.bottom;
} else {
arrowLp.gravity = lp.gravity = Gravity.TOP;
lp.topMargin = y + insets.top;
- arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffset;
+ arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffsetVertical;
}
}
@@ -397,39 +694,10 @@
private void animateOpen() {
setVisibility(View.VISIBLE);
- final AnimatorSet openAnim = new AnimatorSet();
- final Resources res = getResources();
- final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
- final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration);
- final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
-
- // Rectangular reveal.
- mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
- final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
- .createRevealAnimator(this, false);
- revealAnim.setDuration(revealDuration);
- revealAnim.setInterpolator(revealInterpolator);
- // Clip the popup to the initial outline while the notification dot and arrow animate.
- revealAnim.start();
- revealAnim.pause();
-
- ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1);
- fadeIn.setDuration(revealDuration + arrowDuration);
- fadeIn.setInterpolator(revealInterpolator);
- fadeIn.addUpdateListener(anim -> {
- float alpha = (float) anim.getAnimatedValue();
- mArrow.setAlpha(alpha);
- setAlpha(revealAnim.isStarted() ? alpha : 0);
- });
- openAnim.play(fadeIn);
-
- // Animate the arrow.
- mArrow.setScaleX(0);
- mArrow.setScaleY(0);
- Animator arrowScale = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 1)
- .setDuration(arrowDuration);
-
- openAnim.addListener(new AnimatorListenerAdapter() {
+ mOpenCloseAnimator = getOpenCloseAnimator(true, OPEN_DURATION, OPEN_FADE_START_DELAY,
+ OPEN_FADE_DURATION, OPEN_CHILD_FADE_START_DELAY, OPEN_CHILD_FADE_DURATION,
+ DECELERATED_EASE);
+ mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setAlpha(1f);
@@ -437,56 +705,77 @@
mOpenCloseAnimator = null;
}
});
-
- mOpenCloseAnimator = openAnim;
- openAnim.playSequentially(arrowScale, revealAnim);
- openAnim.start();
+ mOpenCloseAnimator.start();
}
+ private AnimatorSet getOpenCloseAnimator(boolean isOpening, int totalDuration,
+ int fadeStartDelay, int fadeDuration, int childFadeStartDelay,
+ int childFadeDuration, Interpolator interpolator) {
+ final AnimatorSet animatorSet = new AnimatorSet();
+ float[] alphaValues = isOpening ? new float[] {0, 1} : new float[] {1, 0};
+ float[] scaleValues = isOpening ? new float[] {0.5f, 1} : new float[] {1, 0.5f};
+
+ ValueAnimator fade = ValueAnimator.ofFloat(alphaValues);
+ fade.setStartDelay(fadeStartDelay);
+ fade.setDuration(fadeDuration);
+ fade.setInterpolator(LINEAR);
+ fade.addUpdateListener(anim -> {
+ float alpha = (float) anim.getAnimatedValue();
+ mArrow.setAlpha(alpha);
+ setAlpha(alpha);
+ });
+ animatorSet.play(fade);
+
+ setPivotX(mIsLeftAligned ? 0 : getMeasuredWidth());
+ setPivotY(mIsAboveIcon ? getMeasuredHeight() : 0);
+ Animator scale = ObjectAnimator.ofFloat(this, View.SCALE_Y, scaleValues);
+ scale.setDuration(totalDuration);
+ scale.setInterpolator(interpolator);
+ animatorSet.play(scale);
+
+ fadeInChildViews(this, alphaValues, childFadeStartDelay, childFadeDuration, animatorSet);
+
+ return animatorSet;
+ }
+
+ private void fadeInChildViews(ViewGroup group, float[] alphaValues, long startDelay,
+ long duration, AnimatorSet out) {
+ for (int i = group.getChildCount() - 1; i >= 0; --i) {
+ View view = group.getChildAt(i);
+ if (view.getVisibility() == VISIBLE && view instanceof ViewGroup) {
+ if (mIterateChildrenTag.equals(view.getTag())) {
+ fadeInChildViews((ViewGroup) view, alphaValues, startDelay, duration, out);
+ continue;
+ }
+ for (int j = ((ViewGroup) view).getChildCount() - 1; j >= 0; --j) {
+ View childView = ((ViewGroup) view).getChildAt(j);
+ childView.setAlpha(alphaValues[0]);
+ ValueAnimator childFade = ObjectAnimator.ofFloat(childView, ALPHA, alphaValues);
+ childFade.setStartDelay(startDelay);
+ childFade.setDuration(duration);
+ childFade.setInterpolator(LINEAR);
+
+ out.play(childFade);
+ }
+ }
+ }
+ }
+
+
protected void animateClose() {
if (!mIsOpen) {
return;
}
- if (getOutlineProvider() instanceof RevealOutlineAnimation) {
- ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
- } else {
- mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
- }
if (mOpenCloseAnimator != null) {
mOpenCloseAnimator.cancel();
}
mIsOpen = false;
-
- final AnimatorSet closeAnim = new AnimatorSet();
- final Resources res = getResources();
- final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
- final long revealDuration = res.getInteger(R.integer.config_popupOpenCloseDuration);
- final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration);
-
- // Hide the arrow
- Animator scaleArrow = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0)
- .setDuration(arrowDuration);
-
- // Rectangular reveal (reversed).
- final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
- .createRevealAnimator(this, true);
- revealAnim.setDuration(revealDuration);
- revealAnim.setInterpolator(revealInterpolator);
- closeAnim.playSequentially(revealAnim, scaleArrow);
-
- ValueAnimator fadeOut = ValueAnimator.ofFloat(getAlpha(), 0);
- fadeOut.setDuration(revealDuration + arrowDuration);
- fadeOut.setInterpolator(revealInterpolator);
- fadeOut.addUpdateListener(anim -> {
- float alpha = (float) anim.getAnimatedValue();
- mArrow.setAlpha(alpha);
- setAlpha(scaleArrow.isStarted() ? 0 : alpha);
- });
- closeAnim.play(fadeOut);
-
- onCreateCloseAnimation(closeAnim);
- closeAnim.addListener(new AnimatorListenerAdapter() {
+ mOpenCloseAnimator = getOpenCloseAnimator(false, CLOSE_DURATION, CLOSE_FADE_START_DELAY,
+ CLOSE_FADE_DURATION, CLOSE_CHILD_FADE_START_DELAY, CLOSE_CHILD_FADE_DURATION,
+ ACCELERATED_EASE);
+ onCreateCloseAnimation(mOpenCloseAnimator);
+ mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mOpenCloseAnimator = null;
@@ -497,8 +786,7 @@
}
}
});
- mOpenCloseAnimator = closeAnim;
- closeAnim.start();
+ mOpenCloseAnimator.start();
}
/**
@@ -506,25 +794,6 @@
*/
protected void onCreateCloseAnimation(AnimatorSet anim) { }
- private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
- Resources res = getResources();
- int arrowCenterX = res.getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
- R.dimen.popup_arrow_horizontal_center_start:
- R.dimen.popup_arrow_horizontal_center_end);
- int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
- float arrowCornerRadius = res.getDimension(R.dimen.popup_arrow_corner_radius);
- if (!mIsLeftAligned) {
- arrowCenterX = getMeasuredWidth() - arrowCenterX;
- }
- int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0;
-
- mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
- arrowCenterY);
-
- return new RoundedRectRevealOutlineProvider
- (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
- }
-
/**
* Closes the popup without animation.
*/
@@ -537,6 +806,20 @@
mDeferContainerRemoval = false;
getPopupContainer().removeView(this);
getPopupContainer().removeView(mArrow);
+ mOnCloseCallback.run();
+ mArrowColorRectString = null;
+ mViewForRect.clear();
+ if (mColorExtractor != null) {
+ mColorExtractor.removeLocations();
+ mColorExtractor.setListener(null);
+ }
+ }
+
+ /**
+ * Callback to be called when the popup is closed
+ */
+ public void setOnCloseCallback(@NonNull Runnable callback) {
+ mOnCloseCallback = callback;
}
protected BaseDragLayer getPopupContainer() {
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 614cf14..18f263a 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -19,13 +19,8 @@
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
import static com.android.launcher3.Utilities.squaredHypot;
import static com.android.launcher3.Utilities.squaredTouchSlop;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.notification.NotificationMainView.NOTIFICATION_ITEM_INFO;
import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.animation.AnimatorSet;
@@ -51,8 +46,8 @@
import com.android.launcher3.DropTarget;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
import com.android.launcher3.dot.DotInfo;
@@ -69,12 +64,14 @@
import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
+import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.ShortcutUtil;
import com.android.launcher3.views.BaseDragLayer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -86,8 +83,8 @@
*
* @param <T> The activity on with the popup shows
*/
-public class PopupContainerWithArrow<T extends BaseDraggingActivity> extends ArrowPopup<T>
- implements DragSource, DragController.DragListener {
+public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
+ extends ArrowPopup<T> implements DragSource, DragController.DragListener {
private final List<DeepShortcutView> mShortcuts = new ArrayList<>();
private final PointF mInterceptTouchDown = new PointF();
@@ -97,6 +94,11 @@
private BubbleTextView mOriginalIcon;
private NotificationItemView mNotificationItemView;
private int mNumNotifications;
+ private ViewGroup mNotificationContainer;
+
+ private ViewGroup mWidgetContainer;
+
+ private ViewGroup mDeepShortcutContainer;
private ViewGroup mSystemShortcutContainer;
@@ -136,29 +138,10 @@
}
@Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (mNotificationItemView != null) {
- return mNotificationItemView.onTouchEvent(ev) || super.onTouchEvent(ev);
- }
- return super.onTouchEvent(ev);
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_ACTION_POPUP) != 0;
}
- @Override
- public void logActionCommand(int command) {
- mLauncher.getUserEventDispatcher().logActionCommand(
- command, mOriginalIcon, getLogContainerType());
- }
-
- @Override
- public int getLogContainerType() {
- return ContainerType.DEEPSHORTCUTS;
- }
-
public OnClickListener getItemClickListener() {
return (view) -> {
mLauncher.getItemOnClickListener().onClick(view);
@@ -175,8 +158,7 @@
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
BaseDragLayer dl = getPopupContainer();
if (!dl.isEventOverView(this, ev)) {
- mLauncher.getUserEventDispatcher().logActionTapOutside(
- newContainerTarget(ContainerType.DEEPSHORTCUTS));
+ // TODO: add WW log if want to log if tap closed deep shortcut container.
close(true);
// We let touches on the original icon go through so that users can launch
@@ -187,6 +169,14 @@
return false;
}
+ @Override
+ protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) {
+ super.setChildColor(view, color, animatorSetOut);
+ if (view.getId() == R.id.notification_container && mNotificationItemView != null) {
+ mNotificationItemView.updateBackgroundColor(color, animatorSetOut);
+ }
+ }
+
/**
* Returns true if we can show the container.
*/
@@ -224,6 +214,7 @@
.filter(Objects::nonNull)
.collect(Collectors.toList()));
launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
+ container.requestFocus();
return container;
}
@@ -232,6 +223,13 @@
mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
launcher.getDragController().addDragListener(this);
+ addPreDrawForColorExtraction(launcher);
+ }
+
+ @Override
+ protected List<View> getChildrenForColorExtraction() {
+ return Arrays.asList(mSystemShortcutContainer, mWidgetContainer, mDeepShortcutContainer,
+ mNotificationContainer);
}
@Override
@@ -239,20 +237,6 @@
if (isReversed && mNotificationItemView != null) {
mNotificationItemView.inverseGutterMargin();
}
-
- // Update dividers
- int count = getChildCount();
- DeepShortcutView lastView = null;
- for (int i = 0; i < count; i++) {
- View view = getChildAt(i);
- if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) {
- if (lastView != null) {
- lastView.setDividerVisibility(VISIBLE);
- }
- lastView = (DeepShortcutView) view;
- lastView.setDividerVisibility(INVISIBLE);
- }
- }
}
@TargetApi(Build.VERSION_CODES.P)
@@ -274,44 +258,64 @@
// Add views
if (mNumNotifications > 0) {
// Add notification entries
- View.inflate(getContext(), R.layout.notification_content, this);
- mNotificationItemView = new NotificationItemView(this);
- if (mNumNotifications == 1) {
- mNotificationItemView.removeFooter();
+ if (mNotificationContainer == null) {
+ mNotificationContainer = findViewById(R.id.notification_container);
+ mNotificationContainer.setVisibility(VISIBLE);
}
- else {
- mNotificationItemView.setFooterWidth(containerWidth);
- }
+ View.inflate(getContext(), R.layout.notification_content, mNotificationContainer);
+ mNotificationItemView = new NotificationItemView(this, mNotificationContainer);
updateNotificationHeader();
}
int viewsToFlip = getChildCount();
mSystemShortcutContainer = this;
+ if (mDeepShortcutContainer == null) {
+ mDeepShortcutContainer = findViewById(R.id.deep_shortcuts_container);
+ }
if (hasDeepShortcuts) {
+ mDeepShortcutContainer.setVisibility(View.VISIBLE);
+
if (mNotificationItemView != null) {
mNotificationItemView.addGutter();
}
for (int i = shortcutCount; i > 0; i--) {
- DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, this);
+ DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, mDeepShortcutContainer);
v.getLayoutParams().width = containerWidth;
mShortcuts.add(v);
}
updateHiddenShortcuts();
if (!systemShortcuts.isEmpty()) {
- mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this);
for (SystemShortcut shortcut : systemShortcuts) {
- initializeSystemShortcut(
- R.layout.system_shortcut_icon_only, mSystemShortcutContainer, shortcut);
+ if (shortcut instanceof SystemShortcut.Widgets) {
+ if (mWidgetContainer == null) {
+ mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container,
+ this);
+ }
+ initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer,
+ shortcut);
+ }
+ }
+ mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this);
+
+ for (SystemShortcut shortcut : systemShortcuts) {
+ if (!(shortcut instanceof SystemShortcut.Widgets)) {
+ initializeSystemShortcut(
+ R.layout.system_shortcut_icon_only, mSystemShortcutContainer,
+ shortcut);
+ }
}
}
- } else if (!systemShortcuts.isEmpty()) {
- if (mNotificationItemView != null) {
- mNotificationItemView.addGutter();
- }
+ } else {
+ mDeepShortcutContainer.setVisibility(View.GONE);
+ if (!systemShortcuts.isEmpty()) {
+ if (mNotificationItemView != null) {
+ mNotificationItemView.addGutter();
+ }
- for (SystemShortcut shortcut : systemShortcuts) {
- initializeSystemShortcut(R.layout.system_shortcut, this, shortcut);
+ for (SystemShortcut shortcut : systemShortcuts) {
+ initializeSystemShortcut(R.layout.system_shortcut, this, shortcut);
+ }
}
}
@@ -359,34 +363,11 @@
private void updateHiddenShortcuts() {
int allowedCount = mNotificationItemView != null
? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS;
- int originalHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height);
- int itemHeight = mNotificationItemView != null ?
- getResources().getDimensionPixelSize(R.dimen.bg_popup_item_condensed_height)
- : originalHeight;
- float iconScale = ((float) itemHeight) / originalHeight;
int total = mShortcuts.size();
for (int i = 0; i < total; i++) {
DeepShortcutView view = mShortcuts.get(i);
view.setVisibility(i >= allowedCount ? GONE : VISIBLE);
- view.getLayoutParams().height = itemHeight;
- view.getIconView().setScaleX(iconScale);
- view.getIconView().setScaleY(iconScale);
- }
- }
-
- private void updateDividers() {
- int count = getChildCount();
- DeepShortcutView lastView = null;
- for (int i = 0; i < count; i++) {
- View view = getChildAt(i);
- if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) {
- if (lastView != null) {
- lastView.setDividerVisibility(VISIBLE);
- }
- lastView = (DeepShortcutView) view;
- lastView.setDividerVisibility(INVISIBLE);
- }
}
}
@@ -400,9 +381,7 @@
} else if (view instanceof ImageView) {
// Only the system shortcut icon shows on a gray background header.
info.setIconAndContentDescriptionFor((ImageView) view);
- if (Utilities.ATLEAST_OREO) {
- view.setTooltipText(view.getContentDescription());
- }
+ view.setTooltipText(view.getContentDescription());
}
view.setTag(info);
view.setOnClickListener(info);
@@ -452,7 +431,9 @@
// Make sure we keep the original icon hidden while it is being dragged.
mOriginalIcon.setVisibility(INVISIBLE);
} else {
- mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
+ // TODO: add WW logging if want to add logging for long press on popup
+ // container.
+ // mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
if (!mIsAboveIcon) {
// Show the icon but keep the text hidden.
mOriginalIcon.setVisibility(VISIBLE);
@@ -467,8 +448,7 @@
ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag();
DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo);
if (mNotificationItemView != null && dotInfo != null) {
- mNotificationItemView.updateHeader(
- dotInfo.getNotificationCount(), itemInfo.bitmap.color);
+ mNotificationItemView.updateHeader(dotInfo.getNotificationCount());
}
}
@@ -499,18 +479,6 @@
}
@Override
- public void fillInLogContainerData(ItemInfo childInfo, Target child,
- ArrayList<Target> parents) {
- if (childInfo == NOTIFICATION_ITEM_INFO) {
- child.itemType = ItemType.NOTIFICATION;
- } else {
- child.itemType = ItemType.DEEPSHORTCUT;
- child.rank = childInfo.rank;
- }
- parents.add(newContainerTarget(ContainerType.DEEPSHORTCUTS));
- }
-
- @Override
protected void onCreateCloseAnimation(AnimatorSet anim) {
// Animate original icon's text back in.
anim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
@@ -557,25 +525,34 @@
mLauncher.getPopupDataProvider().setChangeListener(null);
}
+ private View getWidgetsView(ViewGroup container) {
+ for (int i = container.getChildCount() - 1; i >= 0; --i) {
+ View systemShortcutView = container.getChildAt(i);
+ if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
+ return systemShortcutView;
+ }
+ }
+ return null;
+ }
+
@Override
public void onWidgetsBound() {
ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo);
- View widgetsView = null;
- int count = mSystemShortcutContainer.getChildCount();
- for (int i = 0; i < count; i++) {
- View systemShortcutView = mSystemShortcutContainer.getChildAt(i);
- if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
- widgetsView = systemShortcutView;
- break;
- }
+ View widgetsView = getWidgetsView(PopupContainerWithArrow.this);
+ if (widgetsView == null && mWidgetContainer != null) {
+ widgetsView = getWidgetsView(mWidgetContainer);
}
if (widgetInfo != null && widgetsView == null) {
// We didn't have any widgets cached but now there are some, so enable the shortcut.
if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
- initializeSystemShortcut(R.layout.system_shortcut_icon_only,
- mSystemShortcutContainer, widgetInfo);
+ if (mWidgetContainer == null) {
+ mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container,
+ PopupContainerWithArrow.this);
+ }
+ initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer,
+ widgetInfo);
} else {
// If using the expanded system shortcut (as opposed to just the icon), we need
// to reopen the container to ensure measurements etc. all work out. While this
@@ -587,8 +564,10 @@
}
} else if (widgetInfo == null && widgetsView != null) {
// No widgets exist, but we previously added the shortcut so remove it.
- if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
- mSystemShortcutContainer.removeView(widgetsView);
+ if (mSystemShortcutContainer
+ != PopupContainerWithArrow.this
+ && mWidgetContainer != null) {
+ mWidgetContainer.removeView(widgetsView);
} else {
close(false);
PopupContainerWithArrow.showForIcon(mOriginalIcon);
@@ -620,8 +599,9 @@
// No more notifications, remove the notification views and expand all shortcuts.
mNotificationItemView.removeAllViews();
mNotificationItemView = null;
+ mNotificationContainer.setVisibility(GONE);
updateHiddenShortcuts();
- updateDividers();
+ assignMarginsAndBackgrounds(PopupContainerWithArrow.this);
} else {
mNotificationItemView.trimNotifications(
NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
@@ -630,6 +610,17 @@
}
/**
+ * Dismisses the popup if it is no longer valid
+ */
+ public static void dismissInvalidPopup(BaseDraggingActivity activity) {
+ PopupContainerWithArrow popup = getOpen(activity);
+ if (popup != null && (!popup.mOriginalIcon.isAttachedToWindow()
+ || !canShow(popup.mOriginalIcon, (ItemInfo) popup.mOriginalIcon.getTag()))) {
+ popup.animateClose();
+ }
+ }
+
+ /**
* Handler to control drag-and-drop for popup items
*/
public interface PopupItemDragHandler extends OnLongClickListener, OnTouchListener { }
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 5a5f668..6f9f0d7 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -31,16 +31,16 @@
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.ShortcutUtil;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
import java.io.PrintWriter;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -59,8 +59,11 @@
private HashMap<ComponentKey, Integer> mDeepShortcutMap = new HashMap<>();
/** Maps packages to their DotInfo's . */
private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
- /** Maps packages to their Widgets */
- private ArrayList<WidgetListRowEntry> mAllWidgets = new ArrayList<>();
+
+ /** All installed widgets. */
+ private List<WidgetsListBaseEntry> mAllWidgets = List.of();
+ /** Widgets that can be recommended to the users. */
+ private List<ItemInfo> mRecommendedWidgets = List.of();
private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
@@ -188,7 +191,16 @@
notificationListener.cancelNotificationFromLauncher(notificationKey);
}
- public void setAllWidgets(ArrayList<WidgetListRowEntry> allWidgets) {
+ /**
+ * Sets a list of recommended widgets ordered by their order of appearance in the widgets
+ * recommendation UI.
+ */
+ public void setRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
+ mRecommendedWidgets = recommendedWidgets;
+ mChangeListener.onRecommendedWidgetsBound();
+ }
+
+ public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
mAllWidgets = allWidgets;
mChangeListener.onWidgetsBound();
}
@@ -197,25 +209,33 @@
mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
}
- public ArrayList<WidgetListRowEntry> getAllWidgets() {
+ public List<WidgetsListBaseEntry> getAllWidgets() {
return mAllWidgets;
}
+ /** Returns a list of recommended widgets. */
+ public List<WidgetItem> getRecommendedWidgets() {
+ HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
+ mAllWidgets.stream()
+ .filter(entry -> entry instanceof WidgetsListContentEntry)
+ .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets
+ .forEach(widget -> allWidgetItems.put(
+ new ComponentKey(widget.componentName, widget.user), widget)));
+ return mRecommendedWidgets.stream()
+ .map(recommendedWidget -> allWidgetItems.get(
+ new ComponentKey(recommendedWidget.getTargetComponent(),
+ recommendedWidget.user)))
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
- for (WidgetListRowEntry entry : mAllWidgets) {
- if (entry.pkgItem.packageName.equals(packageUserKey.mPackageName)) {
- ArrayList<WidgetItem> widgets = new ArrayList<>(entry.widgets);
- // Remove widgets not associated with the correct user.
- Iterator<WidgetItem> iterator = widgets.iterator();
- while (iterator.hasNext()) {
- if (!iterator.next().user.equals(packageUserKey.mUser)) {
- iterator.remove();
- }
- }
- return widgets.isEmpty() ? null : widgets;
- }
- }
- return null;
+ return mAllWidgets.stream()
+ .filter(row -> row instanceof WidgetsListContentEntry
+ && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
+ .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
+ .filter(widget -> packageUserKey.mUser.equals(widget.user))
+ .collect(Collectors.toList());
}
/**
@@ -253,5 +273,8 @@
default void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) { }
default void onWidgetsBound() { }
+
+ /** A callback to get notified when recommended widgets are bound. */
+ default void onRecommendedWidgetsBound() { }
}
}
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index 61829c0..7c393ad 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -37,7 +37,6 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
@TargetApi(Build.VERSION_CODES.Q)
public class RemoteActionShortcut extends SystemShortcut<BaseDraggingActivity> {
@@ -107,9 +106,6 @@
Toast.LENGTH_SHORT)
.show();
}
-
- mTarget.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
- LauncherLogProto.ControlType.REMOTE_ACTION_SHORTCUT, view);
}
@Override
diff --git a/src/com/android/launcher3/popup/RoundedArrowDrawable.java b/src/com/android/launcher3/popup/RoundedArrowDrawable.java
new file mode 100644
index 0000000..e662d5c
--- /dev/null
+++ b/src/com/android/launcher3/popup/RoundedArrowDrawable.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.popup;
+
+import static java.lang.Math.atan;
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import static java.lang.Math.toDegrees;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A drawable for a very specific purpose. Used for the caret arrow on a rounded rectangle popup
+ * bubble.
+ * Draws a triangle with one rounded tip, the opposite edge is clipped by the body of the popup
+ * so there is no overlap when drawing them together.
+ */
+public class RoundedArrowDrawable extends Drawable {
+
+ private final Path mPath;
+ private final Paint mPaint;
+
+ /**
+ * Default constructor.
+ *
+ * @param width of the arrow.
+ * @param height of the arrow.
+ * @param radius of the tip of the arrow.
+ * @param popupRadius of the rect to clip this by.
+ * @param popupWidth of the rect to clip this by.
+ * @param popupHeight of the rect to clip this by.
+ * @param arrowOffsetX from the edge of the popup to the arrow.
+ * @param arrowOffsetY how much the arrow will overlap the popup.
+ * @param isPointingUp or not.
+ * @param leftAligned or false for right aligned.
+ * @param color to draw the triangle.
+ */
+ public RoundedArrowDrawable(float width, float height, float radius, float popupRadius,
+ float popupWidth, float popupHeight,
+ float arrowOffsetX, float arrowOffsetY, boolean isPointingUp, boolean leftAligned,
+ int color) {
+ mPath = new Path();
+ mPaint = new Paint();
+ mPaint.setColor(color);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setAntiAlias(true);
+
+ // Make the drawable with the triangle pointing down and positioned on the left..
+ addDownPointingRoundedTriangleToPath(width, height, radius, mPath);
+ clipPopupBodyFromPath(popupRadius, popupWidth, popupHeight, arrowOffsetX, arrowOffsetY,
+ mPath);
+
+ // ... then flip it horizontal or vertical based on where it will be used.
+ Matrix pathTransform = new Matrix();
+ pathTransform.setScale(
+ leftAligned ? 1 : -1, isPointingUp ? -1 : 1, width * 0.5f, height * 0.5f);
+ mPath.transform(pathTransform);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawPath(mPath, mPaint);
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ outline.setPath(mPath);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha(int i) {
+ mPaint.setAlpha(i);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ }
+
+ private static void addDownPointingRoundedTriangleToPath(float width, float height,
+ float radius, Path path) {
+ // Calculated for the arrow pointing down, will be flipped later if needed.
+
+ // Theta is half of the angle inside the triangle tip
+ float tanTheta = width / (2.0f * height);
+ float theta = (float) atan(tanTheta);
+
+ // Some trigonometry to find the center of the circle for the rounded tip
+ float roundedPointCenterY = (float) (height - (radius / sin(theta)));
+
+ // p is the distance along the triangle side to the intersection with the point circle
+ float p = radius / tanTheta;
+ float lineRoundPointIntersectFromCenter = (float) (p * sin(theta));
+ float lineRoundPointIntersectFromTop = (float) (height - (p * cos(theta)));
+
+ float centerX = width / 2.0f;
+ float thetaDeg = (float) toDegrees(theta);
+
+ path.reset();
+ path.moveTo(0, 0);
+ // Draw the top
+ path.lineTo(width, 0);
+ // Draw the right side up to the circle intersection
+ path.lineTo(
+ centerX + lineRoundPointIntersectFromCenter,
+ lineRoundPointIntersectFromTop);
+ // Draw the rounded point
+ path.arcTo(
+ centerX - radius,
+ roundedPointCenterY - radius,
+ centerX + radius,
+ roundedPointCenterY + radius,
+ thetaDeg,
+ 180 - (2 * thetaDeg),
+ false);
+ // Draw the left edge to close
+ path.lineTo(0, 0);
+ path.close();
+ }
+
+ private static void clipPopupBodyFromPath(float popupRadius, float popupWidth,
+ float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path) {
+ // Make a path that is used to clip the triangle, this represents the body of the popup
+ Path clipPiece = new Path();
+ clipPiece.addRoundRect(
+ 0, 0, popupWidth, popupHeight,
+ popupRadius, popupRadius, Path.Direction.CW);
+ // clipping is performed as if the arrow is pointing down and positioned on the left, the
+ // resulting path will be flipped as needed later.
+ // The extra 0.5 in the vertical offset is to close the gap between this anti-aliased object
+ // and the anti-aliased body of the popup.
+ clipPiece.offset(-arrowOffsetX, -popupHeight + arrowOffsetY - 0.5f);
+ path.op(clipPiece, Path.Op.DIFFERENCE);
+ }
+}
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index fd292a3..d3f4909 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -21,8 +21,6 @@
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
import com.android.launcher3.util.InstantAppResolver;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
@@ -47,6 +45,13 @@
protected final T mTarget;
protected final ItemInfo mItemInfo;
+ /**
+ * Indicates if it's invokable or not through some disabled UI
+ */
+ private boolean isEnabled = true;
+
+ private boolean mHasFinishRecentsInAction = false;
+
public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) {
mIconResId = iconResId;
mLabelResId = labelResId;
@@ -85,10 +90,26 @@
mAccessibilityActionId, context.getText(mLabelResId));
}
+ public void setEnabled(boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
public boolean hasHandlerForAction(int action) {
return mAccessibilityActionId == action;
}
+ public void setHasFinishRecentsInAction(boolean hasFinishRecentsInAction) {
+ mHasFinishRecentsInAction = hasFinishRecentsInAction;
+ }
+
+ public boolean hasFinishRecentsInAction() {
+ return mHasFinishRecentsInAction;
+ }
+
public interface Factory<T extends BaseDraggingActivity> {
@Nullable SystemShortcut<T> getShortcut(T activity, ItemInfo itemInfo);
@@ -99,7 +120,7 @@
final List<WidgetItem> widgets =
launcher.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
- if (widgets == null) {
+ if (widgets.isEmpty()) {
return null;
}
return new Widgets(launcher, itemInfo);
@@ -117,8 +138,6 @@
(WidgetsBottomSheet) mTarget.getLayoutInflater().inflate(
R.layout.widgets_bottom_sheet, mTarget.getDragLayer(), false);
widgetsBottomSheet.populateAndShow(mItemInfo);
- mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
- ControlType.WIDGETS_BUTTON, view);
mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
.log(LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP);
}
@@ -139,8 +158,6 @@
Rect sourceBounds = mTarget.getViewBounds(view);
new PackageManagerHelper(mTarget).startDetailsActivityForInfo(
mItemInfo, sourceBounds, ActivityOptions.makeBasic().toBundle());
- mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
- ControlType.APPINFO_TARGET, view);
mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
.log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP);
}
@@ -174,7 +191,7 @@
public void onClick(View view) {
Intent intent = new PackageManagerHelper(view.getContext()).getMarketIntent(
mItemInfo.getTargetComponent().getPackageName());
- mTarget.startActivitySafely(view, intent, mItemInfo, null);
+ mTarget.startActivitySafely(view, intent, mItemInfo);
AbstractFloatingView.closeAllOpenViews(mTarget);
}
}
diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java
index a5462a6..c9af2fe 100644
--- a/src/com/android/launcher3/provider/ImportDataTask.java
+++ b/src/com/android/launcher3/provider/ImportDataTask.java
@@ -284,8 +284,9 @@
insertOperations.clear();
}
- IntSparseArrayMap<Object> hotseatItems = GridSizeMigrationTask.removeBrokenHotseatItems(mContext);
- int myHotseatCount = LauncherAppState.getIDP(mContext).numHotseatIcons;
+ IntSparseArrayMap<Object> hotseatItems = GridSizeMigrationTask
+ .removeBrokenHotseatItems(mContext);
+ int myHotseatCount = LauncherAppState.getIDP(mContext).numDatabaseHotseatIcons;
if (hotseatItems.size() < myHotseatCount) {
// Insufficient hotseat items. Add a few more.
HotseatParserCallback parserCallback = new HotseatParserCallback(
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 53183bf..223f4f1 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -103,7 +103,7 @@
*/
private void backupWorkspace(Context context, SQLiteDatabase db) throws Exception {
InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
- new GridBackupTable(context, db, idp.numHotseatIcons, idp.numColumns, idp.numRows)
+ new GridBackupTable(context, db, idp.numDatabaseHotseatIcons, idp.numColumns, idp.numRows)
.doBackup(getDefaultProfileId(db), GridBackupTable.OPTION_REQUIRES_SANITIZATION);
}
@@ -111,7 +111,7 @@
@NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)
throws Exception {
final InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
- GridBackupTable backupTable = new GridBackupTable(context, db, idp.numHotseatIcons,
+ GridBackupTable backupTable = new GridBackupTable(context, db, idp.numDatabaseHotseatIcons,
idp.numColumns, idp.numRows);
if (backupTable.restoreFromRawBackupIfAvailable(getDefaultProfileId(db))) {
int itemsDeleted = sanitizeDB(helper, db, backupManager);
diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 289e0d8..23ee251 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -30,7 +30,6 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.graphics.Rect;
import android.os.Bundle;
import android.provider.Settings;
import android.util.AttributeSet;
@@ -42,13 +41,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.launcher3.AppWidgetResizeFrame;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.FragmentWithPreview;
+import com.android.launcher3.widget.util.WidgetSizes;
/**
* A frame layout which contains a QSB. This internally uses fragment to bind the view, which
@@ -158,7 +157,7 @@
protected String mKeyWidgetId = "qsb_widget_id";
private QsbWidgetHost mQsbWidgetHost;
- private AppWidgetProviderInfo mWidgetInfo;
+ protected AppWidgetProviderInfo mWidgetInfo;
private QsbWidgetHostView mQsb;
// We need to store the orientation here, due to a bug (b/64916689) that results in widgets
@@ -292,15 +291,8 @@
protected Bundle createBindOptions() {
InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
-
- Bundle opts = new Bundle();
- Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(),
- idp.numColumns, 1, null);
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
- return opts;
+ return WidgetSizes.getWidgetSizeOptions(getContext(), mWidgetInfo.provider,
+ idp.numColumns, 1);
}
protected View getDefaultView(ViewGroup container, boolean showSetupIcon) {
diff --git a/src/com/android/launcher3/recyclerview/ViewHolderBinder.java b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java
new file mode 100644
index 0000000..5b8d5bc
--- /dev/null
+++ b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.recyclerview;
+
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * Creates and populates views with data
+ *
+ * @param <T> A data model which is used to populate the {@link ViewHolder}.
+ * @param <V> A subclass of {@link ViewHolder} which holds references to views.
+ */
+public interface ViewHolderBinder<T, V extends ViewHolder> {
+ /**
+ * Creates a new view, and attach it to the parent {@link ViewGroup}. Then, populates UI
+ * references in a {@link ViewHolder}.
+ */
+ V newViewHolder(ViewGroup parent);
+
+ /** Populate UI references in {@link ViewHolder} with data. */
+ void bindViewHolder(V viewHolder, T data, int position);
+
+ /**
+ * Called when the view is recycled. Views are recycled in batches once they are sufficiently
+ * far off screen that it is unlikely the user will scroll back to them soon. Optionally
+ * override this to free expensive resources.
+ */
+ default void unbindViewHolder(V viewHolder) {}
+}
diff --git a/src/com/android/launcher3/search/SearchAlgorithm.java b/src/com/android/launcher3/search/SearchAlgorithm.java
new file mode 100644
index 0000000..a1720c7
--- /dev/null
+++ b/src/com/android/launcher3/search/SearchAlgorithm.java
@@ -0,0 +1,39 @@
+/*
+ * 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.search;
+
+/**
+ * An interface for handling search.
+ *
+ * @param <T> Search Result type
+ */
+public interface SearchAlgorithm<T> {
+
+ /**
+ * Performs search and sends the result to {@link SearchCallback}.
+ */
+ void doSearch(String query, SearchCallback<T> callback);
+
+ /**
+ * Cancels any active request.
+ */
+ void cancel(boolean interruptActiveRequests);
+
+ /**
+ * Cleans up after search is no longer needed.
+ */
+ default void destroy() {};
+}
diff --git a/src/com/android/launcher3/search/SearchCallback.java b/src/com/android/launcher3/search/SearchCallback.java
new file mode 100644
index 0000000..5796116
--- /dev/null
+++ b/src/com/android/launcher3/search/SearchCallback.java
@@ -0,0 +1,46 @@
+/*
+ * 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.search;
+
+import java.util.ArrayList;
+
+/**
+ * An interface for receiving search results.
+ *
+ * @param <T> Search Result type
+ */
+public interface SearchCallback<T> {
+
+ /**
+ * Called when the search from primary source is complete.
+ *
+ * @param items list of search results
+ */
+ void onSearchResult(String query, ArrayList<T> items);
+
+ /**
+ * Called when the search from secondary source is complete.
+ *
+ * @param items list of search results
+ */
+ void onAppendSearchResult(String query, ArrayList<T> items);
+
+ /**
+ * Called when the search results should be cleared.
+ */
+ void clearSearchResult();
+}
+
diff --git a/src/com/android/launcher3/search/StringMatcherUtility.java b/src/com/android/launcher3/search/StringMatcherUtility.java
new file mode 100644
index 0000000..acab52b
--- /dev/null
+++ b/src/com/android/launcher3/search/StringMatcherUtility.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.search;
+
+import java.text.Collator;
+
+/**
+ * Utilities for matching query string to target string.
+ */
+public class StringMatcherUtility {
+
+ /**
+ * Returns {@code true} is {@code query} is a prefix substring of a complete word/phrase in
+ * {@code target}.
+ */
+ public static boolean matches(String query, String target, StringMatcher matcher) {
+ int queryLength = query.length();
+
+ int targetLength = target.length();
+
+ if (targetLength < queryLength || queryLength <= 0) {
+ return false;
+ }
+
+ if (requestSimpleFuzzySearch(query)) {
+ return target.toLowerCase().contains(query);
+ }
+
+ int lastType;
+ int thisType = Character.UNASSIGNED;
+ int nextType = Character.getType(target.codePointAt(0));
+
+ int end = targetLength - queryLength;
+ for (int i = 0; i <= end; i++) {
+ lastType = thisType;
+ thisType = nextType;
+ nextType = i < (targetLength - 1)
+ ? Character.getType(target.codePointAt(i + 1)) : Character.UNASSIGNED;
+ if (isBreak(thisType, lastType, nextType)
+ && matcher.matches(query, target.substring(i, i + queryLength))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the current point should be a break point. Following cases
+ * are considered as break points:
+ * 1) Any non space character after a space character
+ * 2) Any digit after a non-digit character
+ * 3) Any capital character after a digit or small character
+ * 4) Any capital character before a small character
+ */
+ private static boolean isBreak(int thisType, int prevType, int nextType) {
+ switch (prevType) {
+ case Character.UNASSIGNED:
+ case Character.SPACE_SEPARATOR:
+ case Character.LINE_SEPARATOR:
+ case Character.PARAGRAPH_SEPARATOR:
+ return true;
+ }
+ switch (thisType) {
+ case Character.UPPERCASE_LETTER:
+ if (nextType == Character.UPPERCASE_LETTER) {
+ return true;
+ }
+ // Follow through
+ case Character.TITLECASE_LETTER:
+ // Break point if previous was not a upper case
+ return prevType != Character.UPPERCASE_LETTER;
+ case Character.LOWERCASE_LETTER:
+ // Break point if previous was not a letter.
+ return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
+ case Character.DECIMAL_DIGIT_NUMBER:
+ case Character.LETTER_NUMBER:
+ case Character.OTHER_NUMBER:
+ // Break point if previous was not a number
+ return !(prevType == Character.DECIMAL_DIGIT_NUMBER
+ || prevType == Character.LETTER_NUMBER
+ || prevType == Character.OTHER_NUMBER);
+ case Character.MATH_SYMBOL:
+ case Character.CURRENCY_SYMBOL:
+ case Character.OTHER_PUNCTUATION:
+ case Character.DASH_PUNCTUATION:
+ // Always a break point for a symbol
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Performs locale sensitive string comparison using {@link Collator}.
+ */
+ public static class StringMatcher {
+
+ private static final char MAX_UNICODE = '\uFFFF';
+
+ private final Collator mCollator;
+
+ StringMatcher() {
+ // On android N and above, Collator uses ICU implementation which has a much better
+ // support for non-latin locales.
+ mCollator = Collator.getInstance();
+ mCollator.setStrength(Collator.PRIMARY);
+ mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
+ }
+
+ /**
+ * Returns true if {@param query} is a prefix of {@param target}
+ */
+ public boolean matches(String query, String target) {
+ switch (mCollator.compare(query, target)) {
+ case 0:
+ return true;
+ case -1:
+ // The target string can contain a modifier which would make it larger than
+ // the query string (even though the length is same). If the query becomes
+ // larger after appending a unicode character, it was originally a prefix of
+ // the target string and hence should match.
+ return mCollator.compare(query + MAX_UNICODE, target) > -1;
+ default:
+ return false;
+ }
+ }
+
+ public static StringMatcher getInstance() {
+ return new StringMatcher();
+ }
+ }
+
+ /**
+ * Matching optimization to search in Chinese.
+ */
+ private static boolean requestSimpleFuzzySearch(String s) {
+ for (int i = 0; i < s.length(); ) {
+ int codepoint = s.codePointAt(i);
+ i += Character.charCount(codepoint);
+ switch (Character.UnicodeScript.of(codepoint)) {
+ case HAN:
+ //Character.UnicodeScript.HAN: use String.contains to match
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 21ad275..5999091 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -15,11 +15,8 @@
*/
package com.android.launcher3.secondarydisplay;
-import static com.android.launcher3.model.AppLaunchTracker.CONTAINER_ALL_APPS;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.app.ActivityOptions;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
@@ -37,9 +34,10 @@
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
@@ -47,7 +45,7 @@
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.views.BaseDragLayer;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.util.ArrayList;
import java.util.HashMap;
@@ -89,8 +87,8 @@
if (mDragLayer != null) {
return;
}
- InvariantDeviceProfile currentDisplayIdp =
- new InvariantDeviceProfile(this, getWindow().getDecorView().getDisplay());
+ InvariantDeviceProfile currentDisplayIdp = new InvariantDeviceProfile(
+ this, getWindow().getDecorView().getDisplay());
// Disable transpose layout and use multi-window mode so that the icons are scaled properly
mDeviceProfile = currentDisplayIdp.getDeviceProfile(this)
@@ -169,11 +167,6 @@
}
@Override
- public ActivityOptions getActivityLaunchOptions(View v) {
- return null;
- }
-
- @Override
protected void reapplyUi() { }
@Override
@@ -196,9 +189,6 @@
public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
@Override
- public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
-
- @Override
public void bindScreens(IntArray orderedScreenIds) { }
@Override
@@ -219,12 +209,12 @@
ArrayList<ItemInfo> addAnimated) { }
@Override
- public void bindPromiseAppProgressUpdated(PromiseAppInfo app) {
- mAppsView.getAppsStore().updatePromiseAppProgress(app);
+ public void bindIncrementalDownloadProgressUpdated(AppInfo app) {
+ mAppsView.getAppsStore().updateProgressBar(app);
}
@Override
- public void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated) { }
+ public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
@Override
public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
@@ -236,7 +226,7 @@
public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
@Override
- public void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets) { }
+ public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
@Override
public void onPageBoundSynchronously(int page) { }
@@ -298,6 +288,7 @@
@Override
public void bindAllApplications(AppInfo[] apps, int flags) {
mAppsView.getAppsStore().setApps(apps, flags);
+ PopupContainerWithArrow.dismissInvalidPopup(this);
}
public PopupDataProvider getPopupDataProvider() {
@@ -318,16 +309,18 @@
if (tag instanceof ItemInfo) {
ItemInfo item = (ItemInfo) tag;
Intent intent;
- if (item instanceof PromiseAppInfo) {
- PromiseAppInfo promiseAppInfo = (PromiseAppInfo) item;
- intent = promiseAppInfo.getMarketIntent(this);
+ if (item instanceof ItemInfoWithIcon
+ && (((ItemInfoWithIcon) item).runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+ ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item;
+ intent = appInfo.getMarketIntent(this);
} else {
intent = item.getIntent();
}
if (intent == null) {
throw new IllegalArgumentException("Input must have a valid intent");
}
- startActivitySafely(v, intent, item, CONTAINER_ALL_APPS);
+ startActivitySafely(v, intent, item);
}
}
}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
index 40630d3..f78f6dd 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -29,7 +29,6 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.model.data.ItemInfo;
@@ -109,8 +108,6 @@
setMeasuredDimension(width, height);
DeviceProfile grid = mActivity.getDeviceProfile();
- InvariantDeviceProfile idp = grid.inv;
-
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
@@ -118,10 +115,10 @@
int padding = 2 * (grid.desiredWorkspaceLeftRightMarginPx
+ grid.cellLayoutPaddingLeftRightPx);
- int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
+ int maxWidth = grid.allAppsCellWidthPx * grid.numShownAllAppsColumns + padding;
int appsWidth = Math.min(width, maxWidth);
- int maxHeight = grid.allAppsCellHeightPx * idp.numAllAppsColumns + padding;
+ int maxHeight = grid.allAppsCellHeightPx * grid.numShownAllAppsColumns + padding;
int appsHeight = Math.min(height, maxHeight);
mAppsView.measure(
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index b12d04f..4d63218 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -15,7 +15,10 @@
*/
package com.android.launcher3.settings;
+import static android.content.pm.PackageManager.GET_RESOLVED_FILTER;
import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
@@ -26,25 +29,29 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
+import android.text.Editable;
+import android.text.TextWatcher;
import android.util.ArrayMap;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
+import android.widget.EditText;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreference;
@@ -97,6 +104,65 @@
initFlags();
loadPluginPrefs();
maybeAddSandboxCategory();
+
+ if (getActivity() != null) {
+ getActivity().setTitle("Developer Options");
+ }
+ }
+
+ private void filterPreferences(String query, PreferenceGroup pg) {
+ int count = pg.getPreferenceCount();
+ int hidden = 0;
+ for (int i = 0; i < count; i++) {
+ Preference preference = pg.getPreference(i);
+ if (preference instanceof PreferenceGroup) {
+ filterPreferences(query, (PreferenceGroup) preference);
+ } else {
+ String title = preference.getTitle().toString().toLowerCase().replace("_", " ");
+ if (query.isEmpty() || title.contains(query)) {
+ preference.setVisible(true);
+ } else {
+ preference.setVisible(false);
+ hidden++;
+ }
+ }
+ }
+ pg.setVisible(hidden != count);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ EditText filterBox = view.findViewById(R.id.filter_box);
+ filterBox.setVisibility(VISIBLE);
+ filterBox.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String query = editable.toString().toLowerCase().replace("_", " ");
+ filterPreferences(query, mPreferenceScreen);
+ }
+ });
+
+ View listView = getListView();
+ final int bottomPadding = listView.getPaddingBottom();
+ listView.setOnApplyWindowInsetsListener((v, insets) -> {
+ v.setPadding(
+ v.getPaddingLeft(),
+ v.getPaddingTop(),
+ v.getPaddingRight(),
+ bottomPadding + insets.getSystemWindowInsetBottom());
+ return insets.consumeSystemWindowInsets();
+ });
}
@Override
@@ -161,7 +227,7 @@
Set<String> pluginActions = manager.getPluginActions();
- ArrayMap<Pair<String, String>, ArrayList<Pair<String, ServiceInfo>>> plugins =
+ ArrayMap<Pair<String, String>, ArrayList<Pair<String, ResolveInfo>>> plugins =
new ArrayMap<>();
Set<String> pluginPermissionApps = pm.getPackagesHoldingPermissions(
@@ -173,7 +239,7 @@
for (String action : pluginActions) {
String name = toName(action);
List<ResolveInfo> result = pm.queryIntentServices(
- new Intent(action), MATCH_DISABLED_COMPONENTS);
+ new Intent(action), MATCH_DISABLED_COMPONENTS | GET_RESOLVED_FILTER);
for (ResolveInfo info : result) {
String packageName = info.serviceInfo.packageName;
if (!pluginPermissionApps.contains(packageName)) {
@@ -184,7 +250,7 @@
if (!plugins.containsKey(key)) {
plugins.put(key, new ArrayList<>());
}
- plugins.get(key).add(Pair.create(name, info.serviceInfo));
+ plugins.get(key).add(Pair.create(name, info));
}
}
@@ -192,11 +258,11 @@
plugins.forEach((key, si) -> {
String packageName = key.first;
List<ComponentName> componentNames = si.stream()
- .map(p -> new ComponentName(packageName, p.second.name))
+ .map(p -> new ComponentName(packageName, p.second.serviceInfo.name))
.collect(Collectors.toList());
if (!componentNames.isEmpty()) {
SwitchPreference pref = new PluginPreference(
- prefContext, si.get(0).second.applicationInfo, enabler, componentNames);
+ prefContext, si.get(0).second, enabler, componentNames);
pref.setSummary("Plugins: "
+ si.stream().map(p -> p.first).collect(Collectors.joining(", ")));
mPluginsCategory.addPreference(pref);
@@ -218,13 +284,28 @@
}
PreferenceCategory sandboxCategory = newCategory("Gesture Navigation Sandbox");
sandboxCategory.setSummary("Learn and practice navigation gestures");
+ Preference launchOnboardingTutorialPreference = new Preference(context);
+ launchOnboardingTutorialPreference.setKey("launchOnboardingTutorial");
+ launchOnboardingTutorialPreference.setTitle("Launch Onboarding Tutorial");
+ launchOnboardingTutorialPreference.setSummary("Learn the basic navigation gestures.");
+ launchOnboardingTutorialPreference.setOnPreferenceClickListener(preference -> {
+ startActivity(launchSandboxIntent.putExtra(
+ "tutorial_steps",
+ new String[] {
+ "HOME_NAVIGATION",
+ "BACK_NAVIGATION",
+ "OVERVIEW_NAVIGATION"}));
+ return true;
+ });
+ sandboxCategory.addPreference(launchOnboardingTutorialPreference);
Preference launchBackTutorialPreference = new Preference(context);
launchBackTutorialPreference.setKey("launchBackTutorial");
launchBackTutorialPreference.setTitle("Launch Back Tutorial");
launchBackTutorialPreference.setSummary("Learn how to use the Back gesture");
launchBackTutorialPreference.setOnPreferenceClickListener(preference -> {
startActivity(launchSandboxIntent.putExtra(
- "tutorial_type", "RIGHT_EDGE_BACK_NAVIGATION"));
+ "tutorial_steps",
+ new String[] {"BACK_NAVIGATION"}));
return true;
});
sandboxCategory.addPreference(launchBackTutorialPreference);
@@ -233,7 +314,9 @@
launchHomeTutorialPreference.setTitle("Launch Home Tutorial");
launchHomeTutorialPreference.setSummary("Learn how to use the Home gesture");
launchHomeTutorialPreference.setOnPreferenceClickListener(preference -> {
- startActivity(launchSandboxIntent.putExtra("tutorial_type", "HOME_NAVIGATION"));
+ startActivity(launchSandboxIntent.putExtra(
+ "tutorial_steps",
+ new String[] {"HOME_NAVIGATION"}));
return true;
});
sandboxCategory.addPreference(launchHomeTutorialPreference);
@@ -242,7 +325,9 @@
launchOverviewTutorialPreference.setTitle("Launch Overview Tutorial");
launchOverviewTutorialPreference.setSummary("Learn how to use the Overview gesture");
launchOverviewTutorialPreference.setOnPreferenceClickListener(preference -> {
- startActivity(launchSandboxIntent.putExtra("tutorial_type", "OVERVIEW_NAVIGATION"));
+ startActivity(launchSandboxIntent.putExtra(
+ "tutorial_steps",
+ new String[] {"OVERVIEW_NAVIGATION"}));
return true;
});
sandboxCategory.addPreference(launchOverviewTutorialPreference);
@@ -251,10 +336,23 @@
launchAssistantTutorialPreference.setTitle("Launch Assistant Tutorial");
launchAssistantTutorialPreference.setSummary("Learn how to use the Assistant gesture");
launchAssistantTutorialPreference.setOnPreferenceClickListener(preference -> {
- startActivity(launchSandboxIntent.putExtra("tutorial_type", "ASSISTANT"));
+ startActivity(launchSandboxIntent.putExtra(
+ "tutorial_steps",
+ new String[] {"ASSISTANT"}));
return true;
});
sandboxCategory.addPreference(launchAssistantTutorialPreference);
+ Preference launchSandboxModeTutorialPreference = new Preference(context);
+ launchSandboxModeTutorialPreference.setKey("launchSandboxMode");
+ launchSandboxModeTutorialPreference.setTitle("Launch Sandbox Mode");
+ launchSandboxModeTutorialPreference.setSummary("Practice navigation gestures");
+ launchSandboxModeTutorialPreference.setOnPreferenceClickListener(preference -> {
+ startActivity(launchSandboxIntent.putExtra(
+ "tutorial_steps",
+ new String[] {"SANDBOX_MODE"}));
+ return true;
+ });
+ sandboxCategory.addPreference(launchSandboxModeTutorialPreference);
}
private String toName(String action) {
@@ -272,21 +370,33 @@
}
private static class PluginPreference extends SwitchPreference {
- private final boolean mHasSettings;
- private final PreferenceDataStore mPluginEnabler;
private final String mPackageName;
+ private final ResolveInfo mSettingsInfo;
+ private final PreferenceDataStore mPluginEnabler;
private final List<ComponentName> mComponentNames;
- PluginPreference(Context prefContext, ApplicationInfo info,
+ PluginPreference(Context prefContext, ResolveInfo pluginInfo,
PreferenceDataStore pluginEnabler, List<ComponentName> componentNames) {
super(prefContext);
PackageManager pm = prefContext.getPackageManager();
- mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS)
- .setPackage(info.packageName), 0) != null;
- mPackageName = info.packageName;
- mComponentNames = componentNames;
+ mPackageName = pluginInfo.serviceInfo.applicationInfo.packageName;
+ Intent settingsIntent = new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName);
+ // If any Settings activity in app has category filters, set plugin action as category.
+ List<ResolveInfo> settingsInfos =
+ pm.queryIntentActivities(settingsIntent, GET_RESOLVED_FILTER);
+ if (pluginInfo.filter != null) {
+ for (ResolveInfo settingsInfo : settingsInfos) {
+ if (settingsInfo.filter != null && settingsInfo.filter.countCategories() > 0) {
+ settingsIntent.addCategory(pluginInfo.filter.getAction(0));
+ break;
+ }
+ }
+ }
+
+ mSettingsInfo = pm.resolveActivity(settingsIntent, 0);
mPluginEnabler = pluginEnabler;
- setTitle(info.loadLabel(pm));
+ mComponentNames = componentNames;
+ setTitle(pluginInfo.loadLabel(pm));
setChecked(isPluginEnabled());
setWidgetLayoutResource(R.layout.switch_preference_with_settings);
}
@@ -327,17 +437,14 @@
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- holder.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE
- : View.GONE);
- holder.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE
- : View.GONE);
+ boolean hasSettings = mSettingsInfo != null;
+ holder.findViewById(R.id.settings).setVisibility(hasSettings ? VISIBLE : GONE);
+ holder.findViewById(R.id.divider).setVisibility(hasSettings ? VISIBLE : GONE);
holder.findViewById(R.id.settings).setOnClickListener(v -> {
- ResolveInfo result = v.getContext().getPackageManager().resolveActivity(
- new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName), 0);
- if (result != null) {
+ if (hasSettings) {
v.getContext().startActivity(new Intent().setComponent(
- new ComponentName(result.activityInfo.packageName,
- result.activityInfo.name)));
+ new ComponentName(mSettingsInfo.activityInfo.packageName,
+ mSettingsInfo.activityInfo.name)));
}
});
holder.itemView.setOnLongClickListener(v -> {
diff --git a/src/com/android/launcher3/settings/NotificationDotsPreference.java b/src/com/android/launcher3/settings/NotificationDotsPreference.java
index a91303a..0ee2744 100644
--- a/src/com/android/launcher3/settings/NotificationDotsPreference.java
+++ b/src/com/android/launcher3/settings/NotificationDotsPreference.java
@@ -17,6 +17,8 @@
import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
import static com.android.launcher3.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGS;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -24,6 +26,7 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.database.ContentObserver;
import android.os.Bundle;
import android.provider.Settings;
import android.util.AttributeSet;
@@ -35,20 +38,28 @@
import com.android.launcher3.R;
import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.SettingsCache;
/**
* A {@link Preference} for indicating notification dots status.
* Also has utility methods for updating UI based on dots status changes.
*/
public class NotificationDotsPreference extends Preference
- implements SecureSettingsObserver.OnChangeListener {
+ implements SettingsCache.OnChangeListener {
private boolean mWidgetFrameVisible = false;
/** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners";
+ private final ContentObserver mListenerListObserver =
+ new ContentObserver(MAIN_EXECUTOR.getHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updateUI();
+ }
+ };
+
public NotificationDotsPreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
@@ -66,6 +77,35 @@
super(context);
}
+ @Override
+ public void onAttached() {
+ super.onAttached();
+ SettingsCache.INSTANCE.get(getContext()).register(NOTIFICATION_BADGING_URI, this);
+ getContext().getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS),
+ false, mListenerListObserver);
+ updateUI();
+
+ // Update intent
+ Bundle extras = new Bundle();
+ extras.putString(EXTRA_FRAGMENT_ARG_KEY, "notification_badging");
+ setIntent(new Intent("android.settings.NOTIFICATION_SETTINGS")
+ .putExtra(EXTRA_SHOW_FRAGMENT_ARGS, extras));
+ }
+
+ private void updateUI() {
+ onSettingsChanged(SettingsCache.INSTANCE.get(getContext())
+ .getValue(NOTIFICATION_BADGING_URI));
+ }
+
+ @Override
+ public void onDetached() {
+ super.onDetached();
+ SettingsCache.INSTANCE.get(getContext()).unregister(NOTIFICATION_BADGING_URI, this);
+ getContext().getContentResolver().unregisterContentObserver(mListenerListObserver);
+
+ }
+
private void setWidgetFrameVisible(boolean isVisible) {
if (mWidgetFrameVisible != isVisible) {
mWidgetFrameVisible = isVisible;
diff --git a/src/com/android/launcher3/settings/PreferenceHighlighter.java b/src/com/android/launcher3/settings/PreferenceHighlighter.java
index 8ba8146..96ee216 100644
--- a/src/com/android/launcher3/settings/PreferenceHighlighter.java
+++ b/src/com/android/launcher3/settings/PreferenceHighlighter.java
@@ -24,16 +24,18 @@
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
+import android.graphics.RectF;
import android.util.Property;
import android.view.View;
-import com.android.launcher3.util.Themes;
-
+import androidx.preference.Preference;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
import androidx.recyclerview.widget.RecyclerView.State;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import com.android.launcher3.util.Themes;
+
/**
* Utility class for highlighting a preference
*/
@@ -62,14 +64,16 @@
private final Paint mPaint = new Paint();
private final RecyclerView mRv;
private final int mIndex;
+ private final Preference mPreference;
+ private final RectF mDrawRect = new RectF();
private boolean mHighLightStarted = false;
private int mHighlightColor = END_COLOR;
-
- public PreferenceHighlighter(RecyclerView rv, int index) {
+ public PreferenceHighlighter(RecyclerView rv, int index, Preference preference) {
mRv = rv;
mIndex = index;
+ mPreference = preference;
}
@Override
@@ -92,7 +96,8 @@
if (!mHighLightStarted) {
// Start highlight
int colorTo = setColorAlphaBound(Themes.getColorAccent(mRv.getContext()), 66);
- ObjectAnimator anim = ObjectAnimator.ofArgb(this, HIGHLIGHT_COLOR, END_COLOR, colorTo);
+ ObjectAnimator anim = ObjectAnimator.ofArgb(this, HIGHLIGHT_COLOR, END_COLOR,
+ colorTo);
anim.setDuration(HIGHLIGHT_FADE_IN_DURATION);
anim.setRepeatMode(ValueAnimator.REVERSE);
anim.setRepeatCount(4);
@@ -108,7 +113,11 @@
View view = holder.itemView;
mPaint.setColor(mHighlightColor);
- c.drawRect(0, view.getY(), parent.getWidth(), view.getY() + view.getHeight(), mPaint);
+ mDrawRect.set(0, view.getY(), parent.getWidth(), view.getY() + view.getHeight());
+ if (mPreference instanceof HighlightDelegate) {
+ ((HighlightDelegate) mPreference).offsetHighlight(view, mDrawRect);
+ }
+ c.drawRect(mDrawRect, mPaint);
}
private void removeHighlight() {
@@ -124,4 +133,16 @@
});
anim.start();
}
+
+ /**
+ * Interface to be implemented by a preference to customize the highlight are
+ */
+ public interface HighlightDelegate {
+
+ /**
+ * Allows the preference to update the highlight area
+ */
+ void offsetHighlight(View prefView, RectF bounds);
+
+ }
}
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index d3213a1..915e140 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -18,17 +18,18 @@
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
-import static com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY;
import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
-import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
-import android.provider.Settings;
import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.View;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.view.WindowCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -41,12 +42,17 @@
import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherFiles;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.util.SecureSettingsObserver;
+
+import java.util.Collections;
+import java.util.List;
/**
* Settings activity for Launcher. Currently implements the following setting: Allow rotation
@@ -55,55 +61,97 @@
implements OnPreferenceStartFragmentCallback, OnPreferenceStartScreenCallback,
SharedPreferences.OnSharedPreferenceChangeListener{
+ /** List of fragments that can be hosted by this activity. */
+ private static final List<String> VALID_PREFERENCE_FRAGMENTS = Collections.singletonList(
+ DeveloperOptionsFragment.class.getName());
+
private static final String DEVELOPER_OPTIONS_KEY = "pref_developer_options";
private static final String FLAGS_PREFERENCE_KEY = "flag_toggler";
private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging";
- /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
- private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners";
public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
public static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600;
public static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted";
+ @VisibleForTesting
+ static final String EXTRA_FRAGMENT = ":settings:fragment";
+ @VisibleForTesting
+ static final String EXTRA_FRAGMENT_ARGS = ":settings:fragment_args";
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setContentView(R.layout.settings_activity);
+ setActionBar(findViewById(R.id.action_bar));
+ WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
+
+ Intent intent = getIntent();
+ if (intent.hasExtra(EXTRA_FRAGMENT) || intent.hasExtra(EXTRA_FRAGMENT_ARGS)) {
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
if (savedInstanceState == null) {
- Bundle args = new Bundle();
- String prefKey = getIntent().getStringExtra(EXTRA_FRAGMENT_ARG_KEY);
+ Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS);
+ if (args == null) {
+ args = new Bundle();
+ }
+
+ String prefKey = intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY);
if (!TextUtils.isEmpty(prefKey)) {
args.putString(EXTRA_FRAGMENT_ARG_KEY, prefKey);
}
final FragmentManager fm = getSupportFragmentManager();
final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(),
- getString(R.string.settings_fragment_name));
+ getPreferenceFragment());
f.setArguments(args);
// Display the fragment as the main content.
- fm.beginTransaction().replace(android.R.id.content, f).commit();
+ fm.beginTransaction().replace(R.id.content_frame, f).commit();
}
Utilities.getPrefs(getApplicationContext()).registerOnSharedPreferenceChangeListener(this);
}
+ /**
+ * Obtains the preference fragment to instantiate in this activity.
+ *
+ * @return the preference fragment class
+ * @throws IllegalArgumentException if the fragment is unknown to this activity
+ */
+ private String getPreferenceFragment() {
+ String preferenceFragment = getIntent().getStringExtra(EXTRA_FRAGMENT);
+ String defaultFragment = getString(R.string.settings_fragment_name);
+
+ if (TextUtils.isEmpty(preferenceFragment)) {
+ return defaultFragment;
+ } else if (!preferenceFragment.equals(defaultFragment)
+ && !VALID_PREFERENCE_FRAGMENTS.contains(preferenceFragment)) {
+ throw new IllegalArgumentException(
+ "Invalid fragment for this activity: " + preferenceFragment);
+ } else {
+ return preferenceFragment;
+ }
+ }
+
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { }
- private boolean startFragment(String fragment, Bundle args, String key) {
+ private boolean startPreference(String fragment, Bundle args, String key) {
if (Utilities.ATLEAST_P && getSupportFragmentManager().isStateSaved()) {
// Sometimes onClick can come after onPause because of being posted on the handler.
- // Skip starting new fragments in that case.
+ // Skip starting new preferences in that case.
return false;
}
final FragmentManager fm = getSupportFragmentManager();
final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(), fragment);
- f.setArguments(args);
if (f instanceof DialogFragment) {
- ((DialogFragment) f).show(getSupportFragmentManager(), key);
+ f.setArguments(args);
+ ((DialogFragment) f).show(fm, key);
} else {
- fm.beginTransaction().replace(android.R.id.content, f).addToBackStack(key).commit();
+ startActivity(new Intent(this, SettingsActivity.class)
+ .putExtra(EXTRA_FRAGMENT, fragment)
+ .putExtra(EXTRA_FRAGMENT_ARGS, args));
}
return true;
}
@@ -111,14 +159,23 @@
@Override
public boolean onPreferenceStartFragment(
PreferenceFragmentCompat preferenceFragment, Preference pref) {
- return startFragment(pref.getFragment(), pref.getExtras(), pref.getKey());
+ return startPreference(pref.getFragment(), pref.getExtras(), pref.getKey());
}
@Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) {
Bundle args = new Bundle();
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.getKey());
- return startFragment(getString(R.string.settings_fragment_name), args, pref.getKey());
+ return startPreference(getString(R.string.settings_fragment_name), args, pref.getKey());
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
}
/**
@@ -126,10 +183,9 @@
*/
public static class LauncherSettingsFragment extends PreferenceFragmentCompat {
- private SecureSettingsObserver mNotificationDotsObserver;
-
private String mHighLightKey;
private boolean mPreferenceHighlighted = false;
+ private Preference mDeveloperOptionPref;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -153,6 +209,25 @@
screen.removePreference(preference);
}
}
+
+ if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) {
+ getActivity().setTitle(getPreferenceScreen().getTitle());
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ View listView = getListView();
+ final int bottomPadding = listView.getPaddingBottom();
+ listView.setOnApplyWindowInsetsListener((v, insets) -> {
+ v.setPadding(
+ v.getPaddingLeft(),
+ v.getPaddingTop(),
+ v.getPaddingRight(),
+ bottomPadding + insets.getSystemWindowInsetBottom());
+ return insets.consumeSystemWindowInsets();
+ });
}
@Override
@@ -172,32 +247,17 @@
protected boolean initPreference(Preference preference) {
switch (preference.getKey()) {
case NOTIFICATION_DOTS_PREFERENCE_KEY:
- if (!Utilities.ATLEAST_OREO ||
- !getResources().getBoolean(R.bool.notification_dots_enabled)) {
- return false;
- }
-
- // Listen to system notification dot settings while this UI is active.
- mNotificationDotsObserver = newNotificationSettingsObserver(
- getActivity(), (NotificationDotsPreference) preference);
- mNotificationDotsObserver.register();
- // Also listen if notification permission changes
- mNotificationDotsObserver.getResolver().registerContentObserver(
- Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS), false,
- mNotificationDotsObserver);
- mNotificationDotsObserver.dispatchOnChange();
- return true;
-
- case ADD_ICON_PREFERENCE_KEY:
- return Utilities.ATLEAST_OREO;
+ return !WidgetsModel.GO_DISABLE_NOTIFICATION_DOTS;
case ALLOW_ROTATION_PREFERENCE_KEY:
- if (getResources().getBoolean(R.bool.allow_rotation)) {
+ DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.get(
+ getContext()).getDeviceProfile(getContext());
+ if (deviceProfile.allowRotation) {
// Launcher supports rotation by default. No need to show this setting.
return false;
}
// Initialize the UI once
- preference.setDefaultValue(getAllowRotationDefaultValue());
+ preference.setDefaultValue(false);
return true;
case FLAGS_PREFERENCE_KEY:
@@ -205,18 +265,37 @@
return FeatureFlags.showFlagTogglerUi(getContext());
case DEVELOPER_OPTIONS_KEY:
- // Show if plugins are enabled or flag UI is enabled.
- return FeatureFlags.showFlagTogglerUi(getContext()) ||
- PluginManagerWrapper.hasPlugins(getContext());
+ mDeveloperOptionPref = preference;
+ return updateDeveloperOption();
}
return true;
}
+ /**
+ * Show if plugins are enabled or flag UI is enabled.
+ * @return True if we should show the preference option.
+ */
+ private boolean updateDeveloperOption() {
+ boolean showPreference = FeatureFlags.showFlagTogglerUi(getContext())
+ || PluginManagerWrapper.hasPlugins(getContext());
+ if (mDeveloperOptionPref != null) {
+ mDeveloperOptionPref.setEnabled(showPreference);
+ if (showPreference) {
+ getPreferenceScreen().addPreference(mDeveloperOptionPref);
+ } else {
+ getPreferenceScreen().removePreference(mDeveloperOptionPref);
+ }
+ }
+ return showPreference;
+ }
+
@Override
public void onResume() {
super.onResume();
+ updateDeveloperOption();
+
if (isAdded() && !mPreferenceHighlighted) {
PreferenceHighlighter highlighter = createHighlighter();
if (highlighter != null) {
@@ -241,7 +320,9 @@
RecyclerView list = getListView();
PreferencePositionCallback callback = (PreferencePositionCallback) list.getAdapter();
int position = callback.getPreferenceAdapterPosition(mHighLightKey);
- return position >= 0 ? new PreferenceHighlighter(list, position) : null;
+ return position >= 0 ? new PreferenceHighlighter(
+ list, position, screen.findPreference(mHighLightKey))
+ : null;
}
private void requestAccessibilityFocus(@NonNull final RecyclerView rv) {
@@ -252,14 +333,5 @@
}
});
}
-
- @Override
- public void onDestroy() {
- if (mNotificationDotsObserver != null) {
- mNotificationDotsObserver.unregister();
- mNotificationDotsObserver = null;
- }
- super.onDestroy();
- }
}
}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
index 2daa2fe..ad3f8df 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
@@ -23,7 +23,6 @@
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
-import android.view.MotionEvent;
import android.widget.Toast;
import com.android.launcher3.BubbleTextView;
@@ -106,12 +105,12 @@
}
@Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- // Show toast if user touches the drag handle (long clicks still start the drag).
- mShowInstructionToast = mDragHandleBounds.contains((int) ev.getX(), (int) ev.getY());
- }
- return super.onTouchEvent(ev);
+ protected boolean shouldIgnoreTouchDown(float x, float y) {
+ // Show toast if user touches the drag handle (long clicks still start the drag).
+ mShowInstructionToast = mDragHandleBounds.contains((int) x, (int) y);
+
+ // assume the whole view as clickable
+ return false;
}
@Override
@@ -133,6 +132,12 @@
mLoadingStatePlaceholder.draw(canvas);
}
+ @Override
+ protected void drawDotIfNecessary(Canvas canvas) {
+ // This view (with the text label to the side of the icon) is not designed for a dot to be
+ // drawn on top of it, so never draw one even if a notification for this shortcut exists.
+ }
+
private void showLoadingState(boolean loading) {
if (loading == mShowLoadingState) {
return;
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index e9b92e2..71d288c 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -18,7 +18,13 @@
import android.content.Context;
import android.content.pm.ShortcutInfo;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
import android.graphics.Point;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.RippleDrawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -30,18 +36,22 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.BubbleTextHolder;
/**
- * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}.
- * This lets us animate the DeepShortcutView (icon and text) separately from the background.
+ * A {@link android.widget.FrameLayout} that contains an icon and a {@link BubbleTextView} for text.
+ * This lets us animate the child BubbleTextView's background (transparent ripple) separately from
+ * the {@link DeepShortcutView} background color.
*/
-public class DeepShortcutView extends FrameLayout {
+public class DeepShortcutView extends FrameLayout implements BubbleTextHolder {
private static final Point sTempPoint = new Point();
+ private final Drawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+
private BubbleTextView mBubbleText;
private View mIconView;
- private View mDivider;
private WorkspaceItemInfo mInfo;
private ShortcutInfo mDetail;
@@ -63,13 +73,46 @@
super.onFinishInflate();
mBubbleText = findViewById(R.id.bubble_text);
mIconView = findViewById(R.id.icon);
- mDivider = findViewById(R.id.divider);
+ tryUpdateTextBackground();
}
- public void setDividerVisibility(int visibility) {
- mDivider.setVisibility(visibility);
+ @Override
+ public void setBackground(Drawable background) {
+ super.setBackground(background);
+ tryUpdateTextBackground();
}
+ @Override
+ public void setBackgroundResource(int resid) {
+ super.setBackgroundResource(resid);
+ tryUpdateTextBackground();
+ }
+
+ /**
+ * Updates the text background to match the shape of this background (when applicable).
+ */
+ private void tryUpdateTextBackground() {
+ if (!(getBackground() instanceof GradientDrawable) || mBubbleText == null) {
+ return;
+ }
+ GradientDrawable background = (GradientDrawable) getBackground();
+
+ int color = Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight);
+ GradientDrawable backgroundMask = new GradientDrawable();
+ backgroundMask.setColor(color);
+ backgroundMask.setShape(GradientDrawable.RECTANGLE);
+ if (background.getCornerRadii() != null) {
+ backgroundMask.setCornerRadii(background.getCornerRadii());
+ } else {
+ backgroundMask.setCornerRadius(background.getCornerRadius());
+ }
+
+ RippleDrawable drawable = new RippleDrawable(ColorStateList.valueOf(color),
+ mTransparentDrawable, backgroundMask);
+ mBubbleText.setBackground(drawable);
+ }
+
+ @Override
public BubbleTextView getBubbleText() {
return mBubbleText;
}
diff --git a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
index 3e59b61..cecbb0d 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
@@ -28,6 +28,7 @@
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.DragPreviewProvider;
import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.FastBitmapDrawable;
/**
* Extension of {@link DragPreviewProvider} which generates bitmaps scaled to the default icon size.
@@ -42,15 +43,16 @@
}
@Override
- public Bitmap createDragBitmap() {
+ public Drawable createDrawable() {
if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
- return BitmapRenderer.createHardwareBitmap(
- size + blurSizeOutline,
- size + blurSizeOutline,
- (c) -> drawDragViewOnBackground(c, size));
+ return new FastBitmapDrawable(
+ BitmapRenderer.createHardwareBitmap(
+ size + blurSizeOutline,
+ size + blurSizeOutline,
+ (c) -> drawDragViewOnBackground(c, size)));
} else {
- return createDragBitmapLegacy();
+ return new FastBitmapDrawable(createDragBitmapLegacy());
}
}
@@ -81,7 +83,7 @@
}
@Override
- public float getScaleAndPosition(Bitmap preview, int[] outPos) {
+ public float getScaleAndPosition(Drawable preview, int[] outPos) {
Launcher launcher = Launcher.getLauncher(mView.getContext());
int iconSize = getDrawableBounds(mView.getBackground()).width();
float scale = launcher.getDragLayer().getLocationInDragLayer(mView, outPos);
@@ -91,9 +93,10 @@
iconLeft = mView.getWidth() - iconSize - iconLeft;
}
- outPos[0] += Math.round(scale * iconLeft + (scale * iconSize - preview.getWidth()) / 2 +
- mPositionShift.x);
- outPos[1] += Math.round((scale * mView.getHeight() - preview.getHeight()) / 2
+ outPos[0] += Math.round(
+ scale * iconLeft + (scale * iconSize - preview.getIntrinsicWidth()) / 2
+ + mPositionShift.x);
+ outPos[1] += Math.round((scale * mView.getHeight() - preview.getIntrinsicHeight()) / 2
+ mPositionShift.y);
float size = launcher.getDeviceProfile().iconSizePx;
return scale * iconSize / size;
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index 3ca9490..0c6d675 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -30,6 +30,10 @@
return componentName.getClassName();
}
+ public String getPackageName() {
+ return componentName.getPackageName();
+ }
+
/**
* Creates a {@link ShortcutRequest} for this key
*/
diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
index daec1d8..122573c 100644
--- a/src/com/android/launcher3/statemanager/BaseState.java
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -17,6 +17,8 @@
import android.content.Context;
+import com.android.launcher3.DeviceProfile;
+
/**
* Interface representing a state of a StatefulActivity
*/
@@ -52,4 +54,11 @@
* Returns if the state has the provided flag
*/
boolean hasFlag(int flagMask);
+
+ /**
+ * For this state, whether tasks should layout as a grid rather than a list.
+ */
+ default boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) {
+ return false;
+ }
}
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 60b87d9..b34af97 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -16,7 +16,10 @@
package com.android.launcher3.statemanager;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
+import static com.android.launcher3.anim.AnimatorPlaybackController.callListenerCommandRecursively;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
@@ -24,15 +27,12 @@
import android.animation.AnimatorSet;
import android.os.Handler;
import android.os.Looper;
-import android.util.Log;
-import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
-import com.android.launcher3.testing.TestProtocol;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -78,6 +78,15 @@
return mCurrentStableState;
}
+ @Override
+ public String toString() {
+ return " StateManager(mLastStableState:" + mLastStableState
+ + ", mCurrentStableState:" + mCurrentStableState
+ + ", mState:" + mState
+ + ", mRestState:" + mRestState
+ + ", isInTransition:" + (mConfig.currentAnimation != null) + ")";
+ }
+
public void dump(String prefix, PrintWriter writer) {
writer.println(prefix + "StateManager:");
writer.println(prefix + "\tmLastStableState:" + mLastStableState);
@@ -89,7 +98,9 @@
public StateHandler[] getStateHandlers() {
if (mStateHandlers == null) {
- mStateHandlers = mActivity.createStateHandlers();
+ ArrayList<StateHandler> handlers = new ArrayList<>();
+ mActivity.collectStateHandlers(handlers);
+ mStateHandlers = handlers.toArray(new StateHandler[handlers.size()]);
}
return mStateHandlers;
}
@@ -119,14 +130,14 @@
}
/**
- * @see #goToState(STATE_TYPE, boolean, Runnable)
+ * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
*/
public void goToState(STATE_TYPE state) {
goToState(state, shouldAnimateStateChange());
}
/**
- * @see #goToState(STATE_TYPE, boolean, Runnable)
+ * @see #goToState(STATE_TYPE, boolean, AnimatorListener)
*/
public void goToState(STATE_TYPE state, boolean animated) {
goToState(state, animated, 0, null);
@@ -139,15 +150,15 @@
* true otherwise
* @paras onCompleteRunnable any action to perform at the end of the transition, of null.
*/
- public void goToState(STATE_TYPE state, boolean animated, Runnable onCompleteRunnable) {
- goToState(state, animated, 0, onCompleteRunnable);
+ public void goToState(STATE_TYPE state, boolean animated, AnimatorListener listener) {
+ goToState(state, animated, 0, listener);
}
/**
* Changes the Launcher state to the provided state after the given delay.
*/
- public void goToState(STATE_TYPE state, long delay, Runnable onCompleteRunnable) {
- goToState(state, true, delay, onCompleteRunnable);
+ public void goToState(STATE_TYPE state, long delay, AnimatorListener listener) {
+ goToState(state, true, delay, listener);
}
/**
@@ -177,21 +188,20 @@
}
}
- private void goToState(STATE_TYPE state, boolean animated, long delay,
- final Runnable onCompleteRunnable) {
- animated &= Utilities.areAnimationsEnabled(mActivity);
+ private void goToState(
+ STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
+ animated &= areAnimatorsEnabled();
if (mActivity.isInState(state)) {
if (mConfig.currentAnimation == null) {
// Run any queued runnable
- if (onCompleteRunnable != null) {
- onCompleteRunnable.run();
+ if (listener != null) {
+ listener.onAnimationEnd(null);
}
return;
} else if (!mConfig.userControlled && animated && mConfig.targetState == state) {
// We are running the same animation as requested
- if (onCompleteRunnable != null) {
- mConfig.currentAnimation.addListener(
- AnimationSuccessListener.forRunnable(onCompleteRunnable));
+ if (listener != null) {
+ mConfig.currentAnimation.addListener(listener);
}
return;
}
@@ -199,7 +209,7 @@
// Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
STATE_TYPE fromState = mState;
- mConfig.reset();
+ cancelAnimation();
if (!animated) {
mAtomicAnimationFactory.cancelAllStateElementAnimation();
@@ -211,8 +221,8 @@
onStateTransitionEnd(state);
// Run any queued runnable
- if (onCompleteRunnable != null) {
- onCompleteRunnable.run();
+ if (listener != null) {
+ listener.onAnimationEnd(null);
}
return;
}
@@ -223,16 +233,16 @@
int startChangeId = mConfig.changeId;
mUiHandler.postDelayed(() -> {
if (mConfig.changeId == startChangeId) {
- goToStateAnimated(state, fromState, onCompleteRunnable);
+ goToStateAnimated(state, fromState, listener);
}
}, delay);
} else {
- goToStateAnimated(state, fromState, onCompleteRunnable);
+ goToStateAnimated(state, fromState, listener);
}
}
private void goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState,
- Runnable onCompleteRunnable) {
+ AnimatorListener listener) {
// Since state mBaseState can be reached from multiple states, just assume that the
// transition plays in reverse and use the same duration as previous state.
mConfig.duration = state == mBaseState
@@ -240,8 +250,8 @@
: state.getTransitionDuration(mActivity);
prepareForAtomicAnimation(fromState, state, mConfig);
AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
- if (onCompleteRunnable != null) {
- animation.addListener(AnimationSuccessListener.forRunnable(onCompleteRunnable));
+ if (listener != null) {
+ animation.addListener(listener);
}
mUiHandler.post(new StartAnimRunnable(animation));
}
@@ -279,21 +289,21 @@
*/
public AnimatorPlaybackController createAnimationToNewWorkspace(
STATE_TYPE state, long duration) {
- return createAnimationToNewWorkspace(state, duration, ANIM_ALL_COMPONENTS);
+ return createAnimationToNewWorkspace(state, duration, 0 /* animFlags */);
}
public AnimatorPlaybackController createAnimationToNewWorkspace(
- STATE_TYPE state, long duration, @AnimationFlags int animComponents) {
+ STATE_TYPE state, long duration, @AnimationFlags int animFlags) {
StateAnimationConfig config = new StateAnimationConfig();
config.duration = duration;
- config.animFlags = animComponents;
+ config.animFlags = animFlags;
return createAnimationToNewWorkspace(state, config);
}
public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
StateAnimationConfig config) {
config.userControlled = true;
- mConfig.reset();
+ cancelAnimation();
config.copyTo(mConfig);
mConfig.playbackController = createAnimationToNewWorkspaceInternal(state)
.createPlaybackController();
@@ -301,17 +311,19 @@
}
private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "createAnimationToNewWorkspaceInternal: "
- + state);
- }
PendingAnimation builder = new PendingAnimation(mConfig.duration);
- if (mConfig.getAnimComponents() != 0) {
+ if (!mConfig.hasAnimationFlag(SKIP_ALL_ANIMATIONS)) {
for (StateHandler handler : getStateHandlers()) {
handler.setStateWithAnimation(state, mConfig, builder);
}
}
- builder.addListener(new AnimationSuccessListener() {
+ builder.addListener(createStateAnimationListener(state));
+ mConfig.setAnimation(builder.buildAnim(), state);
+ return builder;
+ }
+
+ private AnimatorListener createStateAnimationListener(STATE_TYPE state) {
+ return new AnimationSuccessListener() {
@Override
public void onAnimationStart(Animator animation) {
@@ -321,14 +333,9 @@
@Override
public void onAnimationSuccess(Animator animator) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "onAnimationSuccess: " + state);
- }
onStateTransitionEnd(state);
}
- });
- mConfig.setAnimation(builder.buildAnim(), state);
- return builder;
+ };
}
private void onStateTransitionStart(STATE_TYPE state) {
@@ -386,6 +393,11 @@
*/
public void cancelAnimation() {
mConfig.reset();
+ // It could happen that a new animation is set as a result of an endListener on the
+ // existing animation.
+ while (mConfig.currentAnimation != null || mConfig.playbackController != null) {
+ mConfig.reset();
+ }
}
public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
@@ -396,6 +408,19 @@
}
/**
+ * @see #setCurrentAnimation(AnimatorSet, Animator...). Using this method tells the StateManager
+ * that this is a custom animation to the given state, and thus the StateManager will add an
+ * animation listener to call {@link #onStateTransitionStart} and {@link #onStateTransitionEnd}.
+ * @param anim The custom animation to the given state.
+ * @param toState The state we are animating towards.
+ */
+ public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
+ cancelAnimation();
+ setCurrentAnimation(anim);
+ anim.addListener(createStateAnimationListener(toState));
+ }
+
+ /**
* Sets the animation as the current state animation, i.e., canceled when
* starting another animation and may block some launcher interactions while running.
*
@@ -488,20 +513,28 @@
* Cancels the current animation and resets config variables.
*/
public void reset() {
+ AnimatorSet anim = currentAnimation;
+ AnimatorPlaybackController pc = playbackController;
+
DEFAULT.copyTo(this);
targetState = null;
-
- if (playbackController != null) {
- playbackController.getAnimationPlayer().cancel();
- playbackController.dispatchOnCancel();
- } else if (currentAnimation != null) {
- currentAnimation.setDuration(0);
- currentAnimation.cancel();
- }
-
currentAnimation = null;
playbackController = null;
changeId++;
+
+ if (pc != null) {
+ pc.getAnimationPlayer().cancel();
+ pc.dispatchOnCancel().dispatchOnEnd();
+ } else if (anim != null) {
+ anim.setDuration(0);
+ if (!anim.isStarted()) {
+ // If the animation is not started the listeners do not get notified,
+ // notify manually.
+ callListenerCommandRecursively(anim, AnimatorListener::onAnimationCancel);
+ callListenerCommandRecursively(anim, AnimatorListener::onAnimationEnd);
+ }
+ anim.cancel();
+ }
}
@Override
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index dbe5f42..8a35cb3 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -30,6 +30,8 @@
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.views.BaseDragLayer;
+import java.util.List;
+
/**
* Abstract activity with state management
* @param <STATE_TYPE> Type of state object
@@ -46,7 +48,7 @@
/**
* Create handlers to control the property changes for this activity
*/
- protected abstract StateHandler<STATE_TYPE>[] createStateHandlers();
+ protected abstract void collectStateHandlers(List<StateHandler> out);
/**
* Returns true if the activity is in the provided state
@@ -121,7 +123,9 @@
final int origDragLayerChildCount = dragLayer.getChildCount();
super.onStop();
- getStateManager().moveToRestState();
+ if (!isChangingConfigurations()) {
+ getStateManager().moveToRestState();
+ }
// Workaround for b/78520668, explicitly trim memory once UI is hidden
onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
@@ -148,8 +152,8 @@
private void handleDeferredResume() {
if (hasBeenResumed() && !getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)) {
- onDeferredResumed();
addActivityFlags(ACTIVITY_STATE_DEFERRED_RESUMED);
+ onDeferredResumed();
mDeferredResumePending = false;
} else {
diff --git a/src/com/android/launcher3/states/HintState.java b/src/com/android/launcher3/states/HintState.java
index b8a184f..8b52016 100644
--- a/src/com/android/launcher3/states/HintState.java
+++ b/src/com/android/launcher3/states/HintState.java
@@ -15,11 +15,16 @@
*/
package com.android.launcher3.states;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+
import android.content.Context;
+import androidx.core.graphics.ColorUtils;
+
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
/**
* Scale down workspace/hotseat to hint at going to either overview (on pause) or first home screen.
@@ -30,7 +35,11 @@
| FLAG_HAS_SYS_UI_SCRIM;
public HintState(int id) {
- super(id, ContainerType.DEFAULT_CONTAINERTYPE, STATE_FLAGS);
+ this(id, LAUNCHER_STATE_HOME);
+ }
+
+ public HintState(int id, int statsLogOrdinal) {
+ super(id, statsLogOrdinal, STATE_FLAGS);
}
@Override
@@ -44,18 +53,13 @@
}
@Override
- public float getOverviewScrimAlpha(Launcher launcher) {
- return 0.4f;
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return ColorUtils.setAlphaComponent(
+ Themes.getAttrColor(launcher, R.attr.overviewScrimColor), 100);
}
@Override
public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
return new ScaleAndTranslation(0.92f, 0, 0);
}
-
- @Override
- public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
- // Treat the QSB as part of the hotseat so they move together.
- return getHotseatScaleAndTranslation(launcher);
- }
}
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 5a60f53..87871b1 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -18,63 +18,40 @@
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
-import android.app.Activity;
-import android.content.ContentResolver;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.os.Handler;
-import android.provider.Settings;
-import android.util.Log;
-import com.android.launcher3.R;
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.ActivityTracker;
import com.android.launcher3.util.UiThreadHelper;
/**
* Utility class to manage launcher rotation
*/
-public class RotationHelper implements OnSharedPreferenceChangeListener {
+public class RotationHelper implements OnSharedPreferenceChangeListener,
+ DeviceProfile.OnDeviceProfileChangeListener {
private static final String TAG = "RotationHelper";
public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
- private final ContentResolver mContentResolver;
- private boolean mSystemAutoRotateEnabled;
-
- private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- updateAutoRotateSetting();
- }
- };
-
- public static boolean getAllowRotationDefaultValue() {
- // If the device's pixel density was scaled (usually via settings for A11y), use the
- // original dimensions to determine if rotation is allowed of not.
- Resources res = Resources.getSystem();
- int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
- * res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE;
- return originalSmallestWidth >= 600;
- }
-
public static final int REQUEST_NONE = 0;
public static final int REQUEST_ROTATE = 1;
public static final int REQUEST_LOCK = 2;
- private final Activity mActivity;
- private final SharedPreferences mSharedPrefs;
+ private BaseActivity mActivity;
+ private SharedPreferences mSharedPrefs = null;
private boolean mIgnoreAutoRotateSettings;
+ private boolean mForceAllowRotationForTesting;
private boolean mHomeRotationEnabled;
/**
* Rotation request made by
- * {@link com.android.launcher3.util.ActivityTracker.SchedulerCallback}.
+ * {@link ActivityTracker.SchedulerCallback}.
* This supersedes any other request.
*/
private int mStateHandlerRequest = REQUEST_NONE;
@@ -91,45 +68,48 @@
private boolean mInitialized;
private boolean mDestroyed;
- private int mLastActivityFlags = -1;
+ // Initialize mLastActivityFlags to a value not used by SCREEN_ORIENTATION flags
+ private int mLastActivityFlags = -999;
- public RotationHelper(Activity activity) {
+ public RotationHelper(BaseActivity activity) {
mActivity = activity;
-
- // On large devices we do not handle auto-rotate differently.
- mIgnoreAutoRotateSettings = mActivity.getResources().getBoolean(R.bool.allow_rotation);
- if (!mIgnoreAutoRotateSettings) {
- mSharedPrefs = Utilities.getPrefs(mActivity);
- mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
- mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
- getAllowRotationDefaultValue());
- } else {
- mSharedPrefs = null;
- }
-
- mContentResolver = activity.getContentResolver();
}
- private void updateAutoRotateSetting() {
- int autoRotateEnabled = 0;
- try {
- autoRotateEnabled = Settings.System.getInt(mContentResolver,
- Settings.System.ACCELEROMETER_ROTATION);
- } catch (Settings.SettingNotFoundException e) {
- Log.e(TAG, "autorotate setting not found", e);
+ private void setIgnoreAutoRotateSettings(boolean ignoreAutoRotateSettings) {
+ // On large devices we do not handle auto-rotate differently.
+ mIgnoreAutoRotateSettings = ignoreAutoRotateSettings;
+ if (!mIgnoreAutoRotateSettings) {
+ if (mSharedPrefs == null) {
+ mSharedPrefs = Utilities.getPrefs(mActivity);
+ mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
+ }
+ mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
+ mActivity.getDeviceProfile().allowRotation);
+ } else {
+ if (mSharedPrefs != null) {
+ mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
+ mSharedPrefs = null;
+ }
}
-
- mSystemAutoRotateEnabled = autoRotateEnabled == 1;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+ if (mDestroyed) return;
boolean wasRotationEnabled = mHomeRotationEnabled;
mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
- getAllowRotationDefaultValue());
+ mActivity.getDeviceProfile().allowRotation);
if (mHomeRotationEnabled != wasRotationEnabled) {
notifyChange();
- updateAutoRotateSetting();
+ }
+ }
+
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile dp) {
+ boolean ignoreAutoRotateSettings = dp.allowRotation;
+ if (mIgnoreAutoRotateSettings != ignoreAutoRotateSettings) {
+ setIgnoreAutoRotateSettings(ignoreAutoRotateSettings);
+ notifyChange();
}
}
@@ -156,30 +136,27 @@
// Used by tests only.
public void forceAllowRotationForTesting(boolean allowRotation) {
- mIgnoreAutoRotateSettings =
- allowRotation || mActivity.getResources().getBoolean(R.bool.allow_rotation);
+ mForceAllowRotationForTesting = allowRotation;
notifyChange();
}
public void initialize() {
if (!mInitialized) {
mInitialized = true;
+ setIgnoreAutoRotateSettings(mActivity.getDeviceProfile().allowRotation);
+ mActivity.addOnDeviceProfileChangeListener(this);
notifyChange();
-
- mContentResolver.registerContentObserver(
- Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
- false, mSystemAutoRotateObserver);
- updateAutoRotateSetting();
}
}
public void destroy() {
if (!mDestroyed) {
mDestroyed = true;
+ mActivity.removeOnDeviceProfileChangeListener(this);
+ mActivity = null;
if (mSharedPrefs != null) {
mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
}
- mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
}
}
@@ -198,7 +175,7 @@
} else if (mCurrentStateRequest == REQUEST_LOCK) {
activityFlags = SCREEN_ORIENTATION_LOCKED;
} else if (mIgnoreAutoRotateSettings || mCurrentStateRequest == REQUEST_ROTATE
- || mHomeRotationEnabled) {
+ || mHomeRotationEnabled || mForceAllowRotationForTesting) {
activityFlags = SCREEN_ORIENTATION_UNSPECIFIED;
} else {
// If auto rotation is off, allow rotation on the activity, in case the user is using
@@ -224,10 +201,10 @@
@Override
public String toString() {
- return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d,"
- + " mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, mHomeRotationEnabled=%b,"
- + " mSystemAutoRotateEnabled=%b]",
+ return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d, "
+ + "mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, "
+ + "mHomeRotationEnabled=%b, mForceAllowRotationForTesting=%b]",
mStateHandlerRequest, mCurrentStateRequest, mLastActivityFlags,
- mIgnoreAutoRotateSettings, mHomeRotationEnabled, mSystemAutoRotateEnabled);
+ mIgnoreAutoRotateSettings, mHomeRotationEnabled, mForceAllowRotationForTesting);
}
}
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 2a4f887..8db1dbe 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.states;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+
import android.content.Context;
import android.graphics.Rect;
@@ -22,7 +24,6 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Workspace;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
/**
* Definition for spring loaded state used during drag and drop.
@@ -35,7 +36,7 @@
| FLAG_HIDE_BACK_BUTTON;
public SpringLoadedState(int id) {
- super(id, ContainerType.WORKSPACE, STATE_FLAGS);
+ super(id, LAUNCHER_STATE_HOME, STATE_FLAGS);
}
@Override
@@ -58,10 +59,11 @@
float scale = grid.workspaceSpringLoadShrinkFactor;
Rect insets = launcher.getDragLayer().getInsets();
+ int insetsBottom = grid.isTaskbarPresent ? grid.taskbarSize : insets.bottom;
float scaledHeight = scale * ws.getNormalChildHeight();
float shrunkTop = insets.top + grid.dropTargetBarSizePx;
- float shrunkBottom = ws.getMeasuredHeight() - insets.bottom
+ float shrunkBottom = ws.getMeasuredHeight() - insetsBottom
- grid.workspacePadding.bottom
- grid.workspaceSpringLoadedBottomSpace;
float totalShrunkSpace = shrunkBottom - shrunkTop;
@@ -86,7 +88,12 @@
}
@Override
- public float getWorkspaceScrimAlpha(Launcher launcher) {
- return 0.3f;
+ public float getWorkspaceBackgroundAlpha(Launcher launcher) {
+ return 0.2f;
+ }
+
+ @Override
+ public int getVisibleElements(Launcher launcher) {
+ return (super.getVisibleElements(launcher) | HOTSEAT_ICONS) & ~TASKBAR;
}
}
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index f90ad3c..bd6f7d3 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -27,32 +27,23 @@
*/
public class StateAnimationConfig {
- // We separate the state animations into "atomic" and "non-atomic" components. The atomic
- // components may be run atomically - that is, all at once, instead of user-controlled. However,
- // atomic components are not restricted to this purpose; they can be user-controlled alongside
- // non atomic components as well. Note that each gesture model has exactly one atomic component,
- // PLAY_ATOMIC_OVERVIEW_SCALE *or* PLAY_ATOMIC_OVERVIEW_PEEK.
@IntDef(flag = true, value = {
- PLAY_NON_ATOMIC,
- PLAY_ATOMIC_OVERVIEW_SCALE,
- PLAY_ATOMIC_OVERVIEW_PEEK,
+ SKIP_ALL_ANIMATIONS,
SKIP_OVERVIEW,
- SKIP_DEPTH_CONTROLLER
+ SKIP_DEPTH_CONTROLLER,
+ SKIP_SCRIM,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AnimationFlags {}
- public static final int PLAY_NON_ATOMIC = 1 << 0;
- public static final int PLAY_ATOMIC_OVERVIEW_SCALE = 1 << 1;
- public static final int PLAY_ATOMIC_OVERVIEW_PEEK = 1 << 2;
- public static final int SKIP_OVERVIEW = 1 << 3;
- public static final int SKIP_DEPTH_CONTROLLER = 1 << 4;
+ public static final int SKIP_ALL_ANIMATIONS = 1 << 0;
+ public static final int SKIP_OVERVIEW = 1 << 1;
+ public static final int SKIP_DEPTH_CONTROLLER = 1 << 2;
+ public static final int SKIP_SCRIM = 1 << 3;
public long duration;
public boolean userControlled;
- public @AnimationFlags int animFlags = ANIM_ALL_COMPONENTS;
+ public @AnimationFlags int animFlags = 0;
- public static final int ANIM_ALL_COMPONENTS = PLAY_NON_ATOMIC | PLAY_ATOMIC_OVERVIEW_SCALE
- | PLAY_ATOMIC_OVERVIEW_PEEK;
// Various types of animation state transition
@IntDef(value = {
@@ -67,10 +58,10 @@
ANIM_OVERVIEW_TRANSLATE_Y,
ANIM_OVERVIEW_FADE,
ANIM_ALL_APPS_FADE,
- ANIM_OVERVIEW_SCRIM_FADE,
- ANIM_ALL_APPS_HEADER_FADE,
+ ANIM_SCRIM_FADE,
ANIM_OVERVIEW_MODAL,
ANIM_DEPTH,
+ ANIM_OVERVIEW_ACTIONS_FADE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AnimType {}
@@ -85,14 +76,14 @@
public static final int ANIM_OVERVIEW_TRANSLATE_Y = 8;
public static final int ANIM_OVERVIEW_FADE = 9;
public static final int ANIM_ALL_APPS_FADE = 10;
- public static final int ANIM_OVERVIEW_SCRIM_FADE = 11;
- public static final int ANIM_ALL_APPS_HEADER_FADE = 12; // e.g. predictions
- public static final int ANIM_OVERVIEW_MODAL = 13;
- public static final int ANIM_DEPTH = 14;
+ public static final int ANIM_SCRIM_FADE = 11;
+ public static final int ANIM_OVERVIEW_MODAL = 12;
+ public static final int ANIM_DEPTH = 13;
+ public static final int ANIM_OVERVIEW_ACTIONS_FADE = 14;
private static final int ANIM_TYPES_COUNT = 15;
- private final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
+ protected final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
public StateAnimationConfig() { }
@@ -125,37 +116,9 @@
}
/**
- * @return Whether Overview is scaling as part of this animation. If this is the only
- * component (i.e. NON_ATOMIC_COMPONENT isn't included), then this scaling is happening
- * atomically, rather than being part of a normal state animation. StateHandlers can use
- * this to designate part of their animation that should scale with Overview.
- */
- public boolean playAtomicOverviewScaleComponent() {
- return hasAnimationFlag(StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE);
- }
-
- /**
- * @return Whether this animation will play atomically at the same time as a different,
- * user-controlled state transition. StateHandlers, which contribute to both animations, can
- * use this to avoid animating the same properties in both animations, since they'd conflict
- * with one another.
- */
- public boolean onlyPlayAtomicComponent() {
- return getAnimComponents() == StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE
- || getAnimComponents() == StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
- }
-
- /**
* Returns true if the config and any of the provided component flags
*/
public boolean hasAnimationFlag(@AnimationFlags int a) {
return (animFlags & a) != 0;
}
-
- /**
- * @return Only the flags that determine which animation components to play.
- */
- public @AnimationFlags int getAnimComponents() {
- return animFlags & StateAnimationConfig.ANIM_ALL_COMPONENTS;
- }
}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 38b3712..4261d08 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -33,6 +33,7 @@
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
@@ -92,6 +93,11 @@
l -> l.getAppsView().getActiveRecyclerView().getCurrentScrollY());
}
+ case TestProtocol.REQUEST_WIDGETS_SCROLL_Y: {
+ return getLauncherUIProperty(Bundle::putInt,
+ l -> WidgetsFullSheet.getWidgetsView(l).getCurrentScrollY());
+ }
+
case TestProtocol.REQUEST_WINDOW_INSETS: {
return getUIProperty(Bundle::putParcelable, a -> {
WindowInsets insets = a.getWindow()
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 3ca08fd..b6da7fc 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -27,12 +27,13 @@
public static final int NORMAL_STATE_ORDINAL = 0;
public static final int SPRING_LOADED_STATE_ORDINAL = 1;
public static final int OVERVIEW_STATE_ORDINAL = 2;
- public static final int OVERVIEW_PEEK_STATE_ORDINAL = 3;
- public static final int OVERVIEW_MODAL_TASK_STATE_ORDINAL = 4;
- public static final int QUICK_SWITCH_STATE_ORDINAL = 5;
- public static final int ALL_APPS_STATE_ORDINAL = 6;
- public static final int BACKGROUND_APP_STATE_ORDINAL = 7;
- public static final int HINT_STATE_ORDINAL = 8;
+ public static final int OVERVIEW_MODAL_TASK_STATE_ORDINAL = 3;
+ public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
+ public static final int ALL_APPS_STATE_ORDINAL = 5;
+ public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+ public static final int HINT_STATE_ORDINAL = 7;
+ public static final int HINT_STATE_TWO_BUTTON_ORDINAL = 8;
+ public static final int OVERVIEW_SPLIT_SELECT_ORDINAL = 9;
public static final String TAPL_EVENTS_TAG = "TaplEvents";
public static final String SEQUENCE_MAIN = "Main";
public static final String SEQUENCE_TIS = "TIS";
@@ -46,8 +47,6 @@
return "SpringLoaded";
case OVERVIEW_STATE_ORDINAL:
return "Overview";
- case OVERVIEW_PEEK_STATE_ORDINAL:
- return "OverviewPeek";
case OVERVIEW_MODAL_TASK_STATE_ORDINAL:
return "OverviewModal";
case QUICK_SWITCH_STATE_ORDINAL:
@@ -58,6 +57,10 @@
return "Background";
case HINT_STATE_ORDINAL:
return "Hint";
+ case HINT_STATE_TWO_BUTTON_ORDINAL:
+ return "Hint2Button";
+ case OVERVIEW_SPLIT_SELECT_ORDINAL:
+ return "OverviewSplitSelect";
default:
return "Unknown";
}
@@ -81,11 +84,10 @@
public static final String REQUEST_UNFREEZE_APP_LIST = "unfreeze-app-list";
public static final String REQUEST_APP_LIST_FREEZE_FLAGS = "app-list-freeze-flags";
public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
+ public static final String REQUEST_WIDGETS_SCROLL_Y = "widgets-scroll-y";
public static final String REQUEST_WINDOW_INSETS = "window-insets";
public static final String REQUEST_PID = "pid";
- public static final String REQUEST_TOTAL_PSS_KB = "total_pss";
- public static final String REQUEST_JAVA_LEAK = "java-leak";
- public static final String REQUEST_NATIVE_LEAK = "native-leak";
+ public static final String REQUEST_FORCE_GC = "gc";
public static final String REQUEST_VIEW_LEAK = "view-leak";
public static final String REQUEST_RECENT_TASKS_LIST = "recent-tasks-list";
public static final String REQUEST_START_EVENT_LOGGING = "start-event-logging";
@@ -97,13 +99,14 @@
public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
public static final String REQUEST_DISABLE_DEBUG_TRACING = "disable-debug-tracing";
- public static final String REQUEST_OVERVIEW_ACTIONS_ENABLED = "overview-actions-enabled";
+ public static final String REQUEST_OVERVIEW_SHARE_ENABLED = "overview-share-enabled";
+ public static final String REQUEST_OVERVIEW_CONTENT_PUSH_ENABLED =
+ "overview-content-push-enabled";
public static boolean sDisableSensorRotation;
public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
public static final String PERMANENT_DIAG_TAG = "TaplTarget";
- public static final String PAUSE_NOT_DETECTED = "b/139891609";
- public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
- public static final String NO_SWIPE_TO_HOME = "b/158017601";
+ public static final String WORK_PROFILE_REMOVED = "b/159671700";
+ public static final String FALLBACK_ACTIVITY_NO_SET = "b/181019015";
}
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 3c78b08..5f8a4d4 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -15,43 +15,32 @@
*/
package com.android.launcher3.touch;
-import static com.android.launcher3.LauncherAnimUtils.MIN_PROGRESS_TO_ALL_APPS;
+import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_NON_ATOMIC;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
+import android.animation.Animator.AnimatorListener;
import android.animation.ValueAnimator;
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
-import com.android.launcher3.testing.TestProtocol;
-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.launcher3.util.TouchController;
@@ -61,51 +50,30 @@
public abstract class AbstractStateChangeTouchController
implements TouchController, SingleAxisSwipeDetector.Listener {
- // Progress after which the transition is assumed to be a success in case user does not fling
- public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
-
- /**
- * Play an atomic recents animation when the progress from NORMAL to OVERVIEW reaches this.
- */
- public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 0.5f;
- protected final long ATOMIC_DURATION = getAtomicDuration();
-
protected final Launcher mLauncher;
protected final SingleAxisSwipeDetector mDetector;
protected final SingleAxisSwipeDetector.Direction mSwipeDirection;
- private boolean mNoIntercept;
- private boolean mIsLogContainerSet;
+ protected final AnimatorListener mClearStateOnCancelListener =
+ newCancelListener(this::clearState);
+ private final FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
+
protected int mStartContainerType;
protected LauncherState mStartState;
protected LauncherState mFromState;
protected LauncherState mToState;
protected AnimatorPlaybackController mCurrentAnimation;
- protected PendingAnimation mPendingAnimation;
-
- private float mStartProgress;
+ protected boolean mGoingBetweenStates = true;
// Ratio of transition process [0, 1] to drag displacement (px)
- private float mProgressMultiplier;
+ protected float mProgressMultiplier;
+
+ private boolean mNoIntercept;
+ private boolean mIsLogContainerSet;
+ private float mStartProgress;
private float mDisplacementShift;
private boolean mCanBlockFling;
- private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
-
- protected AnimatorSet mAtomicAnim;
- // True if we want to resume playing atomic components when mAtomicAnim completes.
- private boolean mScheduleResumeAtomicComponent;
- private AutoPlayAtomicAnimationInfo mAtomicAnimAutoPlayInfo;
-
- private boolean mPassedOverviewAtomicThreshold;
- // mAtomicAnim plays the atomic components of the state animations when we pass the threshold.
- // However, if we reinit to transition to a new state (e.g. OVERVIEW -> ALL_APPS) before the
- // atomic animation finishes, we only control the non-atomic components so that we don't
- // interfere with the atomic animation. When the atomic animation ends, we start controlling
- // the atomic components as well, using this controller.
- private AnimatorPlaybackController mAtomicComponentsController;
- private LauncherState mAtomicComponentsTargetState = NORMAL;
-
- private float mAtomicComponentsStartProgress;
+ private boolean mAllAppsOvershootStarted;
public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
mLauncher = l;
@@ -113,14 +81,10 @@
mSwipeDirection = dir;
}
- protected long getAtomicDuration() {
- return 200;
- }
-
protected abstract boolean canInterceptTouch(MotionEvent ev);
@Override
- public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = !canInterceptTouch(ev);
if (mNoIntercept) {
@@ -168,9 +132,6 @@
@Override
public final boolean onControllerTouchEvent(MotionEvent ev) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "onControllerTouchEvent");
- }
return mDetector.onTouchEvent(ev);
}
@@ -185,61 +146,35 @@
protected abstract LauncherState getTargetState(LauncherState fromState,
boolean isDragTowardPositive);
- protected abstract float initCurrentAnimation(@AnimationFlags int animComponents);
-
- /**
- * Returns the container that the touch started from when leaving NORMAL state.
- */
- protected abstract int getLogContainerTypeForNormalState(MotionEvent ev);
+ protected abstract float initCurrentAnimation();
private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) {
LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState()
: reachedToState ? mToState : mFromState;
LauncherState newToState = getTargetState(newFromState, isDragTowardPositive);
+ onReinitToState(newToState);
+
if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) {
return false;
}
mFromState = newFromState;
mToState = newToState;
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "reinitCurrentAnimation: "
- + newToState.ordinal + " " + getClass().getSimpleName());
- }
mStartProgress = 0;
- mPassedOverviewAtomicThreshold = false;
if (mCurrentAnimation != null) {
- mCurrentAnimation.setOnCancelRunnable(null);
+ mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
}
- int animComponents = goingBetweenNormalAndOverview(mFromState, mToState)
- ? PLAY_NON_ATOMIC : ANIM_ALL_COMPONENTS;
- mScheduleResumeAtomicComponent = false;
- if (mAtomicAnim != null) {
- animComponents = PLAY_NON_ATOMIC;
- // Control the non-atomic components until the atomic animation finishes, then control
- // the atomic components as well.
- mScheduleResumeAtomicComponent = true;
- }
- if (goingBetweenNormalAndOverview(mFromState, mToState)
- || mAtomicComponentsTargetState != mToState) {
- cancelAtomicComponentsController();
- }
-
- if (mAtomicComponentsController != null) {
- animComponents &= ~PLAY_ATOMIC_OVERVIEW_SCALE;
- }
- mProgressMultiplier = initCurrentAnimation(animComponents);
+ mProgressMultiplier = initCurrentAnimation();
mCurrentAnimation.dispatchOnStart();
return true;
}
- protected boolean goingBetweenNormalAndOverview(LauncherState fromState,
- LauncherState toState) {
- return (fromState == NORMAL || fromState == OVERVIEW)
- && (toState == NORMAL || toState == OVERVIEW)
- && mPendingAnimation == null;
+ protected void onReinitToState(LauncherState newToState) {
+ }
+
+ protected void onReachedFinalState(LauncherState newToState) {
}
@Override
@@ -256,18 +191,9 @@
} else {
mCurrentAnimation.pause();
mStartProgress = mCurrentAnimation.getProgressFraction();
-
- mAtomicAnimAutoPlayInfo = null;
- if (mAtomicComponentsController != null) {
- mAtomicComponentsController.pause();
- }
}
mCanBlockFling = mFromState == NORMAL;
mFlingBlockCheck.unblockFling();
- // Must be called after all the animation controllers have been paused
- if (mToState == ALL_APPS || mToState == NORMAL) {
- mLauncher.getAllAppsController().onDragStart(mToState == ALL_APPS);
- }
}
@Override
@@ -291,8 +217,15 @@
mFlingBlockCheck.blockFling();
}
}
+ if (mToState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
+ mAllAppsOvershootStarted = true;
+ // 1f, value when all apps container hit the top
+ mLauncher.getAppsView().onPull(progress - 1f, progress - 1f);
+ }
+
} else {
mFlingBlockCheck.onEvent();
+
}
return true;
@@ -302,11 +235,11 @@
public boolean onDrag(float displacement, MotionEvent ev) {
if (!mIsLogContainerSet) {
if (mStartState == ALL_APPS) {
- mStartContainerType = ContainerType.ALLAPPS;
+ mStartContainerType = LAUNCHER_STATE_ALLAPPS;
} else if (mStartState == NORMAL) {
- mStartContainerType = getLogContainerTypeForNormalState(ev);
+ mStartContainerType = LAUNCHER_STATE_HOME;
} else if (mStartState == OVERVIEW) {
- mStartContainerType = ContainerType.TASKSWITCHER;
+ mStartContainerType = LAUNCHER_STATE_OVERVIEW;
}
mIsLogContainerSet = true;
}
@@ -318,71 +251,6 @@
return;
}
mCurrentAnimation.setPlayFraction(fraction);
- if (mAtomicComponentsController != null) {
- // Make sure we don't divide by 0, and have at least a small runway.
- float start = Math.min(mAtomicComponentsStartProgress, 0.9f);
- mAtomicComponentsController.setPlayFraction((fraction - start) / (1 - start));
- }
- maybeUpdateAtomicAnim(mFromState, mToState, fraction);
- }
-
- /**
- * When going between normal and overview states, see if we passed the overview threshold and
- * play the appropriate atomic animation if so.
- */
- private void maybeUpdateAtomicAnim(LauncherState fromState, LauncherState toState,
- float progress) {
- if (!goingBetweenNormalAndOverview(fromState, toState)) {
- return;
- }
- float threshold = toState == OVERVIEW ? ATOMIC_OVERVIEW_ANIM_THRESHOLD
- : 1f - ATOMIC_OVERVIEW_ANIM_THRESHOLD;
- boolean passedThreshold = progress >= threshold;
- if (passedThreshold != mPassedOverviewAtomicThreshold) {
- LauncherState atomicFromState = passedThreshold ? fromState: toState;
- LauncherState atomicToState = passedThreshold ? toState : fromState;
- mPassedOverviewAtomicThreshold = passedThreshold;
- if (mAtomicAnim != null) {
- mAtomicAnim.cancel();
- }
- mAtomicAnim = createAtomicAnimForState(atomicFromState, atomicToState, ATOMIC_DURATION);
- mAtomicAnim.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- mAtomicAnim = null;
- mScheduleResumeAtomicComponent = false;
- }
-
- @Override
- public void onAnimationSuccess(Animator animator) {
- if (!mScheduleResumeAtomicComponent) {
- return;
- }
- cancelAtomicComponentsController();
-
- if (mCurrentAnimation != null) {
- mAtomicComponentsStartProgress = mCurrentAnimation.getProgressFraction();
- long duration = (long) (getShiftRange() * 2);
- mAtomicComponentsController = AnimatorPlaybackController.wrap(
- createAtomicAnimForState(mFromState, mToState, duration), duration);
- mAtomicComponentsController.dispatchOnStart();
- mAtomicComponentsTargetState = mToState;
- maybeAutoPlayAtomicComponentsAnim();
- }
- }
- });
- mAtomicAnim.start();
- mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
- }
- }
-
- private AnimatorSet createAtomicAnimForState(LauncherState fromState, LauncherState targetState,
- long duration) {
- StateAnimationConfig config = getConfigForStates(fromState, targetState);
- config.animFlags = PLAY_ATOMIC_OVERVIEW_SCALE;
- config.duration = duration;
- return mLauncher.getStateManager().createAtomicAnimation(fromState, targetState, config);
}
/**
@@ -395,8 +263,13 @@
@Override
public void onDragEnd(float velocity) {
+ if (mCurrentAnimation == null) {
+ // Unlikely, but we may have been canceled just before onDragEnd(). We assume whoever
+ // canceled us will handle a new state transition to clean up.
+ return;
+ }
+
boolean fling = mDetector.isFling(velocity);
- final int logAction = fling ? Touch.FLING : Touch.SWIPE;
boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
if (blockedFling) {
@@ -413,9 +286,8 @@
? mToState : mFromState;
// snap to top or bottom using the release velocity
} else {
- float successProgress = mToState == ALL_APPS
- ? MIN_PROGRESS_TO_ALL_APPS : SUCCESS_TRANSITION_PROGRESS;
- targetState = (interpolatedProgress > successProgress) ? mToState : mFromState;
+ targetState =
+ (interpolatedProgress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState;
}
final float endProgress;
@@ -439,7 +311,8 @@
} else {
// Let the state manager know that the animation didn't go to the target state,
// but don't cancel ourselves (we already clean up when the animation completes).
- mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable();
+ mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
+ mCurrentAnimation.dispatchOnCancel();
endProgress = 0;
if (progress <= 0) {
@@ -452,71 +325,23 @@
Math.min(progress, 1) - endProgress) * durationMultiplier;
}
}
-
- mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
+ if (targetState != mStartState) {
+ logReachedState(targetState);
+ }
+ mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState));
ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
anim.setFloatValues(startProgress, endProgress);
- maybeUpdateAtomicAnim(mFromState, targetState, targetState == mToState ? 1f : 0f);
- updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
- targetState, velocity, fling);
+ updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling);
mCurrentAnimation.dispatchOnStart();
- if (fling && targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
- mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
+ if (targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
+ if (mAllAppsOvershootStarted) {
+ mLauncher.getAppsView().onRelease();
+ mAllAppsOvershootStarted = false;
+ } else {
+ mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity, progress);
+ }
}
anim.start();
- mAtomicAnimAutoPlayInfo = new AutoPlayAtomicAnimationInfo(endProgress, anim.getDuration());
- maybeAutoPlayAtomicComponentsAnim();
- }
-
- /**
- * Animates the atomic components from the current progress to the final progress.
- *
- * Note that this only applies when we are controlling the atomic components separately from
- * the non-atomic components, which only happens if we reinit before the atomic animation
- * finishes.
- */
- private void maybeAutoPlayAtomicComponentsAnim() {
- if (mAtomicComponentsController == null || mAtomicAnimAutoPlayInfo == null) {
- return;
- }
-
- final AnimatorPlaybackController controller = mAtomicComponentsController;
- ValueAnimator atomicAnim = controller.getAnimationPlayer();
- atomicAnim.setFloatValues(controller.getProgressFraction(),
- mAtomicAnimAutoPlayInfo.toProgress);
- long duration = mAtomicAnimAutoPlayInfo.endTime - SystemClock.elapsedRealtime();
- mAtomicAnimAutoPlayInfo = null;
- if (duration <= 0) {
- atomicAnim.start();
- atomicAnim.end();
- mAtomicComponentsController = null;
- } else {
- atomicAnim.setDuration(duration);
- atomicAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- if (mAtomicComponentsController == controller) {
- mAtomicComponentsController = null;
- }
- }
- });
- atomicAnim.start();
- }
- }
-
- private long getRemainingAtomicDuration() {
- if (mAtomicAnim == null) {
- return 0;
- }
- if (Utilities.ATLEAST_OREO) {
- return mAtomicAnim.getTotalDuration() - mAtomicAnim.getCurrentPlayTime();
- } else {
- long remainingDuration = 0;
- for (Animator anim : mAtomicAnim.getChildAnimations()) {
- remainingDuration = Math.max(remainingDuration, anim.getDuration());
- }
- return remainingDuration;
- }
}
protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
@@ -525,94 +350,49 @@
.setInterpolator(scrollInterpolatorForVelocity(velocity));
}
- protected int getDirectionForLog() {
- return mToState.ordinal > mFromState.ordinal ? Direction.UP : Direction.DOWN;
- }
-
- protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
- if (mAtomicComponentsController != null) {
- mAtomicComponentsController.getAnimationPlayer().end();
- mAtomicComponentsController = null;
- }
+ protected void onSwipeInteractionCompleted(LauncherState targetState) {
+ onReachedFinalState(mToState);
clearState();
- boolean shouldGoToTargetState = true;
- if (mPendingAnimation != null) {
- boolean reachedTarget = mToState == targetState;
- mPendingAnimation.finish(reachedTarget, logAction);
- mPendingAnimation = null;
- shouldGoToTargetState = !reachedTarget;
- }
+ boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState);
if (shouldGoToTargetState) {
- goToTargetState(targetState, logAction);
+ goToTargetState(targetState);
}
}
- protected void goToTargetState(LauncherState targetState, int logAction) {
- if (targetState != mStartState) {
- logReachedState(logAction, targetState);
- }
+ protected void goToTargetState(LauncherState targetState) {
if (!mLauncher.isInState(targetState)) {
// If we're already in the target state, don't jump to it at the end of the animation in
// case the user started interacting with it before the animation finished.
mLauncher.getStateManager().goToState(targetState, false /* animated */);
}
- mLauncher.getDragLayer().getScrim().createSysuiMultiplierAnim(1f).setDuration(0).start();
+ mLauncher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(
+ 1f).setDuration(0).start();
}
- private void logReachedState(int logAction, LauncherState targetState) {
+ private void logReachedState(LauncherState targetState) {
// Transition complete. log the action
- mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
- getDirectionForLog(), mDetector.getDownX(), mDetector.getDownY(),
- mStartContainerType /* e.g., hotseat */,
- mStartState.containerType /* e.g., workspace */,
- targetState.containerType,
- mLauncher.getWorkspace().getCurrentPage());
mLauncher.getStatsLogManager().logger()
- .withSrcState(StatsLogManager.containerTypeToAtomState(mStartState.containerType))
- .withDstState(StatsLogManager.containerTypeToAtomState(targetState.containerType))
+ .withSrcState(mStartState.statsLogOrdinal)
+ .withDstState(targetState.statsLogOrdinal)
.withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
.setWorkspace(
LauncherAtom.WorkspaceContainer.newBuilder()
.setPageIndex(mLauncher.getWorkspace().getCurrentPage()))
.build())
- .log(StatsLogManager.getLauncherAtomEvent(mStartState.containerType,
- targetState.containerType, mToState.ordinal > mFromState.ordinal
+ .log(StatsLogManager.getLauncherAtomEvent(mStartState.statsLogOrdinal,
+ targetState.statsLogOrdinal, mToState.ordinal > mFromState.ordinal
? LAUNCHER_UNKNOWN_SWIPEUP
: LAUNCHER_UNKNOWN_SWIPEDOWN));
}
protected void clearState() {
cancelAnimationControllers();
- if (mAtomicAnim != null) {
- mAtomicAnim.cancel();
- mAtomicAnim = null;
- }
- mScheduleResumeAtomicComponent = false;
+ mGoingBetweenStates = true;
mDetector.finishedScrolling();
mDetector.setDetectableScrollConditions(0, false);
}
private void cancelAnimationControllers() {
mCurrentAnimation = null;
- cancelAtomicComponentsController();
- }
-
- private void cancelAtomicComponentsController() {
- if (mAtomicComponentsController != null) {
- mAtomicComponentsController.getAnimationPlayer().cancel();
- mAtomicComponentsController = null;
- }
- mAtomicAnimAutoPlayInfo = null;
- }
-
- private static class AutoPlayAtomicAnimationInfo {
-
- public final float toProgress;
- public final long endTime;
-
- AutoPlayAtomicAnimationInfo(float toProgress, long duration) {
- this.toProgress = toProgress;
- this.endTime = duration + SystemClock.elapsedRealtime();
- }
}
}
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index 4a202b6..ab2652a 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -23,25 +23,18 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
/**
* TouchController to switch between NORMAL and ALL_APPS state.
*/
public class AllAppsSwipeController extends AbstractStateChangeTouchController {
- private MotionEvent mTouchDownEvent;
-
public AllAppsSwipeController(Launcher l) {
super(l, SingleAxisSwipeDetector.VERTICAL);
}
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mTouchDownEvent = ev;
- }
if (mCurrentAnimation != null) {
// If we are already animating from a previous state, we can intercept.
return true;
@@ -70,17 +63,11 @@
}
@Override
- protected int getLogContainerTypeForNormalState(MotionEvent ev) {
- return mLauncher.getDragLayer().isEventOverView(mLauncher.getHotseat(), mTouchDownEvent)
- ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
- }
-
- @Override
- protected float initCurrentAnimation(@AnimationFlags int animComponents) {
+ protected float initCurrentAnimation() {
float range = getShiftRange();
long maxAccuracy = (long) (2 * range);
mCurrentAnimation = mLauncher.getStateManager()
- .createAnimationToNewWorkspace(mToState, maxAccuracy, animComponents);
+ .createAnimationToNewWorkspace(mToState, maxAccuracy);
float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
float totalShift = endVerticalShift - startVerticalShift;
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 01b33d8..1276ece 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -26,8 +26,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
-import com.android.launcher3.testing.TestProtocol;
-
import java.util.LinkedList;
import java.util.Queue;
@@ -175,9 +173,6 @@
if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {
setState(ScrollState.DRAGGING);
}
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "before report dragging");
- }
if (mState == ScrollState.DRAGGING) {
reportDragging(ev);
}
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 8486666..b53f96e 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -17,8 +17,8 @@
import static com.android.launcher3.Launcher.REQUEST_BIND_PENDING_APPWIDGET;
import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
-import static com.android.launcher3.model.AppLaunchTracker.CONTAINER_ALL_APPS;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
@@ -26,7 +26,10 @@
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.content.Context;
import android.content.Intent;
+import android.content.IntentSender;
import android.content.pm.LauncherApps;
import android.content.pm.PackageInstaller.SessionInfo;
import android.os.Process;
@@ -37,11 +40,8 @@
import android.view.View.OnClickListener;
import android.widget.Toast;
-import androidx.annotation.Nullable;
-
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.folder.Folder;
@@ -50,14 +50,16 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
+import com.android.launcher3.model.data.SearchActionItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAppWidgetHostView;
import com.android.launcher3.widget.WidgetAddFlowHandler;
import com.android.launcher3.widget.WidgetManagerHelper;
@@ -72,13 +74,9 @@
/**
* Instance used for click handling on items
*/
- public static final OnClickListener INSTANCE = getInstance(null);
+ public static final OnClickListener INSTANCE = ItemClickHandler::onClick;
- public static final OnClickListener getInstance(String sourceContainer) {
- return v -> onClick(v, sourceContainer);
- }
-
- private static void onClick(View v, String sourceContainer) {
+ private static void onClick(View v) {
// Make sure that rogue clicks don't get through while allapps is launching, or after the
// view has detached (it's possible for this to happen if the view is removed mid touch).
if (v.getWindowToken() == null) return;
@@ -88,18 +86,20 @@
Object tag = v.getTag();
if (tag instanceof WorkspaceItemInfo) {
- onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher, sourceContainer);
+ onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
onClickFolderIcon(v);
}
} else if (tag instanceof AppInfo) {
- startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher,
- sourceContainer == null ? CONTAINER_ALL_APPS: sourceContainer);
+ startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher
+ );
} else if (tag instanceof LauncherAppWidgetInfo) {
if (v instanceof PendingAppWidgetHostView) {
onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
}
+ } else if (tag instanceof SearchActionItemInfo) {
+ onClickSearchAction(launcher, (SearchActionItemInfo) tag);
}
}
@@ -181,7 +181,7 @@
LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
try {
launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
- launcher.getActivityLaunchOptionsAsBundle(v));
+ launcher.getActivityLaunchOptions(v, item).toBundle());
return;
} catch (Exception e) {
Log.e(TAG, "Unable to launch market intent for package=" + packageName, e);
@@ -191,7 +191,40 @@
// Fallback to using custom market intent.
Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName);
- launcher.startActivitySafely(v, intent, item, null);
+ launcher.startActivitySafely(v, intent, item);
+ }
+
+ /**
+ * Handles clicking on a disabled shortcut
+ *
+ * @return true iff the disabled item click has been handled.
+ */
+ public static boolean handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context) {
+ final int disabledFlags = shortcut.runtimeStatusFlags
+ & WorkspaceItemInfo.FLAG_DISABLED_MASK;
+ if ((disabledFlags
+ & ~FLAG_DISABLED_SUSPENDED
+ & ~FLAG_DISABLED_QUIET_USER) == 0) {
+ // If the app is only disabled because of the above flags, launch activity anyway.
+ // Framework will tell the user why the app is suspended.
+ return false;
+ } else {
+ if (!TextUtils.isEmpty(shortcut.disabledMessage)) {
+ // Use a message specific to this shortcut, if it has one.
+ Toast.makeText(context, shortcut.disabledMessage, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ // Otherwise just use a generic error message.
+ int error = R.string.activity_not_available;
+ if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) {
+ error = R.string.safemode_shortcut_error;
+ } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0
+ || (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) {
+ error = R.string.shortcut_not_available;
+ }
+ Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
+ return true;
+ }
}
/**
@@ -199,33 +232,9 @@
*
* @param v The view that was clicked. Must be a tagged with a {@link WorkspaceItemInfo}.
*/
- public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher,
- @Nullable String sourceContainer) {
- if (shortcut.isDisabled()) {
- final int disabledFlags = shortcut.runtimeStatusFlags
- & WorkspaceItemInfo.FLAG_DISABLED_MASK;
- if ((disabledFlags &
- ~FLAG_DISABLED_SUSPENDED &
- ~FLAG_DISABLED_QUIET_USER) == 0) {
- // If the app is only disabled because of the above flags, launch activity anyway.
- // Framework will tell the user why the app is suspended.
- } else {
- if (!TextUtils.isEmpty(shortcut.disabledMessage)) {
- // Use a message specific to this shortcut, if it has one.
- Toast.makeText(launcher, shortcut.disabledMessage, Toast.LENGTH_SHORT).show();
- return;
- }
- // Otherwise just use a generic error message.
- int error = R.string.activity_not_available;
- if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) {
- error = R.string.safemode_shortcut_error;
- } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0 ||
- (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) {
- error = R.string.shortcut_not_available;
- }
- Toast.makeText(launcher, error, Toast.LENGTH_SHORT).show();
- return;
- }
+ public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
+ if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) {
+ return;
}
// Check for abandoned promise
@@ -234,24 +243,60 @@
? shortcut.getIntent().getComponent().getPackageName()
: shortcut.getIntent().getPackage();
if (!TextUtils.isEmpty(packageName)) {
- onClickPendingAppItem(v, launcher, packageName,
- shortcut.hasStatusFlag(WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE));
+ onClickPendingAppItem(
+ v,
+ launcher,
+ packageName,
+ (shortcut.runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0);
return;
}
}
// Start activities
- startAppShortcutOrInfoActivity(v, shortcut, launcher, sourceContainer);
+ startAppShortcutOrInfoActivity(v, shortcut, launcher);
}
- private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher,
- @Nullable String sourceContainer) {
+ /**
+ * Event handler for a {@link SearchActionItemInfo} click
+ */
+ public static void onClickSearchAction(Launcher launcher, SearchActionItemInfo itemInfo) {
+ if (itemInfo.getIntent() != null) {
+ if (itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START_FOR_RESULT)) {
+ launcher.startActivityForResult(itemInfo.getIntent(), 0);
+ } else {
+ launcher.startActivity(itemInfo.getIntent());
+ }
+ } else if (itemInfo.getPendingIntent() != null) {
+ try {
+ PendingIntent pendingIntent = itemInfo.getPendingIntent();
+ if (!itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START)) {
+ pendingIntent.send();
+ } else if (itemInfo.hasFlags(SearchActionItemInfo.FLAG_SHOULD_START_FOR_RESULT)) {
+ launcher.startIntentSenderForResult(pendingIntent.getIntentSender(), 0, null, 0,
+ 0, 0);
+ } else {
+ launcher.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
+ }
+ } catch (PendingIntent.CanceledException | IntentSender.SendIntentException e) {
+ Toast.makeText(launcher,
+ launcher.getResources().getText(R.string.shortcut_not_available),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ launcher.getStatsLogManager().logger().withItemInfo(itemInfo).log(LAUNCHER_APP_LAUNCH_TAP);
+ }
+
+ private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity");
Intent intent;
- if (item instanceof PromiseAppInfo) {
- PromiseAppInfo promiseAppInfo = (PromiseAppInfo) item;
- intent = promiseAppInfo.getMarketIntent(launcher);
+ if (item instanceof ItemInfoWithIcon
+ && (((ItemInfoWithIcon) item).runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+ ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item;
+ intent = new PackageManagerHelper(launcher)
+ .getMarketIntent(appInfo.getTargetComponent().getPackageName());
} else {
intent = item.getIntent();
}
@@ -270,10 +315,10 @@
intent.setPackage(null);
}
}
- if (v != null && launcher.getAppTransitionManager().supportsAdaptiveIconAnimation()) {
+ if (v != null && launcher.supportsAdaptiveIconAnimation(v)) {
// Preload the icon to reduce latency b/w swapping the floating view with the original.
FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
}
- launcher.startActivitySafely(v, intent, item, sourceContainer);
+ launcher.startActivitySafely(v, intent, item);
}
}
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 7baeab8..f876dd9 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -21,6 +21,7 @@
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.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED;
import android.view.View;
import android.view.View.OnLongClickListener;
@@ -32,6 +33,7 @@
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.folder.Folder;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.TestProtocol;
@@ -55,7 +57,7 @@
if (!(v.getTag() instanceof ItemInfo)) return false;
launcher.setWaitingForResult(null);
- beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
+ beginDrag(v, launcher, (ItemInfo) v.getTag(), launcher.getDefaultWorkspaceDragOptions());
return true;
}
@@ -86,6 +88,12 @@
if (!launcher.isInState(ALL_APPS) && !launcher.isInState(OVERVIEW)) return false;
if (launcher.getWorkspace().isSwitchingState()) return false;
+ StatsLogger logger = launcher.getStatsLogManager().logger();
+ if (v.getTag() instanceof ItemInfo) {
+ logger.withItemInfo((ItemInfo) v.getTag());
+ }
+ logger.log(LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED);
+
// Start the drag
final DragController dragController = launcher.getDragController();
dragController.addDragListener(new DragController.DragListener() {
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 48c7734..d047eca 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -17,14 +17,19 @@
package com.android.launcher3.touch;
import static android.widget.ListPopupWindow.WRAP_CONTENT;
+
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.ShapeDrawable;
import android.util.FloatProperty;
import android.view.MotionEvent;
import android.view.Surface;
@@ -33,13 +38,28 @@
import android.view.accessibility.AccessibilityEvent;
import android.widget.LinearLayout;
-import com.android.launcher3.PagedView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.views.BaseDragLayer;
+
+import java.util.Collections;
+import java.util.List;
public class LandscapePagedViewHandler implements PagedOrientationHandler {
@Override
+ public <T> T getPrimaryValue(T x, T y) {
+ return y;
+ }
+
+ @Override
+ public <T> T getSecondaryValue(T x, T y) {
+ return x;
+ }
+
+ @Override
public int getPrimaryValue(int x, int y) {
return y;
}
@@ -50,31 +70,13 @@
}
@Override
- public void delegateScrollTo(PagedView pagedView, int secondaryScroll, int minMaxScroll) {
- pagedView.superScrollTo(secondaryScroll, minMaxScroll);
+ public float getPrimaryValue(float x, float y) {
+ return y;
}
@Override
- public void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y) {
- pagedView.scrollTo(pagedView.getScrollX() + x, unboundedScroll + y);
- }
-
- @Override
- public void scrollerStartScroll(OverScroller scroller, int newPosition) {
- scroller.startScroll(scroller.getCurrPos(), newPosition - scroller.getCurrPos());
- }
-
- @Override
- public void getCurveProperties(PagedView view, Rect insets, CurveProperties out) {
- out.scroll = view.getScrollY();
- out.halfPageSize = view.getNormalChildHeight() / 2;
- out.halfScreenSize = view.getMeasuredHeight() / 2;
- out.screenCenter = insets.top + view.getPaddingTop() + out.scroll + out.halfPageSize;
- }
-
- @Override
- public boolean isGoingUp(float displacement, boolean isRtl) {
- return isRtl ? displacement < 0 : displacement > 0;
+ public float getSecondaryValue(float x, float y) {
+ return x;
}
@Override
@@ -90,11 +92,6 @@
}
@Override
- public void delegateScrollTo(PagedView pagedView, int primaryScroll) {
- pagedView.superScrollTo(pagedView.getScrollX(), primaryScroll);
- }
-
- @Override
public <T> void set(T target, Int2DAction<T> action, int param) {
action.call(target, 0, param);
}
@@ -105,6 +102,11 @@
}
@Override
+ public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
+ action.call(target, param, 0);
+ }
+
+ @Override
public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
return event.getY(pointerIndex);
}
@@ -120,12 +122,27 @@
}
@Override
+ public int getPrimarySize(View view) {
+ return view.getHeight();
+ }
+
+ @Override
public float getPrimarySize(RectF rect) {
return rect.height();
}
@Override
- public int getClearAllScrollOffset(View view, boolean isRtl) {
+ public float getStart(RectF rect) {
+ return rect.top;
+ }
+
+ @Override
+ public float getEnd(RectF rect) {
+ return rect.bottom;
+ }
+
+ @Override
+ public int getClearAllSidePadding(View view, boolean isRtl) {
return (isRtl ? view.getPaddingBottom() : - view.getPaddingTop()) / 2;
}
@@ -145,9 +162,16 @@
}
@Override
- public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
- view.setTranslationX(0);
- view.setTranslationY(translation);
+ public int getSplitTaskViewDismissDirection(SplitPositionOption splitPosition,
+ DeviceProfile dp) {
+ // Don't use device profile here because we know we're in fake landscape, only split option
+ // available is top/left
+ if (splitPosition.mStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+ // Top (visually left) side
+ return SPLIT_TRANSLATE_PRIMARY_NEGATIVE;
+ }
+ throw new IllegalStateException("Invalid split stage position: " +
+ splitPosition.mStagePosition);
}
@Override
@@ -207,28 +231,36 @@
}
@Override
- public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
- return HORIZONTAL;
+ public int getPrimaryTranslationDirectionFactor() {
+ return -1;
}
- @Override
- public int getTaskDismissDirectionFactor() {
+ public int getSecondaryTranslationDirectionFactor() {
return 1;
}
@Override
- public int getTaskDragDisplacementFactor(boolean isRtl) {
- return isRtl ? 1 : -1;
+ public int getSplitTranslationDirectionFactor(int stagePosition) {
+ if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ return -1;
+ } else {
+ return 1;
+ }
}
@Override
- public float getTaskMenuX(float x, View thumbnailView) {
+ public int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ return translationOffset;
+ }
+
+ @Override
+ public float getTaskMenuX(float x, View thumbnailView, int overScroll) {
return thumbnailView.getMeasuredWidth() + x;
}
@Override
- public float getTaskMenuY(float y, View thumbnailView) {
- return y;
+ public float getTaskMenuY(float y, View thumbnailView, int overScroll) {
+ return y + overScroll;
}
@Override
@@ -237,18 +269,63 @@
}
@Override
- public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
- return LinearLayout.HORIZONTAL;
+ public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
+ LinearLayout taskMenuLayout, int dividerSpacing,
+ ShapeDrawable dividerDrawable) {
+ taskMenuLayout.setOrientation(LinearLayout.HORIZONTAL);
+ dividerDrawable.setIntrinsicWidth(dividerSpacing);
+ taskMenuLayout.setDividerDrawable(dividerDrawable);
}
@Override
- public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp) {
+ public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
+ LinearLayout viewGroup, DeviceProfile deviceProfile) {
+ // Phone fake landscape
+ viewGroup.setOrientation(LinearLayout.VERTICAL);
lp.width = 0;
lp.height = WRAP_CONTENT;
lp.weight = 1;
+ Utilities.setStartMarginForView(viewGroup.findViewById(R.id.text), 0);
+ Utilities.setStartMarginForView(viewGroup.findViewById(R.id.icon), 0);
}
@Override
+ public void setTaskMenuAroundTaskView(LinearLayout taskView, float margin) {
+ BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskView.getLayoutParams();
+ lp.topMargin += margin;
+ }
+
+ @Override
+ public PointF getAdditionalInsetForTaskMenu(float margin) {
+ return new PointF(margin, 0);
+ }
+
+ /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+
+ @Override
+ public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
+ return HORIZONTAL;
+ }
+
+ @Override
+ public int getUpDirection(boolean isRtl) {
+ return isRtl ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+ : SingleAxisSwipeDetector.DIRECTION_POSITIVE;
+ }
+
+ @Override
+ public boolean isGoingUp(float displacement, boolean isRtl) {
+ return isRtl ? displacement < 0 : displacement > 0;
+ }
+
+ @Override
+ public int getTaskDragDisplacementFactor(boolean isRtl) {
+ return isRtl ? 1 : -1;
+ }
+
+ /* -------------------- */
+
+ @Override
public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
boolean layoutChild) {
final int childHeight = child.getMeasuredHeight();
@@ -260,4 +337,24 @@
}
return new ChildBounds(childHeight, childWidth, childBottom, childLeft);
}
+
+ @SuppressWarnings("SuspiciousNameCombination")
+ @Override
+ public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+ return rect.left;
+ }
+
+ @Override
+ public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
+ // Add "left" side of phone which is actually the top
+ return Collections.singletonList(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_left,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ }
+
+ @Override
+ public FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
+ DeviceProfile deviceProfile) {
+ return primary;
+ }
}
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 65b1a7a..266e05f 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -22,6 +22,7 @@
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.ShapeDrawable;
import android.util.FloatProperty;
import android.view.MotionEvent;
import android.view.VelocityTracker;
@@ -30,8 +31,10 @@
import android.widget.LinearLayout;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.PagedView;
-import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+
+import java.util.List;
/**
* Abstraction layer to separate horizontal and vertical specific implementations
@@ -40,6 +43,10 @@
*/
public interface PagedOrientationHandler {
+ int SPLIT_TRANSLATE_PRIMARY_POSITIVE = 0;
+ int SPLIT_TRANSLATE_PRIMARY_NEGATIVE = 1;
+ int SPLIT_TRANSLATE_SECONDARY_NEGATIVE = 2;
+
PagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler();
PagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler();
PagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler();
@@ -57,15 +64,25 @@
<T> void set(T target, Int2DAction<T> action, int param);
<T> void set(T target, Float2DAction<T> action, float param);
+ <T> void setSecondary(T target, Float2DAction<T> action, float param);
float getPrimaryDirection(MotionEvent event, int pointerIndex);
float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
int getMeasuredSize(View view);
+ int getPrimarySize(View view);
float getPrimarySize(RectF rect);
- int getClearAllScrollOffset(View view, boolean isRtl);
+ float getStart(RectF rect);
+ float getEnd(RectF rect);
+ int getClearAllSidePadding(View view, boolean isRtl);
int getSecondaryDimension(View view);
FloatProperty<View> getPrimaryViewTranslate();
FloatProperty<View> getSecondaryViewTranslate();
- void setPrimaryAndResetSecondaryTranslate(View view, float translation);
+
+ /**
+ * @param splitPosition The position where the view to be split will go
+ * @return {@link #SPLIT_TRANSLATE_*} constants to indicate which direction the
+ * dismissal should happen
+ */
+ int getSplitTaskViewDismissDirection(SplitPositionOption splitPosition, DeviceProfile dp);
int getPrimaryScroll(View view);
float getPrimaryScale(View view);
int getChildStart(View view);
@@ -73,29 +90,68 @@
int getCenterForPage(View view, Rect insets);
int getScrollOffsetStart(View view, Rect insets);
int getScrollOffsetEnd(View view, Rect insets);
- SingleAxisSwipeDetector.Direction getOppositeSwipeDirection();
- int getTaskDismissDirectionFactor();
- int getTaskDragDisplacementFactor(boolean isRtl);
+ int getPrimaryTranslationDirectionFactor();
+ int getSecondaryTranslationDirectionFactor();
+ int getSplitTranslationDirectionFactor(@StagePosition int stagePosition);
+ int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp);
ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
void setMaxScroll(AccessibilityEvent event, int maxScroll);
boolean getRecentsRtlSetting(Resources resources);
float getDegreesRotated();
int getRotation();
+
+ <T> T getPrimaryValue(T x, T y);
+ <T> T getSecondaryValue(T x, T y);
+
int getPrimaryValue(int x, int y);
int getSecondaryValue(int x, int y);
- void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll);
- /** Uses {@params pagedView}.getScroll[X|Y]() method for the secondary amount*/
- void delegateScrollTo(PagedView pagedView, int primaryScroll);
- void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y);
- void scrollerStartScroll(OverScroller scroller, int newPosition);
- void getCurveProperties(PagedView view, Rect insets, CurveProperties out);
- boolean isGoingUp(float displacement, boolean isRtl);
+
+ float getPrimaryValue(float x, float y);
+ float getSecondaryValue(float x, float y);
+
boolean isLayoutNaturalToLauncher();
- float getTaskMenuX(float x, View thumbnailView);
- float getTaskMenuY(float y, View thumbnailView);
+ FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
+ DeviceProfile deviceProfile);
+ int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
+ List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
+
+ // Overview TaskMenuView methods
+ float getTaskMenuX(float x, View thumbnailView, int overScroll);
+ float getTaskMenuY(float y, View thumbnailView, int overScroll);
int getTaskMenuWidth(View view);
- int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout);
- void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
+ /**
+ * Sets linear layout orientation for {@link com.android.launcher3.popup.SystemShortcut} items
+ * inside task menu view.
+ */
+ void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
+ LinearLayout taskMenuLayout, int dividerSpacing,
+ ShapeDrawable dividerDrawable);
+ /**
+ * Sets layout param attributes for {@link com.android.launcher3.popup.SystemShortcut} child
+ * views inside task menu view.
+ */
+ void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
+ LinearLayout viewGroup, DeviceProfile deviceProfile);
+ /**
+ * Adjusts margins for the entire task menu view itself, which comprises of both app title and
+ * shortcut options.
+ */
+ void setTaskMenuAroundTaskView(LinearLayout taskView, float margin);
+ /**
+ * Since the task menu layout is manually positioned on top of recents view, this method returns
+ * additional adjustments to the positioning based on fake land/seascape
+ */
+ PointF getAdditionalInsetForTaskMenu(float margin);
+
+ // The following are only used by TaskViewTouchHandler.
+ /** @return Either VERTICAL or HORIZONTAL. */
+ SingleAxisSwipeDetector.Direction getUpDownSwipeDirection();
+ /** @return Given {@link #getUpDownSwipeDirection()}, whether POSITIVE or NEGATIVE is up. */
+ int getUpDirection(boolean isRtl);
+ /** @return Whether the displacement is going towards the top of the screen. */
+ boolean isGoingUp(float displacement, boolean isRtl);
+ /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
+ int getTaskDragDisplacementFactor(boolean isRtl);
/**
* Maps the velocity from the coordinate plane of the foreground app to that
@@ -103,13 +159,6 @@
*/
void adjustFloatingIconStartVelocity(PointF velocity);
- class CurveProperties {
- public int scroll;
- public int halfPageSize;
- public int screenCenter;
- public int halfScreenSize;
- }
-
class ChildBounds {
public final int primaryDimension;
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 79e5c87..dd97af5 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -19,11 +19,15 @@
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.ShapeDrawable;
import android.util.FloatProperty;
import android.view.MotionEvent;
import android.view.Surface;
@@ -32,13 +36,28 @@
import android.view.accessibility.AccessibilityEvent;
import android.widget.LinearLayout;
-import com.android.launcher3.PagedView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.views.BaseDragLayer;
+
+import java.util.ArrayList;
+import java.util.List;
public class PortraitPagedViewHandler implements PagedOrientationHandler {
@Override
+ public <T> T getPrimaryValue(T x, T y) {
+ return x;
+ }
+
+ @Override
+ public <T> T getSecondaryValue(T x, T y) {
+ return y;
+ }
+
+ @Override
public int getPrimaryValue(int x, int y) {
return x;
}
@@ -49,32 +68,13 @@
}
@Override
- public void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll) {
- pagedView.superScrollTo(primaryScroll, secondaryScroll);
+ public float getPrimaryValue(float x, float y) {
+ return x;
}
@Override
- public void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y) {
- pagedView.scrollTo(unboundedScroll + x, pagedView.getScrollY() + y);
- }
-
- @Override
- public void scrollerStartScroll(OverScroller scroller, int newPosition) {
- scroller.startScroll(newPosition - scroller.getCurrPos(), scroller.getCurrPos());
- }
-
- @Override
- public void getCurveProperties(PagedView view, Rect insets, CurveProperties out) {
- out.scroll = view.getScrollX();
- out.halfPageSize = view.getNormalChildWidth() / 2;
- out.halfScreenSize = view.getMeasuredWidth() / 2;
- out.screenCenter = insets.left + view.getPaddingLeft() + out.scroll + out.halfPageSize;
- }
-
- @Override
- public boolean isGoingUp(float displacement, boolean isRtl) {
- // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
- return displacement < 0;
+ public float getSecondaryValue(float x, float y) {
+ return y;
}
@Override
@@ -88,11 +88,6 @@
}
@Override
- public void delegateScrollTo(PagedView pagedView, int primaryScroll) {
- pagedView.superScrollTo(primaryScroll, pagedView.getScrollY());
- }
-
- @Override
public <T> void set(T target, Int2DAction<T> action, int param) {
action.call(target, param, 0);
}
@@ -103,6 +98,11 @@
}
@Override
+ public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
+ action.call(target, 0, param);
+ }
+
+ @Override
public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
return event.getX(pointerIndex);
}
@@ -118,12 +118,27 @@
}
@Override
+ public int getPrimarySize(View view) {
+ return view.getWidth();
+ }
+
+ @Override
public float getPrimarySize(RectF rect) {
return rect.width();
}
@Override
- public int getClearAllScrollOffset(View view, boolean isRtl) {
+ public float getStart(RectF rect) {
+ return rect.left;
+ }
+
+ @Override
+ public float getEnd(RectF rect) {
+ return rect.right;
+ }
+
+ @Override
+ public int getClearAllSidePadding(View view, boolean isRtl) {
return (isRtl ? view.getPaddingRight() : - view.getPaddingLeft()) / 2;
}
@@ -143,9 +158,22 @@
}
@Override
- public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
- view.setTranslationX(translation);
- view.setTranslationY(0);
+ public int getSplitTaskViewDismissDirection(SplitPositionOption splitPosition,
+ DeviceProfile dp) {
+ if (splitPosition.mStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+ if (dp.isLandscape) {
+ // Left side
+ return SPLIT_TRANSLATE_PRIMARY_NEGATIVE;
+ } else {
+ // Top side
+ return SPLIT_TRANSLATE_SECONDARY_NEGATIVE;
+ }
+ } else if (splitPosition.mStagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ // We don't have a bottom option, so should be right
+ return SPLIT_TRANSLATE_PRIMARY_POSITIVE;
+ }
+ throw new IllegalStateException("Invalid split stage position: " +
+ splitPosition.mStagePosition);
}
@Override
@@ -205,28 +233,38 @@
}
@Override
- public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
- return VERTICAL;
+ public int getPrimaryTranslationDirectionFactor() {
+ return 1;
}
- @Override
- public int getTaskDismissDirectionFactor() {
+ public int getSecondaryTranslationDirectionFactor() {
return -1;
}
@Override
- public int getTaskDragDisplacementFactor(boolean isRtl) {
- // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
- return 1;
+ public int getSplitTranslationDirectionFactor(int stagePosition) {
+ if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ return -1;
+ } else {
+ return 1;
+ }
}
@Override
- public float getTaskMenuX(float x, View thumbnailView) {
- return x;
+ public int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ if (dp.isLandscape) {
+ return translationOffset;
+ }
+ return 0;
}
@Override
- public float getTaskMenuY(float y, View thumbnailView) {
+ public float getTaskMenuX(float x, View thumbnailView, int overScroll) {
+ return x + overScroll;
+ }
+
+ @Override
+ public float getTaskMenuY(float y, View thumbnailView, int overScroll) {
return y;
}
@@ -236,16 +274,80 @@
}
@Override
- public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
- return taskMenuLayout.getOrientation();
+ public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
+ LinearLayout taskMenuLayout, int dividerSpacing,
+ ShapeDrawable dividerDrawable) {
+ if (deviceProfile.isLandscape && !deviceProfile.isTablet) {
+ // Phone landscape
+ taskMenuLayout.setOrientation(LinearLayout.HORIZONTAL);
+ dividerDrawable.setIntrinsicWidth(dividerSpacing);
+ } else {
+ // Phone Portrait, LargeScreen Landscape/Portrait
+ taskMenuLayout.setOrientation(LinearLayout.VERTICAL);
+ dividerDrawable.setIntrinsicHeight(dividerSpacing);
+ }
+ taskMenuLayout.setDividerDrawable(dividerDrawable);
}
@Override
- public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp) {
- // no-op, defaults are fine
+ public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
+ LinearLayout viewGroup, DeviceProfile deviceProfile) {
+ if (deviceProfile.isLandscape && !deviceProfile.isTablet) {
+ // Phone landscape
+ viewGroup.setOrientation(LinearLayout.VERTICAL);
+ lp.width = 0;
+ lp.weight = 1;
+ Utilities.setStartMarginForView(viewGroup.findViewById(R.id.text), 0);
+ Utilities.setStartMarginForView(viewGroup.findViewById(R.id.icon), 0);
+ } else {
+ // Phone Portrait, LargeScreen Landscape/Portrait
+ viewGroup.setOrientation(LinearLayout.HORIZONTAL);
+ lp.width = LinearLayout.LayoutParams.MATCH_PARENT;
+ }
+
+ lp.height = LinearLayout.LayoutParams.WRAP_CONTENT;
}
@Override
+ public void setTaskMenuAroundTaskView(LinearLayout taskView, float margin) {
+ BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskView.getLayoutParams();
+ lp.topMargin += margin;
+ lp.leftMargin += margin;
+ }
+
+ @Override
+ public PointF getAdditionalInsetForTaskMenu(float margin) {
+ return new PointF(0, 0);
+ }
+
+ /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+
+ @Override
+ public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
+ return VERTICAL;
+ }
+
+ @Override
+ public int getUpDirection(boolean isRtl) {
+ // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+ return SingleAxisSwipeDetector.DIRECTION_POSITIVE;
+ }
+
+ @Override
+ public boolean isGoingUp(float displacement, boolean isRtl) {
+ // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+ return displacement < 0;
+ }
+
+ @Override
+ public int getTaskDragDisplacementFactor(boolean isRtl) {
+ // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+ return 1;
+ }
+
+ /* -------------------- */
+
+ @Override
public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
boolean layoutChild) {
final int childWidth = child.getMeasuredWidth();
@@ -257,4 +359,51 @@
}
return new ChildBounds(childWidth, childHeight, childRight, childTop);
}
+
+ @Override
+ public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+ return dp.heightPx - rect.bottom;
+ }
+
+ @Override
+ public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
+ List<SplitPositionOption> options = new ArrayList<>(1);
+ // Add both left and right options if we're in tablet mode
+ // TODO: Add in correct icons
+ if (dp.isTablet && dp.isLandscape) {
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_right,
+ STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_left,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ } else {
+ if (dp.isSeascape()) {
+ // Add left/right options
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_right,
+ STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
+ } else if (dp.isLandscape) {
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_left,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ } else {
+ // Only add top option
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_top,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ }
+ }
+ return options;
+ }
+
+ @Override
+ public FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
+ DeviceProfile dp) {
+ if (dp.isLandscape) { // or seascape
+ return primary;
+ } else {
+ return secondary;
+ }
+ }
}
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index d5ae2dc..91d44bd 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -16,23 +16,46 @@
package com.android.launcher3.touch;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
+
import android.content.res.Resources;
import android.graphics.PointF;
+import android.graphics.Rect;
import android.view.Surface;
import android.view.View;
+import android.widget.LinearLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.views.BaseDragLayer;
+
+import java.util.Collections;
+import java.util.List;
public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
@Override
- public int getTaskDismissDirectionFactor() {
+ public int getSecondaryTranslationDirectionFactor() {
return -1;
}
@Override
- public int getTaskDragDisplacementFactor(boolean isRtl) {
- return isRtl ? -1 : 1;
+ public int getSplitTranslationDirectionFactor(int stagePosition) {
+ if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ return translationOffset;
}
@Override
@@ -51,11 +74,6 @@
}
@Override
- public boolean isGoingUp(float displacement, boolean isRtl) {
- return isRtl ? displacement > 0 : displacement < 0;
- }
-
- @Override
public void adjustFloatingIconStartVelocity(PointF velocity) {
float oldX = velocity.x;
float oldY = velocity.y;
@@ -63,18 +81,61 @@
}
@Override
- public float getTaskMenuX(float x, View thumbnailView) {
+ public float getTaskMenuX(float x, View thumbnailView, int overScroll) {
return x;
}
@Override
- public float getTaskMenuY(float y, View thumbnailView) {
- return y + thumbnailView.getMeasuredHeight();
+ public float getTaskMenuY(float y, View thumbnailView, int overScroll) {
+ return y + thumbnailView.getMeasuredHeight() + overScroll;
}
@Override
- public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
- view.setTranslationX(0);
- view.setTranslationY(translation);
+ public void setTaskMenuAroundTaskView(LinearLayout taskView, float margin) {
+ BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskView.getLayoutParams();
+ lp.bottomMargin += margin;
}
+
+ @Override
+ public PointF getAdditionalInsetForTaskMenu(float margin) {
+ return new PointF(-margin, margin);
+ }
+
+ @Override
+ public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+ return dp.widthPx - rect.right;
+ }
+
+ @Override
+ public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
+ // Add "right" option which is actually the top
+ return Collections.singletonList(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_right,
+ STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
+ }
+
+ /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+
+ @Override
+ public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
+ return HORIZONTAL;
+ }
+
+ @Override
+ public int getUpDirection(boolean isRtl) {
+ return isRtl ? SingleAxisSwipeDetector.DIRECTION_POSITIVE
+ : SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+ }
+
+ @Override
+ public boolean isGoingUp(float displacement, boolean isRtl) {
+ return isRtl ? displacement > 0 : displacement < 0;
+ }
+
+ @Override
+ public int getTaskDragDisplacementFactor(boolean isRtl) {
+ return isRtl ? -1 : 1;
+ }
+
+ /* -------------------- */
}
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index 875eefb..f751b7d 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -17,7 +17,6 @@
import android.content.Context;
import android.graphics.PointF;
-import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
@@ -25,7 +24,6 @@
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
/**
* One dimensional scroll/drag/swipe gesture detector (either HORIZONTAL or VERTICAL).
@@ -60,6 +58,11 @@
return direction.x;
}
+ @NonNull
+ @Override
+ public String toString() {
+ return "VERTICAL";
+ }
};
public static final Direction HORIZONTAL = new Direction() {
@@ -86,6 +89,11 @@
return direction.y;
}
+ @NonNull
+ @Override
+ public String toString() {
+ return "HORIZONTAL";
+ }
};
private final Direction mDir;
@@ -94,6 +102,8 @@
private int mScrollDirections;
+ private float mTouchSlopMultiplier = 1f;
+
public SingleAxisSwipeDetector(@NonNull Context context, @NonNull Listener l,
@NonNull Direction dir) {
this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
@@ -105,11 +115,19 @@
super(config, isRtl);
mListener = l;
mDir = dir;
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector.ctor "
- + l.getClass().getSimpleName()
- + " @ " + android.util.Log.getStackTraceString(new Throwable()));
- }
+ }
+
+ /**
+ * Provides feasibility to adjust touch slop when visible window size changed. When visible
+ * bounds translate become smaller, multiply a larger multiplier could ensure the UX
+ * more consistent.
+ *
+ * @see #shouldScrollStart(PointF)
+ *
+ * @param touchSlopMultiplier the value to multiply original touch slop.
+ */
+ public void setTouchSlopMultiplier(float touchSlopMultiplier) {
+ mTouchSlopMultiplier = touchSlopMultiplier;
}
public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
@@ -117,10 +135,6 @@
mIgnoreSlopWhenSettling = ignoreSlop;
}
- public int getScrollDirections() {
- return mScrollDirections;
- }
-
/**
* Returns if the start drag was towards the positive direction or negative.
*
@@ -134,7 +148,7 @@
@Override
protected boolean shouldScrollStart(PointF displacement) {
// Reject cases where the angle or slop condition is not met.
- float minDisplacement = Math.max(mTouchSlop,
+ float minDisplacement = Math.max(mTouchSlop * mTouchSlopMultiplier,
Math.abs(mDir.extractOrthogonalDirection(displacement)));
if (Math.abs(mDir.extractDirection(displacement)) < minDisplacement) {
return false;
@@ -161,10 +175,6 @@
@Override
protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector "
- + mListener.getClass().getSimpleName());
- }
mListener.onDrag(mDir.extractDirection(displacement),
mDir.extractOrthogonalDirection(displacement), event);
}
diff --git a/src/com/android/launcher3/util/ActivityOptionsWrapper.java b/src/com/android/launcher3/util/ActivityOptionsWrapper.java
new file mode 100644
index 0000000..99cc1f7
--- /dev/null
+++ b/src/com/android/launcher3/util/ActivityOptionsWrapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+
+import android.app.ActivityOptions;
+import android.os.Bundle;
+
+/**
+ * A wrapper around {@link ActivityOptions} to allow custom functionality in launcher
+ */
+public class ActivityOptionsWrapper {
+
+ public final ActivityOptions options;
+ public final RunnableList onEndCallback;
+
+ public ActivityOptionsWrapper(ActivityOptions options, RunnableList onEndCallback) {
+ this.options = options;
+ this.onEndCallback = onEndCallback;
+ }
+
+ /**
+ * @see {@link ActivityOptions#toBundle()}
+ */
+ public Bundle toBundle() {
+ return options.toBundle();
+ }
+}
diff --git a/src/com/android/launcher3/util/ActivityTracker.java b/src/com/android/launcher3/util/ActivityTracker.java
index 59266b4..7af1a13 100644
--- a/src/com/android/launcher3/util/ActivityTracker.java
+++ b/src/com/android/launcher3/util/ActivityTracker.java
@@ -15,15 +15,14 @@
*/
package com.android.launcher3.util;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.IBinder;
-
import androidx.annotation.Nullable;
import com.android.launcher3.BaseActivity;
import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* Helper class to statically track activity creation
@@ -32,8 +31,7 @@
public final class ActivityTracker<T extends BaseActivity> {
private WeakReference<T> mCurrentActivity = new WeakReference<>(null);
-
- private static final String EXTRA_SCHEDULER_CALLBACK = "launcher.scheduler_callback";
+ private CopyOnWriteArrayList<SchedulerCallback<T>> mCallbacks = new CopyOnWriteArrayList<>();
@Nullable
public <R extends T> R getCreatedActivity() {
@@ -47,44 +45,50 @@
}
/**
- * Call {@link SchedulerCallback#init(BaseActivity, boolean)} when the activity is ready.
- * If the activity is already created, this is called immediately, otherwise we add the
- * callback as an extra on the intent, and will call init() when we get handleIntent().
+ * Call {@link SchedulerCallback#init(BaseActivity, boolean)} when the
+ * activity is ready. If the activity is already created, this is called immediately.
+ *
+ * The tracker maintains a strong ref to the callback, so it is up to the caller to return
+ * {@code false} in the callback OR to unregister the callback explicitly.
+ *
* @param callback The callback to call init() on when the activity is ready.
- * @param intent The intent that will be used to initialize the activity, if the activity
- * doesn't already exist. We add the callback as an extra on this intent.
*/
- public void runCallbackWhenActivityExists(SchedulerCallback<T> callback, Intent intent) {
+ public void registerCallback(SchedulerCallback<T> callback) {
T activity = mCurrentActivity.get();
+ mCallbacks.add(callback);
if (activity != null) {
- callback.init(activity, activity.isStarted());
- } else {
- callback.addToIntent(intent);
+ if (!callback.init(activity, activity.isStarted())) {
+ unregisterCallback(callback);
+ }
}
}
+ /**
+ * Unregisters a registered callback.
+ */
+ public void unregisterCallback(SchedulerCallback<T> callback) {
+ mCallbacks.remove(callback);
+ }
+
public boolean handleCreate(T activity) {
mCurrentActivity = new WeakReference<>(activity);
- return handleIntent(activity, activity.getIntent(), false);
+ return handleIntent(activity, false /* alreadyOnHome */);
}
- public boolean handleNewIntent(T activity, Intent intent) {
- return handleIntent(activity, intent, activity.isStarted());
+ public boolean handleNewIntent(T activity) {
+ return handleIntent(activity, activity.isStarted());
}
- private boolean handleIntent(T activity, Intent intent, boolean alreadyOnHome) {
- if (intent != null && intent.getExtras() != null) {
- IBinder stateBinder = intent.getExtras().getBinder(EXTRA_SCHEDULER_CALLBACK);
- if (stateBinder instanceof ObjectWrapper) {
- SchedulerCallback<T> handler =
- ((ObjectWrapper<SchedulerCallback>) stateBinder).get();
- if (!handler.init(activity, alreadyOnHome)) {
- intent.getExtras().remove(EXTRA_SCHEDULER_CALLBACK);
- }
- return true;
+ private boolean handleIntent(T activity, boolean alreadyOnHome) {
+ boolean handled = false;
+ for (SchedulerCallback<T> cb : mCallbacks) {
+ if (!cb.init(activity, alreadyOnHome)) {
+ // Callback doesn't want any more updates
+ unregisterCallback(cb);
}
+ handled = true;
}
- return false;
+ return handled;
}
public interface SchedulerCallback<T extends BaseActivity> {
@@ -95,17 +99,5 @@
* @return Whether to continue receiving callbacks (i.e. if the activity is recreated).
*/
boolean init(T activity, boolean alreadyOnHome);
-
- /**
- * Adds this callback as an extra on the intent, so we can retrieve it in handleIntent() and
- * call {@link #init}. The intent should be used to start the activity after calling this
- * method in order for us to get handleIntent().
- */
- default Intent addToIntent(Intent intent) {
- Bundle extras = new Bundle();
- extras.putBinder(EXTRA_SCHEDULER_CALLBACK, ObjectWrapper.wrap(this));
- intent.putExtras(extras);
- return intent;
- }
}
}
diff --git a/src/com/android/launcher3/util/BgObjectWithLooper.java b/src/com/android/launcher3/util/BgObjectWithLooper.java
new file mode 100644
index 0000000..1483c43
--- /dev/null
+++ b/src/com/android/launcher3/util/BgObjectWithLooper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.os.Looper;
+
+import androidx.annotation.WorkerThread;
+
+/**
+ * Utility class to define an object which does most of it's processing on a
+ * dedicated background thread.
+ */
+public abstract class BgObjectWithLooper {
+
+ /**
+ * Start initialization of the object
+ */
+ public final void initializeInBackground(String threadName) {
+ new Thread(this::runOnThread, threadName).start();
+ }
+
+ private void runOnThread() {
+ Looper.prepare();
+ onInitialized(Looper.myLooper());
+ Looper.loop();
+ }
+
+ /**
+ * Called on the background thread to handle initialization
+ */
+ @WorkerThread
+ protected abstract void onInitialized(Looper looper);
+}
diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java
deleted file mode 100644
index 0f81520..0000000
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.android.launcher3.util;
-
-/**
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.graphics.Point;
-import android.util.Log;
-
-import java.util.function.Consumer;
-
-/**
- * {@link BroadcastReceiver} which watches configuration changes and
- * notifies the callback in case changes which affect the device profile occur.
- */
-public class ConfigMonitor extends BroadcastReceiver implements
- DefaultDisplay.DisplayInfoChangeListener {
-
- private static final String TAG = "ConfigMonitor";
-
- private final Point mTmpPoint1 = new Point();
- private final Point mTmpPoint2 = new Point();
-
- private final Context mContext;
- private final float mFontScale;
- private final int mDensity;
-
- private final int mDisplayId;
- private final Point mRealSize;
- private final Point mSmallestSize, mLargestSize;
-
- private Consumer<Context> mCallback;
-
- public ConfigMonitor(Context context, Consumer<Context> callback) {
- mContext = context;
-
- Configuration config = context.getResources().getConfiguration();
- mFontScale = config.fontScale;
- mDensity = config.densityDpi;
-
- DefaultDisplay display = DefaultDisplay.INSTANCE.get(context);
- display.addChangeListener(this);
- DefaultDisplay.Info displayInfo = display.getInfo();
- mDisplayId = displayInfo.id;
-
- mRealSize = new Point(displayInfo.realSize);
- mSmallestSize = new Point(displayInfo.smallestSize);
- mLargestSize = new Point(displayInfo.largestSize);
-
- mCallback = callback;
-
- // Listen for configuration change
- mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- Configuration config = context.getResources().getConfiguration();
- if (mFontScale != config.fontScale || mDensity != config.densityDpi) {
- Log.d(TAG, "Configuration changed.");
- notifyChange();
- }
- }
-
- @Override
- public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
- if (info.id != mDisplayId) {
- return;
- }
- mTmpPoint1.set(info.realSize.x, info.realSize.y);
- if (!mRealSize.equals(mTmpPoint1) && !mRealSize.equals(mTmpPoint1.y, mTmpPoint1.x)) {
- Log.d(TAG, String.format("Display size changed from %s to %s", mRealSize, mTmpPoint1));
- notifyChange();
- return;
- }
-
- mTmpPoint1.set(info.smallestSize.x, info.smallestSize.y);
- mTmpPoint2.set(info.largestSize.x, info.largestSize.y);
- if (!mSmallestSize.equals(mTmpPoint1) || !mLargestSize.equals(mTmpPoint2)) {
- Log.d(TAG, String.format("Available size changed from [%s, %s] to [%s, %s]",
- mSmallestSize, mLargestSize, mTmpPoint1, mTmpPoint2));
- notifyChange();
- }
- }
-
- private synchronized void notifyChange() {
- if (mCallback != null) {
- Consumer<Context> callback = mCallback;
- mCallback = null;
- MAIN_EXECUTOR.execute(() -> callback.accept(mContext));
- }
- }
-
- public void unregister() {
- try {
- mContext.unregisterReceiver(this);
- DefaultDisplay display = DefaultDisplay.INSTANCE.get(mContext);
- display.removeChangeListener(this);
- } catch (Exception e) {
- Log.e(TAG, "Failed to unregister config monitor", e);
- }
- }
-}
diff --git a/src/com/android/launcher3/util/ContentWriter.java b/src/com/android/launcher3/util/ContentWriter.java
index 30c9ff9..ee64e98 100644
--- a/src/com/android/launcher3/util/ContentWriter.java
+++ b/src/com/android/launcher3/util/ContentWriter.java
@@ -113,14 +113,26 @@
public static final class CommitParams {
- final Uri mUri = LauncherSettings.Favorites.CONTENT_URI;
- String mWhere;
- String[] mSelectionArgs;
+ final Uri mUri;
+ final String mWhere;
+ final String[] mSelectionArgs;
public CommitParams(String where, String[] selectionArgs) {
+ this(LauncherSettings.Favorites.CONTENT_URI, where, selectionArgs);
+ }
+
+ private CommitParams(Uri uri, String where, String[] selectionArgs) {
+ mUri = uri;
mWhere = where;
mSelectionArgs = selectionArgs;
}
+ /**
+ * Creates commit params for backup table.
+ */
+ public static CommitParams backupCommitParams(String where, String[] selectionArgs) {
+ return new CommitParams(
+ LauncherSettings.Favorites.BACKUP_CONTENT_URI, where, selectionArgs);
+ }
}
}
diff --git a/src/com/android/launcher3/util/DefaultDisplay.java b/src/com/android/launcher3/util/DefaultDisplay.java
deleted file mode 100644
index 35788a5..0000000
--- a/src/com/android/launcher3/util/DefaultDisplay.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.util;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
-import android.content.Context;
-import android.graphics.Point;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManager.DisplayListener;
-import android.os.Handler;
-import android.os.Message;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Display;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.util.ArrayList;
-
-/**
- * Utility class to cache properties of default display to avoid a system RPC on every call.
- */
-public class DefaultDisplay implements DisplayListener {
-
- public static final MainThreadInitializedObject<DefaultDisplay> INSTANCE =
- new MainThreadInitializedObject<>(DefaultDisplay::new);
-
- private static final String TAG = "DefaultDisplay";
-
- public static final int CHANGE_SIZE = 1 << 0;
- public static final int CHANGE_ROTATION = 1 << 1;
- public static final int CHANGE_FRAME_DELAY = 1 << 2;
-
- public static final int CHANGE_ALL = CHANGE_SIZE | CHANGE_ROTATION | CHANGE_FRAME_DELAY;
-
- private final Context mDisplayContext;
- private final int mId;
- private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
- private final Handler mChangeHandler;
- private Info mInfo;
-
- private DefaultDisplay(Context context) {
- DisplayManager dm = context.getSystemService(DisplayManager.class);
- // Use application context to create display context so that it can have its own Resources.
- mDisplayContext = context.getApplicationContext().createDisplayContext(
- dm.getDisplay(DEFAULT_DISPLAY));
- // Note that the Display object must be obtained from DisplayManager which is associated to
- // the display context, so the Display is isolated from Activity and Application to provide
- // the actual state of device that excludes the additional adjustment and override.
- mInfo = new Info(mDisplayContext);
- mId = mInfo.id;
- mChangeHandler = new Handler(this::onChange);
-
- dm.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
- }
-
- @Override
- public final void onDisplayAdded(int displayId) { }
-
- @Override
- public final void onDisplayRemoved(int displayId) { }
-
- @Override
- public final void onDisplayChanged(int displayId) {
- if (displayId != mId) {
- return;
- }
-
- Info oldInfo = mInfo;
- Info info = new Info(mDisplayContext);
-
- int change = 0;
- if (info.hasDifferentSize(oldInfo)) {
- change |= CHANGE_SIZE;
- }
- if (oldInfo.rotation != info.rotation) {
- change |= CHANGE_ROTATION;
- }
- if (info.singleFrameMs != oldInfo.singleFrameMs) {
- change |= CHANGE_FRAME_DELAY;
- }
-
- if (change != 0) {
- mInfo = info;
- mChangeHandler.sendEmptyMessage(change);
- }
- }
-
- public static int getSingleFrameMs(Context context) {
- return INSTANCE.get(context).getInfo().singleFrameMs;
- }
-
- public Info getInfo() {
- return mInfo;
- }
-
- public void addChangeListener(DisplayInfoChangeListener listener) {
- mListeners.add(listener);
- }
-
- public void removeChangeListener(DisplayInfoChangeListener listener) {
- mListeners.remove(listener);
- }
-
- private boolean onChange(Message msg) {
- for (int i = mListeners.size() - 1; i >= 0; i--) {
- mListeners.get(i).onDisplayInfoChanged(mInfo, msg.what);
- }
- return true;
- }
-
- public static class Info {
-
- public final int id;
- public final int rotation;
- public final int singleFrameMs;
-
- public final Point realSize;
- public final Point smallestSize;
- public final Point largestSize;
-
- public final DisplayMetrics metrics;
-
- @VisibleForTesting
- public Info(int id, int rotation, int singleFrameMs, Point realSize, Point smallestSize,
- Point largestSize, DisplayMetrics metrics) {
- this.id = id;
- this.rotation = rotation;
- this.singleFrameMs = singleFrameMs;
- this.realSize = realSize;
- this.smallestSize = smallestSize;
- this.largestSize = largestSize;
- this.metrics = metrics;
- }
-
- private Info(Context context) {
- this(context, context.getSystemService(DisplayManager.class)
- .getDisplay(DEFAULT_DISPLAY));
- }
-
- public Info(Context context, Display display) {
- id = display.getDisplayId();
- rotation = display.getRotation();
-
- float refreshRate = display.getRefreshRate();
- singleFrameMs = refreshRate > 0 ? (int) (1000 / refreshRate) : 16;
-
- realSize = new Point();
- smallestSize = new Point();
- largestSize = new Point();
- display.getRealSize(realSize);
- display.getCurrentSizeRange(smallestSize, largestSize);
-
- metrics = context.getResources().getDisplayMetrics();
- }
-
- private boolean hasDifferentSize(Info info) {
- if (!realSize.equals(info.realSize)
- && !realSize.equals(info.realSize.y, info.realSize.x)) {
- Log.d(TAG, String.format("Display size changed from %s to %s",
- info.realSize, realSize));
- return true;
- }
-
- if (!smallestSize.equals(info.smallestSize) || !largestSize.equals(info.largestSize)) {
- Log.d(TAG, String.format("Available size changed from [%s, %s] to [%s, %s]",
- smallestSize, largestSize, info.smallestSize, info.largestSize));
- return true;
- }
-
- return false;
- }
- }
-
- /**
- * Interface for listening for display changes
- */
- public interface DisplayInfoChangeListener {
-
- void onDisplayInfoChanged(Info info, int flags);
- }
-}
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
new file mode 100644
index 0000000..e2c0a32
--- /dev/null
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+
+import static com.android.launcher3.Utilities.dpiFromPx;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.WindowManagerCompat.MIN_TABLET_WIDTH;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.ComponentCallbacks;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Build;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowMetrics;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.uioverrides.ApiWrapper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility class to cache properties of default display to avoid a system RPC on every call.
+ */
+@SuppressLint("NewApi")
+public class DisplayController implements DisplayListener, ComponentCallbacks {
+
+ private static final String TAG = "DisplayController";
+
+ public static final MainThreadInitializedObject<DisplayController> INSTANCE =
+ new MainThreadInitializedObject<>(DisplayController::new);
+
+ public static final int CHANGE_ACTIVE_SCREEN = 1 << 0;
+ public static final int CHANGE_ROTATION = 1 << 1;
+ public static final int CHANGE_FRAME_DELAY = 1 << 2;
+ public static final int CHANGE_DENSITY = 1 << 3;
+ public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 4;
+
+ public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
+ | CHANGE_FRAME_DELAY | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS;
+
+ private final Context mContext;
+ private final DisplayManager mDM;
+
+ // Null for SDK < S
+ private final Context mWindowContext;
+
+ private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
+ private Info mInfo;
+
+ private DisplayController(Context context) {
+ mContext = context;
+ mDM = context.getSystemService(DisplayManager.class);
+
+ Display display = mDM.getDisplay(DEFAULT_DISPLAY);
+ if (Utilities.ATLEAST_S) {
+ mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
+ mWindowContext.registerComponentCallbacks(this);
+ } else {
+ mWindowContext = null;
+ SimpleBroadcastReceiver configChangeReceiver =
+ new SimpleBroadcastReceiver(this::onConfigChanged);
+ mContext.registerReceiver(configChangeReceiver,
+ new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
+ }
+
+ // Create a single holder for all internal displays. External display holders created
+ // lazily.
+ Set<PortraitSize> extraInternalDisplays = new ArraySet<>();
+ for (Display d : mDM.getDisplays()) {
+ if (ApiWrapper.isInternalDisplay(display) && d.getDisplayId() != DEFAULT_DISPLAY) {
+ Point size = new Point();
+ d.getRealSize(size);
+ extraInternalDisplays.add(new PortraitSize(size.x, size.y));
+ }
+ }
+ mInfo = new Info(getDisplayInfoContext(display), display, extraInternalDisplays);
+ mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
+ }
+
+ @Override
+ public final void onDisplayAdded(int displayId) { }
+
+ @Override
+ public final void onDisplayRemoved(int displayId) { }
+
+ @WorkerThread
+ @Override
+ public final void onDisplayChanged(int displayId) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ Display display = mDM.getDisplay(DEFAULT_DISPLAY);
+ if (display == null) {
+ return;
+ }
+ if (Utilities.ATLEAST_S) {
+ // Only check for refresh rate. Everything else comes from component callbacks
+ if (getSingleFrameMs(display) == mInfo.singleFrameMs) {
+ return;
+ }
+ }
+ handleInfoChange(display);
+ }
+
+ public static int getSingleFrameMs(Context context) {
+ return INSTANCE.get(context).getInfo().singleFrameMs;
+ }
+
+ /**
+ * Interface for listening for display changes
+ */
+ public interface DisplayInfoChangeListener {
+
+ /**
+ * Invoked when display info has changed.
+ * @param context updated context associated with the display.
+ * @param info updated display information.
+ * @param flags bitmask indicating type of change.
+ */
+ void onDisplayInfoChanged(Context context, Info info, int flags);
+ }
+
+ /**
+ * Only used for pre-S
+ */
+ private void onConfigChanged(Intent intent) {
+ Configuration config = mContext.getResources().getConfiguration();
+ if (mInfo.fontScale != config.fontScale || mInfo.densityDpi != config.densityDpi) {
+ Log.d(TAG, "Configuration changed, notifying listeners");
+ Display display = mDM.getDisplay(DEFAULT_DISPLAY);
+ if (display != null) {
+ handleInfoChange(display);
+ }
+ }
+ }
+
+ @UiThread
+ @Override
+ @TargetApi(Build.VERSION_CODES.S)
+ public final void onConfigurationChanged(Configuration config) {
+ Display display = mWindowContext.getDisplay();
+ if (config.densityDpi != mInfo.densityDpi
+ || config.fontScale != mInfo.fontScale
+ || display.getRotation() != mInfo.rotation
+ || !mInfo.mScreenSizeDp.equals(
+ new PortraitSize(config.screenHeightDp, config.screenWidthDp))) {
+ handleInfoChange(display);
+ }
+ }
+
+ @Override
+ public final void onLowMemory() { }
+
+ public void addChangeListener(DisplayInfoChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeChangeListener(DisplayInfoChangeListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public Info getInfo() {
+ return mInfo;
+ }
+
+ private Context getDisplayInfoContext(Display display) {
+ return Utilities.ATLEAST_S ? mWindowContext : mContext.createDisplayContext(display);
+ }
+
+ @AnyThread
+ private void handleInfoChange(Display display) {
+ Info oldInfo = mInfo;
+ Set<PortraitSize> extraDisplaysSizes = oldInfo.mAllSizes.size() > 1
+ ? oldInfo.mAllSizes : Collections.emptySet();
+
+ Context displayContext = getDisplayInfoContext(display);
+ Info newInfo = new Info(displayContext, display, extraDisplaysSizes);
+ int change = 0;
+ if (!newInfo.mScreenSizeDp.equals(oldInfo.mScreenSizeDp)) {
+ change |= CHANGE_ACTIVE_SCREEN;
+ }
+ if (newInfo.rotation != oldInfo.rotation) {
+ change |= CHANGE_ROTATION;
+ }
+ if (newInfo.singleFrameMs != oldInfo.singleFrameMs) {
+ change |= CHANGE_FRAME_DELAY;
+ }
+ if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale) {
+ change |= CHANGE_DENSITY;
+ }
+ if (!newInfo.supportedBounds.equals(oldInfo.supportedBounds)) {
+ change |= CHANGE_SUPPORTED_BOUNDS;
+ }
+
+ if (change != 0) {
+ mInfo = newInfo;
+ final int flags = change;
+ MAIN_EXECUTOR.execute(() -> notifyChange(displayContext, flags));
+ }
+ }
+
+ private void notifyChange(Context context, int flags) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags);
+ }
+ }
+
+ public static class Info {
+
+ public final int id;
+ public final int singleFrameMs;
+
+ // Configuration properties
+ public final int rotation;
+ public final float fontScale;
+ public final int densityDpi;
+
+ private final PortraitSize mScreenSizeDp;
+ private final Set<PortraitSize> mAllSizes;
+
+ public final Point currentSize;
+
+ public final Set<WindowBounds> supportedBounds = new ArraySet<>();
+
+ public Info(Context context, Display display) {
+ this(context, display, Collections.emptySet());
+ }
+
+ private Info(Context context, Display display, Set<PortraitSize> extraDisplaysSizes) {
+ id = display.getDisplayId();
+
+ rotation = display.getRotation();
+
+ Configuration config = context.getResources().getConfiguration();
+ fontScale = config.fontScale;
+ densityDpi = config.densityDpi;
+ mScreenSizeDp = new PortraitSize(config.screenHeightDp, config.screenWidthDp);
+
+ singleFrameMs = getSingleFrameMs(display);
+ currentSize = new Point();
+
+ display.getRealSize(currentSize);
+
+ if (extraDisplaysSizes.isEmpty() || !Utilities.ATLEAST_S) {
+ Point smallestSize = new Point();
+ Point largestSize = new Point();
+ display.getCurrentSizeRange(smallestSize, largestSize);
+
+ int portraitWidth = Math.min(currentSize.x, currentSize.y);
+ int portraitHeight = Math.max(currentSize.x, currentSize.y);
+
+ supportedBounds.add(new WindowBounds(portraitWidth, portraitHeight,
+ smallestSize.x, largestSize.y));
+ supportedBounds.add(new WindowBounds(portraitHeight, portraitWidth,
+ largestSize.x, smallestSize.y));
+ mAllSizes = Collections.singleton(new PortraitSize(currentSize.x, currentSize.y));
+ } else {
+ mAllSizes = new ArraySet<>(extraDisplaysSizes);
+ mAllSizes.add(new PortraitSize(currentSize.x, currentSize.y));
+ Set<WindowMetrics> metrics = WindowManagerCompat.getDisplayProfiles(
+ context, mAllSizes, densityDpi,
+ ApiWrapper.TASKBAR_DRAWN_IN_PROCESS);
+ metrics.forEach(wm -> supportedBounds.add(WindowBounds.fromWindowMetrics(wm)));
+ }
+ }
+
+ /**
+ * Returns true if the bounds represent a tablet
+ */
+ public boolean isTablet(WindowBounds bounds) {
+ return dpiFromPx(Math.min(bounds.bounds.width(), bounds.bounds.height()),
+ densityDpi) >= MIN_TABLET_WIDTH;
+ }
+ }
+
+ /**
+ * Utility class to hold a size information in an orientation independent way
+ */
+ public static class PortraitSize {
+ public final int width, height;
+
+ public PortraitSize(int w, int h) {
+ width = Math.min(w, h);
+ height = Math.max(w, h);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PortraitSize that = (PortraitSize) o;
+ return width == that.width && height == that.height;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(width, height);
+ }
+ }
+
+ private static int getSingleFrameMs(Display display) {
+ float refreshRate = display.getRefreshRate();
+ return refreshRate > 0 ? (int) (1000 / refreshRate) : 16;
+ }
+}
diff --git a/src/com/android/launcher3/util/EdgeEffectCompat.java b/src/com/android/launcher3/util/EdgeEffectCompat.java
new file mode 100644
index 0000000..491582b
--- /dev/null
+++ b/src/com/android/launcher3/util/EdgeEffectCompat.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.Context;
+import android.widget.EdgeEffect;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Extension of {@link EdgeEffect} to allow backwards compatibility
+ */
+public class EdgeEffectCompat extends EdgeEffect {
+
+ public EdgeEffectCompat(Context context) {
+ super(context);
+ }
+
+ @Override
+ public float getDistance() {
+ return Utilities.ATLEAST_S ? super.getDistance() : 0;
+ }
+
+ @Override
+ public float onPullDistance(float deltaDistance, float displacement) {
+ if (Utilities.ATLEAST_S) {
+ return super.onPullDistance(deltaDistance, displacement);
+ } else {
+ onPull(deltaDistance, displacement);
+ return deltaDistance;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index 0a32734..6329540 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -20,26 +20,25 @@
import android.os.Process;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Various different executors used in Launcher
*/
public class Executors {
- // These values are same as that in {@link AsyncTask}.
- private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
- private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
- private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+ private static final int POOL_SIZE =
+ Math.max(Runtime.getRuntime().availableProcessors(), 2);
private static final int KEEP_ALIVE = 1;
/**
* An {@link ThreadPoolExecutor} to be used with async task with no limit on the queue size.
*/
public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
- CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
- TimeUnit.SECONDS, new LinkedBlockingQueue<>());
+ POOL_SIZE, POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
/**
* Returns the executor for running tasks on the main thread.
@@ -51,7 +50,8 @@
* A background executor for using time sensitive actions where user is waiting for response.
*/
public static final LooperExecutor UI_HELPER_EXECUTOR =
- new LooperExecutor(createAndStartNewForegroundLooper("UiThreadHelper"));
+ new LooperExecutor(
+ createAndStartNewLooper("UiThreadHelper", Process.THREAD_PRIORITY_FOREGROUND));
/**
* Utility method to get a started handler thread statically
@@ -70,17 +70,33 @@
}
/**
- * Similar to {@link #createAndStartNewLooper(String)}, but starts the thread with
- * foreground priority.
- * Think before using
- */
- public static Looper createAndStartNewForegroundLooper(String name) {
- return createAndStartNewLooper(name, Process.THREAD_PRIORITY_FOREGROUND);
- }
-
- /**
* Executor used for running Launcher model related tasks (eg loading icons or updated db)
*/
public static final LooperExecutor MODEL_EXECUTOR =
new LooperExecutor(createAndStartNewLooper("launcher-loader"));
+
+ /**
+ * A simple ThreadFactory to set the thread name and priority when used with executors.
+ */
+ public static class SimpleThreadFactory implements ThreadFactory {
+
+ private final int mPriority;
+ private final String mNamePrefix;
+
+ private final AtomicInteger mCount = new AtomicInteger(0);
+
+ public SimpleThreadFactory(String namePrefix, int priority) {
+ mNamePrefix = namePrefix;
+ mPriority = priority;
+ }
+
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread t = new Thread(() -> {
+ Process.setThreadPriority(mPriority);
+ runnable.run();
+ }, mNamePrefix + mCount.incrementAndGet());
+ return t;
+ }
+ }
}
diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java
index 9d0ad22..c9aa51c 100644
--- a/src/com/android/launcher3/util/FlingAnimation.java
+++ b/src/com/android/launcher3/util/FlingAnimation.java
@@ -70,9 +70,6 @@
mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY);
- // Don't highlight the icon as it's animating
- mDragObject.dragView.setColor(0);
-
final int duration = mDuration + DRAG_END_DELAY;
final long startTime = AnimationUtils.currentAnimationTimeMillis();
diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java
deleted file mode 100644
index 4f4cccd..0000000
--- a/src/com/android/launcher3/util/FocusLogic.java
+++ /dev/null
@@ -1,553 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ShortcutAndWidgetContainer;
-import com.android.launcher3.config.FeatureFlags;
-
-import java.util.Arrays;
-
-/**
- * Calculates the next item that a {@link KeyEvent} should change the focus to.
- *<p>
- * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates.
- * Currently supports:
- * <ul>
- * <li> full matrix of cells that are 1x1
- * <li> sparse matrix of cells that are 1x1
- * [ 1][ ][ 2][ ]
- * [ ][ ][ 3][ ]
- * [ ][ 4][ ][ ]
- * [ ][ 5][ 6][ 7]
- * </ul>
- * *<p>
- * For testing, one can use a BT keyboard, or use following adb command.
- * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT
- */
-public class FocusLogic {
-
- private static final String TAG = "FocusLogic";
- private static final boolean DEBUG = false;
-
- /** Item and page index related constant used by {@link #handleKeyEvent}. */
- public static final int NOOP = -1;
-
- public static final int PREVIOUS_PAGE_RIGHT_COLUMN = -2;
- public static final int PREVIOUS_PAGE_FIRST_ITEM = -3;
- public static final int PREVIOUS_PAGE_LAST_ITEM = -4;
- public static final int PREVIOUS_PAGE_LEFT_COLUMN = -5;
-
- public static final int CURRENT_PAGE_FIRST_ITEM = -6;
- public static final int CURRENT_PAGE_LAST_ITEM = -7;
-
- public static final int NEXT_PAGE_FIRST_ITEM = -8;
- public static final int NEXT_PAGE_LEFT_COLUMN = -9;
- public static final int NEXT_PAGE_RIGHT_COLUMN = -10;
-
- public static final int ALL_APPS_COLUMN = -11;
-
- // Matrix related constant.
- public static final int EMPTY = -1;
- public static final int PIVOT = 100;
-
- /**
- * Returns true only if this utility class handles the key code.
- */
- public static boolean shouldConsume(int keyCode) {
- return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||
- keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
- keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END ||
- keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN);
- }
-
- public static int handleKeyEvent(int keyCode, int [][] map, int iconIdx, int pageIndex,
- int pageCount, boolean isRtl) {
-
- int cntX = map == null ? -1 : map.length;
- int cntY = map == null ? -1 : map[0].length;
-
- if (DEBUG) {
- Log.v(TAG, String.format(
- "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d",
- cntX, cntY, iconIdx, pageIndex, pageCount));
- }
-
- int newIndex = NOOP;
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_LEFT:
- newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/, isRtl);
- if (!isRtl && newIndex == NOOP && pageIndex > 0) {
- newIndex = PREVIOUS_PAGE_RIGHT_COLUMN;
- } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
- newIndex = NEXT_PAGE_RIGHT_COLUMN;
- }
- break;
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/, isRtl);
- if (!isRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
- newIndex = NEXT_PAGE_LEFT_COLUMN;
- } else if (isRtl && newIndex == NOOP && pageIndex > 0) {
- newIndex = PREVIOUS_PAGE_LEFT_COLUMN;
- }
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1 /*increment*/);
- break;
- case KeyEvent.KEYCODE_DPAD_UP:
- newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1 /*increment*/);
- break;
- case KeyEvent.KEYCODE_MOVE_HOME:
- newIndex = handleMoveHome();
- break;
- case KeyEvent.KEYCODE_MOVE_END:
- newIndex = handleMoveEnd();
- break;
- case KeyEvent.KEYCODE_PAGE_DOWN:
- newIndex = handlePageDown(pageIndex, pageCount);
- break;
- case KeyEvent.KEYCODE_PAGE_UP:
- newIndex = handlePageUp(pageIndex);
- break;
- default:
- break;
- }
-
- if (DEBUG) {
- Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]",
- iconIdx, getStringIndex(newIndex)));
- }
- return newIndex;
- }
-
- /**
- * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}.
- *
- * @param m number of columns in the matrix
- * @param n number of rows in the matrix
- */
- // TODO: get rid of dynamic matrix creation.
- private static int[][] createFullMatrix(int m, int n) {
- int[][] matrix = new int [m][n];
-
- for (int i=0; i < m;i++) {
- Arrays.fill(matrix[i], EMPTY);
- }
- return matrix;
- }
-
- /**
- * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the
- * index of the child view.
- */
- // TODO: get rid of the dynamic matrix creation
- public static int[][] createSparseMatrix(CellLayout layout) {
- ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets();
- final int m = layout.getCountX();
- final int n = layout.getCountY();
- final boolean invert = parent.invertLayoutHorizontally();
-
- int[][] matrix = createFullMatrix(m, n);
-
- // Iterate thru the children.
- for (int i = 0; i < parent.getChildCount(); i++ ) {
- View cell = parent.getChildAt(i);
- if (!cell.isFocusable()) {
- continue;
- }
- int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX;
- int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY;
- int x = invert ? (m - cx - 1) : cx;
- if (x < m && cy < n) { // check if view fits into matrix, else skip
- matrix[x][cy] = i;
- }
- }
- if (DEBUG) {
- printMatrix(matrix);
- }
- return matrix;
- }
-
- /**
- * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout.
- * The size of the returning matrix is [icon column count x (icon + hotseat row count)]
- * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)]
- */
- // TODO: get rid of the dynamic matrix creation
- public static int[][] createSparseMatrixWithHotseat(
- CellLayout iconLayout, CellLayout hotseatLayout, DeviceProfile dp) {
-
- ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
- ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets();
-
- boolean isHotseatHorizontal = !dp.isVerticalBarLayout();
-
- int m, n;
- if (isHotseatHorizontal) {
- m = hotseatLayout.getCountX();
- n = iconLayout.getCountY() + hotseatLayout.getCountY();
- } else {
- m = iconLayout.getCountX() + hotseatLayout.getCountX();
- n = hotseatLayout.getCountY();
- }
- int[][] matrix = createFullMatrix(m, n);
- // Iterate through the children of the workspace.
- for (int i = 0; i < iconParent.getChildCount(); i++) {
- View cell = iconParent.getChildAt(i);
- if (!cell.isFocusable()) {
- continue;
- }
- int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX;
- int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY;
- matrix[cx][cy] = i;
- }
-
- // Iterate thru the children of the hotseat.
- for (int i = hotseatParent.getChildCount() - 1; i >= 0; i--) {
- if (isHotseatHorizontal) {
- int cx = ((CellLayout.LayoutParams)
- hotseatParent.getChildAt(i).getLayoutParams()).cellX;
- matrix[cx][iconLayout.getCountY()] = iconParent.getChildCount() + i;
- } else {
- int cy = ((CellLayout.LayoutParams)
- hotseatParent.getChildAt(i).getLayoutParams()).cellY;
- matrix[iconLayout.getCountX()][cy] = iconParent.getChildCount() + i;
- }
- }
- if (DEBUG) {
- printMatrix(matrix);
- }
- return matrix;
- }
-
- /**
- * Creates a sparse matrix that merges the icon of previous/next page and last column of
- * current page. When left key is triggered on the leftmost column, sparse matrix is created
- * that combines previous page matrix and an extra column on the right. Likewise, when right
- * key is triggered on the rightmost column, sparse matrix is created that combines this column
- * on the 0th column and the next page matrix.
- *
- * @param pivotX x coordinate of the focused item in the current page
- * @param pivotY y coordinate of the focused item in the current page
- */
- // TODO: get rid of the dynamic matrix creation
- public static int[][] createSparseMatrixWithPivotColumn(CellLayout iconLayout,
- int pivotX, int pivotY) {
-
- ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
-
- int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY());
-
- // Iterate thru the children of the top parent.
- for (int i = 0; i < iconParent.getChildCount(); i++) {
- View cell = iconParent.getChildAt(i);
- if (!cell.isFocusable()) {
- continue;
- }
- int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX;
- int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY;
- if (pivotX < 0) {
- matrix[cx - pivotX][cy] = i;
- } else {
- matrix[cx][cy] = i;
- }
- }
-
- if (pivotX < 0) {
- matrix[0][pivotY] = PIVOT;
- } else {
- matrix[pivotX][pivotY] = PIVOT;
- }
- if (DEBUG) {
- printMatrix(matrix);
- }
- return matrix;
- }
-
- //
- // key event handling methods.
- //
-
- /**
- * Calculates icon that has is closest to the horizontal axis in reference to the cur icon.
- *
- * Example of the check order for KEYCODE_DPAD_RIGHT:
- * [ ][ ][13][14][15]
- * [ ][ 6][ 8][10][12]
- * [ X][ 1][ 2][ 3][ 4]
- * [ ][ 5][ 7][ 9][11]
- */
- // TODO: add unit tests to verify all permutation.
- private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY,
- int[][] matrix, int increment, boolean isRtl) {
- if(matrix == null) {
- throw new IllegalStateException("Dpad navigation requires a matrix.");
- }
- int newIconIndex = NOOP;
-
- int xPos = -1;
- int yPos = -1;
- // Figure out the location of the icon.
- for (int i = 0; i < cntX; i++) {
- for (int j = 0; j < cntY; j++) {
- if (matrix[i][j] == iconIdx) {
- xPos = i;
- yPos = j;
- }
- }
- }
- if (DEBUG) {
- Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d",
- xPos, yPos, iconIdx));
- }
-
- // Rule1: check first in the horizontal direction
- for (int x = xPos + increment; 0 <= x && x < cntX; x += increment) {
- if ((newIconIndex = inspectMatrix(x, yPos, cntX, cntY, matrix)) != NOOP
- && newIconIndex != ALL_APPS_COLUMN) {
- return newIconIndex;
- }
- }
-
- // Rule2: check (x1-n, yPos + increment), (x1-n, yPos - increment)
- // (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment)
- int nextYPos1;
- int nextYPos2;
- boolean haveCrossedAllAppsColumn1 = false;
- boolean haveCrossedAllAppsColumn2 = false;
- int x = -1;
- for (int coeff = 1; coeff < cntY; coeff++) {
- nextYPos1 = yPos + coeff * increment;
- nextYPos2 = yPos - coeff * increment;
- x = xPos + increment * coeff;
- if (inspectMatrix(x, nextYPos1, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
- haveCrossedAllAppsColumn1 = true;
- }
- if (inspectMatrix(x, nextYPos2, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
- haveCrossedAllAppsColumn2 = true;
- }
- for (; 0 <= x && x < cntX; x += increment) {
- int offset1 = haveCrossedAllAppsColumn1 && x < cntX - 1 ? increment : 0;
- newIconIndex = inspectMatrix(x, nextYPos1 + offset1, cntX, cntY, matrix);
- if (newIconIndex != NOOP) {
- return newIconIndex;
- }
- int offset2 = haveCrossedAllAppsColumn2 && x < cntX - 1 ? -increment : 0;
- newIconIndex = inspectMatrix(x, nextYPos2 + offset2, cntX, cntY, matrix);
- if (newIconIndex != NOOP) {
- return newIconIndex;
- }
- }
- }
-
- // Rule3: if switching between pages, do a brute-force search to find an item that was
- // missed by rules 1 and 2 (such as when going from a bottom right icon to top left)
- if (iconIdx == PIVOT) {
- if (isRtl) {
- return increment < 0 ? NEXT_PAGE_FIRST_ITEM : PREVIOUS_PAGE_LAST_ITEM;
- }
- return increment < 0 ? PREVIOUS_PAGE_LAST_ITEM : NEXT_PAGE_FIRST_ITEM;
- }
- return newIconIndex;
- }
-
- /**
- * Calculates icon that is closest to the vertical axis in reference to the current icon.
- *
- * Example of the check order for KEYCODE_DPAD_DOWN:
- * [ ][ ][ ][ X][ ][ ][ ]
- * [ ][ ][ 5][ 1][ 4][ ][ ]
- * [ ][10][ 7][ 2][ 6][ 9][ ]
- * [14][12][ 9][ 3][ 8][11][13]
- */
- // TODO: add unit tests to verify all permutation.
- private static int handleDpadVertical(int iconIndex, int cntX, int cntY,
- int [][] matrix, int increment) {
- int newIconIndex = NOOP;
- if(matrix == null) {
- throw new IllegalStateException("Dpad navigation requires a matrix.");
- }
-
- int xPos = -1;
- int yPos = -1;
- // Figure out the location of the icon.
- for (int i = 0; i< cntX; i++) {
- for (int j = 0; j < cntY; j++) {
- if (matrix[i][j] == iconIndex) {
- xPos = i;
- yPos = j;
- }
- }
- }
-
- if (DEBUG) {
- Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d",
- xPos, yPos, iconIndex));
- }
-
- // Rule1: check first in the dpad direction
- for (int y = yPos + increment; 0 <= y && y <cntY && 0 <= y; y += increment) {
- if ((newIconIndex = inspectMatrix(xPos, y, cntX, cntY, matrix)) != NOOP
- && newIconIndex != ALL_APPS_COLUMN) {
- return newIconIndex;
- }
- }
-
- // Rule2: check (xPos + increment, y_(1-n)), (xPos - increment, y_(1-n))
- // (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n))
- int nextXPos1;
- int nextXPos2;
- boolean haveCrossedAllAppsColumn1 = false;
- boolean haveCrossedAllAppsColumn2 = false;
- int y = -1;
- for (int coeff = 1; coeff < cntX; coeff++) {
- nextXPos1 = xPos + coeff * increment;
- nextXPos2 = xPos - coeff * increment;
- y = yPos + increment * coeff;
- if (inspectMatrix(nextXPos1, y, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
- haveCrossedAllAppsColumn1 = true;
- }
- if (inspectMatrix(nextXPos2, y, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
- haveCrossedAllAppsColumn2 = true;
- }
- for (; 0 <= y && y < cntY; y = y + increment) {
- int offset1 = haveCrossedAllAppsColumn1 && y < cntY - 1 ? increment : 0;
- newIconIndex = inspectMatrix(nextXPos1 + offset1, y, cntX, cntY, matrix);
- if (newIconIndex != NOOP) {
- return newIconIndex;
- }
- int offset2 = haveCrossedAllAppsColumn2 && y < cntY - 1 ? -increment : 0;
- newIconIndex = inspectMatrix(nextXPos2 + offset2, y, cntX, cntY, matrix);
- if (newIconIndex != NOOP) {
- return newIconIndex;
- }
- }
- }
- return newIconIndex;
- }
-
- private static int handleMoveHome() {
- return CURRENT_PAGE_FIRST_ITEM;
- }
-
- private static int handleMoveEnd() {
- return CURRENT_PAGE_LAST_ITEM;
- }
-
- private static int handlePageDown(int pageIndex, int pageCount) {
- if (pageIndex < pageCount -1) {
- return NEXT_PAGE_FIRST_ITEM;
- }
- return CURRENT_PAGE_LAST_ITEM;
- }
-
- private static int handlePageUp(int pageIndex) {
- if (pageIndex > 0) {
- return PREVIOUS_PAGE_FIRST_ITEM;
- } else {
- return CURRENT_PAGE_FIRST_ITEM;
- }
- }
-
- //
- // Helper methods.
- //
-
- private static boolean isValid(int xPos, int yPos, int countX, int countY) {
- return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY);
- }
-
- private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) {
- int newIconIndex = NOOP;
- if (isValid(x, y, cntX, cntY)) {
- if (matrix[x][y] != -1) {
- newIconIndex = matrix[x][y];
- if (DEBUG) {
- Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d",
- x, y, matrix[x][y]));
- }
- return newIconIndex;
- }
- }
- return newIconIndex;
- }
-
- /**
- * Only used for debugging.
- */
- private static String getStringIndex(int index) {
- switch(index) {
- case NOOP: return "NOOP";
- case PREVIOUS_PAGE_FIRST_ITEM: return "PREVIOUS_PAGE_FIRST";
- case PREVIOUS_PAGE_LAST_ITEM: return "PREVIOUS_PAGE_LAST";
- case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN";
- case CURRENT_PAGE_FIRST_ITEM: return "CURRENT_PAGE_FIRST";
- case CURRENT_PAGE_LAST_ITEM: return "CURRENT_PAGE_LAST";
- case NEXT_PAGE_FIRST_ITEM: return "NEXT_PAGE_FIRST";
- case NEXT_PAGE_LEFT_COLUMN: return "NEXT_PAGE_LEFT_COLUMN";
- case ALL_APPS_COLUMN: return "ALL_APPS_COLUMN";
- default:
- return Integer.toString(index);
- }
- }
-
- /**
- * Only used for debugging.
- */
- private static void printMatrix(int[][] matrix) {
- Log.v(TAG, "\tprintMap:");
- int m = matrix.length;
- int n = matrix[0].length;
-
- for (int j=0; j < n; j++) {
- String colY = "\t\t";
- for (int i=0; i < m; i++) {
- colY += String.format("%3d",matrix[i][j]);
- }
- Log.v(TAG, colY);
- }
- }
-
- /**
- * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or
- * {@link #NEXT_PAGE_RIGHT_COLUMN}
- * @return the view adjacent to {@param oldView} in the {@param nextPage} of the folder.
- */
- public static View getAdjacentChildInNextFolderPage(
- ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) {
- final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY;
-
- int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally()
- ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1);
-
- for (; column >= 0; column--) {
- for (int row = newRow; row >= 0; row--) {
- View newView = nextPage.getChildAt(column, row);
- if (newView != null) {
- return newView;
- }
- }
- }
- return null;
- }
-}
diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java
index fcb96d7..1cec0ec 100644
--- a/src/com/android/launcher3/util/IOUtils.java
+++ b/src/com/android/launcher3/util/IOUtils.java
@@ -16,20 +16,19 @@
package com.android.launcher3.util;
-import android.content.Context;
+import android.os.FileUtils;
import android.util.Log;
+import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.UUID;
/**
* Supports various IO utility functions
@@ -52,6 +51,9 @@
}
public static long copy(InputStream from, OutputStream to) throws IOException {
+ if (Utilities.ATLEAST_Q) {
+ return FileUtils.copy(from, to);
+ }
byte[] buf = new byte[BUF_SIZE];
long total = 0;
int r;
@@ -62,25 +64,6 @@
return total;
}
- /**
- * Utility method to debug binary data
- */
- public static String createTempFile(Context context, byte[] data) {
- if (!FeatureFlags.IS_STUDIO_BUILD) {
- throw new IllegalStateException("Method only allowed in development mode");
- }
-
- String name = UUID.randomUUID().toString();
- File file = new File(context.getCacheDir(), name);
- try (FileOutputStream fo = new FileOutputStream(file)) {
- fo.write(data);
- fo.flush();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- return file.getAbsolutePath();
- }
-
public static void closeSilently(Closeable c) {
if (c != null) {
try {
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index 4d5405d..354609d 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -20,13 +20,11 @@
import android.os.UserHandle;
import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shortcuts.ShortcutKey;
import java.util.HashSet;
+import java.util.Set;
/**
* A utility class to check for {@link ItemInfo}
@@ -36,34 +34,15 @@
boolean matches(ItemInfo info, ComponentName cn);
/**
- * Filters {@param infos} to those satisfying the {@link #matches(ItemInfo, ComponentName)}.
+ * Returns true if the itemInfo matches this check
*/
- default HashSet<ItemInfo> filterItemInfos(Iterable<ItemInfo> infos) {
- HashSet<ItemInfo> filtered = new HashSet<>();
- for (ItemInfo i : infos) {
- if (i instanceof WorkspaceItemInfo) {
- WorkspaceItemInfo info = (WorkspaceItemInfo) i;
- ComponentName cn = info.getTargetComponent();
- if (cn != null && matches(info, cn)) {
- filtered.add(info);
- }
- } else if (i instanceof FolderInfo) {
- FolderInfo info = (FolderInfo) i;
- for (WorkspaceItemInfo s : info.contents) {
- ComponentName cn = s.getTargetComponent();
- if (cn != null && matches(s, cn)) {
- filtered.add(s);
- }
- }
- } else if (i instanceof LauncherAppWidgetInfo) {
- LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) i;
- ComponentName cn = info.providerName;
- if (cn != null && matches(info, cn)) {
- filtered.add(info);
- }
- }
+ default boolean matchesInfo(ItemInfo info) {
+ if (info != null) {
+ ComponentName cn = info.getTargetComponent();
+ return cn != null && matches(info, cn);
+ } else {
+ return false;
}
- return filtered;
}
/**
@@ -81,11 +60,10 @@
}
/**
- * Returns a new matcher which returns the opposite boolean value of the provided
- * {@param matcher}.
+ * Returns a new matcher with returns the opposite value of this.
*/
- static ItemInfoMatcher not(ItemInfoMatcher matcher) {
- return (info, cn) -> !matcher.matches(info, cn);
+ default ItemInfoMatcher negate() {
+ return (info, cn) -> !matches(info, cn);
}
static ItemInfoMatcher ofUser(UserHandle user) {
@@ -96,16 +74,19 @@
return (info, cn) -> components.contains(cn) && info.user.equals(user);
}
- static ItemInfoMatcher ofPackages(HashSet<String> packageNames, UserHandle user) {
+ static ItemInfoMatcher ofPackages(Set<String> packageNames, UserHandle user) {
return (info, cn) -> packageNames.contains(cn.getPackageName()) && info.user.equals(user);
}
- static ItemInfoMatcher ofShortcutKeys(HashSet<ShortcutKey> keys) {
+ static ItemInfoMatcher ofShortcutKeys(Set<ShortcutKey> keys) {
return (info, cn) -> info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
keys.contains(ShortcutKey.fromItemInfo(info));
}
- static ItemInfoMatcher ofItemIds(IntSparseArrayMap<Boolean> ids, Boolean matchDefault) {
- return (info, cn) -> ids.get(info.id, matchDefault);
+ /**
+ * Returns a matcher for items with provided ids
+ */
+ static ItemInfoMatcher ofItemIds(IntSet ids) {
+ return (info, cn) -> ids.contains(info.id);
}
}
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index fc9f8f7..f6003dd 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -46,7 +46,7 @@
if (mValue == null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
- mValue = TraceHelper.whitelistIpcs("main.thread.object",
+ mValue = TraceHelper.allowIpcs("main.thread.object",
() -> mProvider.get(context.getApplicationContext()));
} else {
try {
diff --git a/src/com/android/launcher3/util/MultiHashMap.java b/src/com/android/launcher3/util/MultiHashMap.java
deleted file mode 100644
index b7275c1..0000000
--- a/src/com/android/launcher3/util/MultiHashMap.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-
-/**
- * A utility map from keys to an ArrayList of values.
- */
-public class MultiHashMap<K, V> extends HashMap<K, ArrayList<V>> {
-
- public MultiHashMap() { }
-
- public MultiHashMap(int size) {
- super(size);
- }
-
- public void addToList(K key, V value) {
- ArrayList<V> list = get(key);
- if (list == null) {
- list = new ArrayList<>();
- list.add(value);
- put(key, list);
- } else {
- list.add(value);
- }
- }
-
- @Override
- public MultiHashMap<K, V> clone() {
- MultiHashMap<K, V> map = new MultiHashMap<>(size());
- for (Entry<K, ArrayList<V>> entry : entrySet()) {
- map.put(entry.getKey(), new ArrayList<V>(entry.getValue()));
- }
- return map;
- }
-}
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index a8642b0..5be9529 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -19,6 +19,8 @@
import android.util.FloatProperty;
import android.view.View;
+import com.android.launcher3.anim.AlphaUpdateListener;
+
import java.util.Arrays;
/**
@@ -44,6 +46,8 @@
private final AlphaProperty[] mMyProperties;
private int mValidMask;
+ // Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
+ private boolean mUpdateVisibility;
public MultiValueAlpha(View view, int size) {
mView = view;
@@ -66,6 +70,11 @@
return mMyProperties[index];
}
+ /** Sets whether we should update between INVISIBLE and VISIBLE based on alpha. */
+ public void setUpdateVisibility(boolean updateVisibility) {
+ mUpdateVisibility = updateVisibility;
+ }
+
public class AlphaProperty {
private final int mMyMask;
@@ -99,6 +108,9 @@
mValue = value;
mView.setAlpha(mOthers * mValue);
+ if (mUpdateVisibility) {
+ AlphaUpdateListener.updateVisibility(mView);
+ }
}
public float getValue() {
diff --git a/src/com/android/launcher3/util/ObjectWrapper.java b/src/com/android/launcher3/util/ObjectWrapper.java
index e5b4707..a715821 100644
--- a/src/com/android/launcher3/util/ObjectWrapper.java
+++ b/src/com/android/launcher3/util/ObjectWrapper.java
@@ -42,4 +42,11 @@
public static IBinder wrap(Object obj) {
return new ObjectWrapper<>(obj);
}
+
+ public static <T> T unwrap(IBinder binder) {
+ if (binder instanceof ObjectWrapper) {
+ return ((ObjectWrapper<T>) binder).get();
+ }
+ return null;
+ }
}
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index d4e074c..40bc9c3 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -33,42 +33,43 @@
public class OnboardingPrefs<T extends Launcher> {
public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
- public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
- public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
- public static final String ALL_APPS_COUNT = "launcher.all_apps_count";
public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen";
+ public static final String SEARCH_EDU_SEEN = "launcher.search_edu_seen";
+ public static final String SEARCH_SNACKBAR_COUNT = "launcher.keyboard_snackbar_count";
/**
* Events that either have happened or have not (booleans).
*/
@StringDef(value = {
HOME_BOUNCE_SEEN,
- SHELF_BOUNCE_SEEN
+ HOTSEAT_LONGPRESS_TIP_SEEN,
+ SEARCH_EDU_SEEN
})
@Retention(RetentionPolicy.SOURCE)
- public @interface EventBoolKey {}
+ public @interface EventBoolKey {
+ }
/**
* Events that occur multiple times, which we count up to a max defined in {@link #MAX_COUNTS}.
*/
@StringDef(value = {
HOME_BOUNCE_COUNT,
- SHELF_BOUNCE_COUNT,
- ALL_APPS_COUNT,
- HOTSEAT_DISCOVERY_TIP_COUNT
+ HOTSEAT_DISCOVERY_TIP_COUNT,
+ SEARCH_SNACKBAR_COUNT
})
@Retention(RetentionPolicy.SOURCE)
- public @interface EventCountKey {}
+ public @interface EventCountKey {
+ }
private static final Map<String, Integer> MAX_COUNTS;
+
static {
- Map<String, Integer> maxCounts = new ArrayMap<>(3);
+ Map<String, Integer> maxCounts = new ArrayMap<>(4);
maxCounts.put(HOME_BOUNCE_COUNT, 3);
- maxCounts.put(SHELF_BOUNCE_COUNT, 3);
- maxCounts.put(ALL_APPS_COUNT, 5);
maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
+ maxCounts.put(SEARCH_SNACKBAR_COUNT, 3);
MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
}
@@ -108,6 +109,7 @@
/**
* Add 1 to the given event count, if we haven't already reached the max count.
+ *
* @return Whether we have now reached the max count.
*/
public boolean incrementEventCount(@EventCountKey String eventKey) {
diff --git a/src/com/android/launcher3/util/OverScroller.java b/src/com/android/launcher3/util/OverScroller.java
deleted file mode 100644
index 87e6986..0000000
--- a/src/com/android/launcher3/util/OverScroller.java
+++ /dev/null
@@ -1,867 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import static com.android.launcher3.anim.Interpolators.SCROLL;
-
-import android.animation.TimeInterpolator;
-import android.content.Context;
-import android.hardware.SensorManager;
-import android.util.Log;
-import android.view.ViewConfiguration;
-import android.view.animation.AnimationUtils;
-import android.view.animation.Interpolator;
-
-import androidx.dynamicanimation.animation.FloatPropertyCompat;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
-
-import com.android.launcher3.R;
-import com.android.systemui.plugins.ResourceProvider;
-
-/**
- * Based on {@link android.widget.OverScroller} supporting only 1-d scrolling and with more
- * customization options.
- */
-public class OverScroller {
- private int mMode;
-
- private final SplineOverScroller mScroller;
-
- private TimeInterpolator mInterpolator;
-
- private final boolean mFlywheel;
-
- private static final int DEFAULT_DURATION = 250;
- private static final int SCROLL_MODE = 0;
- private static final int FLING_MODE = 1;
-
- /**
- * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
- * @param context
- */
- public OverScroller(Context context) {
- this(context, null);
- }
-
- /**
- * Creates an OverScroller with flywheel enabled.
- * @param context The context of this application.
- * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
- * be used.
- */
- public OverScroller(Context context, Interpolator interpolator) {
- this(context, interpolator, true);
- }
-
- /**
- * Creates an OverScroller.
- * @param context The context of this application.
- * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
- * be used.
- * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
- */
- public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
- if (interpolator == null) {
- mInterpolator = SCROLL;
- } else {
- mInterpolator = interpolator;
- }
- mFlywheel = flywheel;
- mScroller = new SplineOverScroller(context);
- }
-
- public void setInterpolator(TimeInterpolator interpolator) {
- if (interpolator == null) {
- mInterpolator = SCROLL;
- } else {
- mInterpolator = interpolator;
- }
- }
-
- /**
- * The amount of friction applied to flings. The default value
- * is {@link ViewConfiguration#getScrollFriction}.
- *
- * @param friction A scalar dimension-less value representing the coefficient of
- * friction.
- */
- public final void setFriction(float friction) {
- mScroller.setFriction(friction);
- }
-
- /**
- *
- * Returns whether the scroller has finished scrolling.
- *
- * @return True if the scroller has finished scrolling, false otherwise.
- */
- public final boolean isFinished() {
- return mScroller.mFinished;
- }
-
- /**
- * Force the finished field to a particular value. Contrary to
- * {@link #abortAnimation()}, forcing the animation to finished
- * does NOT cause the scroller to move to the final x and y
- * position.
- *
- * @param finished The new finished value.
- */
- public final void forceFinished(boolean finished) {
- mScroller.mFinished = finished;
- }
-
- /**
- * Returns the current offset in the scroll.
- *
- * @return The new offset as an absolute distance from the origin.
- */
- public final int getCurrPos() {
- return mScroller.mCurrentPosition;
- }
-
- /**
- * Returns the absolute value of the current velocity.
- *
- * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
- */
- public float getCurrVelocity() {
- return mScroller.mCurrVelocity;
- }
-
- /**
- * Returns the start offset in the scroll.
- *
- * @return The start offset as an absolute distance from the origin.
- */
- public final int getStartPos() {
- return mScroller.mStart;
- }
-
- /**
- * Returns where the scroll will end. Valid only for "fling" scrolls.
- *
- * @return The final offset as an absolute distance from the origin.
- */
- public final int getFinalPos() {
- return mScroller.mFinal;
- }
-
- /**
- * Returns how long the scroll event will take, in milliseconds.
- *
- * Note that if mScroller.mState == SPRING, this duration is ignored, so can only
- * serve as an estimate for how long the spring-controlled scroll will take.
- *
- * @return The duration of the scroll in milliseconds.
- */
- public final int getDuration() {
- return mScroller.mDuration;
- }
-
- /**
- * Extend the scroll animation. This allows a running animation to scroll
- * further and longer, when used with {@link #setFinalPos(int)}.
- *
- * @param extend Additional time to scroll in milliseconds.
- * @see #setFinalPos(int)
- */
- public void extendDuration(int extend) {
- mScroller.extendDuration(extend);
- }
-
- /**
- * Sets the final position for this scroller.
- *
- * @param newPos The new offset as an absolute distance from the origin.
- * @see #extendDuration(int)
- */
- public void setFinalPos(int newPos) {
- mScroller.setFinalPosition(newPos);
- }
-
- /**
- * Call this when you want to know the new location. If it returns true, the
- * animation is not yet finished.
- */
- public boolean computeScrollOffset() {
- if (isFinished()) {
- return false;
- }
-
- switch (mMode) {
- case SCROLL_MODE:
- if (isSpringing()) {
- return true;
- }
- long time = AnimationUtils.currentAnimationTimeMillis();
- // Any scroller can be used for time, since they were started
- // together in scroll mode. We use X here.
- final long elapsedTime = time - mScroller.mStartTime;
-
- final int duration = mScroller.mDuration;
- if (elapsedTime < duration) {
- final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
- mScroller.updateScroll(q);
- } else {
- abortAnimation();
- }
- break;
-
- case FLING_MODE:
- if (!mScroller.mFinished) {
- if (!mScroller.update()) {
- if (!mScroller.continueWhenFinished()) {
- mScroller.finish();
- }
- }
- }
-
- break;
- }
-
- return true;
- }
-
- /**
- * Start scrolling by providing a starting point and the distance to travel.
- * The scroll will use the default value of 250 milliseconds for the
- * duration.
- *
- * @param start Starting horizontal scroll offset in pixels. Positive
- * numbers will scroll the content to the left.
- * @param delta Distance to travel. Positive numbers will scroll the
- * content to the left.
- */
- public void startScroll(int start, int delta) {
- startScroll(start, delta, DEFAULT_DURATION);
- }
-
- /**
- * Start scrolling by providing a starting point and the distance to travel.
- *
- * @param start Starting scroll offset in pixels. Positive
- * numbers will scroll the content to the left.
- * @param delta Distance to travel. Positive numbers will scroll the
- * content to the left.
- * @param duration Duration of the scroll in milliseconds.
- */
- public void startScroll(int start, int delta, int duration) {
- mMode = SCROLL_MODE;
- mScroller.startScroll(start, delta, duration);
- }
-
- /**
- * Start scrolling using a spring by providing a starting point and the distance to travel.
- *
- * @param start Starting scroll offset in pixels. Positive
- * numbers will scroll the content to the left.
- * @param delta Distance to travel. Positive numbers will scroll the
- * content to the left.
- * @param duration Duration of the scroll in milliseconds.
- * @param velocity The starting velocity for the spring in px per ms.
- */
- public void startScrollSpring(int start, int delta, int duration, float velocity) {
- mMode = SCROLL_MODE;
- mScroller.mState = mScroller.SPRING;
- mScroller.startScroll(start, delta, duration, velocity);
- }
-
- /**
- * Call this when you want to 'spring back' into a valid coordinate range.
- *
- * @param start Starting X coordinate
- * @param min Minimum valid X value
- * @param max Maximum valid X value
- * @return true if a springback was initiated, false if startX and startY were
- * already within the valid range.
- */
- public boolean springBack(int start, int min, int max) {
- mMode = FLING_MODE;
- return mScroller.springback(start, min, max);
- }
-
- public void fling(int start, int velocity, int min, int max) {
- fling(start, velocity, min, max, 0);
- }
-
- /**
- * Start scrolling based on a fling gesture. The distance traveled will
- * depend on the initial velocity of the fling.
- * @param start Starting point of the scroll (X)
- * @param velocity Initial velocity of the fling (X) measured in pixels per
- * second.
- * @param min Minimum X value. The scroller will not scroll past this point
- * unless overX > 0. If overfling is allowed, it will use minX as
- * a springback boundary.
- * @param max Maximum X value. The scroller will not scroll past this point
-* unless overX > 0. If overfling is allowed, it will use maxX as
-* a springback boundary.
- * @param over Overfling range. If > 0, horizontal overfling in either
-* direction will be possible.
- */
- public void fling(int start, int velocity, int min, int max, int over) {
- // Continue a scroll or fling in progress
- if (mFlywheel && !isFinished()) {
- float oldVelocityX = mScroller.mCurrVelocity;
- if (Math.signum(velocity) == Math.signum(oldVelocityX)) {
- velocity += oldVelocityX;
- }
- }
-
- mMode = FLING_MODE;
- mScroller.fling(start, velocity, min, max, over);
- }
-
- /**
- * Notify the scroller that we've reached a horizontal boundary.
- * Normally the information to handle this will already be known
- * when the animation is started, such as in a call to one of the
- * fling functions. However there are cases where this cannot be known
- * in advance. This function will transition the current motion and
- * animate from startX to finalX as appropriate.
- * @param start Starting/current X position
- * @param finalPos Desired final X position
- * @param over Magnitude of overscroll allowed. This should be the maximum
- */
- public void notifyEdgeReached(int start, int finalPos, int over) {
- mScroller.notifyEdgeReached(start, finalPos, over);
- }
-
- /**
- * Returns whether the current Scroller is currently returning to a valid position.
- * Valid bounds were provided by the
- * {@link #fling(int, int, int, int, int)} method.
- *
- * One should check this value before calling
- * {@link #startScroll(int, int)} as the interpolation currently in progress
- * to restore a valid position will then be stopped. The caller has to take into account
- * the fact that the started scroll will start from an overscrolled position.
- *
- * @return true when the current position is overscrolled and in the process of
- * interpolating back to a valid value.
- */
- public boolean isOverScrolled() {
- return (!mScroller.mFinished && mScroller.mState != SplineOverScroller.SPLINE);
- }
-
- /**
- * Stops the animation. Contrary to {@link #forceFinished(boolean)},
- * aborting the animating causes the scroller to move to the final x and y
- * positions.
- *
- * @see #forceFinished(boolean)
- */
- public void abortAnimation() {
- mScroller.finish();
- }
-
- /**
- * Returns the time elapsed since the beginning of the scrolling.
- *
- * @return The elapsed time in milliseconds.
- *
- * @hide
- */
- public int timePassed() {
- final long time = AnimationUtils.currentAnimationTimeMillis();
- return (int) (time - mScroller.mStartTime);
- }
-
- public boolean isSpringing() {
- return mScroller.mState == SplineOverScroller.SPRING && !isFinished();
- }
-
- static class SplineOverScroller {
- // Initial position
- private int mStart;
-
- // Current position
- private int mCurrentPosition;
-
- // Final position
- private int mFinal;
-
- // Initial velocity
- private int mVelocity;
-
- // Current velocity
- private float mCurrVelocity;
-
- // Constant current deceleration
- private float mDeceleration;
-
- // Animation starting time, in system milliseconds
- private long mStartTime;
-
- // Animation duration, in milliseconds
- private int mDuration;
-
- // Duration to complete spline component of animation
- private int mSplineDuration;
-
- // Distance to travel along spline animation
- private int mSplineDistance;
-
- // Whether the animation is currently in progress
- private boolean mFinished;
-
- // The allowed overshot distance before boundary is reached.
- private int mOver;
-
- // Fling friction
- private float mFlingFriction = ViewConfiguration.getScrollFriction();
-
- // Current state of the animation.
- private int mState = SPLINE;
-
- private Context mContext;
- private SpringAnimation mSpring;
-
- // Constant gravity value, used in the deceleration phase.
- private static final float GRAVITY = 2000.0f;
-
- // A context-specific coefficient adjusted to physical values.
- private float mPhysicalCoeff;
-
- private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
- private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
- private static final float START_TENSION = 0.5f;
- private static final float END_TENSION = 1.0f;
- private static final float P1 = START_TENSION * INFLEXION;
- private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
-
- private static final int NB_SAMPLES = 100;
- private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
- private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
-
- private static final int SPLINE = 0;
- private static final int CUBIC = 1;
- private static final int BALLISTIC = 2;
- private static final int SPRING = 3;
-
- private static final FloatPropertyCompat<SplineOverScroller> SPRING_PROPERTY =
- new FloatPropertyCompat<SplineOverScroller>("splineOverScrollerSpring") {
- @Override
- public float getValue(SplineOverScroller scroller) {
- return scroller.mCurrentPosition;
- }
-
- @Override
- public void setValue(SplineOverScroller scroller, float value) {
- scroller.mCurrentPosition = (int) value;
- }
- };
-
- static {
- float x_min = 0.0f;
- float y_min = 0.0f;
- for (int i = 0; i < NB_SAMPLES; i++) {
- final float alpha = (float) i / NB_SAMPLES;
-
- float x_max = 1.0f;
- float x, tx, coef;
- while (true) {
- x = x_min + (x_max - x_min) / 2.0f;
- coef = 3.0f * x * (1.0f - x);
- tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
- if (Math.abs(tx - alpha) < 1E-5) break;
- if (tx > alpha) x_max = x;
- else x_min = x;
- }
- SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
-
- float y_max = 1.0f;
- float y, dy;
- while (true) {
- y = y_min + (y_max - y_min) / 2.0f;
- coef = 3.0f * y * (1.0f - y);
- dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
- if (Math.abs(dy - alpha) < 1E-5) break;
- if (dy > alpha) y_max = y;
- else y_min = y;
- }
- SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
- }
- SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
- }
-
- void setFriction(float friction) {
- mFlingFriction = friction;
- }
-
- SplineOverScroller(Context context) {
- mContext = context;
- mFinished = true;
- final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
- mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
- * 39.37f // inch/meter
- * ppi
- * 0.84f; // look and feel tuning
- }
-
- void updateScroll(float q) {
- if (mState == SPRING) {
- return;
- }
- mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
- }
-
- /*
- * Get a signed deceleration that will reduce the velocity.
- */
- static private float getDeceleration(int velocity) {
- return velocity > 0 ? -GRAVITY : GRAVITY;
- }
-
- /*
- * Modifies mDuration to the duration it takes to get from start to newFinal using the
- * spline interpolation. The previous duration was needed to get to oldFinal.
- */
- private void adjustDuration(int start, int oldFinal, int newFinal) {
- final int oldDistance = oldFinal - start;
- final int newDistance = newFinal - start;
- final float x = Math.abs((float) newDistance / oldDistance);
- final int index = (int) (NB_SAMPLES * x);
- if (index < NB_SAMPLES) {
- final float x_inf = (float) index / NB_SAMPLES;
- final float x_sup = (float) (index + 1) / NB_SAMPLES;
- final float t_inf = SPLINE_TIME[index];
- final float t_sup = SPLINE_TIME[index + 1];
- final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
- mDuration *= timeCoef;
- }
- }
-
- void startScroll(int start, int distance, int duration) {
- startScroll(start, distance, duration, 0);
- }
-
- void startScroll(int start, int distance, int duration, float velocity) {
- mFinished = false;
-
- mCurrentPosition = mStart = start;
- mFinal = start + distance;
-
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
- mDuration = duration;
-
- if (mSpring != null) {
- mSpring.cancel();
- }
-
- if (mState == SPRING) {
- mSpring = new SpringAnimation(this, SPRING_PROPERTY);
-
- ResourceProvider rp = DynamicResource.provider(mContext);
- float stiffness = rp.getFloat(R.dimen.horizontal_spring_stiffness);
- float damping = rp.getFloat(R.dimen.horizontal_spring_damping_ratio);
- mSpring.setSpring(new SpringForce(mFinal)
- .setStiffness(stiffness)
- .setDampingRatio(damping));
- mSpring.setStartVelocity(velocity);
- mSpring.animateToFinalPosition(mFinal);
- mSpring.addEndListener((animation, canceled, value, velocity1) -> {
- mSpring = null;
- finish();
- mState = SPLINE;
- });
- }
- // Unused
- mDeceleration = 0.0f;
- mVelocity = 0;
- }
-
- void finish() {
- if (mSpring != null && mSpring.isRunning()) mSpring.cancel();
-
- mCurrentPosition = mFinal;
- // Not reset since WebView relies on this value for fast fling.
- // TODO: restore when WebView uses the fast fling implemented in this class.
- // mCurrVelocity = 0.0f;
- mFinished = true;
- }
-
- void setFinalPosition(int position) {
- mFinal = position;
- if (mState == SPRING && mSpring != null) {
- mSpring.animateToFinalPosition(mFinal);
- }
- mSplineDistance = mFinal - mStart;
- mFinished = false;
- }
-
- void extendDuration(int extend) {
- final long time = AnimationUtils.currentAnimationTimeMillis();
- final int elapsedTime = (int) (time - mStartTime);
- mDuration = mSplineDuration = elapsedTime + extend;
- mFinished = false;
- }
-
- boolean springback(int start, int min, int max) {
- mFinished = true;
-
- mCurrentPosition = mStart = mFinal = start;
- mVelocity = 0;
-
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
- mDuration = 0;
-
- if (start < min) {
- startSpringback(start, min, 0);
- } else if (start > max) {
- startSpringback(start, max, 0);
- }
-
- return !mFinished;
- }
-
- private void startSpringback(int start, int end, int velocity) {
- // mStartTime has been set
- mFinished = false;
- mState = CUBIC;
- mCurrentPosition = mStart = start;
- mFinal = end;
- final int delta = start - end;
- mDeceleration = getDeceleration(delta);
- // TODO take velocity into account
- mVelocity = -delta; // only sign is used
- mOver = Math.abs(delta);
- mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
- }
-
- void fling(int start, int velocity, int min, int max, int over) {
- mOver = over;
- mFinished = false;
- mCurrVelocity = mVelocity = velocity;
- mDuration = mSplineDuration = 0;
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
- mCurrentPosition = mStart = start;
-
- if (start > max || start < min) {
- startAfterEdge(start, min, max, velocity);
- return;
- }
-
- mState = SPLINE;
- double totalDistance = 0.0;
-
- if (velocity != 0) {
- mDuration = mSplineDuration = getSplineFlingDuration(velocity);
- totalDistance = getSplineFlingDistance(velocity);
- }
-
- mSplineDistance = (int) (totalDistance * Math.signum(velocity));
- mFinal = start + mSplineDistance;
-
- // Clamp to a valid final position
- if (mFinal < min) {
- adjustDuration(mStart, mFinal, min);
- mFinal = min;
- }
-
- if (mFinal > max) {
- adjustDuration(mStart, mFinal, max);
- mFinal = max;
- }
- }
-
- private double getSplineDeceleration(int velocity) {
- return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
- }
-
- private double getSplineFlingDistance(int velocity) {
- final double l = getSplineDeceleration(velocity);
- final double decelMinusOne = DECELERATION_RATE - 1.0;
- return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
- }
-
- /* Returns the duration, expressed in milliseconds */
- private int getSplineFlingDuration(int velocity) {
- final double l = getSplineDeceleration(velocity);
- final double decelMinusOne = DECELERATION_RATE - 1.0;
- return (int) (1000.0 * Math.exp(l / decelMinusOne));
- }
-
- private void fitOnBounceCurve(int start, int end, int velocity) {
- // Simulate a bounce that started from edge
- final float durationToApex = - velocity / mDeceleration;
- // The float cast below is necessary to avoid integer overflow.
- final float velocitySquared = (float) velocity * velocity;
- final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration);
- final float distanceToEdge = Math.abs(end - start);
- final float totalDuration = (float) Math.sqrt(
- 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
- mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
- mCurrentPosition = mStart = end;
- mVelocity = (int) (- mDeceleration * totalDuration);
- }
-
- private void startBounceAfterEdge(int start, int end, int velocity) {
- mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
- fitOnBounceCurve(start, end, velocity);
- onEdgeReached();
- }
-
- private void startAfterEdge(int start, int min, int max, int velocity) {
- if (start > min && start < max) {
- Log.e("OverScroller", "startAfterEdge called from a valid position");
- mFinished = true;
- return;
- }
- final boolean positive = start > max;
- final int edge = positive ? max : min;
- final int overDistance = start - edge;
- boolean keepIncreasing = overDistance * velocity >= 0;
- if (keepIncreasing) {
- // Will result in a bounce or a to_boundary depending on velocity.
- startBounceAfterEdge(start, edge, velocity);
- } else {
- final double totalDistance = getSplineFlingDistance(velocity);
- if (totalDistance > Math.abs(overDistance)) {
- fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
- } else {
- startSpringback(start, edge, velocity);
- }
- }
- }
-
- void notifyEdgeReached(int start, int end, int over) {
- // mState is used to detect successive notifications
- if (mState == SPLINE) {
- mOver = over;
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
- // We were in fling/scroll mode before: current velocity is such that distance to
- // edge is increasing. This ensures that startAfterEdge will not start a new fling.
- startAfterEdge(start, end, end, (int) mCurrVelocity);
- }
- }
-
- private void onEdgeReached() {
- // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
- // The float cast below is necessary to avoid integer overflow.
- final float velocitySquared = (float) mVelocity * mVelocity;
- float distance = velocitySquared / (2.0f * Math.abs(mDeceleration));
- final float sign = Math.signum(mVelocity);
-
- if (distance > mOver) {
- // Default deceleration is not sufficient to slow us down before boundary
- mDeceleration = - sign * velocitySquared / (2.0f * mOver);
- distance = mOver;
- }
-
- mOver = (int) distance;
- mState = BALLISTIC;
- mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
- mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
- }
-
- boolean continueWhenFinished() {
- switch (mState) {
- case SPLINE:
- // Duration from start to null velocity
- if (mDuration < mSplineDuration) {
- // If the animation was clamped, we reached the edge
- mCurrentPosition = mStart = mFinal;
- // TODO Better compute speed when edge was reached
- mVelocity = (int) mCurrVelocity;
- mDeceleration = getDeceleration(mVelocity);
- mStartTime += mDuration;
- onEdgeReached();
- } else {
- // Normal stop, no need to continue
- return false;
- }
- break;
- case BALLISTIC:
- mStartTime += mDuration;
- startSpringback(mFinal, mStart, 0);
- break;
- case CUBIC:
- return false;
- }
-
- update();
- return true;
- }
-
- /*
- * Update the current position and velocity for current time. Returns
- * true if update has been done and false if animation duration has been
- * reached.
- */
- boolean update() {
- if (mState == SPRING) {
- return mFinished;
- }
-
- final long time = AnimationUtils.currentAnimationTimeMillis();
- final long currentTime = time - mStartTime;
-
- if (currentTime == 0) {
- // Skip work but report that we're still going if we have a nonzero duration.
- return mDuration > 0;
- }
- if (currentTime > mDuration) {
- return false;
- }
-
- double distance = 0.0;
- switch (mState) {
- case SPLINE: {
- final float t = (float) currentTime / mSplineDuration;
- final int index = (int) (NB_SAMPLES * t);
- float distanceCoef = 1.f;
- float velocityCoef = 0.f;
- if (index < NB_SAMPLES) {
- final float t_inf = (float) index / NB_SAMPLES;
- final float t_sup = (float) (index + 1) / NB_SAMPLES;
- final float d_inf = SPLINE_POSITION[index];
- final float d_sup = SPLINE_POSITION[index + 1];
- velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
- distanceCoef = d_inf + (t - t_inf) * velocityCoef;
- }
-
- distance = distanceCoef * mSplineDistance;
- mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
- break;
- }
-
- case BALLISTIC: {
- final float t = currentTime / 1000.0f;
- mCurrVelocity = mVelocity + mDeceleration * t;
- distance = mVelocity * t + mDeceleration * t * t / 2.0f;
- break;
- }
-
- case CUBIC: {
- final float t = (float) (currentTime) / mDuration;
- final float t2 = t * t;
- final float sign = Math.signum(mVelocity);
- distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
- mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
- break;
- }
- }
-
- mCurrentPosition = mStart + (int) Math.round(distance);
-
- return true;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/util/OverlayEdgeEffect.java b/src/com/android/launcher3/util/OverlayEdgeEffect.java
new file mode 100644
index 0000000..8d455c6
--- /dev/null
+++ b/src/com/android/launcher3/util/OverlayEdgeEffect.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.widget.EdgeEffect;
+
+import com.android.launcher3.Utilities;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
+
+/**
+ * Extension of {@link EdgeEffect} which shows the Launcher overlay
+ */
+public class OverlayEdgeEffect extends EdgeEffectCompat {
+
+ private final LauncherOverlay mOverlay;
+ private final boolean mIsRtl;
+
+ private float mDistance;
+ private boolean mIsScrolling;
+
+ public OverlayEdgeEffect(Context context, LauncherOverlay overlay) {
+ super(context);
+ mOverlay = overlay;
+ mIsRtl = Utilities.isRtl(context.getResources());
+
+ }
+
+ @Override
+ public float getDistance() {
+ return mDistance;
+ }
+
+ public float onPullDistance(float deltaDistance, float displacement) {
+ mDistance = Math.max(0f, deltaDistance + mDistance);
+ if (!mIsScrolling) {
+ mOverlay.onScrollInteractionBegin();
+ mIsScrolling = true;
+ }
+ mOverlay.onScrollChange(mDistance, mIsRtl);
+ return mDistance > 0 ? deltaDistance : 0;
+ }
+
+ @Override
+ public void onAbsorb(int velocity) { }
+
+ @Override
+ public boolean isFinished() {
+ return mDistance <= 0;
+ }
+
+ @Override
+ public void onRelease() {
+ if (mIsScrolling) {
+ mDistance = 0;
+ mOverlay.onScrollInteractionEnd();
+ mIsScrolling = false;
+ }
+ }
+
+ @Override
+ public boolean draw(Canvas canvas) {
+ return false;
+ }
+
+ public void finish() {
+ mDistance = 0;
+ }
+}
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 86f3431..08ec591 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -37,7 +37,6 @@
import android.os.Build;
import android.os.Bundle;
import android.os.PatternMatcher;
-import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
@@ -49,8 +48,8 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.PromiseAppInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import java.net.URISyntaxException;
@@ -93,44 +92,28 @@
}
/**
+ * Returns whether the target app is installed for a given user
+ */
+ public boolean isAppInstalled(String packageName, UserHandle user) {
+ ApplicationInfo info = getApplicationInfo(packageName, user, 0);
+ return info != null;
+ }
+
+ /**
* Returns the application info for the provided package or null
*/
public ApplicationInfo getApplicationInfo(String packageName, UserHandle user, int flags) {
- if (Utilities.ATLEAST_OREO) {
- try {
- ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
- return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
- ? null : info;
- } catch (PackageManager.NameNotFoundException e) {
- return null;
- }
- } else {
- final boolean isPrimaryUser = Process.myUserHandle().equals(user);
- if (!isPrimaryUser && (flags == 0)) {
- // We are looking for an installed app on a secondary profile. Prior to O, the only
- // entry point for work profiles is through the LauncherActivity.
- List<LauncherActivityInfo> activityList =
- mLauncherApps.getActivityList(packageName, user);
- return activityList.size() > 0 ? activityList.get(0).getApplicationInfo() : null;
- }
- try {
- ApplicationInfo info = mPm.getApplicationInfo(packageName, flags);
- // There is no way to check if the app is installed for managed profile. But for
- // primary profile, we can still have this check.
- if (isPrimaryUser && ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0)
- || !info.enabled) {
- return null;
- }
- return info;
- } catch (PackageManager.NameNotFoundException e) {
- // Package not found
- return null;
- }
+ try {
+ ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
+ return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
+ ? null : info;
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
}
}
public boolean isSafeMode() {
- return mContext.getPackageManager().isSafeMode();
+ return mPm.isSafeMode();
}
public Intent getAppLaunchIntent(String pkg, UserHandle user) {
@@ -226,9 +209,12 @@
* Starts the details activity for {@code info}
*/
public void startDetailsActivityForInfo(ItemInfo info, Rect sourceBounds, Bundle opts) {
- if (info instanceof PromiseAppInfo) {
- PromiseAppInfo promiseAppInfo = (PromiseAppInfo) info;
- mContext.startActivity(promiseAppInfo.getMarketIntent(mContext));
+ if (info instanceof ItemInfoWithIcon
+ && (((ItemInfoWithIcon) info).runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+ ItemInfoWithIcon appInfo = (ItemInfoWithIcon) info;
+ mContext.startActivity(new PackageManagerHelper(mContext)
+ .getMarketIntent(appInfo.getTargetComponent().getPackageName()));
return;
}
ComponentName componentName = null;
@@ -345,4 +331,12 @@
}
return false;
}
+
+ /** Returns the incremental download progress for the given shortcut's app. */
+ public static int getLoadingProgress(LauncherActivityInfo info) {
+ if (Utilities.ATLEAST_S) {
+ return (int) (100 * info.getLoadingProgress());
+ }
+ return 100;
+ }
}
diff --git a/src/com/android/launcher3/util/PendingRequestArgs.java b/src/com/android/launcher3/util/PendingRequestArgs.java
index 9b8c6a6..77c8c0c 100644
--- a/src/com/android/launcher3/util/PendingRequestArgs.java
+++ b/src/com/android/launcher3/util/PendingRequestArgs.java
@@ -20,7 +20,9 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.WidgetAddFlowHandler;
/**
@@ -34,11 +36,13 @@
private static final int TYPE_APP_WIDGET = 2;
private final int mArg1;
+ private final int mArg2;
private final int mObjectType;
private final Parcelable mObject;
public PendingRequestArgs(ItemInfo info) {
mArg1 = 0;
+ mArg2 = 0;
mObjectType = TYPE_NONE;
mObject = null;
@@ -46,7 +50,12 @@
}
private PendingRequestArgs(int arg1, int objectType, Parcelable object) {
+ this(arg1, 0, objectType, object);
+ }
+
+ private PendingRequestArgs(int arg1, int arg2, int objectType, Parcelable object) {
mArg1 = arg1;
+ mArg2 = arg2;
mObjectType = objectType;
mObject = object;
}
@@ -56,6 +65,7 @@
user = parcel.readParcelable(null);
mArg1 = parcel.readInt();
+ mArg2 = parcel.readInt();
mObjectType = parcel.readInt();
mObject = parcel.readParcelable(getClass().getClassLoader());
}
@@ -73,6 +83,7 @@
dest.writeParcelable(user, flags);
dest.writeInt(mArg1);
+ dest.writeInt(mArg2);
dest.writeInt(mObjectType);
dest.writeParcelable(mObject, flags);
}
@@ -85,6 +96,10 @@
return mObjectType == TYPE_APP_WIDGET ? mArg1 : 0;
}
+ public int getWidgetSourceContainer() {
+ return mObjectType == TYPE_APP_WIDGET ? mArg2 : Favorites.CONTAINER_UNKNOWN;
+ }
+
public Intent getPendingIntent() {
return mObjectType == TYPE_INTENT ? (Intent) mObject : null;
}
@@ -95,8 +110,13 @@
public static PendingRequestArgs forWidgetInfo(
int appWidgetId, WidgetAddFlowHandler widgetHandler, ItemInfo info) {
+ int sourceContainer = Favorites.CONTAINER_UNKNOWN;
+ if (info instanceof PendingAddWidgetInfo) {
+ sourceContainer = ((PendingAddWidgetInfo) info).sourceContainer;
+ }
PendingRequestArgs args =
- new PendingRequestArgs(appWidgetId, TYPE_APP_WIDGET, widgetHandler);
+ new PendingRequestArgs(
+ appWidgetId, sourceContainer, TYPE_APP_WIDGET, widgetHandler);
args.copyFrom(info);
return args;
}
diff --git a/src/com/android/launcher3/util/PersistedItemArray.java b/src/com/android/launcher3/util/PersistedItemArray.java
new file mode 100644
index 0000000..7ff2abb
--- /dev/null
+++ b/src/com/android/launcher3/util/PersistedItemArray.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Xml;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.AutoInstallsLayout;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.UserCache;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.LongFunction;
+
+/**
+ * Utility class to read/write a list of {@link com.android.launcher3.model.data.ItemInfo} on disk.
+ * This class is not thread safe, the caller should ensure proper threading
+ */
+public class PersistedItemArray<T extends ItemInfo> {
+
+ private static final String TAG = "PersistedItemArray";
+
+ private static final String TAG_ROOT = "items";
+ private static final String TAG_ENTRY = "entry";
+
+ private final String mFileName;
+
+ public PersistedItemArray(String fileName) {
+ mFileName = fileName + ".xml";
+ }
+
+ /**
+ * Writes the provided list of items on the disk
+ */
+ @WorkerThread
+ public void write(Context context, List<T> items) {
+ AtomicFile file = getFile(context);
+ FileOutputStream fos;
+ try {
+ fos = file.startWrite();
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to persist items in " + mFileName, e);
+ return;
+ }
+
+ UserCache userCache = UserCache.INSTANCE.get(context);
+
+ try {
+ XmlSerializer out = Xml.newSerializer();
+ out.setOutput(fos, StandardCharsets.UTF_8.name());
+ out.startDocument(null, true);
+ out.startTag(null, TAG_ROOT);
+ for (T item : items) {
+ Intent intent = item.getIntent();
+ if (intent == null) {
+ continue;
+ }
+
+ out.startTag(null, TAG_ENTRY);
+ out.attribute(null, Favorites.ITEM_TYPE, Integer.toString(item.itemType));
+ out.attribute(null, Favorites.PROFILE_ID,
+ Long.toString(userCache.getSerialNumberForUser(item.user)));
+ out.attribute(null, Favorites.INTENT, intent.toUri(0));
+ out.endTag(null, TAG_ENTRY);
+ }
+ out.endTag(null, TAG_ROOT);
+ out.endDocument();
+ } catch (IOException e) {
+ file.failWrite(fos);
+ Log.e(TAG, "Unable to persist items in " + mFileName, e);
+ return;
+ }
+
+ file.finishWrite(fos);
+ }
+
+ /**
+ * Reads the items from the disk
+ */
+ @WorkerThread
+ public List<T> read(Context context, ItemFactory<T> factory) {
+ return read(context, factory, UserCache.INSTANCE.get(context)::getUserForSerialNumber);
+ }
+
+ /**
+ * Reads the items from the disk
+ * @param userFn method to provide user handle for a given user serial
+ */
+ @WorkerThread
+ public List<T> read(Context context, ItemFactory<T> factory, LongFunction<UserHandle> userFn) {
+ List<T> result = new ArrayList<>();
+ try (FileInputStream fis = getFile(context).openRead()) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(new InputStreamReader(fis, StandardCharsets.UTF_8));
+
+ AutoInstallsLayout.beginDocument(parser, TAG_ROOT);
+ final int depth = parser.getDepth();
+
+ int type;
+ while (((type = parser.next()) != XmlPullParser.END_TAG
+ || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+ if (type != XmlPullParser.START_TAG || !TAG_ENTRY.equals(parser.getName())) {
+ continue;
+ }
+ try {
+ int itemType = Integer.parseInt(
+ parser.getAttributeValue(null, Favorites.ITEM_TYPE));
+ UserHandle user = userFn.apply(Long.parseLong(
+ parser.getAttributeValue(null, Favorites.PROFILE_ID)));
+ Intent intent = Intent.parseUri(
+ parser.getAttributeValue(null, Favorites.INTENT), 0);
+
+ if (user != null && intent != null) {
+ T item = factory.createInfo(itemType, user, intent);
+ if (item != null) {
+ result.add(item);
+ }
+ }
+ } catch (Exception e) {
+ // Ignore this entry
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // Ignore
+ } catch (IOException | XmlPullParserException e) {
+ Log.e(TAG, "Unable to read items in " + mFileName, e);
+ return Collections.emptyList();
+ }
+ return result;
+ }
+
+ /**
+ * Returns the underlying file used for persisting data
+ */
+ public AtomicFile getFile(Context context) {
+ return new AtomicFile(context.getFileStreamPath(mFileName));
+ }
+
+ /**
+ * Interface to create an ItemInfo during parsing
+ */
+ public interface ItemFactory<T extends ItemInfo> {
+
+ /**
+ * Returns an item info or null in which case the entry is ignored
+ */
+ @Nullable
+ T createInfo(int itemType, UserHandle user, Intent intent);
+ }
+}
diff --git a/src/com/android/launcher3/util/RunnableList.java b/src/com/android/launcher3/util/RunnableList.java
new file mode 100644
index 0000000..644537b
--- /dev/null
+++ b/src/com/android/launcher3/util/RunnableList.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to hold a list of runnable
+ */
+public class RunnableList {
+
+ private ArrayList<Runnable> mList = null;
+ private boolean mDestroyed = false;
+
+ /**
+ * Ads a runnable to this list
+ */
+ public void add(Runnable runnable) {
+ if (runnable == null) {
+ return;
+ }
+ if (mDestroyed) {
+ runnable.run();
+ return;
+ }
+ if (mList == null) {
+ mList = new ArrayList<>();
+ }
+ mList.add(runnable);
+ }
+
+ /**
+ * Destroys the list, executing any pending callbacks. All new callbacks are
+ * immediately executed
+ */
+ public void executeAllAndDestroy() {
+ mDestroyed = true;
+ executeAllAndClear();
+ }
+
+ /**
+ * Executes all previously added runnable and clears the list
+ */
+ public void executeAllAndClear() {
+ if (mList != null) {
+ ArrayList<Runnable> list = mList;
+ mList = null;
+ int count = list.size();
+ for (int i = 0; i < count; i++) {
+ list.get(i).run();
+ }
+ }
+ }
+}
diff --git a/src/com/android/launcher3/util/SafeCloseable.java b/src/com/android/launcher3/util/SafeCloseable.java
deleted file mode 100644
index ba8ee04..0000000
--- a/src/com/android/launcher3/util/SafeCloseable.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-/**
- * Extension of closeable which does not throw an exception
- */
-public interface SafeCloseable extends AutoCloseable {
-
- @Override
- void close();
-}
diff --git a/src/com/android/launcher3/util/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java
deleted file mode 100644
index 48aa02b..0000000
--- a/src/com/android/launcher3/util/SecureSettingsObserver.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.os.Handler;
-import android.provider.Settings;
-
-/**
- * Utility class to listen for secure settings changes
- */
-public class SecureSettingsObserver extends ContentObserver {
-
- /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
- public static final String NOTIFICATION_BADGING = "notification_badging";
-
- private final ContentResolver mResolver;
- private final String mKeySetting;
- private final int mDefaultValue;
- private final OnChangeListener mOnChangeListener;
-
- public SecureSettingsObserver(ContentResolver resolver, OnChangeListener listener,
- String keySetting, int defaultValue) {
- super(new Handler());
-
- mResolver = resolver;
- mOnChangeListener = listener;
- mKeySetting = keySetting;
- mDefaultValue = defaultValue;
- }
-
- @Override
- public void onChange(boolean selfChange) {
- mOnChangeListener.onSettingsChanged(getValue());
- }
-
- public boolean getValue() {
- return Settings.Secure.getInt(mResolver, mKeySetting, mDefaultValue) == 1;
- }
-
- public void register() {
- mResolver.registerContentObserver(Settings.Secure.getUriFor(mKeySetting), false, this);
- }
-
- public ContentResolver getResolver() {
- return mResolver;
- }
-
- public void dispatchOnChange() {
- onChange(true);
- }
-
- public void unregister() {
- mResolver.unregisterContentObserver(this);
- }
-
- public interface OnChangeListener {
- void onSettingsChanged(boolean isEnabled);
- }
-
- public static SecureSettingsObserver newNotificationSettingsObserver(Context context,
- OnChangeListener listener) {
- return new SecureSettingsObserver(
- context.getContentResolver(), listener, NOTIFICATION_BADGING, 1);
- }
-}
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
new file mode 100644
index 0000000..10611c7
--- /dev/null
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static android.provider.Settings.System.ACCELEROMETER_ROTATION;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * ContentObserver over Settings keys that also has a caching layer.
+ * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and
+ * {@link #unregister(Uri, OnChangeListener)} methods.
+ *
+ * This can be used as a normal cache without any listeners as well via the
+ * {@link #getValue(Uri, int)} and {@link #onChange)} to update (and subsequently call
+ * get)
+ *
+ * The cache will be invalidated/updated through the normal
+ * {@link ContentObserver#onChange(boolean)} calls
+ *
+ * Cache will also be updated if a key queried is missing (even if it has no listeners registered).
+ */
+public class SettingsCache extends ContentObserver {
+
+ /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
+ public static final Uri NOTIFICATION_BADGING_URI =
+ Settings.Secure.getUriFor("notification_badging");
+ /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */
+ public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled";
+ /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */
+ public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED =
+ "swipe_bottom_to_notification_enabled";
+ public static final Uri ROTATION_SETTING_URI =
+ Settings.System.getUriFor(ACCELEROMETER_ROTATION);
+
+ private static final String SYSTEM_URI_PREFIX = Settings.System.CONTENT_URI.toString();
+
+ /**
+ * Caches the last seen value for registered keys.
+ */
+ private Map<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>();
+ private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>();
+ protected final ContentResolver mResolver;
+
+
+ /**
+ * Singleton instance
+ */
+ public static MainThreadInitializedObject<SettingsCache> INSTANCE =
+ new MainThreadInitializedObject<>(SettingsCache::new);
+
+ private SettingsCache(Context context) {
+ super(new Handler());
+ mResolver = context.getContentResolver();
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ // We use default of 1, but if we're getting an onChange call, can assume a non-default
+ // value will exist
+ boolean newVal = updateValue(uri, 1 /* Effectively Unused */);
+ if (!mListenerMap.containsKey(uri)) {
+ return;
+ }
+
+ for (OnChangeListener listener : mListenerMap.get(uri)) {
+ listener.onSettingsChanged(newVal);
+ }
+ }
+
+ /**
+ * Returns the value for this classes key from the cache. If not in cache, will call
+ * {@link #updateValue(Uri, int)} to fetch.
+ */
+ public boolean getValue(Uri keySetting) {
+ return getValue(keySetting, 1);
+ }
+
+ /**
+ * Returns the value for this classes key from the cache. If not in cache, will call
+ * {@link #updateValue(Uri, int)} to fetch.
+ */
+ public boolean getValue(Uri keySetting, int defaultValue) {
+ if (mKeyCache.containsKey(keySetting)) {
+ return mKeyCache.get(keySetting);
+ } else {
+ return updateValue(keySetting, defaultValue);
+ }
+ }
+
+ /**
+ * Does not de-dupe if you add same listeners for the same key multiple times.
+ * Unregister once complete using {@link #unregister(Uri, OnChangeListener)}
+ */
+ public void register(Uri uri, OnChangeListener changeListener) {
+ if (mListenerMap.containsKey(uri)) {
+ mListenerMap.get(uri).add(changeListener);
+ } else {
+ CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>();
+ l.add(changeListener);
+ mListenerMap.put(uri, l);
+ mResolver.registerContentObserver(uri, false, this);
+ }
+ }
+
+ private boolean updateValue(Uri keyUri, int defaultValue) {
+ String key = keyUri.getLastPathSegment();
+ boolean newVal;
+ if (keyUri.toString().startsWith(SYSTEM_URI_PREFIX)) {
+ newVal = Settings.System.getInt(mResolver, key, defaultValue) == 1;
+ } else { // SETTING_SECURE
+ newVal = Settings.Secure.getInt(mResolver, key, defaultValue) == 1;
+ }
+
+ mKeyCache.put(keyUri, newVal);
+ return newVal;
+ }
+
+ /**
+ * Call to stop receiving updates on the given {@param listener}.
+ * This Uri/Listener pair must correspond to the same pair called with for
+ * {@link #register(Uri, OnChangeListener)}
+ */
+ public void unregister(Uri uri, OnChangeListener listener) {
+ List<OnChangeListener> listenersToRemoveFrom = mListenerMap.get(uri);
+ if (!listenersToRemoveFrom.contains(listener)) {
+ return;
+ }
+
+ listenersToRemoveFrom.remove(listener);
+ if (listenersToRemoveFrom.isEmpty()) {
+ mListenerMap.remove(uri);
+ }
+ }
+
+ /**
+ * Don't use this. Ever.
+ * @param keyCache Cache to replace {@link #mKeyCache}
+ */
+ @VisibleForTesting
+ void setKeyCache(Map<Uri, Boolean> keyCache) {
+ mKeyCache = keyCache;
+ }
+
+ public interface OnChangeListener {
+ void onSettingsChanged(boolean isEnabled);
+ }
+}
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
new file mode 100644
index 0000000..573c8bd
--- /dev/null
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+
+public final class SplitConfigurationOptions {
+
+ ///////////////////////////////////
+ // Taken from
+ // frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+ /**
+ * Stage position isn't specified normally meaning to use what ever it is currently set to.
+ */
+ public static final int STAGE_POSITION_UNDEFINED = -1;
+ /**
+ * Specifies that a stage is positioned at the top half of the screen if
+ * in portrait mode or at the left half of the screen if in landscape mode.
+ */
+ public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
+
+ /**
+ * Specifies that a stage is positioned at the bottom half of the screen if
+ * in portrait mode or at the right half of the screen if in landscape mode.
+ */
+ public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
+
+ @Retention(SOURCE)
+ @IntDef({STAGE_POSITION_UNDEFINED, STAGE_POSITION_TOP_OR_LEFT, STAGE_POSITION_BOTTOM_OR_RIGHT})
+ public @interface StagePosition {}
+
+ /**
+ * Stage type isn't specified normally meaning to use what ever the default is.
+ * E.g. exit split-screen and launch the app in fullscreen.
+ */
+ public static final int STAGE_TYPE_UNDEFINED = -1;
+ /**
+ * The main stage type.
+ */
+ public static final int STAGE_TYPE_MAIN = 0;
+
+ /**
+ * The side stage type.
+ */
+ public static final int STAGE_TYPE_SIDE = 1;
+
+ @IntDef({STAGE_TYPE_UNDEFINED, STAGE_TYPE_MAIN, STAGE_TYPE_SIDE})
+ public @interface StageType {}
+ ///////////////////////////////////
+
+ public static class SplitPositionOption {
+ public final int mIconResId;
+ public final int mTextResId;
+ @StagePosition
+ public final int mStagePosition;
+
+ @StageType
+ public final int mStageType;
+
+ public SplitPositionOption(int iconResId, int textResId, int stagePosition, int stageType) {
+ mIconResId = iconResId;
+ mTextResId = textResId;
+ mStagePosition = stagePosition;
+ mStageType = stageType;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/util/SystemUiController.java b/src/com/android/launcher3/util/SystemUiController.java
index 275c024..630df7e 100644
--- a/src/com/android/launcher3/util/SystemUiController.java
+++ b/src/com/android/launcher3/util/SystemUiController.java
@@ -19,8 +19,6 @@
import android.view.View;
import android.view.Window;
-import com.android.launcher3.Utilities;
-
import java.util.Arrays;
/**
@@ -32,7 +30,8 @@
public static final int UI_STATE_BASE_WINDOW = 0;
public static final int UI_STATE_SCRIM_VIEW = 1;
public static final int UI_STATE_WIDGET_BOTTOM_SHEET = 2;
- public static final int UI_STATE_OVERVIEW = 3;
+ public static final int UI_STATE_FULLSCREEN_TASK = 3;
+ public static final int UI_STATE_ALLAPPS = 4;
public static final int FLAG_LIGHT_NAV = 1 << 0;
public static final int FLAG_DARK_NAV = 1 << 1;
@@ -40,7 +39,7 @@
public static final int FLAG_DARK_STATUS = 1 << 3;
private final Window mWindow;
- private final int[] mStates = new int[4];
+ private final int[] mStates = new int[5];
public SystemUiController(Window window) {
mWindow = window;
@@ -77,12 +76,10 @@
}
private int getSysUiVisibilityFlags(int stateFlag, int currentVisibility) {
- if (Utilities.ATLEAST_OREO) {
- if ((stateFlag & FLAG_LIGHT_NAV) != 0) {
- currentVisibility |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
- } else if ((stateFlag & FLAG_DARK_NAV) != 0) {
- currentVisibility &= ~(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
- }
+ if ((stateFlag & FLAG_LIGHT_NAV) != 0) {
+ currentVisibility |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+ } else if ((stateFlag & FLAG_DARK_NAV) != 0) {
+ currentVisibility &= ~(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
}
if ((stateFlag & FLAG_LIGHT_STATUS) != 0) {
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index b74686f..53a584d 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -16,8 +16,12 @@
package com.android.launcher3.util;
+import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_TEXT;
+import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_THEME;
+
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
import android.content.Context;
-import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.ColorMatrix;
@@ -28,33 +32,50 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.uioverrides.WallpaperColorInfo;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.GraphicsUtils;
/**
* Various utility methods associated with theming.
*/
+@SuppressWarnings("NewApi")
public class Themes {
- public static int getActivityThemeRes(Context context) {
- WallpaperColorInfo wallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context);
- boolean darkTheme;
- if (Utilities.ATLEAST_Q) {
- Configuration configuration = context.getResources().getConfiguration();
- int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
- darkTheme = nightMode == Configuration.UI_MODE_NIGHT_YES;
- } else {
- darkTheme = wallpaperColorInfo.isDark();
- }
+ public static final String KEY_THEMED_ICONS = "themed_icons";
- if (darkTheme) {
- return wallpaperColorInfo.supportsDarkText() ?
- R.style.AppTheme_Dark_DarkText : wallpaperColorInfo.isMainColorDark() ?
- R.style.AppTheme_Dark_DarkMainColor : R.style.AppTheme_Dark;
+ public static int getActivityThemeRes(Context context) {
+ final int colorHints;
+ if (Utilities.ATLEAST_P) {
+ WallpaperColors colors = context.getSystemService(WallpaperManager.class)
+ .getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+ colorHints = colors == null ? 0 : colors.getColorHints();
} else {
- return wallpaperColorInfo.supportsDarkText() ?
- R.style.AppTheme_DarkText : wallpaperColorInfo.isMainColorDark() ?
- R.style.AppTheme_DarkMainColor : R.style.AppTheme;
+ colorHints = 0;
}
+ return getActivityThemeRes(context, colorHints);
+ }
+
+ public static int getActivityThemeRes(Context context, int wallpaperColorHints) {
+ boolean supportsDarkText = Utilities.ATLEAST_S
+ && (wallpaperColorHints & HINT_SUPPORTS_DARK_TEXT) != 0;
+ boolean isMainColorDark = Utilities.ATLEAST_S
+ && (wallpaperColorHints & HINT_SUPPORTS_DARK_THEME) != 0;
+
+ if (Utilities.isDarkTheme(context)) {
+ return supportsDarkText ? R.style.AppTheme_Dark_DarkText
+ : isMainColorDark ? R.style.AppTheme_Dark_DarkMainColor : R.style.AppTheme_Dark;
+ } else {
+ return supportsDarkText ? R.style.AppTheme_DarkText
+ : isMainColorDark ? R.style.AppTheme_DarkMainColor : R.style.AppTheme;
+ }
+ }
+
+ /**
+ * Returns true if workspace icon theming is enabled
+ */
+ public static boolean isThemedIconEnabled(Context context) {
+ return FeatureFlags.ENABLE_THEMED_ICONS.get()
+ && Utilities.getPrefs(context).getBoolean(KEY_THEMED_ICONS, false);
}
public static String getDefaultBodyFont(Context context) {
@@ -81,11 +102,18 @@
return getAttrColor(context, android.R.attr.colorAccent);
}
+ /** Returns the background color attribute. */
+ public static int getColorBackground(Context context) {
+ return getAttrColor(context, android.R.attr.colorBackground);
+ }
+
+ /** Returns the floating background color attribute. */
+ public static int getColorBackgroundFloating(Context context) {
+ return getAttrColor(context, android.R.attr.colorBackgroundFloating);
+ }
+
public static int getAttrColor(Context context, int attr) {
- TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
- int colorAccent = ta.getColor(0, 0);
- ta.recycle();
- return colorAccent;
+ return GraphicsUtils.getAttrColor(context, attr);
}
public static boolean getAttrBoolean(Context context, int attr) {
@@ -110,23 +138,6 @@
}
/**
- * Returns the alpha corresponding to the theme attribute {@param attr}, in the range [0, 255].
- */
- public static int getAlpha(Context context, int attr) {
- return (int) (255 * getFloat(context, attr, 0) + 0.5f);
- }
-
- /**
- * Returns the alpha corresponding to the theme attribute {@param attr}
- */
- public static float getFloat(Context context, int attr, float defValue) {
- TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
- float value = ta.getFloat(0, defValue);
- ta.recycle();
- return value;
- }
-
- /**
* Scales a color matrix such that, when applied to color R G B A, it produces R' G' B' A' where
* R' = r * R
* G' = g * G
diff --git a/src/com/android/launcher3/util/TouchController.java b/src/com/android/launcher3/util/TouchController.java
index fc1d819..9c397c0 100644
--- a/src/com/android/launcher3/util/TouchController.java
+++ b/src/com/android/launcher3/util/TouchController.java
@@ -32,5 +32,10 @@
*/
boolean onControllerInterceptTouchEvent(MotionEvent ev);
+ /**
+ * Called when one handed mode state changed
+ */
+ default void onOneHandedModeStateChanged(boolean activated) { }
+
default void dump(String prefix, PrintWriter writer) { }
}
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index 168227d..c23df77 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -78,7 +78,7 @@
* Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
*/
@MainThread
- public static <T> T whitelistIpcs(String rpcName, Supplier<T> supplier) {
+ public static <T> T allowIpcs(String rpcName, Supplier<T> supplier) {
Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS);
try {
return supplier.get();
diff --git a/src/com/android/launcher3/util/TranslateEdgeEffect.java b/src/com/android/launcher3/util/TranslateEdgeEffect.java
new file mode 100644
index 0000000..8fdc8df
--- /dev/null
+++ b/src/com/android/launcher3/util/TranslateEdgeEffect.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.RenderNode;
+import android.widget.EdgeEffect;
+
+/**
+ * Extension of {@link EdgeEffect} which translates the content instead of the default
+ * platform implementation
+ */
+@SuppressWarnings("NewApi")
+public class TranslateEdgeEffect extends EdgeEffectCompat {
+
+ private final RenderNode mNode;
+
+ public TranslateEdgeEffect(Context context) {
+ super(context);
+ mNode = new RenderNode("TranslateEdgeEffect");
+ }
+
+ @Override
+ public boolean draw(Canvas canvas) {
+ return false;
+ }
+
+ public boolean getTranslationShift(float[] out) {
+ Canvas c = mNode.beginRecording(1, 1);
+ boolean result = super.draw(c);
+ mNode.endRecording();
+
+ out[0] = getDistance();
+ return result;
+ }
+}
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index 0498052..0f40179 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -15,15 +15,22 @@
*/
package com.android.launcher3.util;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
+import android.view.View;
import android.view.inputmethod.InputMethodManager;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.views.ActivityContext;
+
/**
* Utility class for offloading some class from UI thread
*/
@@ -36,9 +43,30 @@
private static final int MSG_HIDE_KEYBOARD = 1;
private static final int MSG_SET_ORIENTATION = 2;
private static final int MSG_RUN_COMMAND = 3;
+ private static final String STATS_LOGGER_KEY = "STATS_LOGGER_KEY";
- public static void hideKeyboardAsync(Context context, IBinder token) {
- Message.obtain(HANDLER.get(context), MSG_HIDE_KEYBOARD, token).sendToTarget();
+ @SuppressLint("NewApi")
+ public static void hideKeyboardAsync(ActivityContext activityContext, IBinder token) {
+ View root = activityContext.getDragLayer();
+
+ // Since the launcher context cannot be accessed directly from callback, adding secondary
+ // message to log keyboard close event asynchronously.
+ Bundle mHideKeyboardLoggerMsg = new Bundle();
+ mHideKeyboardLoggerMsg.putParcelable(
+ STATS_LOGGER_KEY,
+ Message.obtain(
+ HANDLER.get(root.getContext()),
+ () -> Launcher.cast(activityContext)
+ .getStatsLogManager()
+ .logger()
+ .log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED)
+ )
+ );
+
+ Message mHideKeyboardMsg = Message.obtain(HANDLER.get(root.getContext()), MSG_HIDE_KEYBOARD,
+ token);
+ mHideKeyboardMsg.setData(mHideKeyboardLoggerMsg);
+ mHideKeyboardMsg.sendToTarget();
}
public static void setOrientationAsync(Activity activity, int orientation) {
@@ -69,7 +97,11 @@
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_HIDE_KEYBOARD:
- mIMM.hideSoftInputFromWindow((IBinder) message.obj, 0);
+ if (mIMM.hideSoftInputFromWindow((IBinder) message.obj, 0)) {
+ // log keyboard close event only when keyboard is actually closed
+ ((Message) message.getData().getParcelable(STATS_LOGGER_KEY))
+ .sendToTarget();
+ }
return true;
case MSG_SET_ORIENTATION:
((Activity) message.obj).setRequestedOrientation(message.arg1);
diff --git a/src/com/android/launcher3/util/VelocityUtils.java b/src/com/android/launcher3/util/VelocityUtils.java
new file mode 100644
index 0000000..d5962ed
--- /dev/null
+++ b/src/com/android/launcher3/util/VelocityUtils.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+/**
+ * Contains some constants and functions to standardize velocity usage.
+ */
+public class VelocityUtils {
+
+ /**
+ * Unit to pass to {@link android.view.VelocityTracker#computeCurrentVelocity(int)}.
+ */
+ public static final int PX_PER_MS = 1;
+
+}
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index 5b33f18..e413d7f 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -58,7 +58,7 @@
Preconditions.assertUIThread();
Handler handler = new Handler();
- // LayoutInflater is not thread save as it maintains a global variable 'mConstructorArgs'.
+ // LayoutInflater is not thread safe as it maintains a global variable 'mConstructorArgs'.
// Create a different copy to use on the background thread.
LayoutInflater inflater = mInflater.cloneInContext(mInflater.getContext());
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index 2ad80cf..b8554e4 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -145,14 +145,17 @@
msg.sendToTarget();
}
- private void updateOffset() {
- int numPagesForWallpaperParallax;
+ /** Returns the number of pages used for the wallpaper parallax. */
+ public int getNumPagesForWallpaperParallax() {
if (mWallpaperIsLiveWallpaper) {
- numPagesForWallpaperParallax = mNumScreens;
+ return mNumScreens;
} else {
- numPagesForWallpaperParallax = Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens);
+ return Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens);
}
- Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, numPagesForWallpaperParallax, 0,
+ }
+
+ private void updateOffset() {
+ Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, getNumPagesForWallpaperParallax(), 0,
mWindowToken).sendToTarget();
}
diff --git a/src/com/android/launcher3/util/WindowBounds.java b/src/com/android/launcher3/util/WindowBounds.java
index 3c2fb62..c92770e 100644
--- a/src/com/android/launcher3/util/WindowBounds.java
+++ b/src/com/android/launcher3/util/WindowBounds.java
@@ -15,11 +15,16 @@
*/
package com.android.launcher3.util;
+import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
+import android.view.WindowInsets.Type;
+import android.view.WindowMetrics;
import androidx.annotation.Nullable;
+import java.util.Objects;
+
/**
* Utility class to hold information about window position and layout
*/
@@ -36,6 +41,18 @@
bounds.height() - insets.top - insets.bottom);
}
+ public WindowBounds(int width, int height, int availableWidth, int availableHeight) {
+ this.bounds = new Rect(0, 0, width, height);
+ this.availableSize = new Point(availableWidth, availableHeight);
+ // We don't care about insets in this case
+ this.insets = new Rect(0, 0, width - availableWidth, height - availableHeight);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bounds, insets);
+ }
+
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof WindowBounds)) {
@@ -44,4 +61,30 @@
WindowBounds other = (WindowBounds) obj;
return other.bounds.equals(bounds) && other.insets.equals(insets);
}
+
+ @Override
+ public String toString() {
+ return "WindowBounds{"
+ + "bounds=" + bounds
+ + ", insets=" + insets
+ + ", availableSize=" + availableSize
+ + '}';
+ }
+
+ /**
+ * Returns true if the device is in landscape orientation
+ */
+ public final boolean isLandscape() {
+ return availableSize.x > availableSize.y;
+ }
+
+ /**
+ * Returns the bounds corresponding to the provided WindowMetrics
+ */
+ @SuppressWarnings("NewApi")
+ public static WindowBounds fromWindowMetrics(WindowMetrics wm) {
+ Insets insets = wm.getWindowInsets().getInsets(Type.systemBars());
+ return new WindowBounds(wm.getBounds(),
+ new Rect(insets.left, insets.top, insets.right, insets.bottom));
+ }
}
diff --git a/src/com/android/launcher3/util/WindowManagerCompat.java b/src/com/android/launcher3/util/WindowManagerCompat.java
new file mode 100644
index 0000000..38a63de
--- /dev/null
+++ b/src/com/android/launcher3/util/WindowManagerCompat.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE;
+import static com.android.launcher3.Utilities.dpiFromPx;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.util.DisplayController.PortraitSize;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utility class to simulate window manager APIs until proper APIs are available
+ */
+@TargetApi(Build.VERSION_CODES.S)
+public class WindowManagerCompat {
+
+ public static final int MIN_TABLET_WIDTH = 600;
+
+ /**
+ * Returns a set of supported render sizes for a set of internal displays.
+ * This is a temporary workaround which assumes only nav-bar insets change across displays
+ * @param consumeTaskBar if true, it assumes that task bar is part of the app window
+ * and ignores any insets because of task bar.
+ */
+ public static Set<WindowMetrics> getDisplayProfiles(
+ Context windowContext, Collection<PortraitSize> allDisplaySizes,
+ int densityDpi, boolean consumeTaskBar) {
+ WindowInsets metrics = windowContext.getSystemService(WindowManager.class)
+ .getMaximumWindowMetrics().getWindowInsets();
+ boolean hasNavbar = ResourceUtils.getIntegerByName(
+ "config_navBarInteractionMode",
+ windowContext.getResources(),
+ INVALID_RESOURCE_HANDLE) != 0;
+
+ WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(metrics);
+
+ Set<WindowMetrics> result = new HashSet<>();
+ for (PortraitSize size : allDisplaySizes) {
+ int swDP = (int) dpiFromPx(size.width, densityDpi);
+ boolean isTablet = swDP >= MIN_TABLET_WIDTH;
+
+ final Insets portraitNav, landscapeNav;
+ if (isTablet && !consumeTaskBar) {
+ portraitNav = landscapeNav = Insets.of(0, 0, 0, windowContext.getResources()
+ .getDimensionPixelSize(R.dimen.taskbar_size));
+ } else if (hasNavbar) {
+ portraitNav = Insets.of(0, 0, 0,
+ getSystemResource(windowContext, "navigation_bar_height", swDP));
+ landscapeNav = isTablet
+ ? Insets.of(0, 0, 0, getSystemResource(windowContext,
+ "navigation_bar_height_landscape", swDP))
+ : Insets.of(0, 0, getSystemResource(windowContext,
+ "navigation_bar_width", swDP), 0);
+ } else {
+ portraitNav = landscapeNav = Insets.of(0, 0, 0, 0);
+ }
+
+ result.add(new WindowMetrics(
+ new Rect(0, 0, size.width, size.height),
+ insetsBuilder.setInsets(Type.navigationBars(), portraitNav).build()));
+ result.add(new WindowMetrics(
+ new Rect(0, 0, size.height, size.width),
+ insetsBuilder.setInsets(Type.navigationBars(), landscapeNav).build()));
+ }
+ return result;
+ }
+
+ private static int getSystemResource(Context context, String key, int swDp) {
+ int resourceId = context.getResources().getIdentifier(key, "dimen", "android");
+ if (resourceId > 0) {
+ Configuration conf = new Configuration();
+ conf.smallestScreenWidthDp = swDp;
+ return context.createConfigurationContext(conf)
+ .getResources().getDimensionPixelSize(resourceId);
+ }
+ return 0;
+ }
+}
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 11c1029..92ca8a1 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -31,17 +31,21 @@
import android.view.animation.Interpolator;
import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.touch.BaseSwipeDetector;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import java.util.ArrayList;
+import java.util.List;
+
/**
- * Extension of AbstractFloatingView with common methods for sliding in from bottom
+ * Extension of {@link AbstractFloatingView} with common methods for sliding in from bottom.
+ *
+ * @param <T> Type of ActivityContext inflating this view.
*/
-public abstract class AbstractSlideInView extends AbstractFloatingView
- implements SingleAxisSwipeDetector.Listener {
+public abstract class AbstractSlideInView<T extends Context & ActivityContext>
+ extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {
protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
@@ -59,22 +63,24 @@
protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
protected static final float TRANSLATION_SHIFT_OPENED = 0f;
- protected final Launcher mLauncher;
+ protected final T mActivityContext;
+
protected final SingleAxisSwipeDetector mSwipeDetector;
protected final ObjectAnimator mOpenCloseAnimator;
protected View mContent;
- private final View mColorScrim;
+ protected final View mColorScrim;
protected Interpolator mScrollInterpolator;
// range [0, 1], 0=> completely open, 1=> completely closed
protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
protected boolean mNoIntercept;
+ protected List<OnCloseListener> mOnCloseListeners = new ArrayList<>();
public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- mLauncher = Launcher.getLauncher(context);
+ mActivityContext = ActivityContext.lookupContext(context);
mScrollInterpolator = Interpolators.SCROLL_CUBIC;
mSwipeDetector = new SingleAxisSwipeDetector(context, this,
@@ -88,8 +94,8 @@
announceAccessibilityChanges();
}
});
- int scrimColor = getScrimColor(mLauncher);
- mColorScrim = scrimColor != -1 ? createColorScrim(mLauncher, scrimColor) : null;
+ int scrimColor = getScrimColor(context);
+ mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
}
protected void attachToContainer() {
@@ -120,8 +126,8 @@
return false;
}
- int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
- SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
+ int directionsToDetectScroll = mSwipeDetector.isIdleState()
+ ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
mSwipeDetector.setDetectableScrollConditions(
directionsToDetectScroll, false);
mSwipeDetector.onTouchEvent(ev);
@@ -176,6 +182,11 @@
}
}
+ /** Registers an {@link OnCloseListener}. */
+ public void addOnCloseListener(OnCloseListener listener) {
+ mOnCloseListeners.add(listener);
+ }
+
protected void handleClose(boolean animate, long defaultDuration) {
if (!mIsOpen) {
return;
@@ -210,14 +221,14 @@
if (mColorScrim != null) {
getPopupContainer().removeView(mColorScrim);
}
+ mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed);
}
protected BaseDragLayer getPopupContainer() {
- return mLauncher.getDragLayer();
+ return mActivityContext.getDragLayer();
}
-
- protected static View createColorScrim(Context context, int bgColor) {
+ protected View createColorScrim(Context context, int bgColor) {
View view = new View(context);
view.forceHasOverlappingRendering(false);
view.setBackgroundColor(bgColor);
@@ -228,4 +239,15 @@
return view;
}
+
+ /**
+ * Interface to report that the {@link AbstractSlideInView} has closed.
+ */
+ public interface OnCloseListener {
+
+ /**
+ * Called when {@link AbstractSlideInView} closes.
+ */
+ void onSlideInViewClosed();
+ }
}
diff --git a/src/com/android/launcher3/views/AccessibilityActionsView.java b/src/com/android/launcher3/views/AccessibilityActionsView.java
new file mode 100644
index 0000000..1d136c3
--- /dev/null
+++ b/src/com/android/launcher3/views/AccessibilityActionsView.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.views;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.views.OptionsPopupView.OptionItem;
+
+/**
+ * Placeholder view to expose additional Launcher actions via accessibility actions
+ */
+public class AccessibilityActionsView extends View implements StateListener<LauncherState> {
+
+ public AccessibilityActionsView(Context context) {
+ this(context, null);
+ }
+
+ public AccessibilityActionsView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AccessibilityActionsView(Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ Launcher.getLauncher(context).getStateManager().addStateListener(this);
+ setWillNotDraw(true);
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ setImportantForAccessibility(finalState == NORMAL
+ ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO);
+ }
+
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo() {
+ AccessibilityNodeInfo info = super.createAccessibilityNodeInfo();
+ Launcher l = Launcher.getLauncher(getContext());
+ info.addAction(new AccessibilityAction(
+ R.string.all_apps_button_label, l.getText(R.string.all_apps_button_label)));
+ for (OptionItem item : OptionsPopupView.getOptions(l)) {
+ info.addAction(new AccessibilityAction(item.labelRes, item.label));
+ }
+ return info;
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ if (super.performAccessibilityAction(action, arguments)) {
+ return true;
+ }
+ Launcher l = Launcher.getLauncher(getContext());
+ if (action == R.string.all_apps_button_label) {
+ l.getStateManager().goToState(ALL_APPS);
+ return true;
+ }
+ for (OptionItem item : OptionsPopupView.getOptions(l)) {
+ if (item.labelRes == action) {
+ if (item.eventId.getId() > 0) {
+ l.getStatsLogManager().logger().log(item.eventId);
+ }
+ if (item.clickListener.onLongClick(this)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index ae459e1..646b669 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -17,12 +17,15 @@
import android.content.Context;
import android.content.ContextWrapper;
-import android.view.ContextThemeWrapper;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
import android.view.View.AccessibilityDelegate;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.dot.DotInfo;
+import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.ViewCache;
/**
* An interface to be used along with a context for various activities in Launcher. This allows a
@@ -49,6 +52,35 @@
return null;
}
+ default Rect getFolderBoundingBox() {
+ return getDeviceProfile().getAbsoluteOpenFolderBounds();
+ }
+
+ /**
+ * After calling {@link #getFolderBoundingBox()}, we calculate a (left, top) position for a
+ * Folder of size width x height to be within those bounds. However, the chosen position may
+ * not be visually ideal (e.g. uncanny valley of centeredness), so here's a chance to update it.
+ * @param inOutPosition A 2-size array where the first element is the left position of the open
+ * folder and the second element is the top position. Should be updated in place if desired.
+ * @param bounds The bounds that the open folder should fit inside.
+ * @param width The width of the open folder.
+ * @param height The height of the open folder.
+ */
+ default void updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height) {
+ }
+
+ /**
+ * Returns a LayoutInflater that is cloned in this Context, so that Views inflated by it will
+ * have the same Context. (i.e. {@link #lookupContext(Context)} will find this ActivityContext.)
+ */
+ default LayoutInflater getLayoutInflater() {
+ if (this instanceof Context) {
+ Context context = (Context) this;
+ return LayoutInflater.from(context).cloneInContext(context);
+ }
+ return null;
+ }
+
/**
* The root view to support drag-and-drop and popup support.
*/
@@ -56,10 +88,24 @@
DeviceProfile getDeviceProfile();
- static ActivityContext lookupContext(Context context) {
+ default ViewCache getViewCache() {
+ return new ViewCache();
+ }
+
+ /**
+ * Controller for supporting item drag-and-drop
+ */
+ default <T extends DragController> T getDragController() {
+ return null;
+ }
+
+ /**
+ * Returns the ActivityContext associated with the given Context.
+ */
+ static <T extends Context & ActivityContext> T lookupContext(Context context) {
if (context instanceof ActivityContext) {
- return (ActivityContext) context;
- } else if (context instanceof ContextThemeWrapper) {
+ return (T) context;
+ } else if (context instanceof ContextWrapper) {
return lookupContext(((ContextWrapper) context).getBaseContext());
} else {
throw new IllegalArgumentException("Cannot find ActivityContext in parent tree");
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index b4a6b14..e449a4b 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -17,11 +17,13 @@
package com.android.launcher3.views;
import android.content.Context;
+import android.content.res.Configuration;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
+import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.os.Handler;
-import android.util.TypedValue;
+import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
@@ -29,6 +31,8 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
import androidx.core.content.ContextCompat;
import com.android.launcher3.AbstractFloatingView;
@@ -43,6 +47,7 @@
*/
public class ArrowTipView extends AbstractFloatingView {
+ private static final String TAG = ArrowTipView.class.getSimpleName();
private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
private static final long SHOW_DELAY_MS = 200;
private static final long SHOW_DURATION_MS = 300;
@@ -50,11 +55,20 @@
protected final BaseDraggingActivity mActivity;
private final Handler mHandler = new Handler();
+ private final int mArrowWidth;
+ private boolean mIsPointingUp;
private Runnable mOnClosed;
+ private View mArrowView;
public ArrowTipView(Context context) {
+ this(context, false);
+ }
+
+ public ArrowTipView(Context context, boolean isPointingUp) {
super(context, null, 0);
mActivity = BaseDraggingActivity.fromContext(context);
+ mIsPointingUp = isPointingUp;
+ mArrowWidth = context.getResources().getDimensionPixelSize(R.dimen.arrow_toast_arrow_width);
init(context);
}
@@ -62,6 +76,9 @@
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
close(true);
+ if (mActivity.getDragLayer().isEventOverView(this, ev)) {
+ return true;
+ }
}
return false;
}
@@ -87,10 +104,6 @@
}
@Override
- public void logActionCommand(int command) {
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_ON_BOARD_POPUP) != 0;
}
@@ -98,27 +111,9 @@
private void init(Context context) {
inflate(context, R.layout.arrow_toast, this);
setOrientation(LinearLayout.VERTICAL);
- View dismissButton = findViewById(R.id.dismiss);
- dismissButton.setOnClickListener(view -> {
- handleClose(true);
- });
- View arrowView = findViewById(R.id.arrow);
- ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
- ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
- arrowLp.width, arrowLp.height, false));
- Paint arrowPaint = arrowDrawable.getPaint();
- TypedValue typedValue = new TypedValue();
- context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
- arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
- // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
- arrowPaint.setPathEffect(new CornerPathEffect(
- context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
- arrowView.setBackground(arrowDrawable);
-
- mIsOpen = true;
-
- mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
+ mArrowView = findViewById(R.id.arrow);
+ updateArrowTipInView();
}
/**
@@ -134,7 +129,7 @@
* @param text The text to be shown in the tooltip.
* @param gravity The gravity aligns the tooltip center, start, or end.
* @param arrowMarginStart The margin from start to place arrow (ignored if center)
- * @param top The Y coordinate of the bottom of tooltip.
+ * @param top The Y coordinate of the bottom of tooltip.
* @return The tooltip.
*/
public ArrowTipView show(String text, int gravity, int arrowMarginStart, int top) {
@@ -144,19 +139,25 @@
DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
params.gravity = gravity;
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) findViewById(
- R.id.arrow).getLayoutParams();
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mArrowView.getLayoutParams();
lp.gravity = gravity;
+
+ if (parent.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
+ arrowMarginStart = parent.getMeasuredWidth() - arrowMarginStart;
+ }
if (gravity == Gravity.END) {
- lp.setMarginEnd(parent.getMeasuredWidth() - arrowMarginStart);
+ lp.setMarginEnd(parent.getMeasuredWidth() - arrowMarginStart - mArrowWidth);
} else if (gravity == Gravity.START) {
- lp.setMarginStart(arrowMarginStart);
+ lp.setMarginStart(arrowMarginStart - mArrowWidth / 2);
}
requestLayout();
params.leftMargin = mActivity.getDeviceProfile().workspacePadding.left;
params.rightMargin = mActivity.getDeviceProfile().workspacePadding.right;
- post(() -> setY(top - getHeight()));
+ post(() -> setY(top - (mIsPointingUp ? 0 : getHeight())));
+
+ mIsOpen = true;
+ mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
setAlpha(0);
animate()
.alpha(1f)
@@ -169,10 +170,159 @@
}
/**
+ * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
+ * cannot fit on screen in the requested orientation.
+ *
+ * @param text The text to be shown in the tooltip.
+ * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
+ * center of tooltip unless the tooltip goes beyond screen margin.
+ * @param yCoord The Y coordinate of the pointed tip end of the tooltip.
+ * @return The tool tip view. {@code null} if the tip can not be shown.
+ */
+ @Nullable public ArrowTipView showAtLocation(String text, @Px int arrowXCoord, @Px int yCoord) {
+ return showAtLocation(
+ text,
+ arrowXCoord,
+ /* yCoordDownPointingTip= */ yCoord,
+ /* yCoordUpPointingTip= */ yCoord);
+ }
+
+ /**
+ * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
+ * cannot fit on screen in the requested orientation.
+ *
+ * @param text The text to be shown in the tooltip.
+ * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
+ * center of tooltip unless the tooltip goes beyond screen margin.
+ * @param rect The coordinates of the view which requests the tooltip to be shown.
+ * @param margin The margin between {@param rect} and the tooltip.
+ * @return The tool tip view. {@code null} if the tip can not be shown.
+ */
+ @Nullable public ArrowTipView showAroundRect(
+ String text, @Px int arrowXCoord, Rect rect, @Px int margin) {
+ return showAtLocation(
+ text,
+ arrowXCoord,
+ /* yCoordDownPointingTip= */ rect.top - margin,
+ /* yCoordUpPointingTip= */ rect.bottom + margin);
+ }
+
+ /**
+ * Show the ArrowTipView (tooltip) custom aligned. The tooltip is vertically flipped if it
+ * cannot fit on screen in the requested orientation.
+ *
+ * @param text The text to be shown in the tooltip.
+ * @param arrowXCoord The X coordinate for the arrow on the tooltip. The arrow is usually in the
+ * center of tooltip unless the tooltip goes beyond screen margin.
+ * @param yCoordDownPointingTip The Y coordinate of the pointed tip end of the tooltip when the
+ * tooltip is placed pointing downwards.
+ * @param yCoordUpPointingTip The Y coordinate of the pointed tip end of the tooltip when the
+ * tooltip is placed pointing upwards.
+ * @return The tool tip view. {@code null} if the tip can not be shown.
+ */
+ @Nullable private ArrowTipView showAtLocation(String text, @Px int arrowXCoord,
+ @Px int yCoordDownPointingTip, @Px int yCoordUpPointingTip) {
+ ViewGroup parent = mActivity.getDragLayer();
+ @Px int parentViewWidth = parent.getWidth();
+ @Px int parentViewHeight = parent.getHeight();
+ @Px int maxTextViewWidth = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.widget_picker_education_tip_max_width);
+ @Px int minViewMargin = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin);
+ if (parentViewWidth < maxTextViewWidth + 2 * minViewMargin) {
+ Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth);
+ return null;
+ }
+
+ TextView textView = findViewById(R.id.text);
+ textView.setText(text);
+ textView.setMaxWidth(maxTextViewWidth);
+ parent.addView(this);
+ requestLayout();
+
+ post(() -> {
+ // Adjust the tooltip horizontally.
+ float halfWidth = getWidth() / 2f;
+ float xCoord;
+ if (arrowXCoord - halfWidth < minViewMargin) {
+ // If the tooltip is estimated to go beyond the left margin, place its start just at
+ // the left margin.
+ xCoord = minViewMargin;
+ } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) {
+ // If the tooltip is estimated to go beyond the right margin, place it such that its
+ // end is just at the right margin.
+ xCoord = parentViewWidth - minViewMargin - getWidth();
+ } else {
+ // Place the tooltip such that its center is at arrowXCoord.
+ xCoord = arrowXCoord - halfWidth;
+ }
+ setX(xCoord);
+
+ // Adjust the tooltip vertically.
+ @Px int viewHeight = getHeight();
+ if (mIsPointingUp
+ ? (yCoordUpPointingTip + viewHeight > parentViewHeight)
+ : (yCoordDownPointingTip - viewHeight < 0)) {
+ // Flip the view if it exceeds the vertical bounds of screen.
+ mIsPointingUp = !mIsPointingUp;
+ updateArrowTipInView();
+ }
+ // Place the tooltip such that its top is at yCoordUpPointingTip if arrow is displayed
+ // pointing upwards, otherwise place it such that its bottom is at
+ // yCoordDownPointingTip.
+ setY(mIsPointingUp ? yCoordUpPointingTip : yCoordDownPointingTip - viewHeight);
+
+ // Adjust the arrow's relative position on tooltip to make sure the actual position of
+ // arrow's pointed tip is always at arrowXCoord.
+ mArrowView.setX(arrowXCoord - xCoord - mArrowView.getWidth() / 2f);
+ requestLayout();
+ });
+
+ mIsOpen = true;
+ mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
+ setAlpha(0);
+ animate()
+ .alpha(1f)
+ .withLayer()
+ .setStartDelay(SHOW_DELAY_MS)
+ .setDuration(SHOW_DURATION_MS)
+ .setInterpolator(Interpolators.DEACCEL)
+ .start();
+ return this;
+ }
+
+ private void updateArrowTipInView() {
+ ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams();
+ ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+ arrowLp.width, arrowLp.height, mIsPointingUp));
+ Paint arrowPaint = arrowDrawable.getPaint();
+ @Px int arrowTipRadius = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.arrow_toast_corner_radius);
+ arrowPaint.setColor(ContextCompat.getColor(getContext(), R.color.arrow_tip_view_bg));
+ arrowPaint.setPathEffect(new CornerPathEffect(arrowTipRadius));
+ mArrowView.setBackground(arrowDrawable);
+ // Add negative margin so that the rounded corners on base of arrow are not visible.
+ removeView(mArrowView);
+ if (mIsPointingUp) {
+ addView(mArrowView, 0);
+ ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, 0, 0, -1 * arrowTipRadius);
+ } else {
+ addView(mArrowView, 1);
+ ((ViewGroup.MarginLayoutParams) arrowLp).setMargins(0, -1 * arrowTipRadius, 0, 0);
+ }
+ }
+
+ /**
* Register a callback fired when toast is hidden
*/
public ArrowTipView setOnClosedCallback(Runnable runnable) {
mOnClosed = runnable;
return this;
}
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ close(/* animate= */ false);
+ }
}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index b010b4b..01c0b56 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -20,20 +20,16 @@
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
import android.annotation.TargetApi;
-import android.app.WallpaperInfo;
import android.app.WallpaperManager;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
-import android.util.Log;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
@@ -43,16 +39,11 @@
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
-import androidx.annotation.Nullable;
-
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.TouchController;
import java.io.PrintWriter;
@@ -119,9 +110,6 @@
protected final T mActivity;
private final MultiValueAlpha mMultiValueAlpha;
private final WallpaperManager mWallpaperManager;
- private final SimpleBroadcastReceiver mWallpaperChangeReceiver =
- new SimpleBroadcastReceiver(this::onWallpaperChanged);
- private final String[] mWallpapersWithoutSysuiScrims;
// All the touch controllers for the view
protected TouchController[] mControllers;
@@ -132,15 +120,11 @@
private TouchCompleteListener mTouchCompleteListener;
- protected boolean mAllowSysuiScrims = true;
-
public BaseDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
super(context, attrs);
mActivity = (T) ActivityContext.lookupContext(context);
mMultiValueAlpha = new MultiValueAlpha(this, alphaChannelCount);
mWallpaperManager = context.getSystemService(WallpaperManager.class);
- mWallpapersWithoutSysuiScrims = getResources().getStringArray(
- R.array.live_wallpapers_remove_sysui_scrims);
}
/**
@@ -191,12 +175,6 @@
}
private TouchController findControllerToHandleTouch(MotionEvent ev) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "findControllerToHandleTouch ev=" + ev
- + ", isEventInLauncher=" + isEventInLauncher(ev)
- + ", topOpenView=" + AbstractFloatingView.getTopOpenView(mActivity));
- }
-
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null
&& (isEventInLauncher(ev) || topView.canInterceptEventsInSystemGestureRegion())
@@ -214,15 +192,19 @@
protected boolean findActiveController(MotionEvent ev) {
mActiveController = null;
- if ((mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
- | TOUCH_DISPATCHING_FROM_PROXY)) == 0) {
- // Only look for controllers if we are not dispatching from gesture area and proxy is
- // not active
+ if (canFindActiveController()) {
mActiveController = findControllerToHandleTouch(ev);
}
return mActiveController != null;
}
+ protected boolean canFindActiveController() {
+ // Only look for controllers if we are not dispatching from gesture area and proxy is
+ // not active
+ return (mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
+ | TOUCH_DISPATCHING_FROM_PROXY)) == 0;
+ }
+
@Override
public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
// Shortcuts can appear above folder
@@ -463,12 +445,6 @@
}
@Override
- public boolean dispatchUnhandledMove(View focused, int direction) {
- // Consume the unhandled move if a container is open, to avoid switching pages underneath.
- return AbstractFloatingView.getTopOpenView(mActivity) != null;
- }
-
- @Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
View topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null) {
@@ -572,46 +548,4 @@
}
return super.dispatchApplyWindowInsets(insets);
}
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mWallpaperChangeReceiver.register(mActivity, Intent.ACTION_WALLPAPER_CHANGED);
- onWallpaperChanged(null);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mActivity.unregisterReceiver(mWallpaperChangeReceiver);
- }
-
- private void onWallpaperChanged(Intent unusedBroadcastIntent) {
- WallpaperInfo newWallpaperInfo = mWallpaperManager.getWallpaperInfo();
- boolean oldAllowSysuiScrims = mAllowSysuiScrims;
- mAllowSysuiScrims = computeAllowSysuiScrims(newWallpaperInfo);
- if (mAllowSysuiScrims != oldAllowSysuiScrims) {
- // Reapply insets so scrim can be removed or re-added if necessary.
- setInsets(mInsets);
- }
- }
-
- /**
- * Determines whether we can scrim the status bar and nav bar for the given wallpaper by
- * checking against a list of live wallpapers that we don't show the scrims on.
- */
- private boolean computeAllowSysuiScrims(@Nullable WallpaperInfo newWallpaperInfo) {
- if (newWallpaperInfo == null) {
- // New wallpaper is static, not live. Thus, blacklist isn't applicable.
- return true;
- }
- ComponentName newWallpaper = newWallpaperInfo.getComponent();
- for (String wallpaperWithoutScrim : mWallpapersWithoutSysuiScrims) {
- if (newWallpaper.equals(ComponentName.unflattenFromString(wallpaperWithoutScrim))) {
- // New wallpaper is blacklisted from showing a scrim.
- return false;
- }
- }
- return true;
- }
}
diff --git a/src/com/android/launcher3/views/BubbleTextHolder.java b/src/com/android/launcher3/views/BubbleTextHolder.java
new file mode 100644
index 0000000..47d3563
--- /dev/null
+++ b/src/com/android/launcher3/views/BubbleTextHolder.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+import com.android.launcher3.BubbleTextView;
+
+/**
+ * Views that contain {@link BubbleTextView} should implement this interface.
+ */
+public interface BubbleTextHolder {
+ BubbleTextView getBubbleText();
+}
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 1a8e11b..a66b3f9 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -15,10 +15,13 @@
*/
package com.android.launcher3.views;
+import static com.android.launcher3.Utilities.boundToRange;
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+import static java.lang.Math.max;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
@@ -36,6 +39,7 @@
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewOutlineProvider;
import androidx.annotation.Nullable;
@@ -44,8 +48,6 @@
import androidx.dynamicanimation.animation.SpringForce;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InsettableFrameLayout.LayoutParams;
-import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
@@ -94,7 +96,6 @@
}
};
- private final Launcher mLauncher;
private final int mBlurSizeOutline;
private final boolean mIsRtl;
@@ -128,7 +129,6 @@
public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- mLauncher = Launcher.getLauncher(context);
mBlurSizeOutline = getResources().getDimensionPixelSize(
R.dimen.blur_size_medium_outline);
mIsRtl = Utilities.isRtl(getResources());
@@ -143,10 +143,45 @@
.setStiffness(SpringForce.STIFFNESS_LOW));
}
- void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
- boolean isOpening, float scale, float minSize, LayoutParams parentLp,
+ /**
+ * Update the icon UI to match the provided parameters during an animation frame
+ */
+ public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
+ int fgIconAlpha, boolean isOpening, View container, DeviceProfile dp,
boolean isVerticalBarLayout) {
- DeviceProfile dp = mLauncher.getDeviceProfile();
+ MarginLayoutParams lp = (MarginLayoutParams) container.getLayoutParams();
+
+ float dX = mIsRtl
+ ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
+ : rect.left - lp.getMarginStart();
+ float dY = rect.top - lp.topMargin;
+ container.setTranslationX(dX);
+ container.setTranslationY(dY);
+
+ float minSize = Math.min(lp.width, lp.height);
+ float scaleX = rect.width() / minSize;
+ float scaleY = rect.height() / minSize;
+ float scale = Math.max(1f, Math.min(scaleX, scaleY));
+
+ if (Float.isNaN(scale)) {
+ // Views are no longer laid out, do not update.
+ return;
+ }
+
+ update(rect, progress, shapeProgressStart, cornerRadius, fgIconAlpha, isOpening, scale,
+ minSize, lp, isVerticalBarLayout, dp);
+
+ container.setPivotX(0);
+ container.setPivotY(0);
+ container.setScaleX(scale);
+ container.setScaleY(scale);
+
+ container.invalidate();
+ }
+
+ private void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
+ int fgIconAlpha, boolean isOpening, float scale, float minSize,
+ MarginLayoutParams parentLp, boolean isVerticalBarLayout, DeviceProfile dp) {
float dX = mIsRtl
? rect.left - (dp.widthPx - parentLp.getMarginStart() - parentLp.width)
: rect.left - parentLp.getMarginStart();
@@ -154,9 +189,9 @@
// shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
- float shapeRevealProgress = Utilities.boundToRange(mapToRange(
- Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax,
- LINEAR), 0, 1);
+
+ float shapeRevealProgress = boundToRange(mapToRange(max(shapeProgressStart, progress),
+ shapeProgressStart, 1f, 0, toMax, LINEAR), 0, 1);
if (isVerticalBarLayout) {
mOutline.right = (int) (rect.width() / scale);
@@ -198,6 +233,8 @@
sTmpRect.offset(diffX, diffY);
mForeground.setBounds(sTmpRect);
} else {
+ mForeground.setAlpha(fgIconAlpha);
+
// Spring the foreground relative to the icon's movement within the DragLayer.
int diffX = (int) (dX / dp.availableWidthPx * FG_TRANS_X_FACTOR);
int diffY = (int) (dY / dp.availableHeightPx * FG_TRANS_Y_FACTOR);
@@ -228,8 +265,11 @@
}
}
- void setIcon(@Nullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening,
- boolean isVerticalBarLayout) {
+ /**
+ * Sets the icon for this view as part of initial setup
+ */
+ public void setIcon(@Nullable Drawable drawable, int iconOffset, MarginLayoutParams lp,
+ boolean isOpening, boolean isVerticalBarLayout, DeviceProfile dp) {
mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
if (mIsAdaptiveIcon) {
boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
@@ -264,15 +304,14 @@
Utilities.scaleRectAboutCenter(mStartRevealRect, IconShape.getNormalizationScale());
}
- float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
if (isVerticalBarLayout) {
- lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
+ lp.width = (int) Math.max(lp.width, lp.height * dp.aspectRatio);
} else {
- lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
+ lp.height = (int) Math.max(lp.height, lp.width * dp.aspectRatio);
}
int left = mIsRtl
- ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
+ ? dp.widthPx - lp.getMarginStart() - lp.width
: lp.leftMargin;
layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index d89e7f8..a309e6e 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -60,7 +60,7 @@
// We enhance the shadow by drawing the shadow twice
getPaint().setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0,
- setColorAlphaBound(mShadowInfo.ambientShadowColor, alpha));
+ getTextShadowColor(mShadowInfo.ambientShadowColor, alpha));
drawWithoutDot(canvas);
canvas.save();
@@ -68,8 +68,11 @@
getScrollX() + getWidth(),
getScrollY() + getHeight());
- getPaint().setShadowLayer(mShadowInfo.keyShadowBlur, 0.0f, mShadowInfo.keyShadowOffset,
- setColorAlphaBound(mShadowInfo.keyShadowColor, alpha));
+ getPaint().setShadowLayer(
+ mShadowInfo.keyShadowBlur,
+ mShadowInfo.keyShadowOffsetX,
+ mShadowInfo.keyShadowOffsetY,
+ getTextShadowColor(mShadowInfo.keyShadowColor, alpha));
drawWithoutDot(canvas);
canvas.restore();
@@ -81,7 +84,8 @@
public final int ambientShadowColor;
public final float keyShadowBlur;
- public final float keyShadowOffset;
+ public final float keyShadowOffsetX;
+ public final float keyShadowOffsetY;
public final int keyShadowColor;
public ShadowInfo(Context c, AttributeSet attrs, int defStyle) {
@@ -89,11 +93,13 @@
TypedArray a = c.obtainStyledAttributes(
attrs, R.styleable.ShadowInfo, defStyle, 0);
- ambientShadowBlur = a.getDimension(R.styleable.ShadowInfo_ambientShadowBlur, 0);
+ ambientShadowBlur = a.getDimensionPixelSize(
+ R.styleable.ShadowInfo_ambientShadowBlur, 0);
ambientShadowColor = a.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0);
- keyShadowBlur = a.getDimension(R.styleable.ShadowInfo_keyShadowBlur, 0);
- keyShadowOffset = a.getDimension(R.styleable.ShadowInfo_keyShadowOffset, 0);
+ keyShadowBlur = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0);
+ keyShadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0);
+ keyShadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0);
keyShadowColor = a.getColor(R.styleable.ShadowInfo_keyShadowColor, 0);
a.recycle();
}
@@ -105,17 +111,26 @@
if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
textView.getPaint().clearShadowLayer();
return true;
- } else if (ambientShadowAlpha > 0) {
+ } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
textView.getPaint().setShadowLayer(ambientShadowBlur, 0, 0,
- setColorAlphaBound(ambientShadowColor, textAlpha));
+ getTextShadowColor(ambientShadowColor, textAlpha));
return true;
- } else if (keyShadowAlpha > 0) {
- textView.getPaint().setShadowLayer(keyShadowBlur, 0.0f, keyShadowOffset,
- setColorAlphaBound(keyShadowColor, textAlpha));
+ } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
+ textView.getPaint().setShadowLayer(
+ keyShadowBlur,
+ keyShadowOffsetX,
+ keyShadowOffsetY,
+ getTextShadowColor(keyShadowColor, textAlpha));
return true;
} else {
return false;
}
}
}
+
+ // Multiplies the alpha of shadowColor by textAlpha.
+ private static int getTextShadowColor(int shadowColor, int textAlpha) {
+ return setColorAlphaBound(shadowColor,
+ Math.round(Color.alpha(shadowColor) * textAlpha / 255f));
+ }
}
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 177aff4..872adec 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -15,16 +15,13 @@
*/
package com.android.launcher3.views;
-import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA;
import static com.android.launcher3.Utilities.getBadge;
import static com.android.launcher3.Utilities.getFullDrawable;
import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
@@ -47,7 +44,6 @@
import androidx.annotation.WorkerThread;
import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
@@ -55,18 +51,20 @@
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.testing.TestProtocol;
/**
* A view that is created to look like another view with the purpose of creating fluid animations.
*/
@TargetApi(Build.VERSION_CODES.Q)
public class FloatingIconView extends FrameLayout implements
- Animator.AnimatorListener, OnGlobalLayoutListener {
+ Animator.AnimatorListener, OnGlobalLayoutListener, FloatingView {
private static final String TAG = FloatingIconView.class.getSimpleName();
@@ -74,7 +72,6 @@
private static @Nullable IconLoadResult sIconLoadResult;
public static final float SHAPE_PROGRESS_DURATION = 0.10f;
- private static final int FADE_DURATION_MS = 200;
private static final RectF sTmpRectF = new RectF();
private static final Object[] sTmpObjArray = new Object[1];
@@ -89,6 +86,9 @@
private IconLoadResult mIconLoadResult;
+ // Draw the drawable of the BubbleTextView behind ClipIconView to reveal the built in shadow.
+ private View mBtvDrawable;
+
private ClipIconView mClipIconView;
private @Nullable Drawable mBadge;
@@ -98,10 +98,11 @@
private final Rect mFinalDrawableBounds = new Rect();
- private AnimatorSet mFadeAnimatorSet;
private ListenerView mListenerView;
private Runnable mFastFinishRunnable;
+ private float mIconOffsetY;
+
public FloatingIconView(Context context) {
this(context, null);
}
@@ -116,6 +117,8 @@
mIsRtl = Utilities.isRtl(getResources());
mListenerView = new ListenerView(context, attrs);
mClipIconView = new ClipIconView(context, attrs);
+ mBtvDrawable = new ImageView(context, attrs);
+ addView(mBtvDrawable);
addView(mClipIconView);
setWillNotDraw(false);
}
@@ -136,40 +139,18 @@
/**
* Positions this view to match the size and location of {@param rect}.
- * @param alpha The alpha to set this view.
+ * @param alpha The alpha[0, 1] of the entire floating view.
+ * @param fgIconAlpha The alpha[0-255] of the foreground layer of the icon (if applicable).
* @param progress A value from [0, 1] that represents the animation progress.
* @param shapeProgressStart The progress value at which to start the shape reveal.
* @param cornerRadius The corner radius of {@param rect}.
+ * @param isOpening True if view is used for app open animation, false for app close animation.
*/
- public void update(RectF rect, float alpha, float progress, float shapeProgressStart,
- float cornerRadius, boolean isOpening) {
+ public void update(float alpha, int fgIconAlpha, RectF rect, float progress,
+ float shapeProgressStart, float cornerRadius, boolean isOpening) {
setAlpha(alpha);
-
- InsettableFrameLayout.LayoutParams lp =
- (InsettableFrameLayout.LayoutParams) getLayoutParams();
-
- DeviceProfile dp = mLauncher.getDeviceProfile();
- float dX = mIsRtl
- ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
- : rect.left - lp.getMarginStart();
- float dY = rect.top - lp.topMargin;
- setTranslationX(dX);
- setTranslationY(dY);
-
- float minSize = Math.min(lp.width, lp.height);
- float scaleX = rect.width() / minSize;
- float scaleY = rect.height() / minSize;
- float scale = Math.max(1f, Math.min(scaleX, scaleY));
-
- mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale,
- minSize, lp, mIsVerticalBarLayout);
-
- setPivotX(0);
- setPivotY(0);
- setScaleX(scale);
- setScaleY(scale);
-
- invalidate();
+ mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, fgIconAlpha,
+ isOpening, this, mLauncher.getDeviceProfile(), mIsVerticalBarLayout);
}
@Override
@@ -200,6 +181,7 @@
setLayoutParams(lp);
mClipIconView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height));
+ mBtvDrawable.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height));
}
private void updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
@@ -220,16 +202,21 @@
layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
}
+ private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+ RectF outRect) {
+ getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect());
+ }
+
/**
* Gets the location bounds of a view and returns the overall rotation.
* - For DeepShortcutView, we return the bounds of the icon view.
* - For BubbleTextView, we return the icon bounds.
*/
- private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
- RectF outRect) {
+ public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+ RectF outRect, Rect outViewBounds) {
boolean ignoreTransform = !isOpening;
- if (v instanceof DeepShortcutView) {
- v = ((DeepShortcutView) v).getBubbleText();
+ if (v instanceof BubbleTextHolder) {
+ v = ((BubbleTextHolder) v).getBubbleText();
ignoreTransform = false;
} else if (v.getParent() instanceof DeepShortcutView) {
v = ((DeepShortcutView) v.getParent()).getIconView();
@@ -239,24 +226,16 @@
return;
}
- Rect iconBounds = new Rect();
if (v instanceof BubbleTextView) {
- ((BubbleTextView) v).getIconBounds(iconBounds);
+ ((BubbleTextView) v).getIconBounds(outViewBounds);
} else if (v instanceof FolderIcon) {
- ((FolderIcon) v).getPreviewBounds(iconBounds);
+ ((FolderIcon) v).getPreviewBounds(outViewBounds);
} else {
- iconBounds.set(0, 0, v.getWidth(), v.getHeight());
+ outViewBounds.set(0, 0, v.getWidth(), v.getHeight());
}
- float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right,
- iconBounds.bottom};
- Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points,
- false, ignoreTransform);
- outRect.set(
- Math.min(points[0], points[2]),
- Math.min(points[1], points[3]),
- Math.max(points[0], points[2]),
- Math.max(points[1], points[3]));
+ Utilities.getBoundsForViewInDragLayer(launcher.getDragLayer(), v, outViewBounds,
+ ignoreTransform, null /** recycle */, outRect);
}
/**
@@ -272,14 +251,12 @@
@WorkerThread
@SuppressWarnings("WrongThread")
private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos,
- IconLoadResult iconLoadResult) {
- Drawable drawable = null;
- Drawable badge = null;
+ Drawable btvIcon, IconLoadResult iconLoadResult) {
+ Drawable drawable;
boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get()
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& !info.isDisabled(); // Use original icon for disabled icons.
- Drawable btvIcon = originalView instanceof BubbleTextView
- ? ((BubbleTextView) originalView).getIcon() : null;
+
+ Drawable badge = null;
if (info instanceof SystemShortcut) {
if (originalView instanceof ImageView) {
drawable = ((ImageView) originalView).getDrawable();
@@ -288,6 +265,9 @@
} else {
drawable = originalView.getBackground();
}
+ } else if (btvIcon instanceof PreloadIconDrawable) {
+ // Force the progress bar to display.
+ drawable = btvIcon;
} else {
int width = (int) pos.width();
int height = (int) pos.height();
@@ -313,6 +293,8 @@
drawable = drawable == null ? null : drawable.getConstantState().newDrawable();
int iconOffset = getOffsetForIconBounds(l, drawable, pos);
synchronized (iconLoadResult) {
+ iconLoadResult.btvDrawable = btvIcon == null || drawable == btvIcon
+ ? null : btvIcon.getConstantState().newDrawable();
iconLoadResult.drawable = drawable;
iconLoadResult.badge = badge;
iconLoadResult.iconOffset = iconOffset;
@@ -332,11 +314,13 @@
* @param iconOffset The amount of offset needed to match this view with the original view.
*/
@UiThread
- private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge, int iconOffset) {
+ private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge,
+ @Nullable Drawable btvIcon, int iconOffset) {
final InsettableFrameLayout.LayoutParams lp =
(InsettableFrameLayout.LayoutParams) getLayoutParams();
mBadge = badge;
- mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout);
+ mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout,
+ mLauncher.getDeviceProfile());
if (drawable instanceof AdaptiveIconDrawable) {
final int originalHeight = lp.height;
final int originalWidth = lp.width;
@@ -362,10 +346,21 @@
mBadge.setBounds(0, 0, clipViewOgWidth, clipViewOgHeight);
}
}
+
+ if (!mIsOpening && btvIcon != null) {
+ mBtvDrawable.setBackground(btvIcon);
+ }
invalidate();
}
/**
+ * Returns true if the icon is different from main app icon
+ */
+ public boolean isDifferentFromAppIcon() {
+ return mIconLoadResult == null ? false : mIconLoadResult.isThemed;
+ }
+
+ /**
* Checks if the icon result is loaded. If true, we set the icon immediately. Else, we add a
* callback to set the icon once the icon result is loaded.
*/
@@ -380,8 +375,9 @@
synchronized (mIconLoadResult) {
if (mIconLoadResult.isIconLoaded) {
setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
- mIconLoadResult.iconOffset);
- hideOriginalView(originalView);
+ mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset);
+ setVisibility(VISIBLE);
+ setIconAndDotVisible(originalView, false);
} else {
mIconLoadResult.onIconLoaded = () -> {
if (cancellationSignal.isCanceled()) {
@@ -389,30 +385,20 @@
}
setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
- mIconLoadResult.iconOffset);
+ mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset);
setVisibility(VISIBLE);
- hideOriginalView(originalView);
+ setIconAndDotVisible(originalView, false);
};
mLoadIconSignal = cancellationSignal;
}
}
}
- private void hideOriginalView(View originalView) {
- if (originalView instanceof IconLabelDotView) {
- ((IconLabelDotView) originalView).setIconVisible(false);
- ((IconLabelDotView) originalView).setForceHideDot(true);
- } else {
- originalView.setVisibility(INVISIBLE);
- }
- }
-
@WorkerThread
@SuppressWarnings("WrongThread")
private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O
- || !(drawable instanceof AdaptiveIconDrawable)
+ if (!(drawable instanceof AdaptiveIconDrawable)
|| (drawable instanceof FolderAdaptiveIcon)) {
return 0;
}
@@ -451,6 +437,7 @@
mFastFinishRunnable = runnable;
}
+ @Override
public void fastFinish() {
if (mFastFinishRunnable != null) {
mFastFinishRunnable.run();
@@ -464,10 +451,6 @@
mEndRunnable.run();
mEndRunnable = null;
}
- if (mFadeAnimatorSet != null) {
- mFadeAnimatorSet.end();
- mFadeAnimatorSet = null;
- }
}
@Override
@@ -477,7 +460,7 @@
}
if (!mIsOpening) {
// When closing an app, we want the item on the workspace to be invisible immediately
- hideOriginalView(mOriginalIcon);
+ setIconAndDotVisible(mOriginalIcon, false);
}
}
@@ -488,10 +471,16 @@
public void onAnimationRepeat(Animator animator) {}
@Override
+ public void setPositionOffsetY(float y) {
+ mIconOffsetY = y;
+ onGlobalLayout();
+ }
+
+ @Override
public void onGlobalLayout() {
- if (mOriginalIcon.isAttachedToWindow() && mPositionOut != null) {
- getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening,
- sTmpRectF);
+ if (mOriginalIcon != null && mOriginalIcon.isAttachedToWindow() && mPositionOut != null) {
+ getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening, sTmpRectF);
+ sTmpRectF.offset(0, mIconOffsetY);
if (!sTmpRectF.equals(mPositionOut)) {
updatePosition(sTmpRectF, (InsettableFrameLayout.LayoutParams) getLayoutParams());
if (mOnTargetChangeRunnable != null) {
@@ -510,12 +499,28 @@
*/
@UiThread
public static IconLoadResult fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening) {
- IconLoadResult result = new IconLoadResult(info);
- MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
- RectF position = new RectF();
- getLocationBoundsForView(l, v, isOpening, position);
- getIconResult(l, v, info, position, result);
- });
+ RectF position = new RectF();
+ getLocationBoundsForView(l, v, isOpening, position);
+
+ final FastBitmapDrawable btvIcon;
+ if (v instanceof BubbleTextView) {
+ BubbleTextView btv = (BubbleTextView) v;
+ if (info instanceof ItemInfoWithIcon
+ && (((ItemInfoWithIcon) info).runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
+ btvIcon = btv.makePreloadIcon();
+ } else {
+ btvIcon = btv.getIcon();
+ }
+ } else {
+ btvIcon = null;
+ }
+
+ IconLoadResult result = new IconLoadResult(info,
+ btvIcon == null ? false : btvIcon.isThemed());
+
+ MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() ->
+ getIconResult(l, v, info, position, btvIcon, result));
sIconLoadResult = result;
return result;
@@ -561,11 +566,6 @@
view.setVisibility(INVISIBLE);
parent.addView(view);
dragLayer.addView(view.mListenerView);
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "getFloatingIconView. listenerView "
- + "added to dragLayer. listenerView=" + view.mListenerView + ", fiv=" + view,
- new Exception());
- }
view.mListenerView.setListener(view::fastFinish);
view.mEndRunnable = () -> {
@@ -573,16 +573,14 @@
if (hideOriginal) {
if (isOpening) {
- if (originalView instanceof BubbleTextView) {
- ((BubbleTextView) originalView).setIconVisible(true);
- ((BubbleTextView) originalView).setForceHideDot(false);
- } else {
- originalView.setVisibility(VISIBLE);
- }
+ setIconAndDotVisible(originalView, true);
view.finish(dragLayer);
} else {
- view.mFadeAnimatorSet = view.createFadeAnimation(originalView, dragLayer);
- view.mFadeAnimatorSet.start();
+ originalView.setVisibility(VISIBLE);
+ if (originalView instanceof IconLabelDotView) {
+ setIconAndDotVisible(originalView, true);
+ }
+ view.finish(dragLayer);
}
} else {
view.finish(dragLayer);
@@ -599,56 +597,9 @@
return view;
}
- private AnimatorSet createFadeAnimation(View originalView, DragLayer dragLayer) {
- AnimatorSet fade = new AnimatorSet();
- fade.setDuration(FADE_DURATION_MS);
- fade.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- originalView.setVisibility(VISIBLE);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- finish(dragLayer);
- }
- });
-
- if (originalView instanceof IconLabelDotView) {
- IconLabelDotView view = (IconLabelDotView) originalView;
- fade.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- view.setIconVisible(true);
- view.setForceHideDot(false);
- }
- });
- }
-
- if (originalView instanceof BubbleTextView) {
- BubbleTextView btv = (BubbleTextView) originalView;
- fade.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- btv.setIconVisible(true);
- btv.setForceHideDot(true);
- }
- });
- fade.play(ObjectAnimator.ofInt(btv.getIcon(), DRAWABLE_ALPHA, 0, 255));
- } else if (!(originalView instanceof FolderIcon)) {
- fade.play(ObjectAnimator.ofFloat(originalView, ALPHA, 0f, 1f));
- }
-
- return fade;
- }
-
private void finish(DragLayer dragLayer) {
((ViewGroup) dragLayer.getParent()).removeView(this);
dragLayer.removeView(mListenerView);
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "listenerView removed from dragLayer. "
- + "listenerView=" + mListenerView + ", fiv=" + this, new Exception());
- }
recycle();
mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this);
}
@@ -665,11 +616,7 @@
mLoadIconSignal = null;
mEndRunnable = null;
mFinalDrawableBounds.setEmpty();
- if (mFadeAnimatorSet != null) {
- mFadeAnimatorSet.cancel();
- }
mPositionOut = null;
- mFadeAnimatorSet = null;
mListenerView.setListener(null);
mOriginalIcon = null;
mOnTargetChangeRunnable = null;
@@ -677,19 +624,24 @@
sTmpObjArray[0] = null;
mIconLoadResult = null;
mClipIconView.recycle();
+ mBtvDrawable.setBackground(null);
mFastFinishRunnable = null;
+ mIconOffsetY = 0;
}
private static class IconLoadResult {
final ItemInfo itemInfo;
+ final boolean isThemed;
+ Drawable btvDrawable;
Drawable drawable;
Drawable badge;
int iconOffset;
Runnable onIconLoaded;
boolean isIconLoaded;
- IconLoadResult(ItemInfo itemInfo) {
+ IconLoadResult(ItemInfo itemInfo, boolean isThemed) {
this.itemInfo = itemInfo;
+ this.isThemed = isThemed;
}
}
}
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
new file mode 100644
index 0000000..e2e3be7
--- /dev/null
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView;
+import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.GestureNavContract;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.Executors;
+
+/**
+ * Similar to {@link FloatingIconView} but displays a surface with the targetIcon. It then passes
+ * the surfaceHandle to the {@link GestureNavContract}.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class FloatingSurfaceView extends AbstractFloatingView implements
+ OnGlobalLayoutListener, Insettable, SurfaceHolder.Callback2 {
+
+ private final RectF mTmpPosition = new RectF();
+
+ private final Launcher mLauncher;
+ private final RectF mIconPosition = new RectF();
+
+ private final Rect mIconBounds = new Rect();
+ private final Picture mPicture = new Picture();
+ private final Runnable mRemoveViewRunnable = this::removeViewFromParent;
+
+ private final SurfaceView mSurfaceView;
+
+
+ private View mIcon;
+ private GestureNavContract mContract;
+
+ public FloatingSurfaceView(Context context) {
+ this(context, null);
+ }
+
+ public FloatingSurfaceView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FloatingSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+
+ mSurfaceView = new SurfaceView(context);
+ mSurfaceView.setZOrderOnTop(true);
+
+ mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+ mSurfaceView.getHolder().addCallback(this);
+ mIsOpen = true;
+ addView(mSurfaceView);
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ setCurrentIconVisible(true);
+ mLauncher.getViewCache().recycleView(R.layout.floating_surface_view, this);
+ mContract = null;
+ mIcon = null;
+ mIsOpen = false;
+
+ // Remove after some time, to avoid flickering
+ Executors.MAIN_EXECUTOR.getHandler().postDelayed(mRemoveViewRunnable,
+ DisplayController.INSTANCE.get(mLauncher).getInfo().singleFrameMs);
+ }
+
+ private void removeViewFromParent() {
+ mPicture.beginRecording(1, 1);
+ mPicture.endRecording();
+ mLauncher.getDragLayer().removeView(this);
+ }
+
+ /**
+ * Shows the surfaceView for the provided contract
+ */
+ public static void show(Launcher launcher, GestureNavContract contract) {
+ FloatingSurfaceView view = launcher.getViewCache().getView(R.layout.floating_surface_view,
+ launcher, launcher.getDragLayer());
+ view.mContract = contract;
+ view.mIsOpen = true;
+
+ // Cancel any pending remove
+ Executors.MAIN_EXECUTOR.getHandler().removeCallbacks(view.mRemoveViewRunnable);
+ view.removeViewFromParent();
+ launcher.getDragLayer().addView(view);
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_ICON_SURFACE) != 0;
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ close(false);
+ return false;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnGlobalLayoutListener(this);
+ updateIconLocation();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ setCurrentIconVisible(true);
+ }
+
+ @Override
+ public void onGlobalLayout() {
+ updateIconLocation();
+ }
+
+ @Override
+ public void setInsets(Rect insets) { }
+
+ private void updateIconLocation() {
+ if (mContract == null) {
+ return;
+ }
+ View icon = mLauncher.getWorkspace().getFirstMatchForAppClose(-1,
+ mContract.componentName.getPackageName(), mContract.user);
+
+ boolean iconChanged = mIcon != icon;
+ if (iconChanged) {
+ setCurrentIconVisible(true);
+ mIcon = icon;
+ setCurrentIconVisible(false);
+ }
+
+ if (icon != null && icon.isAttachedToWindow()) {
+ getLocationBoundsForView(mLauncher, icon, false, mTmpPosition, mIconBounds);
+
+ if (!mTmpPosition.equals(mIconPosition)) {
+ mIconPosition.set(mTmpPosition);
+ sendIconInfo();
+
+ LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
+ lp.width = Math.round(mIconPosition.width());
+ lp.height = Math.round(mIconPosition.height());
+ lp.leftMargin = Math.round(mIconPosition.left);
+ lp.topMargin = Math.round(mIconPosition.top);
+ }
+ }
+ if (iconChanged && !mIconBounds.isEmpty()) {
+ // Record the icon display
+ setCurrentIconVisible(true);
+ Canvas c = mPicture.beginRecording(mIconBounds.width(), mIconBounds.height());
+ c.translate(-mIconBounds.left, -mIconBounds.top);
+ mIcon.draw(c);
+ mPicture.endRecording();
+ setCurrentIconVisible(false);
+ drawOnSurface();
+ }
+ }
+
+ private void sendIconInfo() {
+ if (mContract != null && !mIconPosition.isEmpty()) {
+ mContract.sendEndPosition(mIconPosition, mSurfaceView.getSurfaceControl());
+ }
+ }
+
+ @Override
+ public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
+ drawOnSurface();
+ sendIconInfo();
+ }
+
+ @Override
+ public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder,
+ int format, int width, int height) {
+ drawOnSurface();
+ }
+
+ @Override
+ public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {}
+
+ @Override
+ public void surfaceRedrawNeeded(@NonNull SurfaceHolder surfaceHolder) {
+ drawOnSurface();
+ }
+
+ private void drawOnSurface() {
+ SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
+
+ Canvas c = surfaceHolder.lockHardwareCanvas();
+ if (c != null) {
+ mPicture.draw(c);
+ surfaceHolder.unlockCanvasAndPost(c);
+ }
+ }
+
+ private void setCurrentIconVisible(boolean isVisible) {
+ if (mIcon != null) {
+ setIconAndDotVisible(mIcon, isVisible);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/views/FloatingView.java b/src/com/android/launcher3/views/FloatingView.java
new file mode 100644
index 0000000..ea4fd15
--- /dev/null
+++ b/src/com/android/launcher3/views/FloatingView.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+/**
+ * Shared interface for floating views.
+ */
+public interface FloatingView {
+
+ /**
+ * Offsets and updates the position of this view by {@param y}.
+ */
+ void setPositionOffsetY(float y);
+
+ /**
+ * Fast finish the animation.
+ */
+ void fastFinish();
+}
diff --git a/src/com/android/launcher3/views/IconLabelDotView.java b/src/com/android/launcher3/views/IconLabelDotView.java
index 057caaf..e9113cf 100644
--- a/src/com/android/launcher3/views/IconLabelDotView.java
+++ b/src/com/android/launcher3/views/IconLabelDotView.java
@@ -15,10 +15,24 @@
*/
package com.android.launcher3.views;
+import android.view.View;
+
/**
* A view that has an icon, label, and notification dot.
*/
public interface IconLabelDotView {
void setIconVisible(boolean visible);
void setForceHideDot(boolean hide);
+
+ /**
+ * Sets the visibility of icon and dot of the view
+ */
+ static void setIconAndDotVisible(View view, boolean visible) {
+ if (view instanceof IconLabelDotView) {
+ ((IconLabelDotView) view).setIconVisible(visible);
+ ((IconLabelDotView) view).setForceHideDot(!visible);
+ } else {
+ view.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
}
diff --git a/src/com/android/launcher3/views/ListenerView.java b/src/com/android/launcher3/views/ListenerView.java
index 3ef778b..b2df0ee 100644
--- a/src/com/android/launcher3/views/ListenerView.java
+++ b/src/com/android/launcher3/views/ListenerView.java
@@ -17,13 +17,11 @@
import android.content.Context;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.testing.TestProtocol;
/**
* An invisible AbstractFloatingView that can run a callback when it is being closed.
@@ -38,20 +36,12 @@
}
public void setListener(Runnable listener) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView setListener lv=" + this
- + ", listener=" + listener, new Exception());
- }
mCloseListener = listener;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView onAttachedToWindow lv=" + this,
- new Exception());
- }
mIsOpen = true;
}
@@ -59,19 +49,10 @@
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsOpen = false;
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView onDetachedFromView lv=" + this,
- new Exception());
- }
}
@Override
protected void handleClose(boolean animate) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView handeClose lv=" + this
- + ", mIsOpen=" + mIsOpen + ", mCloseListener=" + mCloseListener
- + ", getParent()=" + getParent(), new Exception());
- }
if (mIsOpen) {
if (mCloseListener != null) {
mCloseListener.run();
@@ -85,21 +66,12 @@
}
@Override
- public void logActionCommand(int command) {
- // Users do not interact with FloatingIconView, so there is nothing to log here.
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_LISTENER) != 0;
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView touchEvent lv=" + this
- + ", ev=" + ev, new Exception());
- }
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
handleClose(false);
}
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index d558781..ecdd206 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -16,6 +16,7 @@
package com.android.launcher3.views;
import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_FLAVOR;
+import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_LAUNCH_SOURCE;
import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_OFFSET;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS;
@@ -25,6 +26,7 @@
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AttributeSet;
@@ -36,6 +38,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.core.content.ContextCompat;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
@@ -48,7 +51,7 @@
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
import java.util.List;
@@ -61,6 +64,7 @@
private final ArrayMap<View, OptionItem> mItemMap = new ArrayMap<>();
private RectF mTargetRect;
+ private boolean mShouldAddArrow;
public OptionsPopupView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
@@ -85,10 +89,10 @@
if (item == null) {
return false;
}
- if (item.mEventId.getId() > 0) {
- mLauncher.getStatsLogManager().logger().log(item.mEventId);
+ if (item.eventId.getId() > 0) {
+ mLauncher.getStatsLogManager().logger().log(item.eventId);
}
- if (item.mClickListener.onLongClick(view)) {
+ if (item.clickListener.onLongClick(view)) {
close(true);
return true;
}
@@ -108,13 +112,17 @@
}
@Override
- public void logActionCommand(int command) {
- // TODO:
+ protected boolean isOfType(int type) {
+ return (type & TYPE_OPTIONS_POPUP) != 0;
+ }
+
+ public void setShouldAddArrow(boolean shouldAddArrow) {
+ mShouldAddArrow = shouldAddArrow;
}
@Override
- protected boolean isOfType(int type) {
- return (type & TYPE_OPTIONS_POPUP) != 0;
+ protected boolean shouldAddArrow() {
+ return mShouldAddArrow;
}
@Override
@@ -122,27 +130,50 @@
mTargetRect.roundOut(outPos);
}
- public static void show(Launcher launcher, RectF targetRect, List<OptionItem> items) {
+ public static OptionsPopupView show(
+ Launcher launcher, RectF targetRect, List<OptionItem> items, boolean shouldAddArrow) {
+ return show(launcher, targetRect, items, shouldAddArrow, 0 /* width */);
+ }
+
+ public static OptionsPopupView show(
+ Launcher launcher, RectF targetRect, List<OptionItem> items, boolean shouldAddArrow,
+ int width) {
OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater()
.inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false);
popup.mTargetRect = targetRect;
+ popup.setShouldAddArrow(shouldAddArrow);
for (OptionItem item : items) {
DeepShortcutView view =
(DeepShortcutView) popup.inflateAndAdd(R.layout.system_shortcut, popup);
- view.getIconView().setBackgroundResource(item.mIconRes);
- view.getBubbleText().setText(item.mLabelRes);
- view.setDividerVisibility(View.INVISIBLE);
+ if (width > 0) {
+ view.getLayoutParams().width = width;
+ }
+ view.getIconView().setBackgroundDrawable(item.icon);
+ view.getBubbleText().setText(item.label);
view.setOnClickListener(popup);
view.setOnLongClickListener(popup);
popup.mItemMap.put(view, item);
}
- popup.reorderAndShow(popup.getChildCount());
+
+ popup.addPreDrawForColorExtraction(launcher);
+ popup.show();
+ return popup;
+ }
+
+ @Override
+ protected List<View> getChildrenForColorExtraction() {
+ int childCount = getChildCount();
+ ArrayList<View> children = new ArrayList<>(childCount);
+ for (int i = 0; i < childCount; ++i) {
+ children.add(getChildAt(i));
+ }
+ return children;
}
@VisibleForTesting
public static ArrowPopup getOptionsPopup(Launcher launcher) {
- return launcher.findViewById(R.id.deep_shortcuts_container);
+ return launcher.findViewById(R.id.popup_container);
}
public static void showDefaultOptions(Launcher launcher, float x, float y) {
@@ -152,28 +183,39 @@
y = launcher.getDragLayer().getHeight() / 2;
}
RectF target = new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
+ show(launcher, target, getOptions(launcher), false);
+ }
+ /**
+ * Returns the list of supported actions
+ */
+ public static ArrayList<OptionItem> getOptions(Launcher launcher) {
ArrayList<OptionItem> options = new ArrayList<>();
+ options.add(new OptionItem(launcher,
+ R.string.settings_button_text,
+ R.drawable.ic_setting,
+ LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
+ OptionsPopupView::startSettings));
+ if (!WidgetsModel.GO_DISABLE_WIDGETS) {
+ options.add(new OptionItem(launcher,
+ R.string.widget_button_text,
+ R.drawable.ic_widget,
+ LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS,
+ OptionsPopupView::onWidgetsClicked));
+ }
int resString = Utilities.existsStyleWallpapers(launcher) ?
R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
int resDrawable = Utilities.existsStyleWallpapers(launcher) ?
R.drawable.ic_palette : R.drawable.ic_wallpaper;
- options.add(new OptionItem(resString, resDrawable,
+ options.add(new OptionItem(launcher,
+ resString,
+ resDrawable,
IGNORE,
OptionsPopupView::startWallpaperPicker));
- if (!WidgetsModel.GO_DISABLE_WIDGETS) {
- options.add(new OptionItem(R.string.widget_button_text, R.drawable.ic_widget,
- LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS,
- OptionsPopupView::onWidgetsClicked));
- }
- options.add(new OptionItem(R.string.settings_button_text, R.drawable.ic_setting,
- LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
- OptionsPopupView::startSettings));
-
- show(launcher, target, options);
+ return options;
}
- public static boolean onWidgetsClicked(View view) {
+ private static boolean onWidgetsClicked(View view) {
return openWidgets(Launcher.getLauncher(view.getContext())) != null;
}
@@ -188,7 +230,7 @@
}
}
- public static boolean startSettings(View view) {
+ private static boolean startSettings(View view) {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startSettings");
Launcher launcher = Launcher.getLauncher(view.getContext());
launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
@@ -201,7 +243,7 @@
* Event handler for the wallpaper picker button that appears after a long press
* on the home screen.
*/
- public static boolean startWallpaperPicker(View v) {
+ private static boolean startWallpaperPicker(View v) {
Launcher launcher = Launcher.getLauncher(v.getContext());
if (!Utilities.isWallpaperAllowed(launcher)) {
Toast.makeText(launcher, R.string.msg_disabled_by_admin, Toast.LENGTH_SHORT).show();
@@ -210,7 +252,8 @@
Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.putExtra(EXTRA_WALLPAPER_OFFSET,
- launcher.getWorkspace().getWallpaperOffsetForCenterPage());
+ launcher.getWorkspace().getWallpaperOffsetForCenterPage())
+ .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher");
if (!Utilities.existsStyleWallpapers(launcher)) {
intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "wallpaper_only");
} else {
@@ -220,30 +263,43 @@
if (!TextUtils.isEmpty(pickerPackage)) {
intent.setPackage(pickerPackage);
}
- return launcher.startActivitySafely(v, intent, dummyInfo(intent), null);
+ return launcher.startActivitySafely(v, intent, placeholderInfo(intent));
}
- static WorkspaceItemInfo dummyInfo(Intent intent) {
- WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
- dummyInfo.intent = intent;
- dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
- dummyInfo.container = LauncherSettings.Favorites.CONTAINER_SETTINGS;
- return dummyInfo;
+ static WorkspaceItemInfo placeholderInfo(Intent intent) {
+ WorkspaceItemInfo placeholderInfo = new WorkspaceItemInfo();
+ placeholderInfo.intent = intent;
+ placeholderInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+ placeholderInfo.container = LauncherSettings.Favorites.CONTAINER_SETTINGS;
+ return placeholderInfo;
}
public static class OptionItem {
- private final int mLabelRes;
- private final int mIconRes;
- private final EventEnum mEventId;
- private final OnLongClickListener mClickListener;
+ // Used to create AccessibilityNodeInfo in AccessibilityActionsView.java.
+ public final int labelRes;
- public OptionItem(int labelRes, int iconRes, EventEnum eventId,
- OnLongClickListener clickListener) {
- mLabelRes = labelRes;
- mIconRes = iconRes;
- mEventId = eventId;
- mClickListener = clickListener;
+ public final CharSequence label;
+ public final Drawable icon;
+ public final EventEnum eventId;
+ public final OnLongClickListener clickListener;
+
+ public OptionItem(Context context, int labelRes, int iconRes, EventEnum eventId,
+ OnLongClickListener clickListener) {
+ this.labelRes = labelRes;
+ this.label = context.getText(labelRes);
+ this.icon = ContextCompat.getDrawable(context, iconRes);
+ this.eventId = eventId;
+ this.clickListener = clickListener;
+ }
+
+ public OptionItem(CharSequence label, Drawable icon, EventEnum eventId,
+ OnLongClickListener clickListener) {
+ this.labelRes = 0;
+ this.label = label;
+ this.icon = icon;
+ this.eventId = eventId;
+ this.clickListener = clickListener;
}
}
}
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 6a83332..1c2534d 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -16,22 +16,32 @@
package com.android.launcher3.views;
+import static android.view.HapticFeedbackConstants.CLOCK_TICK;
+
+import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
+
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.os.Build;
import android.util.AttributeSet;
+import android.util.Log;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
+import android.view.WindowInsets;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BaseRecyclerView;
@@ -47,8 +57,14 @@
* The track and scrollbar that shows when you scroll the list.
*/
public class RecyclerViewFastScroller extends View {
-
+ private static final String TAG = "RecyclerViewFastScroller";
+ private static final boolean DEBUG = false;
+ private static final int FASTSCROLL_THRESHOLD_MILLIS = 40;
private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
+
+ // Track is very narrow to target and correctly. This is especially the case if a user is
+ // using a hardware case. Even if x is offset by following amount, we consider it to be valid.
+ private static final int SCROLLBAR_LEFT_OFFSET_TOUCH_DELEGATE_DP = 5;
private static final Rect sTempRect = new Rect();
private static final Property<RecyclerViewFastScroller, Integer> TRACK_WIDTH =
@@ -78,6 +94,7 @@
/** Keeps the last known scrolling delta/velocity along y-axis. */
private int mDy = 0;
private final float mDeltaThreshold;
+ private final float mScrollbarLeftOffsetTouchDelegate;
private final ViewConfiguration mConfig;
@@ -97,19 +114,24 @@
private boolean mIsThumbDetached;
private final boolean mCanThumbDetach;
private boolean mIgnoreDragGesture;
+ private boolean mIsRecyclerViewFirstChildInParent = true;
+ private long mDownTimeStampMillis;
// This is the offset from the top of the scrollbar when the user first starts touching. To
// prevent jumping, this offset is applied as the user scrolls.
protected int mTouchOffsetY;
protected int mThumbOffsetY;
+ protected int mRvOffsetY;
// Fast scroller popup
private TextView mPopupView;
private boolean mPopupVisible;
private String mPopupSectionName;
+ private Insets mSystemGestureInsets;
protected BaseRecyclerView mRv;
private RecyclerView.OnScrollListener mOnScrollListener;
+ @Nullable private OnFastScrollChangeListener mOnFastScrollChangeListener;
private int mDownX;
private int mDownY;
@@ -144,6 +166,8 @@
mConfig = ViewConfiguration.get(context);
mDeltaThreshold = res.getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
+ mScrollbarLeftOffsetTouchDelegate = res.getDisplayMetrics().density
+ * SCROLLBAR_LEFT_OFFSET_TOUCH_DELEGATE_DP;
TypedArray ta =
context.obtainStyledAttributes(attrs, R.styleable.RecyclerViewFastScroller, defStyleAttr, 0);
@@ -181,11 +205,18 @@
public void setThumbOffsetY(int y) {
if (mThumbOffsetY == y) {
+ int rvCurrentOffsetY = mRv.getCurrentScrollY();
+ if (mRvOffsetY != rvCurrentOffsetY) {
+ mRvOffsetY = mRv.getCurrentScrollY();
+ notifyScrollChanged();
+ }
return;
}
updatePopupY(y);
mThumbOffsetY = y;
invalidate();
+ mRvOffsetY = mRv.getCurrentScrollY();
+ notifyScrollChanged();
}
public int getThumbOffsetY() {
@@ -219,36 +250,38 @@
public boolean handleTouchEvent(MotionEvent ev, Point offset) {
int x = (int) ev.getX() - offset.x;
int y = (int) ev.getY() - offset.y;
+
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// Keep track of the down positions
mDownX = x;
mDownY = mLastY = y;
+ mDownTimeStampMillis = ev.getDownTime();
if ((Math.abs(mDy) < mDeltaThreshold &&
- mRv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
+ mRv.getScrollState() != SCROLL_STATE_IDLE)) {
// now the touch events are being passed to the {@link WidgetCell} until the
// touch sequence goes over the touch slop.
mRv.stopScroll();
}
if (isNearThumb(x, y)) {
mTouchOffsetY = mDownY - mThumbOffsetY;
- } else if (mRv.supportsFastScrolling()
- && isNearScrollBar(mDownX)) {
- calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
- updateFastScrollSectionNameAndThumbOffset(y);
}
break;
case MotionEvent.ACTION_MOVE:
mLastY = y;
+ int absDeltaY = Math.abs(y - mDownY);
+ int absDeltaX = Math.abs(x - mDownX);
// Check if we should start scrolling, but ignore this fastscroll gesture if we have
// exceeded some fixed movement
- mIgnoreDragGesture |= Math.abs(y - mDownY) > mConfig.getScaledPagingTouchSlop();
- if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
- isNearThumb(mDownX, mLastY) &&
- Math.abs(y - mDownY) > mConfig.getScaledTouchSlop()) {
- calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
+ mIgnoreDragGesture |= absDeltaY > mConfig.getScaledPagingTouchSlop();
+
+ if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling()) {
+ if ((isNearThumb(mDownX, mLastY) && ev.getEventTime() - mDownTimeStampMillis
+ > FASTSCROLL_THRESHOLD_MILLIS)) {
+ calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
+ }
}
if (mIsDragging) {
updateFastScrollSectionNameAndThumbOffset(y);
@@ -267,6 +300,13 @@
}
break;
}
+ if (DEBUG) {
+ Log.d(TAG, (ev.getAction() == MotionEvent.ACTION_DOWN ? "\n" : "")
+ + "handleTouchEvent " + MotionEvent.actionToString(ev.getAction())
+ + " (" + x + "," + y + ")" + " isDragging=" + mIsDragging
+ + " mIgnoreDragGesture=" + mIgnoreDragGesture);
+
+ }
return mIsDragging;
}
@@ -288,6 +328,7 @@
if (!sectionName.equals(mPopupSectionName)) {
mPopupSectionName = sectionName;
mPopupView.setText(sectionName);
+ performHapticFeedback(CLOCK_TICK);
}
animatePopupVisibility(!sectionName.isEmpty());
mLastTouchY = boundedY;
@@ -314,12 +355,27 @@
canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint);
if (Utilities.ATLEAST_Q) {
mThumbBounds.roundOut(SYSTEM_GESTURE_EXCLUSION_RECT.get(0));
+ // swiping very close to the thumb area (not just within it's bound)
+ // will also prevent back gesture
SYSTEM_GESTURE_EXCLUSION_RECT.get(0).offset(mThumbDrawOffset.x, mThumbDrawOffset.y);
+ if (Utilities.ATLEAST_Q && mSystemGestureInsets != null) {
+ SYSTEM_GESTURE_EXCLUSION_RECT.get(0).left =
+ SYSTEM_GESTURE_EXCLUSION_RECT.get(0).right - mSystemGestureInsets.right;
+ }
setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
}
canvas.restoreToCount(saveCount);
}
+ @Override
+ @RequiresApi(Build.VERSION_CODES.Q)
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (Utilities.ATLEAST_Q) {
+ mSystemGestureInsets = insets.getSystemGestureInsets();
+ }
+ return super.onApplyWindowInsets(insets);
+ }
+
private float getScrollThumbRadius() {
return mWidth + mThumbPadding + mThumbPadding;
}
@@ -359,7 +415,8 @@
* Returns whether the specified x position is near the scroll bar.
*/
public boolean isNearScrollBar(int x) {
- return x >= (getWidth() - mMaxWidth) / 2 && x <= (getWidth() + mMaxWidth) / 2;
+ return x >= (getWidth() - mMaxWidth) / 2 - mScrollbarLeftOffsetTouchDelegate
+ && x <= (getWidth() + mMaxWidth) / 2;
}
private void animatePopupVisibility(boolean visible) {
@@ -371,9 +428,6 @@
}
private void updatePopupY(int lastTouchY) {
- if (!mPopupVisible) {
- return;
- }
int height = mPopupView.getHeight();
// Aligns the rounded corner of the pop up with the top of the thumb.
float top = mRv.getScrollBarTop() + lastTouchY + (getScrollThumbRadius() / 2f)
@@ -388,7 +442,9 @@
return false;
}
getHitRect(sTempRect);
- sTempRect.top += mRv.getScrollBarTop();
+ if (mIsRecyclerViewFirstChildInParent) {
+ sTempRect.top += mRv.getScrollBarTop();
+ }
if (outOffset != null) {
outOffset.set(sTempRect.left, sTempRect.top);
}
@@ -401,4 +457,27 @@
// alpha is so low, it does not matter.
return false;
}
+
+ public void setIsRecyclerViewFirstChildInParent(boolean isRecyclerViewFirstChildInParent) {
+ mIsRecyclerViewFirstChildInParent = isRecyclerViewFirstChildInParent;
+ }
+
+ public void setOnFastScrollChangeListener(
+ @Nullable OnFastScrollChangeListener onFastScrollChangeListener) {
+ mOnFastScrollChangeListener = onFastScrollChangeListener;
+ }
+
+ private void notifyScrollChanged() {
+ if (mOnFastScrollChangeListener != null) {
+ mOnFastScrollChangeListener.onScrollChanged();
+ }
+ }
+
+ /**
+ * A callback that is invoked when there is a scroll change in {@link RecyclerViewFastScroller}.
+ */
+ public interface OnFastScrollChangeListener {
+ /** Called when the recycler view scroll has changed. */
+ void onScrollChanged();
+ }
}
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index 22faf97..4c0bfde 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -15,205 +15,45 @@
*/
package com.android.launcher3.views;
-import static android.content.Context.ACCESSIBILITY_SERVICE;
-import static android.view.MotionEvent.ACTION_DOWN;
-
-import static androidx.core.graphics.ColorUtils.compositeColors;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import static com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.Keyframe;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.RectEvaluator;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
-import android.graphics.Point;
import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
+import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
-import android.util.IntProperty;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
import android.view.View;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
-import androidx.customview.widget.ExploreByTouchHelper;
-import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.BaseActivity;
import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.uioverrides.WallpaperColorInfo;
-import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.util.SystemUiController;
-import java.util.List;
+import java.util.ArrayList;
/**
* Simple scrim which draws a flat color
*/
-public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener,
- AccessibilityStateChangeListener {
+public class ScrimView extends View implements Insettable {
+ private static final float STATUS_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.9f;
- public static final IntProperty<ScrimView> DRAG_HANDLE_ALPHA =
- new IntProperty<ScrimView>("dragHandleAlpha") {
-
- @Override
- public Integer get(ScrimView scrimView) {
- return scrimView.mDragHandleAlpha;
- }
-
- @Override
- public void setValue(ScrimView scrimView, int value) {
- scrimView.setDragHandleAlpha(value);
- }
- };
- private static final int WALLPAPERS = R.string.wallpaper_button_text;
- private static final int WIDGETS = R.string.widget_button_text;
- private static final int SETTINGS = R.string.settings_button_text;
- private static final int ALPHA_CHANNEL_COUNT = 1;
-
- private static final long DRAG_HANDLE_BOUNCE_DURATION_MS = 300;
- // How much to delay before repeating the bounce.
- private static final long DRAG_HANDLE_BOUNCE_DELAY_MS = 200;
- // Repeat this many times (i.e. total number of bounces is 1 + this).
- private static final int DRAG_HANDLE_BOUNCE_REPEAT_COUNT = 2;
-
- private final Rect mTempRect = new Rect();
- private final int[] mTempPos = new int[2];
-
- protected final T mLauncher;
- private final WallpaperColorInfo mWallpaperColorInfo;
- private final AccessibilityManager mAM;
- protected final int mEndScrim;
- protected final boolean mIsScrimDark;
-
- private final StateListener<LauncherState> mAccessibilityLauncherStateListener =
- new StateListener<LauncherState>() {
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- setImportantForAccessibility(finalState == ALL_APPS
- ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
- };
-
- protected float mMaxScrimAlpha;
-
- protected float mProgress = 1;
- protected int mScrimColor;
-
- protected int mCurrentFlatColor;
- protected int mEndFlatColor;
- protected int mEndFlatColorAlpha;
-
- protected final Point mDragHandleSize;
- private final int mDragHandleTouchSize;
- private final int mDragHandlePaddingInVerticalBarLayout;
- protected float mDragHandleOffset;
- private final Rect mDragHandleBounds;
- private final RectF mHitRect = new RectF();
- private ObjectAnimator mDragHandleAnim;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- private final AccessibilityHelper mAccessibilityHelper;
- @Nullable
- protected Drawable mDragHandle;
-
- private int mDragHandleAlpha = 255;
+ private final ArrayList<Runnable> mOpaquenessListeners = new ArrayList<>(1);
+ private SystemUiController mSystemUiController;
+ private ScrimDrawingController mDrawingController;
+ private int mBackgroundColor;
+ private boolean mIsVisible = true;
+ private boolean mLastDispatchedOpaqueness;
public ScrimView(Context context, AttributeSet attrs) {
super(context, attrs);
- mLauncher = Launcher.cast(Launcher.getLauncher(context));
- mWallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context);
- mEndScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
- mIsScrimDark = ColorUtils.calculateLuminance(mEndScrim) < 0.5f;
-
- mMaxScrimAlpha = 0.7f;
-
- Resources res = context.getResources();
- mDragHandleSize = new Point(res.getDimensionPixelSize(R.dimen.vertical_drag_handle_width),
- res.getDimensionPixelSize(R.dimen.vertical_drag_handle_height));
- mDragHandleBounds = new Rect(0, 0, mDragHandleSize.x, mDragHandleSize.y);
- mDragHandleTouchSize = res.getDimensionPixelSize(R.dimen.vertical_drag_handle_touch_size);
- mDragHandlePaddingInVerticalBarLayout = context.getResources()
- .getDimensionPixelSize(R.dimen.vertical_drag_handle_padding_in_vertical_bar_layout);
-
- mAccessibilityHelper = createAccessibilityHelper();
- ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
-
- mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
setFocusable(false);
- mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
- }
-
- public AlphaProperty getAlphaProperty(int index) {
- return mMultiValueAlpha.getProperty(index);
- }
-
- @NonNull
- protected AccessibilityHelper createAccessibilityHelper() {
- return new AccessibilityHelper();
}
@Override
public void setInsets(Rect insets) {
- updateDragHandleBounds();
- updateDragHandleVisibility();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- updateDragHandleBounds();
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mWallpaperColorInfo.addOnChangeListener(this);
- onExtractedColorsChanged(mWallpaperColorInfo);
-
- mAM.addAccessibilityStateChangeListener(this);
- onAccessibilityStateChanged(mAM.isEnabled());
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mWallpaperColorInfo.removeOnChangeListener(this);
- mAM.removeAccessibilityStateChangeListener(this);
}
@Override
@@ -222,324 +62,120 @@
}
@Override
- public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
- mScrimColor = wallpaperColorInfo.getMainColor();
- mEndFlatColor = compositeColors(mEndScrim, setColorAlphaBound(
- mScrimColor, Math.round(mMaxScrimAlpha * 255)));
- mEndFlatColorAlpha = Color.alpha(mEndFlatColor);
- updateColors();
- invalidate();
+ protected boolean onSetAlpha(int alpha) {
+ updateSysUiColors();
+ dispatchVisibilityListenersIfNeeded();
+ return super.onSetAlpha(alpha);
}
- public void setProgress(float progress) {
- if (mProgress != progress) {
- mProgress = progress;
- stopDragHandleEducationAnim();
- updateColors();
- updateSysUiColors();
- updateDragHandleAlpha();
- invalidate();
- }
+ @Override
+ public void setBackgroundColor(int color) {
+ mBackgroundColor = color;
+ updateSysUiColors();
+ dispatchVisibilityListenersIfNeeded();
+ super.setBackgroundColor(color);
}
- public void reInitUi() { }
-
- protected void updateColors() {
- mCurrentFlatColor = mProgress >= 1 ? 0 : setColorAlphaBound(
- mEndFlatColor, Math.round((1 - mProgress) * mEndFlatColorAlpha));
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+ mIsVisible = isVisible;
+ dispatchVisibilityListenersIfNeeded();
}
- protected void updateSysUiColors() {
- // Use a light system UI (dark icons) if all apps is behind at least half of the
- // status bar.
- boolean forceChange = mProgress <= 0.1f;
- if (forceChange) {
- mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !mIsScrimDark);
- } else {
- mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
- }
- }
-
- protected void updateDragHandleAlpha() {
- if (mDragHandle != null) {
- mDragHandle.setAlpha(mDragHandleAlpha);
- }
- }
-
- private void setDragHandleAlpha(int alpha) {
- if (alpha != mDragHandleAlpha) {
- mDragHandleAlpha = alpha;
- if (mDragHandle != null) {
- mDragHandle.setAlpha(mDragHandleAlpha);
- invalidate();
- }
- }
+ public boolean isFullyOpaque() {
+ return mIsVisible && getAlpha() == 1 && Color.alpha(mBackgroundColor) == 255;
}
@Override
protected void onDraw(Canvas canvas) {
- if (mCurrentFlatColor != 0) {
- canvas.drawColor(mCurrentFlatColor);
- }
- drawDragHandle(canvas);
- }
-
- protected void drawDragHandle(Canvas canvas) {
- if (mDragHandle != null) {
- canvas.translate(0, -mDragHandleOffset);
- mDragHandle.draw(canvas);
- canvas.translate(0, mDragHandleOffset);
+ super.onDraw(canvas);
+ if (mDrawingController != null) {
+ mDrawingController.drawOnScrim(canvas);
}
}
@Override
- public boolean onTouchEvent(MotionEvent event) {
- boolean superHandledTouch = super.onTouchEvent(event);
- if (event.getAction() == ACTION_DOWN) {
- if (!superHandledTouch && mHitRect.contains(event.getX(), event.getY())) {
- if (startDragHandleEducationAnim()) {
- return true;
- }
- }
- stopDragHandleEducationAnim();
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ updateSysUiColors();
+ }
+
+ private void updateSysUiColors() {
+ // Use a light system UI (dark icons) if all apps is behind at least half of the
+ // status bar.
+ final float threshold = STATUS_BAR_COLOR_FORCE_UPDATE_THRESHOLD;
+ boolean forceChange = getVisibility() == VISIBLE
+ && getAlpha() > threshold
+ && (Color.alpha(mBackgroundColor) / 255f) > threshold;
+ if (forceChange) {
+ getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !isScrimDark());
+ } else {
+ getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
}
- return superHandledTouch;
+ }
+
+ private void dispatchVisibilityListenersIfNeeded() {
+ boolean fullyOpaque = isFullyOpaque();
+ if (mLastDispatchedOpaqueness == fullyOpaque) {
+ return;
+ }
+ mLastDispatchedOpaqueness = fullyOpaque;
+ for (int i = 0; i < mOpaquenessListeners.size(); i++) {
+ mOpaquenessListeners.get(i).run();
+ }
+ }
+
+ private SystemUiController getSystemUiController() {
+ if (mSystemUiController == null) {
+ mSystemUiController = BaseActivity.fromContext(getContext()).getSystemUiController();
+ }
+ return mSystemUiController;
+ }
+
+ private boolean isScrimDark() {
+ if (!(getBackground() instanceof ColorDrawable)) {
+ throw new IllegalStateException(
+ "ScrimView must have a ColorDrawable background, this one has: "
+ + getBackground());
+ }
+ return ColorUtils.calculateLuminance(
+ ((ColorDrawable) getBackground()).getColor()) < 0.5f;
}
/**
- * Animates the drag handle to demonstrate how to get to all apps.
- * @return Whether the animation was started (false if drag handle is invisible).
+ * Sets drawing controller. Invalidates ScrimView if drawerController has changed.
*/
- public boolean startDragHandleEducationAnim() {
- stopDragHandleEducationAnim();
-
- if (mDragHandle == null || mDragHandle.getAlpha() != 255) {
- return false;
- }
-
- final Drawable drawable = mDragHandle;
- mDragHandle = null;
-
- Rect bounds = new Rect(mDragHandleBounds);
- bounds.offset(0, -(int) mDragHandleOffset);
- drawable.setBounds(bounds);
-
- Rect topBounds = new Rect(bounds);
- topBounds.offset(0, -bounds.height());
-
- Rect invalidateRegion = new Rect(bounds);
- invalidateRegion.top = topBounds.top;
-
- final float progressToReachTop = 0.6f;
- Keyframe frameTop = Keyframe.ofObject(progressToReachTop, topBounds);
- frameTop.setInterpolator(DEACCEL);
- Keyframe frameBot = Keyframe.ofObject(1, bounds);
- frameBot.setInterpolator(ACCEL_DEACCEL);
- PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("bounds",
- Keyframe.ofObject(0, bounds), frameTop, frameBot);
- holder.setEvaluator(new RectEvaluator());
-
- mDragHandleAnim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
- long totalBounceDuration = DRAG_HANDLE_BOUNCE_DURATION_MS + DRAG_HANDLE_BOUNCE_DELAY_MS;
- // The bounce finishes by this progress, the rest of the duration just delays next bounce.
- float delayStartProgress = 1f - (float) DRAG_HANDLE_BOUNCE_DELAY_MS / totalBounceDuration;
- mDragHandleAnim.addUpdateListener((v) -> invalidate(invalidateRegion));
- mDragHandleAnim.setDuration(totalBounceDuration);
- mDragHandleAnim.setInterpolator(clampToProgress(LINEAR, 0, delayStartProgress));
- mDragHandleAnim.setRepeatCount(DRAG_HANDLE_BOUNCE_REPEAT_COUNT);
- getOverlay().add(drawable);
-
- mDragHandleAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mDragHandleAnim = null;
- getOverlay().remove(drawable);
- updateDragHandleVisibility(drawable);
- }
- });
- mDragHandleAnim.start();
- return true;
- }
-
- private void stopDragHandleEducationAnim() {
- if (mDragHandleAnim != null) {
- mDragHandleAnim.end();
- }
- }
-
- protected void updateDragHandleBounds() {
- DeviceProfile grid = mLauncher.getDeviceProfile();
- final int left;
- final int width = getMeasuredWidth();
- final int top = getMeasuredHeight() - mDragHandleSize.y - grid.getInsets().bottom;
- final int topMargin;
-
- if (grid.isVerticalBarLayout()) {
- topMargin = grid.workspacePadding.bottom + mDragHandlePaddingInVerticalBarLayout;
- if (grid.isSeascape()) {
- left = width - grid.getInsets().right - mDragHandleSize.x
- - mDragHandlePaddingInVerticalBarLayout;
- } else {
- left = grid.getInsets().left + mDragHandlePaddingInVerticalBarLayout;
- }
- } else {
- left = Math.round((width - mDragHandleSize.x) / 2f);
- topMargin = grid.hotseatBarSizePx;
- }
- mDragHandleBounds.offsetTo(left, top - topMargin);
- mHitRect.set(mDragHandleBounds);
- // Inset outwards to increase touch size.
- mHitRect.inset((mDragHandleSize.x - mDragHandleTouchSize) / 2f,
- (mDragHandleSize.y - mDragHandleTouchSize) / 2f);
-
- if (mDragHandle != null) {
- mDragHandle.setBounds(mDragHandleBounds);
- }
- }
-
- @Override
- public void onAccessibilityStateChanged(boolean enabled) {
- StateManager<LauncherState> stateManager = mLauncher.getStateManager();
- stateManager.removeStateListener(mAccessibilityLauncherStateListener);
-
- if (enabled) {
- stateManager.addStateListener(mAccessibilityLauncherStateListener);
- mAccessibilityLauncherStateListener.onStateTransitionComplete(stateManager.getState());
- } else {
- setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- }
- updateDragHandleVisibility();
- }
-
- public void updateDragHandleVisibility() {
- updateDragHandleVisibility(null);
- }
-
- private void updateDragHandleVisibility(@Nullable Drawable recycle) {
- boolean visible = shouldDragHandleBeVisible();
- boolean wasVisible = mDragHandle != null;
- if (visible != wasVisible) {
- if (visible) {
- mDragHandle = recycle != null ? recycle :
- mLauncher.getDrawable(R.drawable.drag_handle_indicator_shadow);
- mDragHandle.setBounds(mDragHandleBounds);
-
- updateDragHandleAlpha();
- } else {
- mDragHandle = null;
- }
+ public void setDrawingController(ScrimDrawingController drawingController) {
+ if (mDrawingController != drawingController) {
+ mDrawingController = drawingController;
invalidate();
}
}
- protected boolean shouldDragHandleBeVisible() {
- return mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
- }
-
- @Override
- public boolean dispatchHoverEvent(MotionEvent event) {
- return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- return mAccessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
- }
-
- @Override
- public void onFocusChanged(boolean gainFocus, int direction,
- Rect previouslyFocusedRect) {
- super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- }
-
- protected class AccessibilityHelper extends ExploreByTouchHelper {
-
- private static final int DRAG_HANDLE_ID = 1;
-
- public AccessibilityHelper() {
- super(ScrimView.this);
- }
-
- @Override
- protected int getVirtualViewAt(float x, float y) {
- return mHitRect.contains((int) x, (int) y)
- ? DRAG_HANDLE_ID : INVALID_ID;
- }
-
- @Override
- protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
- virtualViewIds.add(DRAG_HANDLE_ID);
- }
-
- @Override
- protected void onPopulateNodeForVirtualView(int virtualViewId,
- AccessibilityNodeInfoCompat node) {
- node.setContentDescription(getContext().getString(R.string.all_apps_button_label));
- node.setBoundsInParent(mDragHandleBounds);
-
- getLocationOnScreen(mTempPos);
- mTempRect.set(mDragHandleBounds);
- mTempRect.offset(mTempPos[0], mTempPos[1]);
- node.setBoundsInScreen(mTempRect);
-
- node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
- node.setClickable(true);
- node.setFocusable(true);
-
- if (mLauncher.isInState(NORMAL)) {
- Context context = getContext();
- if (Utilities.isWallpaperAllowed(context)) {
- node.addAction(
- new AccessibilityActionCompat(WALLPAPERS, context.getText(WALLPAPERS)));
- }
- node.addAction(new AccessibilityActionCompat(WIDGETS, context.getText(WIDGETS)));
- node.addAction(new AccessibilityActionCompat(SETTINGS, context.getText(SETTINGS)));
- }
- }
-
- @Override
- protected boolean onPerformActionForVirtualView(
- int virtualViewId, int action, Bundle arguments) {
- if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
- mLauncher.getUserEventDispatcher().logActionOnControl(
- Action.Touch.TAP, ControlType.ALL_APPS_BUTTON,
- mLauncher.getStateManager().getState().containerType);
- mLauncher.getStateManager().goToState(ALL_APPS);
- return true;
- } else if (action == WALLPAPERS) {
- return OptionsPopupView.startWallpaperPicker(ScrimView.this);
- } else if (action == WIDGETS) {
- int originalImportanceForAccessibility = getImportantForAccessibility();
- setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- WidgetsFullSheet widgetsFullSheet = OptionsPopupView.openWidgets(mLauncher);
- if (widgetsFullSheet == null) {
- setImportantForAccessibility(originalImportanceForAccessibility);
- return false;
- }
- widgetsFullSheet.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View view) {}
-
- @Override
- public void onViewDetachedFromWindow(View view) {
- setImportantForAccessibility(originalImportanceForAccessibility);
- widgetsFullSheet.removeOnAttachStateChangeListener(this);
- }
- });
- return true;
- } else if (action == SETTINGS) {
- return OptionsPopupView.startSettings(ScrimView.this);
- }
-
- return false;
- }
+ /**
+ * Registers a listener to be notified of whether the scrim is occluding other UI elements.
+ * @see #isFullyOpaque()
+ */
+ public void addOpaquenessListener(@NonNull Runnable listener) {
+ mOpaquenessListeners.add(listener);
}
/**
- * @return The top of this scrim view, or {@link Float#MAX_VALUE} if there's no distinct top.
+ * Removes previously registered listener.
+ * @see #addOpaquenessListener(Runnable)
*/
- public float getVisualTop() {
- return Float.MAX_VALUE;
+ public void removeOpaquenessListener(@NonNull Runnable listener) {
+ mOpaquenessListeners.remove(listener);
+ }
+
+ /**
+ * A Utility interface allowing for other surfaces to draw on ScrimView
+ */
+ public interface ScrimDrawingController {
+ /**
+ * Called inside ScrimView#OnDraw
+ */
+ void drawOnScrim(Canvas canvas);
}
}
diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java
index 513ce59..49fcd2e 100644
--- a/src/com/android/launcher3/views/Snackbar.java
+++ b/src/com/android/launcher3/views/Snackbar.java
@@ -167,11 +167,6 @@
}
@Override
- public void logActionCommand(int command) {
- // TODO
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_SNACKBAR) != 0;
}
diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java
index d0ec9d7..923eb19 100644
--- a/src/com/android/launcher3/views/SpringRelativeLayout.java
+++ b/src/com/android/launcher3/views/SpringRelativeLayout.java
@@ -15,51 +15,26 @@
*/
package com.android.launcher3.views;
-import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
-import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
-import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
-
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
-import android.util.SparseBooleanArray;
-import android.view.View;
import android.widget.EdgeEffect;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
-import androidx.dynamicanimation.animation.DynamicAnimation;
-import androidx.dynamicanimation.animation.FloatPropertyCompat;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory;
+import com.android.launcher3.Utilities;
+
+/**
+ * View group to allow rendering overscroll effect in a child at the parent level
+ */
public class SpringRelativeLayout extends RelativeLayout {
- private static final float STIFFNESS = (STIFFNESS_MEDIUM + STIFFNESS_LOW) / 2;
- private static final float DAMPING_RATIO = DAMPING_RATIO_MEDIUM_BOUNCY;
- private static final float VELOCITY_MULTIPLIER = 0.3f;
-
- private static final FloatPropertyCompat<SpringRelativeLayout> DAMPED_SCROLL =
- new FloatPropertyCompat<SpringRelativeLayout>("value") {
-
- @Override
- public float getValue(SpringRelativeLayout object) {
- return object.mDampedScrollShift;
- }
-
- @Override
- public void setValue(SpringRelativeLayout object, float value) {
- object.setDampedScrollShift(value);
- }
- };
-
- protected final SparseBooleanArray mSpringViews = new SparseBooleanArray();
- private final SpringAnimation mSpring;
-
- private float mDampedScrollShift = 0;
- private SpringEdgeEffect mActiveEdge;
+ // fixed edge at the time force is applied
+ private final EdgeEffect mEdgeGlowTop;
+ private final EdgeEffect mEdgeGlowBottom;
public SpringRelativeLayout(Context context) {
this(context, null);
@@ -71,98 +46,79 @@
public SpringRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- mSpring = new SpringAnimation(this, DAMPED_SCROLL, 0);
- mSpring.setSpring(new SpringForce(0)
- .setStiffness(STIFFNESS)
- .setDampingRatio(DAMPING_RATIO));
- }
-
- public void addSpringView(int id) {
- mSpringViews.put(id, true);
- }
-
- public void removeSpringView(int id) {
- mSpringViews.delete(id);
- invalidate();
- }
-
- /**
- * Used to clip the canvas when drawing child views during overscroll.
- */
- public int getCanvasClipTopForOverscroll() {
- return 0;
+ mEdgeGlowTop = Utilities.ATLEAST_S
+ ? new EdgeEffect(context, attrs) : new EdgeEffect(context);
+ mEdgeGlowBottom = Utilities.ATLEAST_S
+ ? new EdgeEffect(context, attrs) : new EdgeEffect(context);
+ setWillNotDraw(false);
}
@Override
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- if (mDampedScrollShift != 0 && mSpringViews.get(child.getId())) {
- int saveCount = canvas.save();
-
- canvas.clipRect(0, getCanvasClipTopForOverscroll(), getWidth(), getHeight());
- canvas.translate(0, mDampedScrollShift);
- boolean result = super.drawChild(canvas, child, drawingTime);
-
- canvas.restoreToCount(saveCount);
-
- return result;
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (!mEdgeGlowTop.isFinished()) {
+ final int restoreCount = canvas.save();
+ canvas.translate(0, 0);
+ mEdgeGlowTop.setSize(getWidth(), getHeight());
+ if (mEdgeGlowTop.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
}
- return super.drawChild(canvas, child, drawingTime);
- }
-
- private void setActiveEdge(SpringEdgeEffect edge) {
- if (mActiveEdge != edge && mActiveEdge != null) {
- mActiveEdge.mDistance = 0;
- }
- mActiveEdge = edge;
- }
-
- protected void setDampedScrollShift(float shift) {
- if (shift != mDampedScrollShift) {
- mDampedScrollShift = shift;
- invalidate();
+ if (!mEdgeGlowBottom.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight();
+ canvas.translate(-width, height);
+ canvas.rotate(180, width, 0);
+ mEdgeGlowBottom.setSize(width, height);
+ if (mEdgeGlowBottom.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
}
}
- private void finishScrollWithVelocity(float velocity) {
- mSpring.setStartVelocity(velocity);
- mSpring.setStartValue(mDampedScrollShift);
- mSpring.start();
+
+ /**
+ * Absorbs the velocity as a result for swipe-up fling
+ */
+ protected void absorbSwipeUpVelocity(int velocity) {
+ mEdgeGlowBottom.onAbsorb(velocity);
+ invalidate();
}
- protected void finishWithShiftAndVelocity(float shift, float velocity,
- DynamicAnimation.OnAnimationEndListener listener) {
- setDampedScrollShift(shift);
- mSpring.addEndListener(listener);
- finishScrollWithVelocity(velocity);
+ protected void absorbPullDeltaDistance(float deltaDistance, float displacement) {
+ mEdgeGlowBottom.onPull(deltaDistance, displacement);
+ invalidate();
+ }
+
+ public void onRelease() {
+ mEdgeGlowBottom.onRelease();
}
public EdgeEffectFactory createEdgeEffectFactory() {
- return new SpringEdgeEffectFactory();
+ return new ProxyEdgeEffectFactory();
}
- private class SpringEdgeEffectFactory extends EdgeEffectFactory {
+ private class ProxyEdgeEffectFactory extends EdgeEffectFactory {
@NonNull @Override
protected EdgeEffect createEdgeEffect(RecyclerView view, int direction) {
- switch (direction) {
- case DIRECTION_TOP:
- return new SpringEdgeEffect(getContext(), +VELOCITY_MULTIPLIER);
- case DIRECTION_BOTTOM:
- return new SpringEdgeEffect(getContext(), -VELOCITY_MULTIPLIER);
+ if (direction == DIRECTION_TOP) {
+ return new EdgeEffectProxy(getContext(), mEdgeGlowTop);
}
return super.createEdgeEffect(view, direction);
}
}
- private class SpringEdgeEffect extends EdgeEffect {
+ private class EdgeEffectProxy extends EdgeEffect {
- private final float mVelocityMultiplier;
+ private final EdgeEffect mParent;
- private float mDistance;
-
- public SpringEdgeEffect(Context context, float velocityMultiplier) {
+ EdgeEffectProxy(Context context, EdgeEffect parent) {
super(context);
- mVelocityMultiplier = velocityMultiplier;
+ mParent = parent;
}
@Override
@@ -170,22 +126,44 @@
return false;
}
+ private void invalidateParentScrollEffect() {
+ if (!mParent.isFinished()) {
+ invalidate();
+ }
+ }
+
@Override
public void onAbsorb(int velocity) {
- finishScrollWithVelocity(velocity * mVelocityMultiplier);
+ mParent.onAbsorb(velocity);
+ invalidateParentScrollEffect();
+ }
+
+ @Override
+ public void onPull(float deltaDistance) {
+ mParent.onPull(deltaDistance);
+ invalidateParentScrollEffect();
}
@Override
public void onPull(float deltaDistance, float displacement) {
- setActiveEdge(this);
- mDistance += deltaDistance * (mVelocityMultiplier / 3f);
- setDampedScrollShift(mDistance * getHeight());
+ mParent.onPull(deltaDistance, displacement);
+ invalidateParentScrollEffect();
}
@Override
public void onRelease() {
- mDistance = 0;
- finishScrollWithVelocity(0);
+ mParent.onRelease();
+ invalidateParentScrollEffect();
+ }
+
+ @Override
+ public void finish() {
+ mParent.finish();
+ }
+
+ @Override
+ public boolean isFinished() {
+ return mParent.isFinished();
}
}
}
\ No newline at end of file
diff --git a/src/com/android/launcher3/views/TopRoundedCornerView.java b/src/com/android/launcher3/views/TopRoundedCornerView.java
index 7888b08..92cce92 100644
--- a/src/com/android/launcher3/views/TopRoundedCornerView.java
+++ b/src/com/android/launcher3/views/TopRoundedCornerView.java
@@ -17,13 +17,10 @@
import android.content.Context;
import android.graphics.Canvas;
-import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
-import android.widget.FrameLayout;
-import com.android.launcher3.R;
import com.android.launcher3.util.Themes;
/**
@@ -35,41 +32,23 @@
private final Path mClipPath = new Path();
private float[] mRadii;
- private final Paint mNavBarScrimPaint;
- private int mNavBarScrimHeight = 0;
-
public TopRoundedCornerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- int radius = getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius);
+ float radius = Themes.getDialogCornerRadius(context);
mRadii = new float[] {radius, radius, radius, radius, 0, 0, 0, 0};
-
- mNavBarScrimPaint = new Paint();
- mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor));
}
public TopRoundedCornerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
- public void setNavBarScrimHeight(int height) {
- if (mNavBarScrimHeight != height) {
- mNavBarScrimHeight = height;
- invalidate();
- }
- }
-
@Override
public void draw(Canvas canvas) {
canvas.save();
canvas.clipPath(mClipPath);
super.draw(canvas);
canvas.restore();
-
- if (mNavBarScrimHeight > 0) {
- canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
- mNavBarScrimPaint);
- }
}
@Override
diff --git a/src/com/android/launcher3/views/WidgetsEduView.java b/src/com/android/launcher3/views/WidgetsEduView.java
new file mode 100644
index 0000000..c2947c7
--- /dev/null
+++ b/src/com/android/launcher3/views/WidgetsEduView.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+
+/**
+ * Education view about widgets.
+ */
+public class WidgetsEduView extends AbstractSlideInView<Launcher> implements Insettable {
+
+ private static final int DEFAULT_CLOSE_DURATION = 200;
+
+ private Rect mInsets = new Rect();
+
+ public WidgetsEduView(Context context, AttributeSet attr) {
+ this(context, attr, 0);
+ }
+
+ public WidgetsEduView(Context context, AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ handleClose(true, DEFAULT_CLOSE_DURATION);
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_WIDGETS_EDUCATION_DIALOG) != 0;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContent = findViewById(R.id.edu_view);
+ findViewById(R.id.edu_close_button)
+ .setOnClickListener(v -> close(/* animate= */ true));
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ mInsets.set(insets);
+ mContent.setPadding(mContent.getPaddingStart(),
+ mContent.getPaddingTop(), mContent.getPaddingEnd(), insets.bottom);
+ }
+
+ private void show() {
+ attachToContainer();
+ animateOpen();
+ }
+
+ @Override
+ protected int getScrimColor(Context context) {
+ return context.getResources().getColor(R.color.widgets_picker_scrim);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = r - l;
+ int height = b - t;
+
+ // Lay out the content as center bottom aligned.
+ int contentWidth = mContent.getMeasuredWidth();
+ int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
+ mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
+ contentLeft + contentWidth, height);
+
+ setTranslationShift(mTranslationShift);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+ int widthUsed;
+ if (mInsets.bottom > 0) {
+ // Extra space between this view and mContent horizontally when the sheet is shown in
+ // portrait mode.
+ widthUsed = mInsets.left + mInsets.right;
+ } else {
+ // Extra space between this view and mContent horizontally when the sheet is shown in
+ // landscape mode.
+ Rect padding = deviceProfile.workspacePadding;
+ widthUsed = Math.max(padding.left + padding.right,
+ 2 * (mInsets.left + mInsets.right));
+ }
+
+ int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
+ measureChildWithMargins(mContent, widthMeasureSpec,
+ widthUsed, heightMeasureSpec, heightUsed);
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
+ private void animateOpen() {
+ if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ return;
+ }
+ mIsOpen = true;
+ mOpenCloseAnimator.setValues(
+ PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+ mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
+ mOpenCloseAnimator.start();
+ }
+
+ /** Shows widget education dialog. */
+ public static WidgetsEduView showEducationDialog(Launcher launcher) {
+ LayoutInflater layoutInflater = LayoutInflater.from(launcher);
+ WidgetsEduView v = (WidgetsEduView) layoutInflater.inflate(
+ R.layout.widgets_edu, launcher.getDragLayer(), false);
+ v.show();
+ return v;
+ }
+}
diff --git a/src/com/android/launcher3/views/WorkEduView.java b/src/com/android/launcher3/views/WorkEduView.java
deleted file mode 100644
index d35a38f..0000000
--- a/src/com/android/launcher3/views/WorkEduView.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.views;
-
-
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsPagedView;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-/**
- * On boarding flow for users right after setting up work profile
- */
-public class WorkEduView extends AbstractSlideInView
- implements Insettable, StateListener<LauncherState> {
-
- private static final int DEFAULT_CLOSE_DURATION = 200;
- public static final String KEY_WORK_EDU_STEP = "showed_work_profile_edu";
- public static final String KEY_LEGACY_WORK_EDU_SEEN = "showed_bottom_user_education";
-
- private static final int WORK_EDU_NOT_STARTED = 0;
- private static final int WORK_EDU_PERSONAL_APPS = 1;
- private static final int WORK_EDU_WORK_APPS = 2;
-
- protected static final int FINAL_SCRIM_BG_COLOR = 0x88000000;
-
-
- private Rect mInsets = new Rect();
- private View mViewWrapper;
- private Button mProceedButton;
- private TextView mContentText;
- private AllAppsPagedView mAllAppsPagedView;
-
- private int mNextWorkEduStep = WORK_EDU_PERSONAL_APPS;
-
-
- public WorkEduView(Context context, AttributeSet attr) {
- this(context, attr, 0);
- }
-
- public WorkEduView(Context context, AttributeSet attrs,
- int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mContent = this;
- }
-
- @Override
- protected void handleClose(boolean animate) {
- mLauncher.getSharedPrefs().edit().putInt(KEY_WORK_EDU_STEP, mNextWorkEduStep).apply();
- handleClose(true, DEFAULT_CLOSE_DURATION);
- }
-
- @Override
- protected void onCloseComplete() {
- super.onCloseComplete();
- mLauncher.getStateManager().removeStateListener(this);
- }
-
- @Override
- public void logActionCommand(int command) {
- // Since this is on-boarding popup, it is not a user controlled action.
- }
-
- @Override
- public int getLogContainerType() {
- return LauncherLogProto.ContainerType.TIP;
- }
-
- @Override
- protected boolean isOfType(int type) {
- return (type & TYPE_ON_BOARD_POPUP) != 0;
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mViewWrapper = findViewById(R.id.view_wrapper);
- mProceedButton = findViewById(R.id.proceed);
- mContentText = findViewById(R.id.content_text);
-
- // make sure layout does not shrink when we change the text
- mContentText.post(() -> mContentText.setMinLines(mContentText.getLineCount()));
- if (mLauncher.getAppsView().getContentView() instanceof AllAppsPagedView) {
- mAllAppsPagedView = (AllAppsPagedView) mLauncher.getAppsView().getContentView();
- }
-
- mProceedButton.setOnClickListener(view -> {
- if (mAllAppsPagedView != null) {
- mAllAppsPagedView.snapToPage(AllAppsContainerView.AdapterHolder.WORK);
- }
- goToWorkTab(true);
- });
- }
-
- private void goToWorkTab(boolean animate) {
- mProceedButton.setText(R.string.work_profile_edu_accept);
- if (animate) {
- ObjectAnimator animator = ObjectAnimator.ofFloat(mContentText, ALPHA, 0);
- animator.addListener(new AnimationSuccessListener() {
- @Override
- public void onAnimationSuccess(Animator animator) {
- mContentText.setText(mLauncher.getString(R.string.work_profile_edu_work_apps));
- ObjectAnimator.ofFloat(mContentText, ALPHA, 1).start();
- }
- });
- animator.start();
- } else {
- mContentText.setText(mLauncher.getString(R.string.work_profile_edu_work_apps));
- }
- mNextWorkEduStep = WORK_EDU_WORK_APPS;
- mProceedButton.setOnClickListener(v -> handleClose(true));
- }
-
- @Override
- public void setInsets(Rect insets) {
- int leftInset = insets.left - mInsets.left;
- int rightInset = insets.right - mInsets.right;
- int bottomInset = insets.bottom - mInsets.bottom;
- mInsets.set(insets);
- setPadding(leftInset, getPaddingTop(), rightInset, 0);
- mViewWrapper.setPaddingRelative(mViewWrapper.getPaddingStart(),
- mViewWrapper.getPaddingTop(), mViewWrapper.getPaddingEnd(), bottomInset);
- }
-
- private void show() {
- attachToContainer();
- animateOpen();
- mLauncher.getStateManager().addStateListener(this);
- }
-
- @Override
- protected int getScrimColor(Context context) {
- return FINAL_SCRIM_BG_COLOR;
- }
-
- private void goToFirstPage() {
- if (mAllAppsPagedView != null) {
- mAllAppsPagedView.snapToPageImmediately(AllAppsContainerView.AdapterHolder.MAIN);
- }
- }
-
- private void animateOpen() {
- if (mIsOpen || mOpenCloseAnimator.isRunning()) {
- return;
- }
- mIsOpen = true;
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- mOpenCloseAnimator.start();
- }
-
- /**
- * Checks if user has not seen onboarding UI yet and shows it when user navigates to all apps
- */
- public static StateListener<LauncherState> showEduFlowIfNeeded(Launcher launcher,
- @Nullable StateListener<LauncherState> oldListener) {
- if (oldListener != null) {
- launcher.getStateManager().removeStateListener(oldListener);
- }
- if (hasSeenLegacyEdu(launcher) || launcher.getSharedPrefs().getInt(KEY_WORK_EDU_STEP,
- WORK_EDU_NOT_STARTED) != WORK_EDU_NOT_STARTED) {
- return null;
- }
-
- StateListener<LauncherState> listener = new StateListener<LauncherState>() {
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- if (finalState != LauncherState.ALL_APPS) return;
- LayoutInflater layoutInflater = LayoutInflater.from(launcher);
- WorkEduView v = (WorkEduView) layoutInflater.inflate(
- R.layout.work_profile_edu, launcher.getDragLayer(),
- false);
- v.show();
- v.goToFirstPage();
- launcher.getStateManager().removeStateListener(this);
- }
- };
- launcher.getStateManager().addStateListener(listener);
- return listener;
- }
-
- /**
- * Shows work apps edu if user had dismissed full edu flow
- */
- public static void showWorkEduIfNeeded(Launcher launcher) {
- if (hasSeenLegacyEdu(launcher) || launcher.getSharedPrefs().getInt(KEY_WORK_EDU_STEP,
- WORK_EDU_NOT_STARTED) != WORK_EDU_PERSONAL_APPS) {
- return;
- }
- LayoutInflater layoutInflater = LayoutInflater.from(launcher);
- WorkEduView v = (WorkEduView) layoutInflater.inflate(
- R.layout.work_profile_edu, launcher.getDragLayer(), false);
- v.show();
- v.goToWorkTab(false);
- }
-
- private static boolean hasSeenLegacyEdu(Launcher launcher) {
- return launcher.getSharedPrefs().getBoolean(KEY_LEGACY_WORK_EDU_SEEN, false);
- }
-
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- close(false);
- }
-}
diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
new file mode 100644
index 0000000..1cc7f53
--- /dev/null
+++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
@@ -0,0 +1,151 @@
+/*
+ * 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.widget;
+
+import static com.android.launcher3.Utilities.ATLEAST_R;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+
+import android.animation.PropertyValuesHolder;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.WindowInsets;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.AddItemActivity;
+import com.android.launcher3.views.AbstractSlideInView;
+
+/**
+ * Bottom sheet for the pin widget.
+ */
+public class AddItemWidgetsBottomSheet extends AbstractSlideInView<AddItemActivity> implements
+ View.OnApplyWindowInsetsListener {
+
+ private static final int DEFAULT_CLOSE_DURATION = 200;
+
+ private final Rect mInsets;
+
+ public AddItemWidgetsBottomSheet(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AddItemWidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mInsets = new Rect();
+ }
+
+ /**
+ * Attaches to activity container and animates open the bottom sheet.
+ */
+ public void show() {
+ ViewParent parent = getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(this);
+ }
+ attachToContainer();
+ setOnApplyWindowInsetsListener(this);
+ animateOpen();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = r - l;
+ int height = b - t;
+
+ // Lay out content as center bottom aligned.
+ int contentWidth = mContent.getMeasuredWidth();
+ int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
+ mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
+ contentLeft + contentWidth, height);
+
+ setTranslationShift(mTranslationShift);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+ int widthUsed;
+ if (mInsets.bottom > 0) {
+ widthUsed = mInsets.left + mInsets.right;
+ } else {
+ Rect padding = deviceProfile.workspacePadding;
+ widthUsed = Math.max(padding.left + padding.right,
+ 2 * (mInsets.left + mInsets.right));
+ }
+
+ int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
+ measureChildWithMargins(mContent, widthMeasureSpec,
+ widthUsed, heightMeasureSpec, heightUsed);
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContent = findViewById(R.id.add_item_bottom_sheet_content);
+ }
+
+ private void animateOpen() {
+ if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ return;
+ }
+ mIsOpen = true;
+ mOpenCloseAnimator.setValues(
+ PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+ mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
+ mOpenCloseAnimator.start();
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ handleClose(animate, DEFAULT_CLOSE_DURATION);
+ }
+
+ @Override
+ protected boolean isOfType(@FloatingViewType int type) {
+ return (type & TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP) != 0;
+ }
+
+ @Override
+ protected int getScrimColor(Context context) {
+ return context.getResources().getColor(R.color.widgets_picker_scrim);
+ }
+
+ @SuppressLint("NewApi") // Already added API check.
+ @Override
+ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
+ if (ATLEAST_R) {
+ Insets insets = windowInsets.getInsets(WindowInsets.Type.systemBars());
+ mInsets.set(insets.left, insets.top, insets.right, insets.bottom);
+ } else {
+ mInsets.set(windowInsets.getSystemWindowInsetLeft(),
+ windowInsets.getSystemWindowInsetTop(),
+ windowInsets.getSystemWindowInsetRight(),
+ windowInsets.getSystemWindowInsetBottom());
+ }
+ mContent.setPadding(mContent.getPaddingStart(),
+ mContent.getPaddingTop(), mContent.getPaddingEnd(), mInsets.bottom);
+ return windowInsets;
+ }
+}
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 3e5113a..9f0b9d9 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -15,43 +15,46 @@
*/
package com.android.launcher3.widget;
-import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-
import android.content.Context;
import android.graphics.Point;
+import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.Toast;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.core.view.ViewCompat;
+
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.uioverrides.WallpaperColorInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.AbstractSlideInView;
-
-import java.util.ArrayList;
+import com.android.launcher3.views.ArrowTipView;
/**
* Base class for various widgets popup
*/
-abstract class BaseWidgetSheet extends AbstractSlideInView
+public abstract class BaseWidgetSheet extends AbstractSlideInView<Launcher>
implements OnClickListener, OnLongClickListener, DragSource,
- PopupDataProvider.PopupDataChangeListener {
+ PopupDataProvider.PopupDataChangeListener, Insettable {
+ protected static final String KEY_WIDGETS_EDUCATION_TIP_SEEN =
+ "launcher.widgets_education_tip_seen";
+ protected final Rect mInsets = new Rect();
/* Touch handling related member variables. */
private Toast mWidgetInstructionToast;
@@ -61,65 +64,109 @@
}
protected int getScrimColor(Context context) {
- WallpaperColorInfo colors = WallpaperColorInfo.INSTANCE.get(context);
- int alpha = context.getResources().getInteger(R.integer.extracted_color_gradient_alpha);
- return setColorAlphaBound(colors.getSecondaryColor(), alpha);
+ return context.getResources().getColor(R.color.widgets_picker_scrim);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- mLauncher.getPopupDataProvider().setChangeListener(this);
+ mActivityContext.getPopupDataProvider().setChangeListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- mLauncher.getPopupDataProvider().setChangeListener(null);
+ mActivityContext.getPopupDataProvider().setChangeListener(null);
}
@Override
public final void onClick(View v) {
- // Let the user know that they have to long press to add a widget
- if (mWidgetInstructionToast != null) {
- mWidgetInstructionToast.cancel();
+ Object tag = null;
+ if (v instanceof WidgetCell) {
+ tag = v.getTag();
+ } else if (v.getParent() instanceof WidgetCell) {
+ tag = ((WidgetCell) v.getParent()).getTag();
+ }
+ if (tag instanceof PendingAddShortcutInfo) {
+ mWidgetInstructionToast = showShortcutToast(getContext(), mWidgetInstructionToast);
+ } else {
+ mWidgetInstructionToast = showWidgetToast(getContext(), mWidgetInstructionToast);
}
- CharSequence msg = Utilities.wrapForTts(
- getContext().getText(R.string.long_press_widget_to_add),
- getContext().getString(R.string.long_accessible_way_to_add));
- mWidgetInstructionToast = Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT);
- mWidgetInstructionToast.show();
}
@Override
public boolean onLongClick(View v) {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
v.cancelLongPress();
- if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
+ if (!ItemLongClickListener.canStartDrag(mActivityContext)) return false;
if (v instanceof WidgetCell) {
return beginDraggingWidget((WidgetCell) v);
+ } else if (v.getParent() instanceof WidgetCell) {
+ return beginDraggingWidget((WidgetCell) v.getParent());
}
return true;
}
+ @Override
+ public void setInsets(Rect insets) {
+ mInsets.set(insets);
+ }
+
+
+ /**
+ * Measures the dimension of this view and its children by taking system insets, navigation bar,
+ * status bar, into account.
+ */
+ @GuardedBy("MainThread")
+ protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+ int widthUsed;
+ if (mInsets.bottom > 0) {
+ widthUsed = mInsets.left + mInsets.right;
+ } else {
+ Rect padding = deviceProfile.workspacePadding;
+ widthUsed = Math.max(padding.left + padding.right,
+ 2 * (mInsets.left + mInsets.right));
+ }
+
+ int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
+ measureChildWithMargins(mContent, widthMeasureSpec,
+ widthUsed, heightMeasureSpec, heightUsed);
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
private boolean beginDraggingWidget(WidgetCell v) {
// Get the widget preview as the drag representation
WidgetImageView image = v.getWidgetView();
// If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and
// we abort the drag.
- if (image.getBitmap() == null) {
+ if (image.getDrawable() == null && v.getAppWidgetHostViewPreview() == null) {
return false;
}
- int[] loc = new int[2];
- getPopupContainer().getLocationInDragLayer(image, loc);
+ PendingItemDragHelper dragHelper = new PendingItemDragHelper(v);
+ dragHelper.setRemoteViewsPreview(v.getRemoteViewsPreview());
+ dragHelper.setAppWidgetHostViewPreview(v.getAppWidgetHostViewPreview());
- new PendingItemDragHelper(v).startDrag(
- image.getBitmapBounds(), image.getBitmap().getWidth(), image.getWidth(),
- new Point(loc[0], loc[1]), this, new DragOptions());
+ if (image.getDrawable() != null) {
+ int[] loc = new int[2];
+ getPopupContainer().getLocationInDragLayer(image, loc);
+
+ dragHelper.startDrag(image.getBitmapBounds(), image.getDrawable().getIntrinsicWidth(),
+ image.getWidth(), new Point(loc[0], loc[1]), this, new DragOptions());
+ } else {
+ View preview = v.getAppWidgetHostViewPreview();
+ int[] loc = new int[2];
+ getPopupContainer().getLocationInDragLayer(preview, loc);
+
+ Rect r = new Rect(0, 0, preview.getWidth(), preview.getHeight());
+ dragHelper.startDrag(r, preview.getMeasuredWidth(), preview.getMeasuredWidth(),
+ new Point(loc[0], loc[1]), this, new DragOptions());
+ }
close(true);
return true;
}
@@ -149,29 +196,67 @@
isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV);
}
- @Override
- public void fillInLogContainerData(ItemInfo childInfo, Target child,
- ArrayList<Target> parents) {
- Target target = newContainerTarget(ContainerType.WIDGETS);
- target.cardinality = getElementsRowCount();
- parents.add(target);
- }
-
- @Override
- public final void logActionCommand(int command) {
- Target target = newContainerTarget(getLogContainerType());
- target.cardinality = getElementsRowCount();
- mLauncher.getUserEventDispatcher().logActionCommand(command, target);
- }
-
- @Override
- public int getLogContainerType() {
- return ContainerType.WIDGETS;
- }
-
- protected abstract int getElementsRowCount();
-
protected SystemUiController getSystemUiController() {
- return mLauncher.getSystemUiController();
+ return mActivityContext.getSystemUiController();
+ }
+
+ /**
+ * Show Widget tap toast prompting user to drag instead
+ */
+ public static Toast showWidgetToast(Context context, Toast toast) {
+ // Let the user know that they have to long press to add a widget
+ if (toast != null) {
+ toast.cancel();
+ }
+
+ CharSequence msg = Utilities.wrapForTts(
+ context.getText(R.string.long_press_widget_to_add),
+ context.getString(R.string.long_accessible_way_to_add));
+ toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
+ toast.show();
+ return toast;
+ }
+
+ /**
+ * Show shortcut tap toast prompting user to drag instead.
+ */
+ private static Toast showShortcutToast(Context context, Toast toast) {
+ // Let the user know that they have to long press to add a widget
+ if (toast != null) {
+ toast.cancel();
+ }
+
+ CharSequence msg = Utilities.wrapForTts(
+ context.getText(R.string.long_press_shortcut_to_add),
+ context.getString(R.string.long_accessible_way_to_add_shortcut));
+ toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
+ toast.show();
+ return toast;
+ }
+
+ /** Shows education tip on top center of {@code view} if view is laid out. */
+ @Nullable
+ protected ArrowTipView showEducationTipOnViewIfPossible(@Nullable View view) {
+ if (view == null || !ViewCompat.isLaidOut(view)) {
+ return null;
+ }
+ int[] coords = new int[2];
+ view.getLocationOnScreen(coords);
+ ArrowTipView arrowTipView =
+ new ArrowTipView(mActivityContext, /* isPointingUp= */ false).showAtLocation(
+ getContext().getString(R.string.long_press_widget_to_add),
+ /* arrowXCoord= */coords[0] + view.getWidth() / 2,
+ /* yCoord= */coords[1]);
+ if (arrowTipView != null) {
+ mActivityContext.getSharedPrefs().edit()
+ .putBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, true).apply();
+ }
+ return arrowTipView;
+ }
+
+ /** Returns {@code true} if tip has previously been shown on any of {@link BaseWidgetSheet}. */
+ protected boolean hasSeenEducationTip() {
+ return mActivityContext.getSharedPrefs().getBoolean(KEY_WIDGETS_EDUCATION_TIP_SEEN, false)
+ || Utilities.IS_RUNNING_IN_TEST_HARNESS;
}
}
diff --git a/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java
new file mode 100644
index 0000000..afceadd
--- /dev/null
+++ b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget;
+
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** Wrapper around {@link DatabaseWidgetPreviewLoader} that contains caching logic. */
+public class CachingWidgetPreviewLoader implements WidgetPreviewLoader {
+
+ @NonNull private final WidgetPreviewLoader mDelegate;
+ @NonNull private final Map<ComponentKey, Map<Size, CacheResult>> mCache = new ArrayMap<>();
+
+ public CachingWidgetPreviewLoader(@NonNull WidgetPreviewLoader delegate) {
+ mDelegate = delegate;
+ }
+
+ /** Returns whether the preview is loaded for the item and size. */
+ public boolean isPreviewLoaded(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ return getPreview(item, previewSize) != null;
+ }
+
+ /** Returns the cached preview for the item and size, or null if there is none. */
+ @Nullable
+ public Bitmap getPreview(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (cacheResult instanceof CacheResult.Loaded) {
+ return ((CacheResult.Loaded) cacheResult).mBitmap;
+ } else {
+ return null;
+ }
+ }
+
+ @NonNull
+ private CacheResult getCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.get(toComponentKey(item));
+ if (cacheResults == null) {
+ return CacheResult.MISS;
+ }
+
+ return cacheResults.getOrDefault(previewSize, CacheResult.MISS);
+ }
+ }
+
+ /**
+ * Puts the result in the cache for the item and size. Returns the value previously in the
+ * cache, or null if there was none.
+ */
+ @Nullable
+ private CacheResult putCacheResult(
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @Nullable CacheResult cacheResult) {
+ ComponentKey key = toComponentKey(item);
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.getOrDefault(key, new ArrayMap<>());
+ CacheResult previous;
+ if (cacheResult == null) {
+ previous = cacheResults.remove(previewSize);
+ if (cacheResults.isEmpty()) {
+ mCache.remove(key);
+ } else {
+ previous = cacheResults.put(previewSize, cacheResult);
+ mCache.put(key, cacheResults);
+ }
+ } else {
+ previous = cacheResults.put(previewSize, cacheResult);
+ mCache.put(key, cacheResults);
+ }
+ return previous;
+ }
+ }
+
+ private void removeCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ ComponentKey key = toComponentKey(item);
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.getOrDefault(key, new ArrayMap<>());
+ cacheResults.remove(previewSize);
+ mCache.put(key, cacheResults);
+ }
+ }
+
+ /**
+ * Gets the preview for the widget item and size, using the value in the cache if stored.
+ *
+ * @return a {@link CancellationSignal}, which can cancel the request before it loads
+ */
+ @Override
+ @UiThread
+ @NonNull
+ public CancellationSignal loadPreview(
+ @NonNull BaseActivity activity, @NonNull WidgetItem item, @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback) {
+ CancellationSignal signal = new CancellationSignal();
+ signal.setOnCancelListener(() -> {
+ synchronized (mCache) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (!(cacheResult instanceof CacheResult.Loading)) {
+ // If the key isn't actively loading, then this is a no-op. Cancelling loading
+ // shouldn't clear the cache if we've already loaded.
+ return;
+ }
+
+ CacheResult.Loading prev = (CacheResult.Loading) cacheResult;
+ CacheResult.Loading updated = prev.withoutCallback(callback);
+
+ if (updated.mCallbacks.isEmpty()) {
+ // If the last callback was removed, then cancel the underlying request in the
+ // delegate.
+ prev.mCancellationSignal.cancel();
+ removeCacheResult(item, previewSize);
+ } else {
+ // If there are other callbacks still active, then don't cancel the delegate's
+ // request, just remove this callback from the set.
+ putCacheResult(item, previewSize, updated);
+ }
+ }
+ });
+
+ synchronized (mCache) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (cacheResult instanceof CacheResult.Loaded) {
+ // If the bitmap is already present in the cache, invoke the callback immediately.
+ callback.onPreviewLoaded(((CacheResult.Loaded) cacheResult).mBitmap);
+ return signal;
+ }
+
+ if (cacheResult instanceof CacheResult.Loading) {
+ // If we're already loading the preview for this key, then just add the callback
+ // to the set we'll call after it loads.
+ CacheResult.Loading prev = (CacheResult.Loading) cacheResult;
+ putCacheResult(item, previewSize, prev.withCallback(callback));
+ return signal;
+ }
+
+ CancellationSignal delegateCancellationSignal =
+ mDelegate.loadPreview(
+ activity,
+ item,
+ previewSize,
+ preview -> {
+ CacheResult prev;
+ synchronized (mCache) {
+ prev = putCacheResult(
+ item, previewSize, new CacheResult.Loaded(preview));
+ }
+ if (prev instanceof CacheResult.Loading) {
+ // Notify each stored callback that the preview has loaded.
+ ((CacheResult.Loading) prev).mCallbacks
+ .forEach(c -> c.onPreviewLoaded(preview));
+ } else {
+ // If there isn't a loading object in the cache, then we were
+ // notified before adding this signal to the cache. Just
+ // call back to the provided callback, there can't be others.
+ callback.onPreviewLoaded(preview);
+ }
+ });
+ ArraySet<WidgetPreviewLoadedCallback> callbacks = new ArraySet<>();
+ callbacks.add(callback);
+ putCacheResult(
+ item,
+ previewSize,
+ new CacheResult.Loading(delegateCancellationSignal, callbacks));
+ }
+
+ return signal;
+ }
+
+ /** Clears all cached previews for {@code items}, cancelling any in-progress preview loading. */
+ public void clearPreviews(Iterable<WidgetItem> items) {
+ List<CacheResult> previousCacheResults = new ArrayList<>();
+ synchronized (mCache) {
+ for (WidgetItem item : items) {
+ Map<Size, CacheResult> previousMap = mCache.remove(toComponentKey(item));
+ if (previousMap != null) {
+ previousCacheResults.addAll(previousMap.values());
+ }
+ }
+ }
+
+ for (CacheResult previousCacheResult : previousCacheResults) {
+ if (previousCacheResult instanceof CacheResult.Loading) {
+ ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel();
+ }
+ }
+ }
+
+ /** Clears all cached previews, cancelling any in-progress preview loading. */
+ public void clearAll() {
+ List<CacheResult> previousCacheResults;
+ synchronized (mCache) {
+ previousCacheResults =
+ mCache
+ .values()
+ .stream()
+ .flatMap(sizeToResult -> sizeToResult.values().stream())
+ .collect(Collectors.toList());
+ mCache.clear();
+ }
+
+ for (CacheResult previousCacheResult : previousCacheResults) {
+ if (previousCacheResult instanceof CacheResult.Loading) {
+ ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel();
+ }
+ }
+ }
+
+ private abstract static class CacheResult {
+ static final CacheResult MISS = new CacheResult() {};
+
+ static final class Loading extends CacheResult {
+ @NonNull final CancellationSignal mCancellationSignal;
+ @NonNull final Set<WidgetPreviewLoadedCallback> mCallbacks;
+
+ Loading(@NonNull CancellationSignal cancellationSignal,
+ @NonNull Set<WidgetPreviewLoadedCallback> callbacks) {
+ mCancellationSignal = cancellationSignal;
+ mCallbacks = callbacks;
+ }
+
+ @NonNull
+ Loading withCallback(@NonNull WidgetPreviewLoadedCallback callback) {
+ if (mCallbacks.contains(callback)) return this;
+ Set<WidgetPreviewLoadedCallback> newCallbacks =
+ new ArraySet<>(mCallbacks.size() + 1);
+ newCallbacks.addAll(mCallbacks);
+ newCallbacks.add(callback);
+ return new Loading(mCancellationSignal, newCallbacks);
+ }
+
+ @NonNull
+ Loading withoutCallback(@NonNull WidgetPreviewLoadedCallback callback) {
+ if (!mCallbacks.contains(callback)) return this;
+ Set<WidgetPreviewLoadedCallback> newCallbacks =
+ new ArraySet<>(mCallbacks.size() - 1);
+ for (WidgetPreviewLoadedCallback existingCallback : mCallbacks) {
+ if (!existingCallback.equals(callback)) {
+ newCallbacks.add(existingCallback);
+ }
+ }
+ return new Loading(mCancellationSignal, newCallbacks);
+ }
+ }
+
+ static final class Loaded extends CacheResult {
+ @NonNull final Bitmap mBitmap;
+
+ Loaded(@NonNull Bitmap bitmap) {
+ mBitmap = bitmap;
+ }
+ }
+ }
+
+ @NonNull
+ private static ComponentKey toComponentKey(@NonNull WidgetItem item) {
+ return new ComponentKey(item.componentName, item.user);
+ }
+}
diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
new file mode 100644
index 0000000..4ec7e60
--- /dev/null
+++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.CancellationSignal;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.Pair;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.GraphicsUtils;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.ShadowGenerator;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SQLiteCacheHelper;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.util.WidgetSizes;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.ExecutionException;
+
+/** {@link WidgetPreviewLoader} that loads preview images from a {@link CacheDb}. */
+public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
+
+ private static final String TAG = "WidgetPreviewLoader";
+ private static final boolean DEBUG = false;
+
+ private final HashMap<String, long[]> mPackageVersions = new HashMap<>();
+
+ /**
+ * Weak reference objects, do not prevent their referents from being made finalizable,
+ * finalized, and then reclaimed.
+ * Note: synchronized block used for this variable is expensive and the block should always
+ * be posted to a background thread.
+ */
+ @Thunk final Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<>());
+
+ private final Context mContext;
+ private final IconCache mIconCache;
+ private final UserCache mUserCache;
+ private final CacheDb mDb;
+ private final float mPreviewBoxCornerRadius;
+
+ public DatabaseWidgetPreviewLoader(Context context, IconCache iconCache) {
+ mContext = context;
+ mIconCache = iconCache;
+ mUserCache = UserCache.INSTANCE.get(context);
+ mDb = new CacheDb(context);
+ float previewCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
+ mPreviewBoxCornerRadius = previewCornerRadius > 0
+ ? previewCornerRadius
+ : mContext.getResources().getDimension(R.dimen.widget_preview_corner_radius);
+ }
+
+ /**
+ * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
+ * called on UI thread.
+ *
+ * @return a request id which can be used to cancel the request.
+ */
+ @Override
+ @NonNull
+ public CancellationSignal loadPreview(
+ @NonNull BaseActivity activity,
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback) {
+ int previewWidth = previewSize.getWidth();
+ int previewHeight = previewSize.getHeight();
+ String size = previewWidth + "x" + previewHeight;
+ WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
+
+ PreviewLoadTask task =
+ new PreviewLoadTask(activity, key, item, previewWidth, previewHeight, callback);
+ task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
+
+ CancellationSignal signal = new CancellationSignal();
+ signal.setOnCancelListener(task);
+ return signal;
+ }
+
+ /** Clears the database storing previews. */
+ public void refresh() {
+ mDb.clear();
+ }
+
+ /**
+ * The DB holds the generated previews for various components. Previews can also have different
+ * sizes (landscape vs portrait).
+ */
+ private static class CacheDb extends SQLiteCacheHelper {
+ private static final int DB_VERSION = 9;
+
+ private static final String TABLE_NAME = "shortcut_and_widget_previews";
+ private static final String COLUMN_COMPONENT = "componentName";
+ private static final String COLUMN_USER = "profileId";
+ private static final String COLUMN_SIZE = "size";
+ private static final String COLUMN_PACKAGE = "packageName";
+ private static final String COLUMN_LAST_UPDATED = "lastUpdated";
+ private static final String COLUMN_VERSION = "version";
+ private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
+
+ CacheDb(Context context) {
+ super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
+ }
+
+ @Override
+ public void onCreateTable(SQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS "
+ + TABLE_NAME
+ + " ("
+ + COLUMN_COMPONENT
+ + " TEXT NOT NULL, "
+ + COLUMN_USER
+ + " INTEGER NOT NULL, "
+ + COLUMN_SIZE
+ + " TEXT NOT NULL, "
+ + COLUMN_PACKAGE
+ + " TEXT NOT NULL, "
+ + COLUMN_LAST_UPDATED
+ + " INTEGER NOT NULL DEFAULT 0, "
+ + COLUMN_VERSION
+ + " INTEGER NOT NULL DEFAULT 0, "
+ + COLUMN_PREVIEW_BITMAP
+ + " BLOB, "
+ + "PRIMARY KEY ("
+ + COLUMN_COMPONENT
+ + ", "
+ + COLUMN_USER
+ + ", "
+ + COLUMN_SIZE
+ + ") "
+ +
+ ");");
+ }
+ }
+
+ @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) {
+ ContentValues values = new ContentValues();
+ values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
+ values.put(CacheDb.COLUMN_USER, mUserCache.getSerialNumberForUser(key.user));
+ values.put(CacheDb.COLUMN_SIZE, key.mSize);
+ values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
+ values.put(CacheDb.COLUMN_VERSION, versions[0]);
+ values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
+ values.put(CacheDb.COLUMN_PREVIEW_BITMAP, GraphicsUtils.flattenBitmap(preview));
+ mDb.insertOrReplace(values);
+ }
+
+ /** Removes the package from the preview database. */
+ public void removePackage(String packageName, UserHandle user) {
+ removePackage(packageName, user, mUserCache.getSerialNumberForUser(user));
+ }
+
+ /** Removes the package from the preview database. */
+ public void removePackage(String packageName, UserHandle user, long userSerial) {
+ synchronized (mPackageVersions) {
+ mPackageVersions.remove(packageName);
+ }
+
+ mDb.delete(
+ CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
+ new String[]{packageName, Long.toString(userSerial)});
+ }
+
+ /**
+ * Updates the persistent DB:
+ * 1. Any preview generated for an old package version is removed
+ * 2. Any preview for an absent package is removed
+ * This ensures that we remove entries for packages which changed while the launcher was dead.
+ *
+ * @param packageUser if provided, specifies that list only contains previews for the
+ * given package/user, otherwise the list contains all previews
+ */
+ public void removeObsoletePreviews(ArrayList<? extends ComponentKey> list,
+ @Nullable PackageUserKey packageUser) {
+ Preconditions.assertWorkerThread();
+
+ LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>();
+
+ for (ComponentKey key : list) {
+ final long userId = mUserCache.getSerialNumberForUser(key.user);
+ HashSet<String> packages = validPackages.get(userId);
+ if (packages == null) {
+ packages = new HashSet<>();
+ validPackages.put(userId, packages);
+ }
+ packages.add(key.componentName.getPackageName());
+ }
+
+ LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
+ long passedUserId = packageUser == null ? 0
+ : mUserCache.getSerialNumberForUser(packageUser.mUser);
+ Cursor c = null;
+ try {
+ c = mDb.query(
+ new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
+ CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
+ null, null);
+ while (c.moveToNext()) {
+ long userId = c.getLong(0);
+ String pkg = c.getString(1);
+ long lastUpdated = c.getLong(2);
+ long version = c.getLong(3);
+
+ if (packageUser != null && (!pkg.equals(packageUser.mPackageName)
+ || userId != passedUserId)) {
+ // This preview is associated with a different package/user, no need to remove.
+ continue;
+ }
+
+ HashSet<String> packages = validPackages.get(userId);
+ if (packages != null && packages.contains(pkg)) {
+ long[] versions = getPackageVersion(pkg);
+ if (versions[0] == version && versions[1] == lastUpdated) {
+ // Every thing checks out
+ continue;
+ }
+ }
+
+ // We need to delete this package.
+ packages = packagesToDelete.get(userId);
+ if (packages == null) {
+ packages = new HashSet<>();
+ packagesToDelete.put(userId, packages);
+ }
+ packages.add(pkg);
+ }
+
+ for (int i = 0; i < packagesToDelete.size(); i++) {
+ long userId = packagesToDelete.keyAt(i);
+ UserHandle user = mUserCache.getUserForSerialNumber(userId);
+ for (String pkg : packagesToDelete.valueAt(i)) {
+ removePackage(pkg, user, userId);
+ }
+ }
+ } catch (SQLException e) {
+ Log.e(TAG, "Error updating widget previews", e);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Reads the preview bitmap from the DB or null if the preview is not in the DB.
+ */
+ @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
+ Cursor cursor = null;
+ try {
+ cursor = mDb.query(
+ new String[]{CacheDb.COLUMN_PREVIEW_BITMAP},
+ CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND "
+ + CacheDb.COLUMN_SIZE + " = ?",
+ new String[]{
+ key.componentName.flattenToShortString(),
+ Long.toString(mUserCache.getSerialNumberForUser(key.user)),
+ key.mSize
+ });
+ // If cancelled, skip getting the blob and decoding it into a bitmap
+ if (loadTask.isCancelled()) {
+ return null;
+ }
+ if (cursor.moveToNext()) {
+ byte[] blob = cursor.getBlob(0);
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inBitmap = recycle;
+ try {
+ if (!loadTask.isCancelled()) {
+ return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ } catch (SQLException e) {
+ Log.w(TAG, "Error loading preview from DB", e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a generated preview for a widget and if the preview should be saved in persistent
+ * storage.
+ * @param launcher
+ * @param item
+ * @param recycle
+ * @param previewWidth
+ * @param previewHeight
+ * @return Pair<Bitmap, Boolean>
+ */
+ private Pair<Bitmap, Boolean> generatePreview(BaseActivity launcher, WidgetItem item,
+ Bitmap recycle,
+ int previewWidth, int previewHeight) {
+ if (item.widgetInfo != null) {
+ return generateWidgetPreview(launcher, item.widgetInfo,
+ previewWidth, recycle, null);
+ } else {
+ return new Pair<>(generateShortcutPreview(launcher, item.activityInfo,
+ previewWidth, previewHeight, recycle), false);
+ }
+ }
+
+ /**
+ * Generates the widget preview from either the {@link WidgetManagerHelper} or cache
+ * and add badge at the bottom right corner.
+ *
+ * @param launcher
+ * @param info information about the widget
+ * @param maxPreviewWidth width of the preview on either workspace or tray
+ * @param preview bitmap that can be recycled
+ * @param preScaledWidthOut return the width of the returned bitmap
+ * @return Pair<Bitmap (the preview) , Boolean (should be stored in db)>
+ */
+ public Pair<Bitmap, Boolean> generateWidgetPreview(BaseActivity launcher,
+ LauncherAppWidgetProviderInfo info,
+ int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
+ // Load the preview image if possible
+ if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
+
+ Drawable drawable = null;
+ if (info.previewImage != 0) {
+ try {
+ drawable = info.loadPreviewImage(mContext, 0);
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "Error loading widget preview for: " + info.provider, e);
+ // During OutOfMemoryError, the previous heap stack is not affected. Catching
+ // an OOM error here should be safe & not affect other parts of launcher.
+ drawable = null;
+ }
+ if (drawable != null) {
+ drawable = mutateOnMainThread(drawable);
+ } else {
+ Log.w(TAG, "Can't load widget preview drawable 0x"
+ + Integer.toHexString(info.previewImage)
+ + " for provider: "
+ + info.provider);
+ }
+ }
+
+ final boolean widgetPreviewExists = (drawable != null);
+ final int spanX = info.spanX;
+ final int spanY = info.spanY;
+
+ int previewWidth;
+ int previewHeight;
+
+ boolean savePreviewImage = widgetPreviewExists || info.previewImage == 0;
+
+ if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
+ && drawable.getIntrinsicHeight() > 0) {
+ previewWidth = drawable.getIntrinsicWidth();
+ previewHeight = drawable.getIntrinsicHeight();
+ } else {
+ DeviceProfile dp = launcher.getDeviceProfile();
+ Size widgetSize = WidgetSizes.getWidgetPaddedSizePx(mContext, info.provider, dp, spanX,
+ spanY);
+ previewWidth = widgetSize.getWidth();
+ previewHeight = widgetSize.getHeight();
+ }
+
+ // Scale to fit width only - let the widget preview be clipped in the
+ // vertical dimension
+ float scale = 1f;
+ if (preScaledWidthOut != null) {
+ preScaledWidthOut[0] = previewWidth;
+ }
+ if (previewWidth > maxPreviewWidth) {
+ scale = maxPreviewWidth / (float) (previewWidth);
+ }
+ if (scale != 1f) {
+ previewWidth = Math.max((int) (scale * previewWidth), 1);
+ previewHeight = Math.max((int) (scale * previewHeight), 1);
+ }
+
+ final Canvas c = new Canvas();
+ if (preview == null) {
+ // If no bitmap was provided, then allocate a new one with the right size.
+ preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
+ c.setBitmap(preview);
+ } else {
+ // If a bitmap was passed in, attempt to reconfigure the bitmap to the same dimensions
+ // as the preview.
+ try {
+ preview.reconfigure(previewWidth, previewHeight, preview.getConfig());
+ } catch (IllegalArgumentException e) {
+ // This occurs if the preview can't be reconfigured for any reason. In this case,
+ // allocate a new bitmap with the right size.
+ preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
+ }
+
+ c.setBitmap(preview);
+ c.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+
+ // Draw the scaled preview into the final bitmap
+ if (widgetPreviewExists) {
+ drawable.setBounds(0, 0, previewWidth, previewHeight);
+ drawable.draw(c);
+ } else {
+ RectF boxRect;
+
+ // Draw horizontal and vertical lines to represent individual columns.
+ final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ if (Utilities.ATLEAST_S) {
+ boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
+ previewWidth, /* bottom= */ previewHeight);
+
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(Color.WHITE);
+ float roundedCorner = mContext.getResources().getDimension(
+ android.R.dimen.system_app_widget_background_radius);
+ c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
+ } else {
+ boxRect = drawBoxWithShadow(c, previewWidth, previewHeight);
+ }
+
+ p.setStyle(Paint.Style.STROKE);
+ p.setStrokeWidth(mContext.getResources()
+ .getDimension(R.dimen.widget_preview_cell_divider_width));
+ p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+
+ float t = boxRect.left;
+ float tileSize = boxRect.width() / spanX;
+ for (int i = 1; i < spanX; i++) {
+ t += tileSize;
+ c.drawLine(t, 0, t, previewHeight, p);
+ }
+
+ t = boxRect.top;
+ tileSize = boxRect.height() / spanY;
+ for (int i = 1; i < spanY; i++) {
+ t += tileSize;
+ c.drawLine(0, t, previewWidth, t, p);
+ }
+
+ // Draw icon in the center.
+ try {
+ Drawable icon =
+ mIconCache.getFullResIcon(info.provider.getPackageName(), info.icon);
+ if (icon != null) {
+ int appIconSize = launcher.getDeviceProfile().iconSizePx;
+ int iconSize = (int) Math.min(appIconSize * scale,
+ Math.min(boxRect.width(), boxRect.height()));
+
+ icon = mutateOnMainThread(icon);
+ int hoffset = (previewWidth - iconSize) / 2;
+ int yoffset = (previewHeight - iconSize) / 2;
+ icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
+ icon.draw(c);
+ }
+ } catch (Resources.NotFoundException e) {
+ savePreviewImage = false;
+ }
+ c.setBitmap(null);
+ }
+ return new Pair<>(preview, savePreviewImage);
+ }
+
+ private RectF drawBoxWithShadow(Canvas c, int width, int height) {
+ Resources res = mContext.getResources();
+
+ ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.WHITE);
+ builder.shadowBlur = res.getDimension(R.dimen.widget_preview_shadow_blur);
+ builder.radius = mPreviewBoxCornerRadius;
+ builder.keyShadowDistance = res.getDimension(R.dimen.widget_preview_key_shadow_distance);
+
+ builder.bounds.set(builder.shadowBlur, builder.shadowBlur,
+ width - builder.shadowBlur,
+ height - builder.shadowBlur - builder.keyShadowDistance);
+ builder.drawShadow(c);
+ return builder.bounds;
+ }
+
+ private Bitmap generateShortcutPreview(BaseActivity launcher, ShortcutConfigActivityInfo info,
+ int maxWidth, int maxHeight, Bitmap preview) {
+ int iconSize = launcher.getDeviceProfile().allAppsIconSizePx;
+ int padding = launcher.getResources()
+ .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
+
+ int size = iconSize + 2 * padding;
+ if (maxHeight < size || maxWidth < size) {
+ throw new RuntimeException("Max size is too small for preview");
+ }
+ final Canvas c = new Canvas();
+ if (preview == null || preview.getWidth() < size || preview.getHeight() < size) {
+ preview = Bitmap.createBitmap(size, size, Config.ARGB_8888);
+ c.setBitmap(preview);
+ } else {
+ if (preview.getWidth() > size || preview.getHeight() > size) {
+ preview.reconfigure(size, size, preview.getConfig());
+ }
+
+ // Reusing bitmap. Clear it.
+ c.setBitmap(preview);
+ c.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+
+ drawBoxWithShadow(c, size, size);
+
+ LauncherIcons li = LauncherIcons.obtain(mContext);
+ Drawable icon = li.createBadgedIconBitmap(
+ mutateOnMainThread(info.getFullResIcon(mIconCache)),
+ Process.myUserHandle(), 0).newIcon(launcher);
+ li.recycle();
+
+ icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
+ icon.draw(c);
+ c.setBitmap(null);
+ return preview;
+ }
+
+ private Drawable mutateOnMainThread(final Drawable drawable) {
+ try {
+ return MAIN_EXECUTOR.submit(drawable::mutate).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @return an array of containing versionCode and lastUpdatedTime for the package.
+ */
+ @Thunk long[] getPackageVersion(String packageName) {
+ synchronized (mPackageVersions) {
+ long[] versions = mPackageVersions.get(packageName);
+ if (versions == null) {
+ versions = new long[2];
+ try {
+ PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName,
+ PackageManager.GET_UNINSTALLED_PACKAGES);
+ versions[0] = info.versionCode;
+ versions[1] = info.lastUpdateTime;
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "PackageInfo not found", e);
+ }
+ mPackageVersions.put(packageName, versions);
+ }
+ return versions;
+ }
+ }
+
+ private class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
+ implements CancellationSignal.OnCancelListener {
+ @Thunk final WidgetCacheKey mKey;
+ private final WidgetItem mInfo;
+ private final int mPreviewHeight;
+ private final int mPreviewWidth;
+ private final WidgetPreviewLoadedCallback mCallback;
+ private final BaseActivity mActivity;
+ @Thunk long[] mVersions;
+ @Thunk Bitmap mBitmapToRecycle;
+
+ @Nullable private Bitmap mUnusedPreviewBitmap;
+ private boolean mSaveToDB = false;
+
+ PreviewLoadTask(BaseActivity activity, WidgetCacheKey key, WidgetItem info,
+ int previewWidth, int previewHeight, WidgetPreviewLoadedCallback callback) {
+ mActivity = activity;
+ mKey = key;
+ mInfo = info;
+ mPreviewHeight = previewHeight;
+ mPreviewWidth = previewWidth;
+ mCallback = callback;
+ if (DEBUG) {
+ Log.d(TAG, String.format("%s, %s, %d, %d",
+ mKey, mInfo, mPreviewHeight, mPreviewWidth));
+ }
+ }
+
+ @Override
+ protected Bitmap doInBackground(Void... params) {
+ Bitmap unusedBitmap = null;
+
+ // If already cancelled before this gets to run in the background, then return early
+ if (isCancelled()) {
+ return null;
+ }
+ synchronized (mUnusedBitmaps) {
+ // Check if we can re-use a bitmap
+ for (Bitmap candidate : mUnusedBitmaps) {
+ if (candidate != null && candidate.isMutable()
+ && candidate.getWidth() == mPreviewWidth
+ && candidate.getHeight() == mPreviewHeight) {
+ unusedBitmap = candidate;
+ mUnusedBitmaps.remove(unusedBitmap);
+ break;
+ }
+ }
+ }
+
+ // creating a bitmap is expensive. Do not do this inside synchronized block.
+ if (unusedBitmap == null) {
+ unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888);
+ }
+ // If cancelled now, don't bother reading the preview from the DB
+ if (isCancelled()) {
+ return unusedBitmap;
+ }
+ Bitmap preview = readFromDb(mKey, unusedBitmap, this);
+ // Only consider generating the preview if we have not cancelled the task already
+ if (!isCancelled() && preview == null) {
+ // Fetch the version info before we generate the preview, so that, in-case the
+ // app was updated while we are generating the preview, we use the old version info,
+ // which would gets re-written next time.
+ boolean persistable = mInfo.activityInfo == null
+ || mInfo.activityInfo.isPersistable();
+ mVersions = persistable ? getPackageVersion(mKey.componentName.getPackageName())
+ : null;
+
+ // it's not in the db... we need to generate it
+ Pair<Bitmap, Boolean> pair = generatePreview(mActivity, mInfo, unusedBitmap,
+ mPreviewWidth, mPreviewHeight);
+ preview = pair.first;
+
+ if (preview != unusedBitmap) {
+ mUnusedPreviewBitmap = unusedBitmap;
+ }
+
+ this.mSaveToDB = pair.second;
+ }
+ return preview;
+ }
+
+ @Override
+ protected void onPostExecute(final Bitmap preview) {
+ mCallback.onPreviewLoaded(preview);
+
+ // Write the generated preview to the DB in the worker thread
+ if (mVersions != null) {
+ MODEL_EXECUTOR.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mUnusedPreviewBitmap != null) {
+ // If we didn't end up using the bitmap, it can be added back into the
+ // recycled set.
+ synchronized (mUnusedBitmaps) {
+ mUnusedBitmaps.add(mUnusedPreviewBitmap);
+ }
+ }
+
+ if (!isCancelled() && mSaveToDB) {
+ // If we are still using this preview, then write it to the DB and then
+ // let the normal clear mechanism recycle the bitmap
+ writeToDb(mKey, mVersions, preview);
+ mBitmapToRecycle = preview;
+ } else {
+ // If we've already cancelled, then skip writing the bitmap to the DB
+ // and manually add the bitmap back to the recycled set
+ synchronized (mUnusedBitmaps) {
+ mUnusedBitmaps.add(preview);
+ }
+ }
+ }
+ });
+ } else {
+ // If we don't need to write to disk, then ensure the preview gets recycled by
+ // the normal clear mechanism
+ mBitmapToRecycle = preview;
+ }
+ }
+
+ @Override
+ protected void onCancelled(final Bitmap preview) {
+ // If we've cancelled while the task is running, then can return the bitmap to the
+ // recycled set immediately. Otherwise, it will be recycled after the preview is written
+ // to disk.
+ if (preview != null) {
+ MODEL_EXECUTOR.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mUnusedBitmaps) {
+ mUnusedBitmaps.add(preview);
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ cancel(true);
+
+ // This only handles the case where the PreviewLoadTask is cancelled after the task has
+ // successfully completed (including having written to disk when necessary). In the
+ // other cases where it is cancelled while the task is running, it will be cleaned up
+ // in the tasks's onCancelled() call, and if cancelled while the task is writing to
+ // disk, it will be cancelled in the task's onPostExecute() call.
+ if (mBitmapToRecycle != null) {
+ MODEL_EXECUTOR.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mUnusedBitmaps) {
+ mUnusedBitmaps.add(mBitmapToRecycle);
+ }
+ mBitmapToRecycle = null;
+ }
+ });
+ }
+ }
+ }
+
+ private static final class WidgetCacheKey extends ComponentKey {
+
+ @Thunk final String mSize;
+
+ WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
+ super(componentName, user);
+ this.mSize = size;
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode() ^ mSize.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return super.equals(o) && ((WidgetCacheKey) o).mSize.equals(mSize);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java
index 3a24c3d..149ac57 100644
--- a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java
@@ -44,7 +44,7 @@
mPaint = new TextPaint();
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
- mLauncher.getDeviceProfile().getFullScreenProfile().iconTextSizePx,
+ mLauncher.getDeviceProfile().iconTextSizePx,
getResources().getDisplayMetrics()));
setBackgroundResource(R.drawable.bg_deferred_app_widget);
}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
new file mode 100644
index 0000000..fe83f3f
--- /dev/null
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget;
+
+import static android.app.Activity.RESULT_CANCELED;
+
+import android.appwidget.AppWidgetHost;
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.SparseArray;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
+
+import java.util.ArrayList;
+import java.util.function.IntConsumer;
+
+
+/**
+ * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView}
+ * which correctly captures all long-press events. This ensures that users can
+ * always pick up and move widgets.
+ */
+public class LauncherAppWidgetHost extends AppWidgetHost {
+
+ private static final int FLAG_LISTENING = 1;
+ private static final int FLAG_STATE_IS_NORMAL = 1 << 1;
+ private static final int FLAG_ACTIVITY_STARTED = 1 << 2;
+ private static final int FLAG_ACTIVITY_RESUMED = 1 << 3;
+ private static final int FLAGS_SHOULD_LISTEN =
+ FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED;
+ // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden
+ private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle";
+ // TODO(b/191735836): Replace with SplashScreen.SPLASH_SCREEN_STYLE_EMPTY when un-hidden
+ private static final int SPLASH_SCREEN_STYLE_EMPTY = 0;
+
+ public static final int APPWIDGET_HOST_ID = 1024;
+
+ private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>();
+ private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
+ private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
+
+ private final Context mContext;
+ private int mFlags = FLAG_STATE_IS_NORMAL;
+
+ private IntConsumer mAppWidgetRemovedCallback = null;
+
+
+ public LauncherAppWidgetHost(Context context) {
+ this(context, null);
+ }
+
+ public LauncherAppWidgetHost(Context context,
+ IntConsumer appWidgetRemovedCallback) {
+ super(context, APPWIDGET_HOST_ID);
+ mContext = context;
+ mAppWidgetRemovedCallback = appWidgetRemovedCallback;
+ }
+
+ @Override
+ protected LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
+ AppWidgetProviderInfo appWidget) {
+ final LauncherAppWidgetHostView view;
+ if (mPendingViews.get(appWidgetId) != null) {
+ view = mPendingViews.get(appWidgetId);
+ mPendingViews.remove(appWidgetId);
+ } else {
+ view = new LauncherAppWidgetHostView(context);
+ }
+ mViews.put(appWidgetId, view);
+ return view;
+ }
+
+ @Override
+ public void startListening() {
+ if (WidgetsModel.GO_DISABLE_WIDGETS) {
+ return;
+ }
+ mFlags |= FLAG_LISTENING;
+ try {
+ super.startListening();
+ } catch (Exception e) {
+ if (!Utilities.isBinderSizeError(e)) {
+ throw new RuntimeException(e);
+ }
+ // We're willing to let this slide. The exception is being caused by the list of
+ // RemoteViews which is being passed back. The startListening relationship will
+ // have been established by this point, and we will end up populating the
+ // widgets upon bind anyway. See issue 14255011 for more context.
+ }
+
+ // We go in reverse order and inflate any deferred widget
+ for (int i = mViews.size() - 1; i >= 0; i--) {
+ LauncherAppWidgetHostView view = mViews.valueAt(i);
+ if (view instanceof DeferredAppWidgetHostView) {
+ view.reInflate();
+ }
+ }
+ }
+
+ @Override
+ public void stopListening() {
+ if (WidgetsModel.GO_DISABLE_WIDGETS) {
+ return;
+ }
+ mFlags &= ~FLAG_LISTENING;
+ super.stopListening();
+ }
+
+ public boolean isListening() {
+ return (mFlags & FLAG_LISTENING) != 0;
+ }
+
+ /**
+ * Sets or unsets a flag the can change whether the widget host should be in the listening
+ * state.
+ */
+ private void setShouldListenFlag(int flag, boolean on) {
+ if (on) {
+ mFlags |= flag;
+ } else {
+ mFlags &= ~flag;
+ }
+
+ final boolean listening = isListening();
+ if (!listening && (mFlags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN) {
+ // Postpone starting listening until all flags are on.
+ startListening();
+ } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) {
+ // Postpone stopping listening until the activity is stopped.
+ stopListening();
+ }
+ }
+
+ /**
+ * Registers an "entering/leaving Normal state" event.
+ */
+ public void setStateIsNormal(boolean isNormal) {
+ setShouldListenFlag(FLAG_STATE_IS_NORMAL, isNormal);
+ }
+
+ /**
+ * Registers an "activity started/stopped" event.
+ */
+ public void setActivityStarted(boolean isStarted) {
+ setShouldListenFlag(FLAG_ACTIVITY_STARTED, isStarted);
+ }
+
+ /**
+ * Registers an "activity paused/resumed" event.
+ */
+ public void setActivityResumed(boolean isResumed) {
+ setShouldListenFlag(FLAG_ACTIVITY_RESUMED, isResumed);
+ }
+
+ @Override
+ public int allocateAppWidgetId() {
+ if (WidgetsModel.GO_DISABLE_WIDGETS) {
+ return AppWidgetManager.INVALID_APPWIDGET_ID;
+ }
+
+ return super.allocateAppWidgetId();
+ }
+
+ public void addProviderChangeListener(ProviderChangedListener callback) {
+ mProviderChangeListeners.add(callback);
+ }
+
+ public void removeProviderChangeListener(ProviderChangedListener callback) {
+ mProviderChangeListeners.remove(callback);
+ }
+
+ protected void onProvidersChanged() {
+ if (!mProviderChangeListeners.isEmpty()) {
+ for (ProviderChangedListener callback : new ArrayList<>(mProviderChangeListeners)) {
+ callback.notifyWidgetProvidersChanged();
+ }
+ }
+ }
+
+ public void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
+ mPendingViews.put(appWidgetId, view);
+ }
+
+ public AppWidgetHostView createView(Context context, int appWidgetId,
+ LauncherAppWidgetProviderInfo appWidget) {
+ if (appWidget.isCustomWidget()) {
+ LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
+ lahv.setAppWidget(0, appWidget);
+ CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
+ return lahv;
+ } else if ((mFlags & FLAG_LISTENING) == 0) {
+ DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
+ view.setAppWidget(appWidgetId, appWidget);
+ mViews.put(appWidgetId, view);
+ return view;
+ } else {
+ try {
+ return super.createView(context, appWidgetId, appWidget);
+ } catch (Exception e) {
+ if (!Utilities.isBinderSizeError(e)) {
+ throw new RuntimeException(e);
+ }
+
+ // If the exception was thrown while fetching the remote views, let the view stay.
+ // This will ensure that if the widget posts a valid update later, the view
+ // will update.
+ LauncherAppWidgetHostView view = mViews.get(appWidgetId);
+ if (view == null) {
+ view = onCreateView(mContext, appWidgetId, appWidget);
+ }
+ view.setAppWidget(appWidgetId, appWidget);
+ view.switchToErrorView();
+ return view;
+ }
+ }
+ }
+
+ /**
+ * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk.
+ */
+ @Override
+ protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) {
+ LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
+ mContext, appWidget);
+ super.onProviderChanged(appWidgetId, info);
+ // The super method updates the dimensions of the providerInfo. Update the
+ // launcher spans accordingly.
+ info.initSpans(mContext, LauncherAppState.getIDP(mContext));
+ }
+
+ /**
+ * Called on an appWidget is removed for a widgetId
+ *
+ * @param appWidgetId TODO: make this override when SDK is updated
+ */
+ public void onAppWidgetRemoved(int appWidgetId) {
+ if (mAppWidgetRemovedCallback == null) {
+ return;
+ }
+ mAppWidgetRemovedCallback.accept(appWidgetId);
+ }
+
+ @Override
+ public void deleteAppWidgetId(int appWidgetId) {
+ super.deleteAppWidgetId(appWidgetId);
+ mViews.remove(appWidgetId);
+ }
+
+ @Override
+ public void clearViews() {
+ super.clearViews();
+ mViews.clear();
+ }
+
+ public void startBindFlow(BaseActivity activity,
+ int appWidgetId, AppWidgetProviderInfo info, int requestCode) {
+
+ if (WidgetsModel.GO_DISABLE_WIDGETS) {
+ sendActionCancelled(activity, requestCode);
+ return;
+ }
+
+ Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.getProfile());
+ // TODO: we need to make sure that this accounts for the options bundle.
+ // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
+ activity.startActivityForResult(intent, requestCode);
+ }
+
+ /**
+ * Launches an app widget's configuration activity.
+ * @param activity The activity from which to launch the configuration activity
+ * @param widgetId The id of the bound app widget to be configured
+ * @param requestCode An optional request code to be returned with the result
+ */
+ public void startConfigActivity(BaseDraggingActivity activity, int widgetId, int requestCode) {
+ if (WidgetsModel.GO_DISABLE_WIDGETS) {
+ sendActionCancelled(activity, requestCode);
+ return;
+ }
+
+ try {
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startConfigActivity");
+ startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode,
+ getConfigurationActivityOptions(activity, widgetId));
+ } catch (ActivityNotFoundException | SecurityException e) {
+ Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+ sendActionCancelled(activity, requestCode);
+ }
+ }
+
+ /**
+ * Returns an {@link android.app.ActivityOptions} bundle from the {code activity} for launching
+ * the configuration of the {@code widgetId} app widget, or null of options cannot be produced.
+ */
+ @Nullable
+ private Bundle getConfigurationActivityOptions(BaseDraggingActivity activity, int widgetId) {
+ LauncherAppWidgetHostView view = mViews.get(widgetId);
+ if (view == null) return null;
+ Object tag = view.getTag();
+ if (!(tag instanceof ItemInfo)) return null;
+ Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle();
+ bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY);
+ return bundle;
+ }
+
+ private void sendActionCancelled(final BaseActivity activity, final int requestCode) {
+ new Handler().post(() -> activity.onActivityResult(requestCode, RESULT_CANCELED, null));
+ }
+
+ /**
+ * Listener for getting notifications on provider changes.
+ */
+ public interface ProviderChangedListener {
+
+ void notifyWidgetProvidersChanged();
+ }
+}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 6f2e179..63bc416 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -19,36 +19,53 @@
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.RectF;
import android.os.Handler;
import android.os.SystemClock;
import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AdapterView;
import android.widget.Advanceable;
import android.widget.RemoteViews;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
+import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
+
+import java.util.List;
/**
* {@inheritDoc}
*/
public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
- implements TouchCompleteListener, View.OnLongClickListener {
+ implements TouchCompleteListener, View.OnLongClickListener,
+ LocalColorExtractor.Listener {
+
+ private static final String LOG_TAG = "LauncherAppWidgetHostView";
// Related to the auto-advancing of widgets
private static final long ADVANCE_INTERVAL = 20000;
@@ -56,42 +73,95 @@
// Maintains a list of widget ids which are supposed to be auto advanced.
private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
+ // Maximum duration for which updates can be deferred.
+ private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
protected final LayoutInflater mInflater;
private final CheckLongPressHelper mLongPressHelper;
protected final Launcher mLauncher;
+ private final Workspace mWorkspace;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mReinflateOnConfigChange;
+ // Maintain the color manager.
+ private final LocalColorExtractor mColorExtractor;
+
private boolean mIsScrollable;
private boolean mIsAttachedToWindow;
private boolean mIsAutoAdvanceRegistered;
+ private boolean mIsInDragMode = false;
private Runnable mAutoAdvanceRunnable;
+ private RectF mLastLocationRegistered = null;
+ @Nullable private AppWidgetHostViewDragListener mDragListener;
+ // Used to store the widget sizes in drag layer coordinates.
+ private final Rect mCurrentWidgetSize = new Rect();
+ private final Rect mWidgetSizeAtDrag = new Rect();
+ private final RectF mTempRectF = new RectF();
+ private final Rect mEnforcedRectangle = new Rect();
+ private final float mEnforcedCornerRadius;
+ private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
+ outline.setEmpty();
+ } else {
+ outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
+ }
+ }
+ };
+ private final Object mUpdateLock = new Object();
+ private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper;
+ private long mDeferUpdatesUntilMillis = 0;
+ private RemoteViews mDeferredRemoteViews;
+ private boolean mHasDeferredColorChange = false;
+ private @Nullable SparseIntArray mDeferredColorChange = null;
+ private boolean mEnableColorExtraction = true;
public LauncherAppWidgetHostView(Context context) {
super(context);
mLauncher = Launcher.getLauncher(context);
+ mWorkspace = mLauncher.getWorkspace();
mLongPressHelper = new CheckLongPressHelper(this, this);
mInflater = LayoutInflater.from(context);
setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
setBackgroundResource(R.drawable.widget_internal_focus_bg);
- if (Utilities.ATLEAST_OREO) {
- setExecutor(Executors.THREAD_POOL_EXECUTOR);
- }
+ setExecutor(Executors.THREAD_POOL_EXECUTOR);
if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
setOnLightBackground(true);
}
+ mColorExtractor = LocalColorExtractor.newInstance(getContext());
+ mColorExtractor.setListener(this);
+
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
+ mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer());
+ }
+
+ @Override
+ public void setColorResources(@Nullable SparseIntArray colors) {
+ if (colors == null) {
+ resetColorResources();
+ } else {
+ super.setColorResources(colors);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mIsInDragMode && mDragListener != null) {
+ mDragListener.onDragContentChanged();
+ }
}
@Override
public boolean onLongClick(View view) {
if (mIsScrollable) {
- DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
+ DragLayer dragLayer = mLauncher.getDragLayer();
dragLayer.requestDisallowInterceptTouchEvent(false);
}
view.performLongClick();
@@ -105,6 +175,14 @@
@Override
public void updateAppWidget(RemoteViews remoteViews) {
+ synchronized (mUpdateLock) {
+ if (isDeferringUpdates()) {
+ mDeferredRemoteViews = remoteViews;
+ return;
+ }
+ mDeferredRemoteViews = null;
+ }
+
super.updateAppWidget(remoteViews);
// The provider info or the views might have changed.
@@ -126,7 +204,7 @@
if (viewGroup instanceof AdapterView) {
return true;
} else {
- for (int i=0; i < viewGroup.getChildCount(); i++) {
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
if (child instanceof ViewGroup) {
if (checkScrollableRecursively((ViewGroup) child)) {
@@ -138,9 +216,56 @@
return false;
}
+ /**
+ * Returns true if the application of {@link RemoteViews} through {@link #updateAppWidget} and
+ * colors through {@link #onColorsChanged} are currently being deferred.
+ * @see #beginDeferringUpdates()
+ */
+ private boolean isDeferringUpdates() {
+ return SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis;
+ }
+
+ /**
+ * Begin deferring the application of any {@link RemoteViews} updates made through
+ * {@link #updateAppWidget} and color changes through {@link #onColorsChanged} until
+ * {@link #endDeferringUpdates()} has been called or the next {@link #updateAppWidget} or
+ * {@link #onColorsChanged} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS} have elapsed.
+ */
+ public void beginDeferringUpdates() {
+ synchronized (mUpdateLock) {
+ mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS;
+ }
+ }
+
+ /**
+ * Stop deferring the application of {@link RemoteViews} updates made through
+ * {@link #updateAppWidget} and color changes made through {@link #onColorsChanged} and apply
+ * any deferred updates.
+ */
+ public void endDeferringUpdates() {
+ RemoteViews remoteViews;
+ SparseIntArray deferredColors;
+ boolean hasDeferredColors;
+ synchronized (mUpdateLock) {
+ mDeferUpdatesUntilMillis = 0;
+ remoteViews = mDeferredRemoteViews;
+ mDeferredRemoteViews = null;
+ deferredColors = mDeferredColorChange;
+ hasDeferredColors = mHasDeferredColorChange;
+ mDeferredColorChange = null;
+ mHasDeferredColorChange = false;
+ }
+ if (remoteViews != null) {
+ updateAppWidget(remoteViews);
+ }
+ if (hasDeferredColors) {
+ onColorsChanged(null /* rectF */, deferredColors);
+ }
+ }
+
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
+ DragLayer dragLayer = mLauncher.getDragLayer();
if (mIsScrollable) {
dragLayer.requestDisallowInterceptTouchEvent(true);
}
@@ -162,6 +287,10 @@
mIsAttachedToWindow = true;
checkIfAutoAdvance();
+
+ if (mLastLocationRegistered != null) {
+ mColorExtractor.addLocation(List.of(mLastLocationRegistered));
+ }
}
@Override
@@ -172,6 +301,7 @@
// state is updated. So isAttachedToWindow() will return true until next frame.
mIsAttachedToWindow = false;
checkIfAutoAdvance();
+ mColorExtractor.removeLocations();
}
@Override
@@ -218,6 +348,110 @@
}
mIsScrollable = checkScrollableRecursively(this);
+ updateColorExtraction();
+
+ enforceRoundedCorners();
+ }
+
+ /** Starts the drag mode. */
+ public void startDrag(AppWidgetHostViewDragListener dragListener) {
+ mIsInDragMode = true;
+ mDragListener = dragListener;
+ }
+
+ /** Handles a drag event occurred on a workspace page, {@code pageId}. */
+ public void handleDrag(Rect rectInDragLayer, int pageId) {
+ mWidgetSizeAtDrag.set(rectInDragLayer);
+ updateColorExtraction(mWidgetSizeAtDrag, pageId);
+ }
+
+ /** Ends the drag mode. */
+ public void endDrag() {
+ mIsInDragMode = false;
+ mDragListener = null;
+ mWidgetSizeAtDrag.setEmpty();
+ }
+
+ /**
+ * @param rectInDragLayer Rect of widget in drag layer coordinates.
+ * @param pageId The workspace page the widget is on.
+ */
+ private void updateColorExtraction(Rect rectInDragLayer, int pageId) {
+ if (!mEnableColorExtraction) return;
+ mColorExtractor.getExtractedRectForViewRect(mLauncher, pageId, rectInDragLayer, mTempRectF);
+
+ if (mTempRectF.isEmpty()) {
+ return;
+ }
+ if (!isSameLocation(mTempRectF, mLastLocationRegistered, /* epsilon= */ 1e-6f)) {
+ if (mLastLocationRegistered != null) {
+ mColorExtractor.removeLocations();
+ }
+ mLastLocationRegistered = new RectF(mTempRectF);
+ mColorExtractor.addLocation(List.of(mLastLocationRegistered));
+ }
+ }
+
+ /**
+ * Update the color extraction, using the current position of the app widget.
+ */
+ private void updateColorExtraction() {
+ if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) {
+ LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
+ mDragLayerRelativeCoordinateHelper.viewToRect(this, mCurrentWidgetSize);
+ updateColorExtraction(mCurrentWidgetSize,
+ mWorkspace.getPageIndexForScreenId(info.screenId));
+ }
+ }
+
+ /**
+ * Enables the local color extraction.
+ *
+ * @param updateColors If true, this will update the color extraction using the current location
+ * of the App Widget.
+ */
+ public void enableColorExtraction(boolean updateColors) {
+ mEnableColorExtraction = true;
+ if (updateColors) {
+ updateColorExtraction();
+ }
+ }
+
+ /**
+ * Disables the local color extraction.
+ */
+ public void disableColorExtraction() {
+ mEnableColorExtraction = false;
+ }
+
+ // Compare two location rectangles. Locations are always in the [0;1] range.
+ private static boolean isSameLocation(@NonNull RectF rect1, @Nullable RectF rect2,
+ float epsilon) {
+ if (rect2 == null) return false;
+ return isSameCoordinate(rect1.left, rect2.left, epsilon)
+ && isSameCoordinate(rect1.right, rect2.right, epsilon)
+ && isSameCoordinate(rect1.top, rect2.top, epsilon)
+ && isSameCoordinate(rect1.bottom, rect2.bottom, epsilon);
+ }
+
+ private static boolean isSameCoordinate(float c1, float c2, float epsilon) {
+ return Math.abs(c1 - c2) < epsilon;
+ }
+
+ @Override
+ public void onColorsChanged(RectF rectF, SparseIntArray colors) {
+ synchronized (mUpdateLock) {
+ if (isDeferringUpdates()) {
+ mDeferredColorChange = colors;
+ mHasDeferredColorChange = true;
+ return;
+ }
+ mDeferredColorChange = null;
+ mHasDeferredColorChange = false;
+ }
+
+ // setColorResources will reapply the view, which must happen in the UI thread.
+ post(() -> setColorResources(colors));
}
@Override
@@ -312,6 +546,10 @@
return;
}
LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
+ if (info == null) {
+ // This occurs when LauncherAppWidgetHostView is used to render a preview layout.
+ return;
+ }
// Remove and rebind the current widget (which was inflated in the wrong
// orientation), but don't delete it from the database
mLauncher.removeItem(this, info, false /* deleteFromDb */);
@@ -326,4 +564,40 @@
}
return false;
}
+
+ @UiThread
+ private void resetRoundedCorners() {
+ setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+ setClipToOutline(false);
+ }
+
+ @UiThread
+ private void enforceRoundedCorners() {
+ if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
+ resetRoundedCorners();
+ return;
+ }
+ View background = RoundedCornerEnforcement.findBackground(this);
+ if (background == null
+ || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+ resetRoundedCorners();
+ return;
+ }
+ RoundedCornerEnforcement.computeRoundedRectangle(this,
+ background,
+ mEnforcedRectangle);
+ setOutlineProvider(mCornerRadiusEnforcementOutline);
+ setClipToOutline(true);
+ }
+
+ /** Returns the corner radius currently enforced, in pixels. */
+ public float getEnforcedCornerRadius() {
+ return mEnforcedCornerRadius;
+ }
+
+ /** Returns true if the corner radius are enforced for this App Widget. */
+ public boolean hasEnforcedCornerRadius() {
+ return getClipToOutline();
+ }
+
}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
new file mode 100644
index 0000000..d77d99d
--- /dev/null
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -0,0 +1,243 @@
+package com.android.launcher3.widget;
+
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.UserHandle;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.ComponentWithLabelAndIcon;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+
+/**
+ * This class is a thin wrapper around the framework AppWidgetProviderInfo class. This class affords
+ * a common object for describing both framework provided AppWidgets as well as custom widgets
+ * (who's implementation is owned by the launcher). This object represents a widget type / class,
+ * as opposed to a widget instance, and so should not be confused with {@link LauncherAppWidgetInfo}
+ */
+public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo
+ implements ComponentWithLabelAndIcon {
+
+ public static final String CLS_CUSTOM_WIDGET_PREFIX = "#custom-widget-";
+
+ /**
+ * The desired number of cells that this widget occupies horizontally in
+ * {@link com.android.launcher3.CellLayout}.
+ */
+ public int spanX;
+
+ /**
+ * The desired number of cells that this widget occupies vertically in
+ * {@link com.android.launcher3.CellLayout}.
+ */
+ public int spanY;
+
+ /**
+ * The minimum number of cells that this widget can occupy horizontally in
+ * {@link com.android.launcher3.CellLayout}.
+ */
+ public int minSpanX;
+
+ /**
+ * The minimum number of cells that this widget can occupy vertically in
+ * {@link com.android.launcher3.CellLayout}.
+ */
+ public int minSpanY;
+
+ /**
+ * The maximum number of cells that this widget can occupy horizontally in
+ * {@link com.android.launcher3.CellLayout}.
+ */
+ public int maxSpanX;
+
+ /**
+ * The maximum number of cells that this widget can occupy vertically in
+ * {@link com.android.launcher3.CellLayout}.
+ */
+ public int maxSpanY;
+
+ private boolean mIsMinSizeFulfilled;
+
+ public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context,
+ AppWidgetProviderInfo info) {
+ final LauncherAppWidgetProviderInfo launcherInfo;
+ if (info instanceof LauncherAppWidgetProviderInfo) {
+ launcherInfo = (LauncherAppWidgetProviderInfo) info;
+ } else {
+
+ // In lieu of a public super copy constructor, we first write the AppWidgetProviderInfo
+ // into a parcel, and then construct a new LauncherAppWidgetProvider info from the
+ // associated super parcel constructor. This allows us to copy non-public members without
+ // using reflection.
+ Parcel p = Parcel.obtain();
+ info.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ launcherInfo = new LauncherAppWidgetProviderInfo(p);
+ p.recycle();
+ }
+ launcherInfo.initSpans(context, LauncherAppState.getIDP(context));
+ return launcherInfo;
+ }
+
+ protected LauncherAppWidgetProviderInfo() {}
+
+ protected LauncherAppWidgetProviderInfo(Parcel in) {
+ super(in);
+ }
+
+ public void initSpans(Context context, InvariantDeviceProfile idp) {
+ int minSpanX = 0;
+ int minSpanY = 0;
+ int maxSpanX = idp.numColumns;
+ int maxSpanY = idp.numRows;
+ int spanX = 0;
+ int spanY = 0;
+
+ Rect widgetPadding = new Rect();
+ Rect localPadding = new Rect();
+ AppWidgetHostView.getDefaultPaddingForWidget(context, provider, widgetPadding);
+
+ Point cellSize = new Point();
+ for (DeviceProfile dp : idp.supportedProfiles) {
+ dp.getCellSize(cellSize);
+ // We want to account for the extra amount of padding that we are adding to the widget
+ // to ensure that it gets the full amount of space that it has requested.
+ // If grids supports insetting widgets, we do not account for widget padding.
+ if (dp.shouldInsetWidgets()) {
+ localPadding.setEmpty();
+ } else {
+ localPadding.set(widgetPadding);
+ }
+ minSpanX = Math.max(minSpanX,
+ getSpanX(localPadding, minResizeWidth, dp.cellLayoutBorderSpacingPx,
+ cellSize.x));
+ minSpanY = Math.max(minSpanY,
+ getSpanY(localPadding, minResizeHeight, dp.cellLayoutBorderSpacingPx,
+ cellSize.y));
+
+ if (ATLEAST_S) {
+ if (maxResizeWidth > 0) {
+ maxSpanX = Math.min(maxSpanX,
+ getSpanX(localPadding, maxResizeWidth, dp.cellLayoutBorderSpacingPx,
+ cellSize.x));
+ }
+ if (maxResizeHeight > 0) {
+ maxSpanY = Math.min(maxSpanY,
+ getSpanY(localPadding, maxResizeHeight, dp.cellLayoutBorderSpacingPx,
+ cellSize.y));
+ }
+ }
+
+ spanX = Math.max(spanX,
+ getSpanX(localPadding, minWidth, dp.cellLayoutBorderSpacingPx, cellSize.x));
+ spanY = Math.max(spanY,
+ getSpanY(localPadding, minHeight, dp.cellLayoutBorderSpacingPx, cellSize.y));
+ }
+
+ if (ATLEAST_S) {
+ // Ensures maxSpan >= minSpan
+ maxSpanX = Math.max(maxSpanX, minSpanX);
+ maxSpanY = Math.max(maxSpanY, minSpanY);
+
+ // Use targetCellWidth/Height if it is within the min/max ranges.
+ // Otherwise, use the span of minWidth/Height.
+ if (targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX
+ && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {
+ spanX = targetCellWidth;
+ spanY = targetCellHeight;
+ }
+ }
+
+ // If minSpanX/Y > spanX/Y, ignore the minSpanX/Y to match the behavior described in
+ // minResizeWidth & minResizeHeight Android documentation. See
+ // https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo
+ this.minSpanX = Math.min(spanX, minSpanX);
+ this.minSpanY = Math.min(spanY, minSpanY);
+ this.maxSpanX = maxSpanX;
+ this.maxSpanY = maxSpanY;
+ this.mIsMinSizeFulfilled = Math.min(spanX, minSpanX) <= idp.numColumns
+ && Math.min(spanY, minSpanY) <= idp.numRows;
+ // Ensures the default span X and span Y will not exceed the current grid size.
+ this.spanX = Math.min(spanX, idp.numColumns);
+ this.spanY = Math.min(spanY, idp.numRows);
+ }
+
+ /**
+ * Returns {@code true} if the widget's minimum size requirement can be fulfilled in the device
+ * grid setting, {@link InvariantDeviceProfile}, that was passed in
+ * {@link #initSpans(Context, InvariantDeviceProfile)}.
+ */
+ public boolean isMinSizeFulfilled() {
+ return mIsMinSizeFulfilled;
+ }
+
+ private int getSpanX(Rect widgetPadding, int widgetWidth, int cellSpacing, float cellWidth) {
+ return Math.max(1, (int) Math.ceil(
+ (widgetWidth + widgetPadding.left + widgetPadding.right + cellSpacing) / (cellWidth
+ + cellSpacing)));
+ }
+
+ private int getSpanY(Rect widgetPadding, int widgetHeight, int cellSpacing, float cellHeight) {
+ return Math.max(1, (int) Math.ceil(
+ (widgetHeight + widgetPadding.top + widgetPadding.bottom + cellSpacing) / (
+ cellHeight + cellSpacing)));
+ }
+
+ public String getLabel(PackageManager packageManager) {
+ return super.loadLabel(packageManager);
+ }
+
+ public Point getMinSpans() {
+ return new Point((resizeMode & RESIZE_HORIZONTAL) != 0 ? minSpanX : -1,
+ (resizeMode & RESIZE_VERTICAL) != 0 ? minSpanY : -1);
+ }
+
+ public boolean isCustomWidget() {
+ return provider.getClassName().startsWith(CLS_CUSTOM_WIDGET_PREFIX);
+ }
+
+ public int getWidgetFeatures() {
+ if (Utilities.ATLEAST_P) {
+ return widgetFeatures;
+ } else {
+ return 0;
+ }
+ }
+
+ public boolean isReconfigurable() {
+ return configure != null && (getWidgetFeatures() & WIDGET_FEATURE_RECONFIGURABLE) != 0;
+ }
+
+ public boolean isConfigurationOptional() {
+ return ATLEAST_S
+ && isReconfigurable()
+ && (getWidgetFeatures() & WIDGET_FEATURE_CONFIGURATION_OPTIONAL) != 0;
+ }
+
+ @Override
+ public final ComponentName getComponent() {
+ return provider;
+ }
+
+ @Override
+ public final UserHandle getUser() {
+ return getProfile();
+ }
+
+ @Override
+ public Drawable getFullResIcon(IconCache cache) {
+ return cache.getFullResIcon(provider.getPackageName(), icon);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/widget/LocalColorExtractor.java b/src/com/android/launcher3/widget/LocalColorExtractor.java
new file mode 100644
index 0000000..23d9e15
--- /dev/null
+++ b/src/com/android/launcher3/widget/LocalColorExtractor.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget;
+
+import android.app.WallpaperColors;
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.SparseIntArray;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+import java.util.List;
+
+/** Extracts the colors we need from the wallpaper at given locations. */
+public class LocalColorExtractor implements ResourceBasedOverride {
+
+ /** Listener for color changes on a screen location. */
+ public interface Listener {
+ /**
+ * Method called when the colors on a registered location has changed.
+ *
+ * {@code extractedColors} maps the color resources {@code android.R.colors.system_*} to
+ * their value, in a format that can be passed directly to
+ * {@link AppWidgetHostView#setColorResources(SparseIntArray)}.
+ */
+ void onColorsChanged(RectF rect, SparseIntArray extractedColors);
+ }
+
+ /**
+ * Creates a new instance of LocalColorExtractor
+ */
+ public static LocalColorExtractor newInstance(Context context) {
+ return Overrides.getObject(LocalColorExtractor.class, context.getApplicationContext(),
+ R.string.local_colors_extraction_class);
+ }
+
+ /** Sets the object that will receive the color changes. */
+ public void setListener(@Nullable Listener listener) {
+ // no-op
+ }
+
+ /** Adds a list of locations to track with this listener. */
+ public void addLocation(List<RectF> locations) {
+ // no-op
+ }
+
+ /** Stops tracking any locations. */
+ public void removeLocations() {
+ // no-op
+ }
+
+ /**
+ * Updates the base context to contain the colors override
+ */
+ public void applyColorsOverride(Context base, WallpaperColors colors) { }
+
+ /**
+ * Generates color resource overrides from {@link WallpaperColors}.
+ */
+ @Nullable
+ public SparseIntArray generateColorsOverride(WallpaperColors colors) {
+ return null;
+ }
+
+ /**
+ * Takes a view and returns its rect that can be used by the wallpaper local color extractor.
+ *
+ * @param launcher Launcher class class.
+ * @param pageId The page the workspace item is on.
+ * @param v The view.
+ * @param colorExtractionRectOut The location rect, but converted to a format expected by the
+ * wallpaper local color extractor.
+ */
+ public void getExtractedRectForView(Launcher launcher, int pageId, View v,
+ RectF colorExtractionRectOut) {
+ // no-op
+ }
+
+ /**
+ * Takes a rect in drag layer coordinates and returns the rect that can be used by the wallpaper
+ * local color extractor.
+ *
+ * @param launcher Launcher class.
+ * @param pageId The page the workspace item is on.
+ * @param rectInDragLayer The relevant bounds of the view in drag layer coordinates.
+ * @param colorExtractionRectOut The location rect, but converted to a format expected by the
+ * wallpaper local color extractor.
+ */
+ public void getExtractedRectForViewRect(Launcher launcher, int pageId, Rect rectInDragLayer,
+ RectF colorExtractionRectOut) {
+ // no-op
+ }
+}
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index ed42bc4..6163b51 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -17,6 +17,7 @@
package com.android.launcher3.widget;
import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -25,8 +26,11 @@
import android.view.ViewDebug;
import android.view.ViewGroup;
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Reorderable;
import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
@@ -50,11 +54,16 @@
private final PointF mTranslationForReorderPreview = new PointF(0, 0);
private float mScaleForReorderBounce = 1f;
+ private final Rect mTempRect = new Rect();
+
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mChildrenFocused;
+ protected final BaseActivity mActivity;
+
public NavigableAppWidgetHostView(Context context) {
super(context);
+ mActivity = ActivityContext.lookupContext(context);
}
@Override
@@ -222,6 +231,25 @@
int width = (int) (getMeasuredWidth() * mScaleToFit);
int height = (int) (getMeasuredHeight() * mScaleToFit);
- bounds.set(0, 0 , width, height);
+ getWidgetInset(mActivity.getDeviceProfile(), mTempRect);
+ bounds.set(mTempRect.left, mTempRect.top, width - mTempRect.right,
+ height - mTempRect.bottom);
+ }
+
+ /**
+ * Widgets have padding added by the system. We may choose to inset this padding if the grid
+ * supports it.
+ */
+ public void getWidgetInset(DeviceProfile grid, Rect out) {
+ if (!grid.shouldInsetWidgets()) {
+ out.setEmpty();
+ return;
+ }
+ AppWidgetProviderInfo info = getAppWidgetInfo();
+ if (info == null) {
+ out.set(grid.inv.defaultWidgetPadding);
+ } else {
+ AppWidgetHostView.getDefaultPaddingForWidget(getContext(), info.provider, out);
+ }
}
}
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index bef9a08..cbec642 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -15,14 +15,16 @@
*/
package com.android.launcher3.widget;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
-
import android.appwidget.AppWidgetHostView;
+import android.content.Context;
import android.os.Bundle;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.PendingAddItemInfo;
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.widget.util.WidgetSizes;
/**
* Meta data used for late binding of {@link LauncherAppWidgetProviderInfo}.
@@ -35,8 +37,9 @@
public LauncherAppWidgetProviderInfo info;
public AppWidgetHostView boundWidget;
public Bundle bindOptions = null;
+ public int sourceContainer;
- public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i) {
+ public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, int container) {
if (i.isCustomWidget()) {
itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
} else {
@@ -52,10 +55,22 @@
spanY = i.spanY;
minSpanX = i.minSpanX;
minSpanY = i.minSpanY;
- this.container = CONTAINER_WIDGETS_TRAY;
+ this.sourceContainer = this.container = container;
}
public WidgetAddFlowHandler getHandler() {
return new WidgetAddFlowHandler(info);
}
+
+ public Bundle getDefaultSizeOptions(Context context) {
+ return WidgetSizes.getWidgetSizeOptions(context, componentName, spanX, spanY);
+ }
+
+ @Override
+ public LauncherAtom.ItemInfo buildProto(FolderInfo folderInfo) {
+ LauncherAtom.ItemInfo info = super.buildProto(folderInfo);
+ return info.toBuilder()
+ .setAttribute(LauncherAppWidgetInfo.getAttribute(sourceContainer))
+ .build();
+ }
}
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 9021d9e..57a6d3f 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,8 +16,8 @@
package com.android.launcher3.widget;
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
+import static com.android.launcher3.model.data.PackageItemInfo.CONVERSATIONS;
import android.content.Context;
import android.graphics.Canvas;
@@ -29,15 +29,18 @@
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
+import android.util.SizeF;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.RemoteViews;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.FastBitmapDrawable;
import com.android.launcher3.R;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -46,13 +49,14 @@
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.Themes;
+import java.util.List;
+
public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
implements OnClickListener, ItemInfoUpdateReceiver {
private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5;
private static final float MIN_SATUNATION = 0.7f;
private final Rect mRect = new Rect();
- private View mDefaultView;
private OnClickListener mClickListener;
private final LauncherAppWidgetInfo mInfo;
private final int mStartState;
@@ -81,8 +85,7 @@
setBackgroundResource(R.drawable.pending_widget_bg);
setWillNotDraw(false);
- setElevation(getResources().getDimension(R.dimen.pending_widget_elevation));
- updateAppWidget(null);
+ super.updateAppWidget(null);
setOnClickListener(ItemClickHandler.INSTANCE);
if (info.pendingItemInfo == null) {
@@ -96,9 +99,9 @@
@Override
public void updateAppWidget(RemoteViews remoteViews) {
- super.updateAppWidget(remoteViews);
WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(getContext());
if (widgetManagerHelper.isAppWidgetRestored(mInfo.appWidgetId)) {
+ super.updateAppWidget(remoteViews);
reInflate();
}
}
@@ -110,13 +113,17 @@
}
@Override
+ public void updateAppWidgetSize(Bundle newOptions, List<SizeF> sizes) {
+ // No-op
+ }
+
+ @Override
protected View getDefaultView() {
- if (mDefaultView == null) {
- mDefaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
- mDefaultView.setOnClickListener(this);
- applyState();
- }
- return mDefaultView;
+ View defaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
+ defaultView.setOnClickListener(this);
+ applyState();
+ invalidate();
+ return defaultView;
}
@Override
@@ -141,21 +148,32 @@
mCenterDrawable = null;
}
if (info.bitmap.icon != null) {
+ Drawable widgetCategoryIcon = getWidgetCategoryIcon();
// The view displays three modes,
// 1) App icon in the center
// 2) Preload icon in the center
- // 3) Setup icon in the center and app icon in the top right corner.
+ // 3) App icon in the center with a setup icon on the top left corner.
if (mDisabledForSafeMode) {
- FastBitmapDrawable disabledIcon = newIcon(getContext(), info);
- disabledIcon.setIsDisabled(true);
- mCenterDrawable = disabledIcon;
+ if (widgetCategoryIcon == null) {
+ FastBitmapDrawable disabledIcon = info.newIcon(getContext());
+ disabledIcon.setIsDisabled(true);
+ mCenterDrawable = disabledIcon;
+ } else {
+ widgetCategoryIcon.setColorFilter(
+ FastBitmapDrawable.getDisabledFColorFilter(/* disabledAlpha= */ 1f));
+ mCenterDrawable = widgetCategoryIcon;
+ }
mSettingIconDrawable = null;
} else if (isReadyForClickSetup()) {
- mCenterDrawable = newIcon(getContext(), info);
+ mCenterDrawable = widgetCategoryIcon == null
+ ? info.newIcon(getContext())
+ : widgetCategoryIcon;
mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate();
updateSettingColor(info.bitmap.color);
} else {
- mCenterDrawable = newPendingIcon(getContext(), info);
+ mCenterDrawable = widgetCategoryIcon == null
+ ? newPendingIcon(getContext(), info)
+ : widgetCategoryIcon;
mSettingIconDrawable = null;
applyState();
}
@@ -311,4 +329,19 @@
}
}
+
+ /**
+ * Returns the widget category icon for {@link #mInfo}.
+ *
+ * <p>If {@link #mInfo}'s category is {@code PackageItemInfo#NO_CATEGORY} or unknown, returns
+ * {@code null}.
+ */
+ @Nullable
+ private Drawable getWidgetCategoryIcon() {
+ switch (mInfo.pendingItemInfo.category) {
+ case CONVERSATIONS:
+ return getContext().getDrawable(R.drawable.ic_conversations_widget_category);
+ }
+ return null;
+ }
}
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 3c11274..cea4de7 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -23,8 +23,11 @@
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.View;
+import android.view.View.MeasureSpec;
import android.widget.RemoteViews;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DragSource;
import com.android.launcher3.Launcher;
@@ -33,9 +36,11 @@
import com.android.launcher3.R;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DraggableView;
-import com.android.launcher3.dragndrop.LivePreviewWidgetCell;
import com.android.launcher3.graphics.DragPreviewProvider;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.RoundDrawableWrapper;
+import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
/**
* Extension of {@link DragPreviewProvider} with logic specific to pending widgets/shortcuts
@@ -48,15 +53,29 @@
private final PendingAddItemInfo mAddInfo;
private int[] mEstimatedCellSize;
- private RemoteViews mPreview;
+ @Nullable private RemoteViews mRemoteViewsPreview;
+ @Nullable private NavigableAppWidgetHostView mAppWidgetHostViewPreview;
+ private final float mEnforcedRoundedCornersForWidget;
public PendingItemDragHelper(View view) {
super(view);
mAddInfo = (PendingAddItemInfo) view.getTag();
+ mEnforcedRoundedCornersForWidget = RoundedCornerEnforcement.computeEnforcedRadius(
+ view.getContext());
}
- public void setPreview(RemoteViews preview) {
- mPreview = preview;
+ /**
+ * Sets a {@link RemoteViews} which shows an app widget preview provided by app developers in
+ * the pin widget flow.
+ */
+ public void setRemoteViewsPreview(@Nullable RemoteViews remoteViewsPreview) {
+ mRemoteViewsPreview = remoteViewsPreview;
+ }
+
+ /** Sets a {@link NavigableAppWidgetHostView} which shows a preview layout of an app widget. */
+ public void setAppWidgetHostViewPreview(
+ @Nullable NavigableAppWidgetHostView appWidgetHostViewPreview) {
+ mAppWidgetHostViewPreview = appWidgetHostViewPreview;
}
/**
@@ -73,7 +92,9 @@
final Launcher launcher = Launcher.getLauncher(mView.getContext());
LauncherAppState app = LauncherAppState.getInstance(launcher);
- Bitmap preview = null;
+ Drawable preview = null;
+ final int previewWidth;
+ final int previewHeight;
final float scale;
final Point dragOffset;
final Rect dragRegion;
@@ -89,13 +110,38 @@
int[] previewSizeBeforeScale = new int[1];
- if (mPreview != null) {
- preview = LivePreviewWidgetCell.generateFromRemoteViews(launcher, mPreview,
- createWidgetInfo.info, maxWidth, previewSizeBeforeScale);
+ if (mRemoteViewsPreview != null) {
+ mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(launcher);
+ mAppWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1,
+ ((PendingAddWidgetInfo) mAddInfo).info);
+ DeviceProfile deviceProfile = launcher.getDeviceProfile();
+ Rect padding = new Rect();
+ mAppWidgetHostViewPreview.getWidgetInset(deviceProfile, padding);
+ mAppWidgetHostViewPreview.setPadding(padding.left, padding.top, padding.right,
+ padding.bottom);
+ mAppWidgetHostViewPreview.updateAppWidget(/* remoteViews= */ mRemoteViewsPreview);
+ int width =
+ deviceProfile.cellWidthPx * mAddInfo.spanX + padding.left + padding.right;
+ int height =
+ deviceProfile.cellHeightPx * mAddInfo.spanY + padding.top + padding.bottom;
+ mAppWidgetHostViewPreview.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
- if (preview == null) {
- preview = app.getWidgetCache().generateWidgetPreview(launcher,
- createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale).first;
+ if (mAppWidgetHostViewPreview != null) {
+ previewSizeBeforeScale[0] = mAppWidgetHostViewPreview.getMeasuredWidth();
+ launcher.getDragController()
+ .addDragListener(new AppWidgetHostViewDragListener(launcher));
+ }
+ if (preview == null && mAppWidgetHostViewPreview == null) {
+ Drawable p = new FastBitmapDrawable(
+ app.getWidgetCache().generateWidgetPreview(launcher,
+ createWidgetInfo.info, maxWidth, null,
+ previewSizeBeforeScale).first);
+ if (RoundedCornerEnforcement.isRoundedCornerEnabled()) {
+ p = new RoundDrawableWrapper(p, mEnforcedRoundedCornersForWidget);
+ }
+ preview = p;
}
if (previewSizeBeforeScale[0] < previewBitmapWidth) {
@@ -108,7 +154,14 @@
previewBounds.left += padding;
previewBounds.right -= padding;
}
- scale = previewBounds.width() / (float) preview.getWidth();
+ if (mAppWidgetHostViewPreview != null) {
+ previewWidth = mAppWidgetHostViewPreview.getMeasuredWidth();
+ previewHeight = mAppWidgetHostViewPreview.getMeasuredHeight();
+ } else {
+ previewWidth = preview.getIntrinsicWidth();
+ previewHeight = preview.getIntrinsicHeight();
+ }
+ scale = previewBounds.width() / (float) previewWidth;
launcher.getDragController().addDragListener(new WidgetHostViewLoader(launcher, mView));
dragOffset = null;
@@ -118,9 +171,12 @@
PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) mAddInfo;
Drawable icon = createShortcutInfo.activityInfo.getFullResIcon(app.getIconCache());
LauncherIcons li = LauncherIcons.obtain(launcher);
- preview = li.createScaledBitmapWithoutShadow(icon, 0);
+ preview = new FastBitmapDrawable(
+ li.createScaledBitmapWithoutShadow(icon, 0));
+ previewWidth = preview.getIntrinsicWidth();
+ previewHeight = preview.getIntrinsicHeight();
li.recycle();
- scale = ((float) launcher.getDeviceProfile().iconSizePx) / preview.getWidth();
+ scale = ((float) launcher.getDeviceProfile().iconSizePx) / previewWidth;
dragOffset = new Point(previewPadding / 2, previewPadding / 2);
@@ -148,13 +204,19 @@
launcher.getWorkspace().prepareDragWithProvider(this);
int dragLayerX = screenPos.x + previewBounds.left
- + (int) ((scale * preview.getWidth() - preview.getWidth()) / 2);
+ + (int) ((scale * previewWidth - previewWidth) / 2);
int dragLayerY = screenPos.y + previewBounds.top
- + (int) ((scale * preview.getHeight() - preview.getHeight()) / 2);
+ + (int) ((scale * previewHeight - previewHeight) / 2);
// Start the drag
- launcher.getDragController().startDrag(preview, draggableView, dragLayerX, dragLayerY,
- source, mAddInfo, dragOffset, dragRegion, scale, scale, options);
+ if (mAppWidgetHostViewPreview != null) {
+ launcher.getDragController().startDrag(mAppWidgetHostViewPreview, draggableView,
+ dragLayerX, dragLayerY, source, mAddInfo, dragOffset, dragRegion, scale, scale,
+ options);
+ } else {
+ launcher.getDragController().startDrag(preview, draggableView, dragLayerX, dragLayerY,
+ source, mAddInfo, dragOffset, dragRegion, scale, scale, options);
+ }
}
@Override
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
new file mode 100644
index 0000000..1e46ffd
--- /dev/null
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget;
+
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utilities to compute the enforced the use of rounded corners on App Widgets.
+ */
+public class RoundedCornerEnforcement {
+ // This class is only a namespace and not meant to be instantiated.
+ private RoundedCornerEnforcement() {
+ }
+
+ /**
+ * Find the background view for a widget.
+ *
+ * @param appWidget the view containing the App Widget (typically the instance of
+ * {@link AppWidgetHostView}).
+ */
+ @Nullable
+ public static View findBackground(@NonNull View appWidget) {
+ List<View> backgrounds = findViewsWithId(appWidget, android.R.id.background);
+ if (backgrounds.size() == 1) {
+ return backgrounds.get(0);
+ }
+ // Really, the argument should contain the widget, so it cannot be the background.
+ if (appWidget instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) appWidget;
+ if (vg.getChildCount() > 0) {
+ return findUndefinedBackground(vg.getChildAt(0));
+ }
+ }
+ return appWidget;
+ }
+
+ /**
+ * Check whether the app widget has opted out of the enforcement.
+ */
+ public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
+ return background.getId() == android.R.id.background && background.getClipToOutline();
+ }
+
+ /** Check if the app widget is in the deny list. */
+ public static boolean isRoundedCornerEnabled() {
+ return Utilities.ATLEAST_S && FeatureFlags.ENABLE_ENFORCED_ROUNDED_CORNERS.get();
+ }
+
+ /**
+ * Computes the rounded rectangle needed for this app widget.
+ *
+ * @param appWidget View onto which the rounded rectangle will be applied.
+ * @param background Background view. This must be either {@code appWidget} or a descendant
+ * of {@code appWidget}.
+ * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame
+ * of {@code appWidget}.
+ */
+ public static void computeRoundedRectangle(@NonNull View appWidget, @NonNull View background,
+ @NonNull Rect outRect) {
+ outRect.left = 0;
+ outRect.right = background.getWidth();
+ outRect.top = 0;
+ outRect.bottom = background.getHeight();
+ while (background != appWidget) {
+ outRect.offset(background.getLeft(), background.getTop());
+ background = (View) background.getParent();
+ }
+ }
+
+ /**
+ * Computes the radius of the rounded rectangle that should be applied to a widget expanded
+ * in the given context.
+ */
+ public static float computeEnforcedRadius(@NonNull Context context) {
+ if (!Utilities.ATLEAST_S) {
+ return 0;
+ }
+ Resources res = context.getResources();
+ float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius);
+ float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius);
+ return Math.min(defaultRadius, systemRadius);
+ }
+
+ private static List<View> findViewsWithId(View view, @IdRes int viewId) {
+ List<View> output = new ArrayList<>();
+ accumulateViewsWithId(view, viewId, output);
+ return output;
+ }
+
+ // Traverse views. If the predicate returns true, continue on the children, otherwise, don't.
+ private static void accumulateViewsWithId(View view, @IdRes int viewId, List<View> output) {
+ if (view.getId() == viewId) {
+ output.add(view);
+ return;
+ }
+ if (view instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) view;
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ accumulateViewsWithId(vg.getChildAt(i), viewId, output);
+ }
+ }
+ }
+
+ private static boolean isViewVisible(View view) {
+ if (view.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ return !view.willNotDraw() || view.getForeground() != null || view.getBackground() != null;
+ }
+
+ @Nullable
+ private static View findUndefinedBackground(View current) {
+ if (current.getVisibility() != View.VISIBLE) {
+ return null;
+ }
+ if (isViewVisible(current)) {
+ return current;
+ }
+ View lastVisibleView = null;
+ // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw
+ // something, or a ViewGroup that contains more than one view.
+ if (current instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) current;
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ View visibleView = findUndefinedBackground(vg.getChildAt(i));
+ if (visibleView != null) {
+ if (lastVisibleView != null) {
+ return current; // At least two visible children
+ }
+ lastVisibleView = visibleView;
+ }
+ }
+ }
+ return lastVisibleView;
+ }
+}
diff --git a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
index ebc2a25..9313266 100644
--- a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
+++ b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
@@ -15,13 +15,15 @@
*/
package com.android.launcher3.widget;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
+
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.util.PendingRequestArgs;
@@ -79,8 +81,22 @@
return true;
}
+ /**
+ * Checks whether the widget needs configuration.
+ *
+ * A widget needs configuration if (1) it has a configuration activity and (2)
+ * it's configuration is not optional.
+ *
+ * @return true if the widget needs configuration, false otherwise.
+ */
public boolean needsConfigure() {
- return mProviderInfo.configure != null;
+ int featureFlags = mProviderInfo.widgetFeatures;
+ // A widget's configuration is optional only if it's configuration is marked as optional AND
+ // it can be reconfigured later.
+ boolean configurationOptional = (featureFlags & WIDGET_FEATURE_CONFIGURATION_OPTIONAL) != 0
+ && (featureFlags & WIDGET_FEATURE_RECONFIGURABLE) != 0;
+
+ return mProviderInfo.configure != null && !configurationOptional;
}
public LauncherAppWidgetProviderInfo getProviderInfo(Context context) {
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index bef91d2..5769ba0 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,27 +16,39 @@
package com.android.launcher3.widget;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.CancellationSignal;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Size;
+import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
-import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
import android.widget.LinearLayout;
+import android.widget.RemoteViews;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.BaseActivity;
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.icons.BaseIconFactory;
+import com.android.launcher3.icons.FastBitmapDrawable;
+import com.android.launcher3.icons.RoundDrawableWrapper;
import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.widget.util.WidgetSizes;
/**
* Represents the individual cell of the widget inside the widget tray. The preview is drawn
@@ -60,12 +72,17 @@
/** Widget preview width is calculated by multiplying this factor to the widget cell width. */
private static final float PREVIEW_SCALE = 0.8f;
+ protected int mPreviewWidth;
+ protected int mPreviewHeight;
protected int mPresetPreviewSize;
private int mCellSize;
+ private float mPreviewScale = 1f;
+ private FrameLayout mWidgetImageContainer;
private WidgetImageView mWidgetImage;
private TextView mWidgetName;
private TextView mWidgetDims;
+ private TextView mWidgetDescription;
protected WidgetItem mItem;
@@ -75,11 +92,16 @@
private boolean mAnimatePreview = true;
private boolean mApplyBitmapDeferred = false;
- private Bitmap mDeferredBitmap;
+ private Drawable mDeferredDrawable;
protected final BaseActivity mActivity;
- protected final DeviceProfile mDeviceProfile;
private final CheckLongPressHelper mLongPressHelper;
+ private final float mEnforcedCornerRadius;
+ private final int mShortcutPreviewPadding;
+
+ private RemoteViews mRemoteViewsPreview;
+ private NavigableAppWidgetHostView mAppWidgetHostViewPreview;
+ private int mSourceContainer = CONTAINER_WIDGETS_TRAY;
public WidgetCell(Context context) {
this(context, null);
@@ -93,28 +115,42 @@
super(context, attrs, defStyle);
mActivity = BaseActivity.fromContext(context);
- mDeviceProfile = mActivity.getDeviceProfile();
mLongPressHelper = new CheckLongPressHelper(this);
-
mLongPressHelper.setLongPressTimeoutFactor(1);
+
setContainerWidth();
setWillNotDraw(false);
setClipToPadding(false);
setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
+ mShortcutPreviewPadding =
+ 2 * getResources().getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
}
private void setContainerWidth() {
- mCellSize = (int) (mDeviceProfile.allAppsIconSizePx * WIDTH_SCALE);
+ mCellSize = (int) (mActivity.getDeviceProfile().allAppsIconSizePx * WIDTH_SCALE);
mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
+ mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- mWidgetImage = (WidgetImageView) findViewById(R.id.widget_preview);
- mWidgetName = ((TextView) findViewById(R.id.widget_name));
- mWidgetDims = ((TextView) findViewById(R.id.widget_dims));
+ mWidgetImageContainer = findViewById(R.id.widget_preview_container);
+ mWidgetImage = findViewById(R.id.widget_preview);
+ mWidgetName = findViewById(R.id.widget_name);
+ mWidgetDims = findViewById(R.id.widget_dims);
+ mWidgetDescription = findViewById(R.id.widget_description);
+ }
+
+ public void setRemoteViewsPreview(RemoteViews view) {
+ mRemoteViewsPreview = view;
+ }
+
+ @Nullable
+ public RemoteViews getRemoteViewsPreview() {
+ return mRemoteViewsPreview;
}
/**
@@ -125,36 +161,116 @@
Log.d(TAG, "reset called on:" + mWidgetName.getText());
}
mWidgetImage.animate().cancel();
- mWidgetImage.setBitmap(null, null);
+ mWidgetImage.setDrawable(null);
+ mWidgetImage.setVisibility(View.VISIBLE);
mWidgetName.setText(null);
mWidgetDims.setText(null);
+ mWidgetDescription.setText(null);
+ mWidgetDescription.setVisibility(GONE);
+ mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
if (mActiveRequest != null) {
mActiveRequest.cancel();
mActiveRequest = null;
}
+ mRemoteViewsPreview = null;
+ if (mAppWidgetHostViewPreview != null) {
+ mWidgetImageContainer.removeView(mAppWidgetHostViewPreview);
+ }
+ mAppWidgetHostViewPreview = null;
+ mItem = null;
+ }
+
+ public void setSourceContainer(int sourceContainer) {
+ this.mSourceContainer = sourceContainer;
}
public void applyFromCellItem(WidgetItem item, WidgetPreviewLoader loader) {
+ applyPreviewOnAppWidgetHostView(item);
+
+ Context context = getContext();
mItem = item;
mWidgetName.setText(mItem.label);
- mWidgetDims.setText(getContext().getString(R.string.widget_dims_format,
+ mWidgetName.setContentDescription(
+ context.getString(R.string.widget_preview_context_description, mItem.label));
+ mWidgetDims.setText(context.getString(R.string.widget_dims_format,
mItem.spanX, mItem.spanY));
- mWidgetDims.setContentDescription(getContext().getString(
+ mWidgetDims.setContentDescription(context.getString(
R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
- mWidgetPreviewLoader = loader;
+ if (ATLEAST_S && mItem.widgetInfo != null) {
+ CharSequence description = mItem.widgetInfo.loadDescription(context);
+ if (description != null && description.length() > 0) {
+ mWidgetDescription.setText(description);
+ mWidgetDescription.setVisibility(VISIBLE);
+ } else {
+ mWidgetDescription.setVisibility(GONE);
+ }
+ }
+ mWidgetPreviewLoader = loader;
if (item.activityInfo != null) {
setTag(new PendingAddShortcutInfo(item.activityInfo));
} else {
- setTag(new PendingAddWidgetInfo(item.widgetInfo));
+ setTag(new PendingAddWidgetInfo(item.widgetInfo, mSourceContainer));
}
}
+
+ private void applyPreviewOnAppWidgetHostView(WidgetItem item) {
+ if (mRemoteViewsPreview != null) {
+ mAppWidgetHostViewPreview = createAppWidgetHostView(getContext());
+ setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo,
+ mRemoteViewsPreview);
+ return;
+ }
+
+ if (!item.hasPreviewLayout()) return;
+
+ Context context = getContext();
+ // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview as
+ // a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, which
+ // supports applying local color extraction during drag & drop.
+ mAppWidgetHostViewPreview = isLauncherContext(context)
+ ? new LauncherAppWidgetHostView(context)
+ : createAppWidgetHostView(context);
+ LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+ LauncherAppWidgetProviderInfo.fromProviderInfo(context, item.widgetInfo.clone());
+ // A hack to force the initial layout to be the preview layout since there is no API for
+ // rendering a preview layout for work profile apps yet. For non-work profile layout, a
+ // proper solution is to use RemoteViews(PackageName, LayoutId).
+ launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
+ setAppWidgetHostViewPreview(mAppWidgetHostViewPreview,
+ launcherAppWidgetProviderInfo, /* remoteViews= */ null);
+ }
+
+ private void setAppWidgetHostViewPreview(
+ NavigableAppWidgetHostView appWidgetHostViewPreview,
+ LauncherAppWidgetProviderInfo providerInfo,
+ @Nullable RemoteViews remoteViews) {
+ appWidgetHostViewPreview.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ appWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1, providerInfo);
+ Rect padding;
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+ if (deviceProfile.shouldInsetWidgets()) {
+ padding = new Rect();
+ appWidgetHostViewPreview.getWidgetInset(deviceProfile, padding);
+ } else {
+ padding = deviceProfile.inv.defaultWidgetPadding;
+ }
+ appWidgetHostViewPreview.setPadding(padding.left, padding.top, padding.right,
+ padding.bottom);
+ appWidgetHostViewPreview.updateAppWidget(remoteViews);
+ }
+
public WidgetImageView getWidgetView() {
return mWidgetImage;
}
+ @Nullable
+ public NavigableAppWidgetHostView getAppWidgetHostViewPreview() {
+ return mAppWidgetHostViewPreview;
+ }
+
/**
* Sets if applying bitmap preview should be deferred. The UI will still load the bitmap, but
* will not cause invalidate, so that when deferring is disabled later, all the bitmaps are
@@ -164,9 +280,9 @@
public void setApplyBitmapDeferred(boolean isDeferred) {
if (mApplyBitmapDeferred != isDeferred) {
mApplyBitmapDeferred = isDeferred;
- if (!mApplyBitmapDeferred && mDeferredBitmap != null) {
- applyPreview(mDeferredBitmap);
- mDeferredBitmap = null;
+ if (!mApplyBitmapDeferred && mDeferredDrawable != null) {
+ applyPreview(mDeferredDrawable);
+ mDeferredDrawable = null;
}
}
}
@@ -176,29 +292,82 @@
}
public void applyPreview(Bitmap bitmap) {
+ FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
+ applyPreview(new RoundDrawableWrapper(drawable, mEnforcedCornerRadius));
+ }
+
+ private void applyPreview(Drawable drawable) {
if (mApplyBitmapDeferred) {
- mDeferredBitmap = bitmap;
+ mDeferredDrawable = drawable;
return;
}
- if (bitmap != null) {
- mWidgetImage.setBitmap(bitmap, mWidgetPreviewLoader.getBadgeForUser(mItem.user,
- BaseIconFactory.getBadgeSizeForIconSize(mDeviceProfile.allAppsIconSizePx)));
- if (mAnimatePreview) {
- mWidgetImage.setAlpha(0f);
- ViewPropertyAnimator anim = mWidgetImage.animate();
- anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS);
- } else {
- mWidgetImage.setAlpha(1f);
+ if (drawable != null) {
+ float scale = 1f;
+ if (getWidth() > 0 && getHeight() > 0) {
+ // Scale down the preview size if it's wider than the cell.
+ float maxWidth = getWidth();
+ float previewWidth = drawable.getIntrinsicWidth() * mPreviewScale;
+ scale = Math.min(maxWidth / previewWidth, 1);
}
+ setContainerSize(
+ Math.round(drawable.getIntrinsicWidth() * scale),
+ Math.round(drawable.getIntrinsicHeight() * scale));
+ mWidgetImage.setDrawable(drawable);
+ mWidgetImage.setVisibility(View.VISIBLE);
+ if (mAppWidgetHostViewPreview != null) {
+ removeView(mAppWidgetHostViewPreview);
+ mAppWidgetHostViewPreview = null;
+ }
+ }
+ if (mAnimatePreview) {
+ mWidgetImageContainer.setAlpha(0f);
+ ViewPropertyAnimator anim = mWidgetImageContainer.animate();
+ anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS);
+ } else {
+ mWidgetImageContainer.setAlpha(1f);
}
}
+ private void setContainerSize(int width, int height) {
+ LayoutParams layoutParams = (LayoutParams) mWidgetImageContainer.getLayoutParams();
+ layoutParams.width = (int) (width * mPreviewScale);
+ layoutParams.height = (int) (height * mPreviewScale);
+ mWidgetImageContainer.setLayoutParams(layoutParams);
+ }
+
public void ensurePreview() {
+ if (mAppWidgetHostViewPreview != null) {
+ setContainerSize(mPreviewWidth, mPreviewHeight);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ mPreviewWidth, mPreviewHeight, Gravity.FILL);
+ mAppWidgetHostViewPreview.setLayoutParams(params);
+ mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0);
+ mWidgetImage.setVisibility(View.GONE);
+ applyPreview((Drawable) null);
+ return;
+ }
if (mActiveRequest != null) {
return;
}
- mActiveRequest = mWidgetPreviewLoader.getPreview(
- mItem, mPresetPreviewSize, mPresetPreviewSize, this);
+ mActiveRequest = mWidgetPreviewLoader.loadPreview(
+ BaseActivity.fromContext(getContext()), mItem,
+ new Size(mPreviewWidth, mPreviewHeight),
+ this::applyPreview);
+ }
+
+ /** Sets the widget preview image size in number of cells. */
+ public Size setPreviewSize(WidgetItem widgetItem) {
+ return setPreviewSize(widgetItem, 1f);
+ }
+
+ /** Sets the widget preview image size, in number of cells, and preview scale. */
+ public Size setPreviewSize(WidgetItem widgetItem, float previewScale) {
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+ Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, widgetItem);
+ mPreviewWidth = widgetSize.getWidth();
+ mPreviewHeight = widgetSize.getHeight();
+ mPreviewScale = previewScale;
+ return widgetSize;
}
@Override
@@ -232,10 +401,22 @@
return "";
}
- @Override
- public void setLayoutParams(ViewGroup.LayoutParams params) {
- params.width = params.height = mCellSize;
- super.setLayoutParams(params);
+ private static NavigableAppWidgetHostView createAppWidgetHostView(Context context) {
+ return new NavigableAppWidgetHostView(context) {
+ @Override
+ protected boolean shouldAllowDirectClick() {
+ return false;
+ }
+ };
+ }
+
+ private static boolean isLauncherContext(Context context) {
+ try {
+ Launcher.getLauncher(context);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
}
@Override
diff --git a/src/com/android/launcher3/widget/WidgetCellPreview.java b/src/com/android/launcher3/widget/WidgetCellPreview.java
new file mode 100644
index 0000000..9f45d71
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetCellPreview.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * View group managing the widget preview: either using a {@link WidgetImageView} or an actual
+ * {@link LauncherAppWidgetHostView}.
+ */
+public class WidgetCellPreview extends FrameLayout {
+ public WidgetCellPreview(Context context) {
+ this(context, null);
+ }
+
+ public WidgetCellPreview(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WidgetCellPreview(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ super.onInterceptTouchEvent(ev);
+ return true;
+ }
+
+ /** Returns {@code true} if this container has a preview layout. */
+ public boolean hasPreviewLayout() {
+ for (int i = 0; i < getChildCount(); i++) {
+ if (getChildAt(i) instanceof LauncherAppWidgetHostView) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@link LauncherAppWidgetHostView} if this container has a preview layout. Otherwise,
+ * returns null.
+ */
+ @Nullable
+ public LauncherAppWidgetHostView getPreviewLayout() {
+ for (int i = 0; i < getChildCount(); i++) {
+ if (getChildAt(i) instanceof LauncherAppWidgetHostView) {
+ return (LauncherAppWidgetHostView) getChildAt(i);
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index c022374..46141e0 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -1,18 +1,14 @@
package com.android.launcher3.widget;
import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetManager;
import android.content.Context;
-import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
-import com.android.launcher3.AppWidgetResizeFrame;
import com.android.launcher3.DropTarget;
import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.DragOptions;
@@ -87,7 +83,7 @@
if (pInfo.isCustomWidget()) {
return false;
}
- final Bundle options = getDefaultOptionsForWidget(mLauncher, mInfo);
+ final Bundle options = mInfo.getDefaultSizeOptions(mLauncher);
// If there is a configuration activity, do not follow thru bound and inflate.
if (mInfo.getHandler().needsConfigure()) {
@@ -151,25 +147,4 @@
return true;
}
- public static Bundle getDefaultOptionsForWidget(Context context, PendingAddWidgetInfo info) {
- Rect rect = new Rect();
- AppWidgetResizeFrame.getWidgetSizeRanges(context, info.spanX, info.spanY, rect);
- Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context,
- info.componentName, null);
-
- float density = context.getResources().getDisplayMetrics().density;
- int xPaddingDips = (int) ((padding.left + padding.right) / density);
- int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
-
- Bundle options = new Bundle();
- options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH,
- rect.left - xPaddingDips);
- options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT,
- rect.top - yPaddingDips);
- options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH,
- rect.right - xPaddingDips);
- options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT,
- rect.bottom - yPaddingDips);
- return options;
- }
}
diff --git a/src/com/android/launcher3/widget/WidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java
index df2bcff..11f4485 100644
--- a/src/com/android/launcher3/widget/WidgetImageView.java
+++ b/src/com/android/launcher3/widget/WidgetImageView.java
@@ -17,9 +17,7 @@
package com.android.launcher3.widget;
import android.content.Context;
-import android.graphics.Bitmap;
import android.graphics.Canvas;
-import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
@@ -27,7 +25,6 @@
import android.view.View;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
/**
* View that draws a bitmap horizontally centered. If the image width is greater than the view
@@ -35,12 +32,10 @@
*/
public class WidgetImageView extends View {
- private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final RectF mDstRectF = new RectF();
private final int mBadgeMargin;
- private Bitmap mBitmap;
- private Drawable mBadge;
+ private Drawable mDrawable;
public WidgetImageView(Context context) {
this(context, null);
@@ -57,26 +52,22 @@
.getDimensionPixelSize(R.dimen.profile_badge_margin);
}
- public void setBitmap(Bitmap bitmap, Drawable badge) {
- mBitmap = bitmap;
- mBadge = badge;
+ /** Set the drawable to use for this view. */
+ public void setDrawable(Drawable drawable) {
+ mDrawable = drawable;
invalidate();
}
- public Bitmap getBitmap() {
- return mBitmap;
+ public Drawable getDrawable() {
+ return mDrawable;
}
@Override
protected void onDraw(Canvas canvas) {
- if (mBitmap != null) {
+ if (mDrawable != null) {
updateDstRectF();
- canvas.drawBitmap(mBitmap, null, mDstRectF, mPaint);
-
- // Only draw the badge if a preview was drawn.
- if (mBadge != null) {
- mBadge.draw(canvas);
- }
+ mDrawable.setBounds(getBitmapBounds());
+ mDrawable.draw(canvas);
}
}
@@ -91,11 +82,11 @@
private void updateDstRectF() {
float myWidth = getWidth();
float myHeight = getHeight();
- float bitmapWidth = mBitmap.getWidth();
+ float bitmapWidth = mDrawable.getIntrinsicWidth();
final float scale = bitmapWidth > myWidth ? myWidth / bitmapWidth : 1;
float scaledWidth = bitmapWidth * scale;
- float scaledHeight = mBitmap.getHeight() * scale;
+ float scaledHeight = mDrawable.getIntrinsicHeight() * scale;
mDstRectF.left = (myWidth - scaledWidth) / 2;
mDstRectF.right = (myWidth + scaledWidth) / 2;
@@ -107,17 +98,6 @@
mDstRectF.top = (myHeight - scaledHeight) / 2;
mDstRectF.bottom = (myHeight + scaledHeight) / 2;
}
-
- if (mBadge != null) {
- Rect bounds = mBadge.getBounds();
- int left = Utilities.boundToRange(
- (int) (mDstRectF.right + mBadgeMargin - bounds.width()),
- mBadgeMargin, getWidth() - bounds.width());
- int top = Utilities.boundToRange(
- (int) (mDstRectF.bottom + mBadgeMargin - bounds.height()),
- mBadgeMargin, getHeight() - bounds.height());
- mBadge.setBounds(left, top, bounds.width() + left, bounds.height() + top);
- }
}
/**
diff --git a/src/com/android/launcher3/widget/WidgetListRowEntry.java b/src/com/android/launcher3/widget/WidgetListRowEntry.java
deleted file mode 100644
index 17e4673..0000000
--- a/src/com/android/launcher3/widget/WidgetListRowEntry.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import java.util.ArrayList;
-
-/**
- * Holder class to store all the information related to a single row in the widget list
- */
-public class WidgetListRowEntry {
-
- public final PackageItemInfo pkgItem;
-
- public final ArrayList<WidgetItem> widgets;
-
- /**
- * Character that is used as a section name for the {@link ItemInfo#title}.
- * (e.g., "G" will be stored if title is "Google")
- */
- public String titleSectionName;
-
- public WidgetListRowEntry(PackageItemInfo pkgItem, ArrayList<WidgetItem> items) {
- this.pkgItem = pkgItem;
- this.widgets = items;
- }
-
- @Override
- public String toString() {
- return pkgItem.packageName + ":" + widgets.size();
- }
-}
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index 4b6c569..15fa844 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -23,24 +23,18 @@
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
-import android.os.Process;
import android.os.UserHandle;
import androidx.annotation.Nullable;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.Utilities;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -84,22 +78,8 @@
return allWidgetsSteam(mContext).collect(Collectors.toList());
}
- if (Utilities.ATLEAST_OREO) {
- return mAppWidgetManager.getInstalledProvidersForPackage(
- packageUser.mPackageName, packageUser.mUser);
- }
-
- String pkg = packageUser.mPackageName;
- return Stream.concat(
- // Only get providers for the given package/user.
- mAppWidgetManager.getInstalledProvidersForProfile(packageUser.mUser)
- .stream()
- .filter(w -> w.provider.equals(pkg)),
- Process.myUserHandle().equals(packageUser.mUser)
- && mContext.getPackageName().equals(pkg)
- ? CustomWidgetManager.INSTANCE.get(mContext).stream()
- : Stream.empty())
- .collect(Collectors.toList());
+ return mAppWidgetManager.getInstalledProvidersForPackage(
+ packageUser.mPackageName, packageUser.mUser);
}
/**
@@ -138,15 +118,6 @@
appWidgetId).getBoolean(WIDGET_OPTION_RESTORE_COMPLETED);
}
- public static Map<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap(Context context) {
- if (WidgetsModel.GO_DISABLE_WIDGETS) {
- return Collections.emptyMap();
- }
- return allWidgetsSteam(context).collect(
- Collectors.toMap(info -> new ComponentKey(info.provider, info.getProfile()),
- Function.identity()));
- }
-
private static Stream<AppWidgetProviderInfo> allWidgetsSteam(Context context) {
AppWidgetManager awm = context.getSystemService(AppWidgetManager.class);
return Stream.concat(
diff --git a/src/com/android/launcher3/widget/WidgetPreviewLoader.java b/src/com/android/launcher3/widget/WidgetPreviewLoader.java
new file mode 100644
index 0000000..ff5c82f
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetPreviewLoader.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget;
+
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.model.WidgetItem;
+
+/** Asynchronous loader of preview bitmaps for {@link WidgetItem}s. */
+public interface WidgetPreviewLoader {
+ /**
+ * Loads a widget preview and calls back to {@code callback} when complete.
+ *
+ * @return a {@link CancellationSignal} which can be used to cancel the request.
+ */
+ @NonNull
+ @UiThread
+ CancellationSignal loadPreview(
+ @NonNull BaseActivity activity,
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback);
+
+ /** Callback class for requests to {@link WidgetPreviewLoader}. */
+ interface WidgetPreviewLoadedCallback {
+ void onPreviewLoaded(@NonNull Bitmap preview);
+ }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 30be7a6..dedcc65 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -16,6 +16,7 @@
package com.android.launcher3.widget;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import android.animation.PropertyValuesHolder;
@@ -26,26 +27,31 @@
import android.util.Pair;
import android.view.Gravity;
import android.view.LayoutInflater;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
+import android.widget.ScrollView;
+import android.widget.TableLayout;
+import android.widget.TableRow;
import android.widget.TextView;
-import com.android.launcher3.Insettable;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
-import com.android.launcher3.ResourceUtils;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
import java.util.List;
/**
* Bottom sheet for the "Widgets" system shortcut in the long-press popup.
*/
-public class WidgetsBottomSheet extends BaseWidgetSheet implements Insettable {
+public class WidgetsBottomSheet extends BaseWidgetSheet {
+ private static final String TAG = "WidgetsBottomSheet";
private static final IntProperty<View> PADDING_BOTTOM =
new IntProperty<View>("paddingBottom") {
@@ -62,8 +68,40 @@
};
private static final int DEFAULT_CLOSE_DURATION = 200;
+ private static final long EDUCATION_TIP_DELAY_MS = 300;
+
private ItemInfo mOriginalItemInfo;
- private Rect mInsets;
+ private final int mMaxTableHeight;
+ private int mMaxHorizontalSpan = 4;
+
+ private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
+ new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if (hasSeenEducationTip()) {
+ removeOnLayoutChangeListener(this);
+ return;
+ }
+ // Widgets are loaded asynchronously, We are adding a delay because we only want
+ // to show the tip when the widget preview has finished loading and rendering in
+ // this view.
+ removeCallbacks(mShowEducationTipTask);
+ postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS);
+ }
+ };
+
+ private final Runnable mShowEducationTipTask = () -> {
+ if (hasSeenEducationTip()) {
+ removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ return;
+ }
+ View viewForTip = ((ViewGroup) ((TableLayout) findViewById(R.id.widgets_table))
+ .getChildAt(0)).getChildAt(0);
+ if (showEducationTipOnViewIfPossible(viewForTip) != null) {
+ removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ }
+ };
public WidgetsBottomSheet(Context context, AttributeSet attrs) {
this(context, attrs, 0);
@@ -72,20 +110,73 @@
public WidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
- mInsets = new Rect();
- mContent = this;
+ DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+ // Set the max table height to 2 / 3 of the grid height so that the bottom picker won't
+ // take over the entire view vertically.
+ mMaxTableHeight = deviceProfile.inv.numRows * 2 / 3 * deviceProfile.cellHeightPx;
+ if (!hasSeenEducationTip()) {
+ addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContent = findViewById(R.id.widgets_bottom_sheet);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ doMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (updateMaxSpansPerRow()) {
+ doMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ /** Returns {@code true} if the max spans have been updated. */
+ private boolean updateMaxSpansPerRow() {
+ if (getMeasuredWidth() == 0) return false;
+
+ int paddingPx = 2 * getResources().getDimensionPixelOffset(
+ R.dimen.widget_cell_horizontal_padding);
+ int maxHorizontalSpan = findViewById(R.id.widgets_table).getMeasuredWidth()
+ / (mActivityContext.getDeviceProfile().cellWidthPx + paddingPx);
+ if (mMaxHorizontalSpan != maxHorizontalSpan) {
+ // Ensure the table layout is showing widgets in the right column after measure.
+ mMaxHorizontalSpan = maxHorizontalSpan;
+ onWidgetsBound();
+ return true;
+ }
+ return false;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
+ int width = r - l;
+ int height = b - t;
+
+ // Content is laid out as center bottom aligned.
+ int contentWidth = mContent.getMeasuredWidth();
+ int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
+ mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
+ contentLeft + contentWidth, height);
+
setTranslationShift(mTranslationShift);
+
+ // Ensure the scroll view height is not larger than mMaxTableHeight, which is a value
+ // smaller than the entire screen height.
+ ScrollView widgetsTableScrollView = findViewById(R.id.widgets_table_scroll_view);
+ if (widgetsTableScrollView.getMeasuredHeight() > mMaxTableHeight) {
+ ViewGroup.LayoutParams layoutParams = widgetsTableScrollView.getLayoutParams();
+ layoutParams.height = mMaxTableHeight;
+ widgetsTableScrollView.setLayoutParams(layoutParams);
+ findViewById(R.id.collapse_handle).setVisibility(VISIBLE);
+ }
}
public void populateAndShow(ItemInfo itemInfo) {
mOriginalItemInfo = itemInfo;
- ((TextView) findViewById(R.id.title)).setText(getContext().getString(
- R.string.widgets_bottom_sheet_title, mOriginalItemInfo.title));
+ ((TextView) findViewById(R.id.title)).setText(mOriginalItemInfo.title);
onWidgetsBound();
attachToContainer();
@@ -95,53 +186,51 @@
@Override
public void onWidgetsBound() {
- List<WidgetItem> widgets = mLauncher.getPopupDataProvider().getWidgetsForPackageUser(
+ List<WidgetItem> widgets = mActivityContext.getPopupDataProvider().getWidgetsForPackageUser(
new PackageUserKey(
mOriginalItemInfo.getTargetComponent().getPackageName(),
mOriginalItemInfo.user));
- ViewGroup widgetRow = findViewById(R.id.widgets);
- ViewGroup widgetCells = widgetRow.findViewById(R.id.widgets_cell_list);
+ TableLayout widgetsTable = findViewById(R.id.widgets_table);
+ widgetsTable.removeAllViews();
- widgetCells.removeAllViews();
-
- for (int i = 0; i < widgets.size(); i++) {
- WidgetCell widget = addItemCell(widgetCells);
- widget.applyFromCellItem(widgets.get(i), LauncherAppState.getInstance(mLauncher)
- .getWidgetCache());
- widget.ensurePreview();
- widget.setVisibility(View.VISIBLE);
- if (i < widgets.size() - 1) {
- addDivider(widgetCells);
- }
- }
-
- if (widgets.size() == 1) {
- // If there is only one widget, we want to center it instead of left-align.
- WidgetsBottomSheet.LayoutParams params = (WidgetsBottomSheet.LayoutParams)
- widgetRow.getLayoutParams();
- params.gravity = Gravity.CENTER_HORIZONTAL;
- } else {
- // Otherwise, add an empty view to the start as padding (but still scroll edge to edge).
- View leftPaddingView = LayoutInflater.from(getContext()).inflate(
- R.layout.widget_list_divider, widgetRow, false);
- leftPaddingView.getLayoutParams().width = ResourceUtils.pxFromDp(
- 16, getResources().getDisplayMetrics());
- widgetCells.addView(leftPaddingView, 0);
- }
+ WidgetsTableUtils.groupWidgetItemsIntoTable(widgets, mMaxHorizontalSpan).forEach(row -> {
+ TableRow tableRow = new TableRow(getContext());
+ tableRow.setGravity(Gravity.TOP);
+ row.forEach(widgetItem -> {
+ WidgetCell widget = addItemCell(tableRow);
+ widget.setPreviewSize(widgetItem);
+ widget.applyFromCellItem(widgetItem, LauncherAppState.getInstance(mActivityContext)
+ .getWidgetCache());
+ widget.ensurePreview();
+ widget.setVisibility(View.VISIBLE);
+ });
+ widgetsTable.addView(tableRow);
+ });
}
- private void addDivider(ViewGroup parent) {
- LayoutInflater.from(getContext()).inflate(R.layout.widget_list_divider, parent, true);
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mNoIntercept = false;
+ ScrollView scrollView = findViewById(R.id.widgets_table_scroll_view);
+ if (getPopupContainer().isEventOverView(scrollView, ev)
+ && scrollView.getScrollY() > 0) {
+ mNoIntercept = true;
+ }
+ }
+ return super.onControllerInterceptTouchEvent(ev);
}
protected WidgetCell addItemCell(ViewGroup parent) {
- WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext()).inflate(
- R.layout.widget_cell, parent, false);
+ WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext())
+ .inflate(R.layout.widget_cell, parent, false);
- widget.setOnClickListener(this);
- widget.setOnLongClickListener(this);
+ View previewContainer = widget.findViewById(R.id.widget_preview_container);
+ previewContainer.setOnClickListener(this);
+ previewContainer.setOnLongClickListener(this);
widget.setAnimatePreview(false);
+ widget.setSourceContainer(CONTAINER_BOTTOM_WIDGETS_TRAY);
parent.addView(widget);
return widget;
@@ -171,17 +260,15 @@
@Override
public void setInsets(Rect insets) {
- // Extend behind left, right, and bottom insets.
- int leftInset = insets.left - mInsets.left;
- int rightInset = insets.right - mInsets.right;
- int bottomInset = insets.bottom - mInsets.bottom;
- mInsets.set(insets);
- setPadding(leftInset, getPaddingTop(), rightInset, bottomInset);
- }
+ super.setInsets(insets);
- @Override
- protected int getElementsRowCount() {
- return 1;
+ mContent.setPadding(mContent.getPaddingStart(),
+ mContent.getPaddingTop(), mContent.getPaddingEnd(), insets.bottom);
+ if (insets.bottom > 0) {
+ setupNavBarColor();
+ } else {
+ clearNavBarColor();
+ }
}
@Override
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
deleted file mode 100644
index df6e2c3..0000000
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.widget;
-
-import android.util.Log;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-
-/**
- * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
- * methods accordingly.
- */
-public class WidgetsDiffReporter {
- private static final boolean DEBUG = false;
- private static final String TAG = "WidgetsDiffReporter";
-
- private final IconCache mIconCache;
- private final RecyclerView.Adapter mListener;
-
- public WidgetsDiffReporter(IconCache iconCache, RecyclerView.Adapter listener) {
- mIconCache = iconCache;
- mListener = listener;
- }
-
- public void process(ArrayList<WidgetListRowEntry> currentEntries,
- ArrayList<WidgetListRowEntry> newEntries, WidgetListRowEntryComparator comparator) {
- if (DEBUG) {
- Log.d(TAG, "process oldEntries#=" + currentEntries.size()
- + " newEntries#=" + newEntries.size());
- }
- // Early exit if either of the list is empty
- if (currentEntries.isEmpty() || newEntries.isEmpty()) {
- // Skip if both list are empty.
- // On rotation, we open the widget tray with empty. Then try to fetch the list again
- // when the animation completes (which still gives empty). And we get the final result
- // when the bind actually completes.
- if (currentEntries.size() != newEntries.size()) {
- currentEntries.clear();
- currentEntries.addAll(newEntries);
- mListener.notifyDataSetChanged();
- }
- return;
- }
- ArrayList<WidgetListRowEntry> orgEntries =
- (ArrayList<WidgetListRowEntry>) currentEntries.clone();
- Iterator<WidgetListRowEntry> orgIter = orgEntries.iterator();
- Iterator<WidgetListRowEntry> newIter = newEntries.iterator();
-
- WidgetListRowEntry orgRowEntry = orgIter.next();
- WidgetListRowEntry newRowEntry = newIter.next();
-
- do {
- int diff = comparePackageName(orgRowEntry, newRowEntry, comparator);
- if (DEBUG) {
- Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
- diff, orgRowEntry != null? orgRowEntry.toString() : null,
- newRowEntry != null? newRowEntry.toString() : null));
- }
- int index = -1;
- if (diff < 0) {
- index = currentEntries.indexOf(orgRowEntry);
- mListener.notifyItemRemoved(index);
- if (DEBUG) {
- Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
- orgRowEntry.titleSectionName));
- }
- currentEntries.remove(index);
- orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
- } else if (diff > 0) {
- index = orgRowEntry != null? currentEntries.indexOf(orgRowEntry):
- currentEntries.size();
- currentEntries.add(index, newRowEntry);
- if (DEBUG) {
- Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
- newRowEntry.titleSectionName));
- }
- newRowEntry = newIter.hasNext() ? newIter.next() : null;
- mListener.notifyItemInserted(index);
-
- } else {
- // same package name but,
- // did the icon, title, etc, change?
- // or did the widget size and desc, span, etc change?
- if (!isSamePackageItemInfo(orgRowEntry.pkgItem, newRowEntry.pkgItem) ||
- !orgRowEntry.widgets.equals(newRowEntry.widgets)) {
- index = currentEntries.indexOf(orgRowEntry);
- currentEntries.set(index, newRowEntry);
- mListener.notifyItemChanged(index);
- if (DEBUG) {
- Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
- newRowEntry.titleSectionName));
- }
- }
- orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
- newRowEntry = newIter.hasNext() ? newIter.next() : null;
- }
- } while(orgRowEntry != null || newRowEntry != null);
- }
-
- /**
- * Compare package name using the same comparator as in {@link WidgetsListAdapter}.
- * Also handle null row pointers.
- */
- private int comparePackageName(WidgetListRowEntry curRow, WidgetListRowEntry newRow,
- WidgetListRowEntryComparator comparator) {
- if (curRow == null && newRow == null) {
- throw new IllegalStateException("Cannot compare PackageItemInfo if both rows are null.");
- }
-
- if (curRow == null && newRow != null) {
- return 1; // new row needs to be inserted
- } else if (curRow != null && newRow == null) {
- return -1; // old row needs to be deleted
- }
- return comparator.compare(curRow, newRow);
- }
-
- private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
- return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
- && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
- }
-}
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
deleted file mode 100644
index 68a3ec5..0000000
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.animation.AnimationUtils;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.views.RecyclerViewFastScroller;
-import com.android.launcher3.views.TopRoundedCornerView;
-
-/**
- * Popup for showing the full list of available widgets
- */
-public class WidgetsFullSheet extends BaseWidgetSheet
- implements Insettable, ProviderChangedListener {
-
- private static final long DEFAULT_OPEN_DURATION = 267;
- private static final long FADE_IN_DURATION = 150;
- private static final float VERTICAL_START_POSITION = 0.3f;
-
- private final Rect mInsets = new Rect();
-
- private final WidgetsListAdapter mAdapter;
-
- private WidgetsRecyclerView mRecyclerView;
-
- public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- LauncherAppState apps = LauncherAppState.getInstance(context);
- mAdapter = new WidgetsListAdapter(context,
- LayoutInflater.from(context), apps.getWidgetCache(), apps.getIconCache(),
- this, this);
-
- }
-
- public WidgetsFullSheet(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mContent = findViewById(R.id.container);
-
- mRecyclerView = findViewById(R.id.widgets_list_view);
- mRecyclerView.setAdapter(mAdapter);
- mAdapter.setApplyBitmapDeferred(true, mRecyclerView);
-
- TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent;
- springLayout.addSpringView(R.id.widgets_list_view);
- mRecyclerView.setEdgeEffectFactory(springLayout.createEdgeEffectFactory());
- onWidgetsBound();
- }
-
- @VisibleForTesting
- public WidgetsRecyclerView getRecyclerView() {
- return mRecyclerView;
- }
-
- @Override
- protected Pair<View, String> getAccessibilityTarget() {
- return Pair.create(mRecyclerView, getContext().getString(
- mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mLauncher.getAppWidgetHost().addProviderChangeListener(this);
- notifyWidgetProvidersChanged();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mLauncher.getAppWidgetHost().removeProviderChangeListener(this);
- }
-
- @Override
- public void setInsets(Rect insets) {
- mInsets.set(insets);
-
- mRecyclerView.setPadding(
- mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingTop(),
- mRecyclerView.getPaddingRight(), insets.bottom);
- if (insets.bottom > 0) {
- setupNavBarColor();
- } else {
- clearNavBarColor();
- }
-
- ((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom);
- requestLayout();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int widthUsed;
- if (mInsets.bottom > 0) {
- widthUsed = mInsets.left + mInsets.right;
- } else {
- Rect padding = mLauncher.getDeviceProfile().workspacePadding;
- widthUsed = Math.max(padding.left + padding.right,
- 2 * (mInsets.left + mInsets.right));
- }
-
- int heightUsed = mInsets.top + mLauncher.getDeviceProfile().edgeMarginPx;
- measureChildWithMargins(mContent, widthMeasureSpec,
- widthUsed, heightMeasureSpec, heightUsed);
- setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
- MeasureSpec.getSize(heightMeasureSpec));
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- int width = r - l;
- int height = b - t;
-
- // Content is laid out as center bottom aligned
- int contentWidth = mContent.getMeasuredWidth();
- int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
- mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
- contentLeft + contentWidth, height);
-
- setTranslationShift(mTranslationShift);
- }
-
- @Override
- public void notifyWidgetProvidersChanged() {
- mLauncher.refreshAndBindWidgetsForPackageUser(null);
- }
-
- @Override
- public void onWidgetsBound() {
- mAdapter.setWidgets(mLauncher.getPopupDataProvider().getAllWidgets());
- }
-
- private void open(boolean animate) {
- if (animate) {
- if (getPopupContainer().getInsets().bottom > 0) {
- mContent.setAlpha(0);
- setTranslationShift(VERTICAL_START_POSITION);
- }
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator
- .setDuration(DEFAULT_OPEN_DURATION)
- .setInterpolator(AnimationUtils.loadInterpolator(
- getContext(), android.R.interpolator.linear_out_slow_in));
- mRecyclerView.setLayoutFrozen(true);
- mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mRecyclerView.setLayoutFrozen(false);
- mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
- mOpenCloseAnimator.removeListener(this);
- }
- });
- post(() -> {
- mOpenCloseAnimator.start();
- mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
- });
- } else {
- setTranslationShift(TRANSLATION_SHIFT_OPENED);
- mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
- post(this::announceAccessibilityChanges);
- }
- }
-
- @Override
- protected void handleClose(boolean animate) {
- handleClose(animate, DEFAULT_OPEN_DURATION);
- }
-
- @Override
- protected boolean isOfType(int type) {
- return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
- }
-
- @Override
- public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- // Disable swipe down when recycler view is scrolling
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mNoIntercept = false;
- RecyclerViewFastScroller scroller = mRecyclerView.getScrollbar();
- if (scroller.getThumbOffsetY() >= 0 &&
- getPopupContainer().isEventOverView(scroller, ev)) {
- mNoIntercept = true;
- } else if (getPopupContainer().isEventOverView(mContent, ev)) {
- mNoIntercept = !mRecyclerView.shouldContainerScroll(ev, getPopupContainer());
- }
- }
- return super.onControllerInterceptTouchEvent(ev);
- }
-
- public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
- WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
- .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
- sheet.attachToContainer();
- sheet.mIsOpen = true;
- sheet.open(animate);
- return sheet;
- }
-
- @VisibleForTesting
- public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
- return launcher.findViewById(R.id.widgets_list_view);
- }
-
- @Override
- protected int getElementsRowCount() {
- return mAdapter.getItemCount();
- }
-
- @Override
- public void addHintCloseAnim(
- float distanceToMove, Interpolator interpolator, PendingAnimation target) {
- target.setFloat(mRecyclerView, VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
- target.setViewAlpha(mRecyclerView, 0.5f, interpolator);
- }
-
- @Override
- protected void onCloseComplete() {
- super.onCloseComplete();
- AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
- }
-}
diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java
deleted file mode 100644
index a45521d..0000000
--- a/src/com/android/launcher3/widget/WidgetsListAdapter.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
-import android.view.ViewGroup;
-
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.LabelComparator;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.Adapter;
-
-/**
- * List view adapter for the widget tray.
- *
- * <p>Memory vs. Performance:
- * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling
- * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is
- * only a single type of view.
- */
-public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> {
-
- private static final String TAG = "WidgetsListAdapter";
- private static final boolean DEBUG = false;
-
- private final WidgetPreviewLoader mWidgetPreviewLoader;
- private final LayoutInflater mLayoutInflater;
-
- private final OnClickListener mIconClickListener;
- private final OnLongClickListener mIconLongClickListener;
- private final int mIndent;
- private ArrayList<WidgetListRowEntry> mEntries = new ArrayList<>();
- private final WidgetsDiffReporter mDiffReporter;
-
- private boolean mApplyBitmapDeferred;
-
- public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
- WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
- OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
- mLayoutInflater = layoutInflater;
- mWidgetPreviewLoader = widgetPreviewLoader;
- mIconClickListener = iconClickListener;
- mIconLongClickListener = iconLongClickListener;
- mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
- mDiffReporter = new WidgetsDiffReporter(iconCache, this);
- }
-
- /**
- * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}
- *
- * @see WidgetCell#setApplyBitmapDeferred(boolean)
- */
- public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
- mApplyBitmapDeferred = isDeferred;
-
- for (int i = rv.getChildCount() - 1; i >= 0; i--) {
- WidgetsRowViewHolder holder = (WidgetsRowViewHolder)
- rv.getChildViewHolder(rv.getChildAt(i));
- for (int j = holder.cellContainer.getChildCount() - 1; j >= 0; j--) {
- View v = holder.cellContainer.getChildAt(j);
- if (v instanceof WidgetCell) {
- ((WidgetCell) v).setApplyBitmapDeferred(mApplyBitmapDeferred);
- }
- }
- }
- }
-
- /**
- * Update the widget list.
- */
- public void setWidgets(ArrayList<WidgetListRowEntry> tempEntries) {
- WidgetListRowEntryComparator rowComparator = new WidgetListRowEntryComparator();
- Collections.sort(tempEntries, rowComparator);
- mDiffReporter.process(mEntries, tempEntries, rowComparator);
- }
-
- @Override
- public int getItemCount() {
- return mEntries.size();
- }
-
- public String getSectionName(int pos) {
- return mEntries.get(pos).titleSectionName;
- }
-
- @Override
- public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) {
- WidgetListRowEntry entry = mEntries.get(pos);
- List<WidgetItem> infoList = entry.widgets;
-
- ViewGroup row = holder.cellContainer;
- if (DEBUG) {
- Log.d(TAG, String.format(
- "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]",
- pos, infoList.size(), row.getChildCount()));
- }
-
- // Add more views.
- // if there are too many, hide them.
- int expectedChildCount = infoList.size() + Math.max(0, infoList.size() - 1);
- int childCount = row.getChildCount();
-
- if (expectedChildCount > childCount) {
- for (int i = childCount ; i < expectedChildCount; i++) {
- if ((i & 1) == 1) {
- // Add a divider for odd index
- mLayoutInflater.inflate(R.layout.widget_list_divider, row);
- } else {
- // Add cell for even index
- WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
- R.layout.widget_cell, row, false);
-
- // set up touch.
- widget.setOnClickListener(mIconClickListener);
- widget.setOnLongClickListener(mIconLongClickListener);
- row.addView(widget);
- }
- }
- } else if (expectedChildCount < childCount) {
- for (int i = expectedChildCount ; i < childCount; i++) {
- row.getChildAt(i).setVisibility(View.GONE);
- }
- }
-
- // Bind the views in the application info section.
- holder.title.applyFromPackageItemInfo(entry.pkgItem);
-
- // Bind the view in the widget horizontal tray region.
- for (int i=0; i < infoList.size(); i++) {
- WidgetCell widget = (WidgetCell) row.getChildAt(2*i);
- widget.applyFromCellItem(infoList.get(i), mWidgetPreviewLoader);
- widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
- widget.ensurePreview();
- widget.setVisibility(View.VISIBLE);
-
- if (i > 0) {
- row.getChildAt(2*i - 1).setVisibility(View.VISIBLE);
- }
- }
- }
-
- @Override
- public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- if (DEBUG) {
- Log.v(TAG, "\nonCreateViewHolder");
- }
-
- ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
- R.layout.widgets_list_row_view, parent, false);
-
- // if the end padding is 0, then container view (horizontal scroll view) doesn't respect
- // the end of the linear layout width + the start padding and doesn't allow scrolling.
- container.findViewById(R.id.widgets_cell_list).setPaddingRelative(mIndent, 0, 1, 0);
-
- return new WidgetsRowViewHolder(container);
- }
-
- @Override
- public void onViewRecycled(WidgetsRowViewHolder holder) {
- int total = holder.cellContainer.getChildCount();
- for (int i = 0; i < total; i+=2) {
- WidgetCell widget = (WidgetCell) holder.cellContainer.getChildAt(i);
- widget.clear();
- }
- }
-
- public boolean onFailedToRecycleView(WidgetsRowViewHolder holder) {
- // If child views are animating, then the RecyclerView may choose not to recycle the view,
- // causing extraneous onCreateViewHolder() calls. It is safe in this case to continue
- // recycling this view, and take care in onViewRecycled() to cancel any existing
- // animations.
- return true;
- }
-
- @Override
- public long getItemId(int pos) {
- return pos;
- }
-
- /**
- * Comparator for sorting WidgetListRowEntry based on package title
- */
- public static class WidgetListRowEntryComparator implements Comparator<WidgetListRowEntry> {
-
- private final LabelComparator mComparator = new LabelComparator();
-
- @Override
- public int compare(WidgetListRowEntry a, WidgetListRowEntry b) {
- return mComparator.compare(a.pkgItem.title.toString(), b.pkgItem.title.toString());
- }
- }
-}
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
deleted file mode 100644
index 82d4110..0000000
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.widget;
-
-import android.content.Context;
-import android.graphics.Point;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-
-import com.android.launcher3.BaseRecyclerView;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
-
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
-
-/**
- * The widgets recycler view.
- */
-public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouchListener {
-
- private WidgetsListAdapter mAdapter;
-
- private final int mScrollbarTop;
-
- private final Point mFastScrollerOffset = new Point();
- private boolean mTouchDownOnScroller;
-
- public WidgetsRecyclerView(Context context) {
- this(context, null);
- }
-
- public WidgetsRecyclerView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
- // API 21 and below only support 3 parameter ctor.
- super(context, attrs, defStyleAttr);
- mScrollbarTop = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
- addOnItemTouchListener(this);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- // create a layout manager with Launcher's context so that scroll position
- // can be preserved during screen rotation.
- setLayoutManager(new LinearLayoutManager(getContext()));
- }
-
- @Override
- public void setAdapter(Adapter adapter) {
- super.setAdapter(adapter);
- mAdapter = (WidgetsListAdapter) adapter;
- }
-
- /**
- * Maps the touch (from 0..1) to the adapter position that should be visible.
- */
- @Override
- public String scrollToPositionAtProgress(float touchFraction) {
- // Skip early if widgets are not bound.
- if (isModelNotReady()) {
- return "";
- }
-
- // Stop the scroller if it is scrolling
- stopScroll();
-
- int rowCount = mAdapter.getItemCount();
- float pos = rowCount * touchFraction;
- int availableScrollHeight = getAvailableScrollHeight();
- LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
- layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
-
- int posInt = (int) ((touchFraction == 1)? pos -1 : pos);
- return mAdapter.getSectionName(posInt);
- }
-
- /**
- * Updates the bounds for the scrollbar.
- */
- @Override
- public void onUpdateScrollbar(int dy) {
- // Skip early if widgets are not bound.
- if (isModelNotReady()) {
- return;
- }
-
- // Skip early if, there no child laid out in the container.
- int scrollY = getCurrentScrollY();
- if (scrollY < 0) {
- mScrollbar.setThumbOffsetY(-1);
- return;
- }
-
- synchronizeScrollBarThumbOffsetToViewScroll(scrollY, getAvailableScrollHeight());
- }
-
- @Override
- public int getCurrentScrollY() {
- // Skip early if widgets are not bound.
- if (isModelNotReady() || getChildCount() == 0) {
- return -1;
- }
-
- View child = getChildAt(0);
- int rowIndex = getChildPosition(child);
- int y = (child.getMeasuredHeight() * rowIndex);
- int offset = getLayoutManager().getDecoratedTop(child);
-
- return getPaddingTop() + y - offset;
- }
-
- /**
- * Returns the available scroll height:
- * AvailableScrollHeight = Total height of the all items - last page height
- */
- @Override
- protected int getAvailableScrollHeight() {
- View child = getChildAt(0);
- return child.getMeasuredHeight() * mAdapter.getItemCount() - getScrollbarTrackHeight()
- - mScrollbarTop;
- }
-
- private boolean isModelNotReady() {
- return mAdapter.getItemCount() == 0;
- }
-
- @Override
- public int getScrollBarTop() {
- return mScrollbarTop;
- }
-
- @Override
- public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
- if (e.getAction() == MotionEvent.ACTION_DOWN) {
- mTouchDownOnScroller =
- mScrollbar.isHitInParent(e.getX(), e.getY(), mFastScrollerOffset);
- }
- if (mTouchDownOnScroller) {
- return mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
- }
- return false;
- }
-
- @Override
- public void onTouchEvent(RecyclerView rv, MotionEvent e) {
- if (mTouchDownOnScroller) {
- mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
- }
- }
-
- @Override
- public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java
deleted file mode 100644
index d26edb6..0000000
--- a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import android.view.ViewGroup;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.R;
-
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-public class WidgetsRowViewHolder extends ViewHolder {
-
- public final ViewGroup cellContainer;
- public final BubbleTextView title;
-
- public WidgetsRowViewHolder(ViewGroup v) {
- super(v);
-
- cellContainer = v.findViewById(R.id.widgets_cell_list);
- title = v.findViewById(R.id.section);
- title.setAccessibilityDelegate(null);
- }
-}
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 1086987..8b3bbce 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -22,8 +22,9 @@
import android.os.Parcel;
import android.os.Parcelable;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
/**
* Custom app widget provider info that can be used as a widget, but provide extra functionality
@@ -57,7 +58,7 @@
}
@Override
- public void initSpans(Context context) { }
+ public void initSpans(Context context, InvariantDeviceProfile idp) { }
@Override
public String getLabel(PackageManager packageManager) {
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 0b66ec0..329a444 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -16,7 +16,7 @@
package com.android.launcher3.widget.custom;
-import static com.android.launcher3.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
@@ -29,12 +29,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.systemui.plugins.CustomWidgetPlugin;
import com.android.systemui.plugins.PluginListener;
diff --git a/src/com/android/launcher3/widget/dragndrop/AppWidgetHostViewDragListener.java b/src/com/android/launcher3/widget/dragndrop/AppWidgetHostViewDragListener.java
new file mode 100644
index 0000000..4a60983
--- /dev/null
+++ b/src/com/android/launcher3/widget/dragndrop/AppWidgetHostViewDragListener.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.dragndrop;
+
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+
+/** A drag listener of {@link LauncherAppWidgetHostView}. */
+public final class AppWidgetHostViewDragListener implements DragController.DragListener {
+ private final Launcher mLauncher;
+ private DropTarget.DragObject mDragObject;
+ private LauncherAppWidgetHostView mAppWidgetHostView;
+
+ public AppWidgetHostViewDragListener(Launcher launcher) {
+ mLauncher = launcher;
+ }
+
+ @Override
+ public void onDragStart(DropTarget.DragObject dragObject, DragOptions unused) {
+ if (dragObject.dragView.getContentView() instanceof LauncherAppWidgetHostView) {
+ mDragObject = dragObject;
+ mAppWidgetHostView = (LauncherAppWidgetHostView) dragObject.dragView.getContentView();
+ mAppWidgetHostView.startDrag(this);
+ } else {
+ mLauncher.getDragController().removeDragListener(this);
+ }
+ }
+
+ @Override
+ public void onDragEnd() {
+ mAppWidgetHostView.endDrag();
+ mLauncher.getDragController().removeDragListener(this);
+ }
+
+ /** Notifies when there is a content change in the drag view. */
+ public void onDragContentChanged() {
+ if (mDragObject.dragView != null) {
+ mDragObject.dragView.invalidate();
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
new file mode 100644
index 0000000..abc79ff
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.model;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.WidgetItemComparator;
+
+import java.lang.annotation.Retention;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Holder class to store the package information of an entry shown in the widgets list. */
+public abstract class WidgetsListBaseEntry {
+ public final PackageItemInfo mPkgItem;
+
+ /**
+ * Character that is used as a section name for the {@link ItemInfo#title}.
+ * (e.g., "G" will be stored if title is "Google")
+ */
+ public final String mTitleSectionName;
+
+ public final List<WidgetItem> mWidgets;
+
+ public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ mPkgItem = pkgItem;
+ mTitleSectionName = titleSectionName;
+ this.mWidgets =
+ items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
+ }
+
+ /**
+ * Returns the ranking of this entry in the
+ * {@link com.android.launcher3.widget.picker.WidgetsListAdapter}.
+ *
+ * <p>Entries with smaller value should be shown first. See
+ * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter} for more details.
+ */
+ @Rank
+ public abstract int getRank();
+
+ /**
+ * Marker interface for subclasses that are headers for widget list items.
+ *
+ * @param <T> The type of this class.
+ */
+ public interface Header<T extends WidgetsListBaseEntry & Header<T>> {
+ /** Returns whether the widget list is currently expanded. */
+ boolean isWidgetListShown();
+
+ /** Returns a copy of the item with the widget list shown. */
+ T withWidgetListShown();
+ }
+
+ @Retention(SOURCE)
+ @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_SEARCH_HEADER, RANK_WIDGETS_LIST_CONTENT})
+ public @interface Rank {
+ }
+
+ public static final int RANK_WIDGETS_LIST_HEADER = 1;
+ public static final int RANK_WIDGETS_LIST_SEARCH_HEADER = 2;
+ public static final int RANK_WIDGETS_LIST_CONTENT = 3;
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
new file mode 100644
index 0000000..73b17f1
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.List;
+
+/**
+ * Holder class to store all the information related to a list of widgets from the same app which is
+ * shown in the {@link com.android.launcher3.widget.picker.WidgetsFullSheet}.
+ */
+public final class WidgetsListContentEntry extends WidgetsListBaseEntry {
+
+ private final int mMaxSpanSizeInCells;
+
+ /**
+ * Constructor for {@link WidgetsListContentEntry}.
+ *
+ * @param pkgItem package info associated with the entry
+ * @param titleSectionName title section name associated with the entry.
+ * @param items list of widgets for the package.
+ */
+ public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ this(pkgItem, titleSectionName, items, /* maxSpanSizeInCells= */ 0);
+ }
+
+ /**
+ * Constructor for {@link WidgetsListContentEntry}.
+ *
+ * @param pkgItem package info associated with the entry
+ * @param titleSectionName title section name associated with the entry.
+ * @param items list of widgets for the package.
+ * @param maxSpanSizeInCells the max horizontal span in cells that is allowed for grouping more
+ * than one widgets in a table row.
+ */
+ public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items, int maxSpanSizeInCells) {
+ super(pkgItem, titleSectionName, items);
+ mMaxSpanSizeInCells = maxSpanSizeInCells;
+ }
+
+ @Override
+ public String toString() {
+ return "Content:" + mPkgItem.packageName + ":" + mWidgets.size() + " maxSpanSizeInCells: "
+ + mMaxSpanSizeInCells;
+ }
+
+ @Override
+ @Rank
+ public int getRank() {
+ return RANK_WIDGETS_LIST_CONTENT;
+ }
+
+ /**
+ * Returns a copy of this {@link WidgetsListContentEntry} with updated
+ * {@param maxSpanSizeInCells}.
+ *
+ * @param maxSpanSizeInCells the maximum horizontal span in cells that is allowed for grouping
+ * more than one widgets in a table row.
+ */
+ public WidgetsListContentEntry withMaxSpanSize(int maxSpanSizeInCells) {
+ if (mMaxSpanSizeInCells == maxSpanSizeInCells) return this;
+ return new WidgetsListContentEntry(
+ mPkgItem,
+ mTitleSectionName,
+ mWidgets,
+ /* maxSpanSizeInCells= */ maxSpanSizeInCells);
+ }
+
+ /**
+ * Returns the max horizontal span size in cells that is allowed for grouping more than one
+ * widget in a table row.
+ */
+ public int getMaxSpanSizeInCells() {
+ return mMaxSpanSizeInCells;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof WidgetsListContentEntry)) return false;
+ WidgetsListContentEntry otherEntry = (WidgetsListContentEntry) obj;
+ return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName)
+ && mMaxSpanSizeInCells == otherEntry.mMaxSpanSizeInCells;
+ }
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
new file mode 100644
index 0000000..5b3ea94
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.List;
+
+/** An information holder for an app which has widgets or/and shortcuts. */
+public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry
+ implements WidgetsListBaseEntry.Header<WidgetsListHeaderEntry> {
+
+ public final int widgetsCount;
+ public final int shortcutsCount;
+
+ private final boolean mIsWidgetListShown;
+
+ public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false);
+ }
+
+ private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items, boolean isWidgetListShown) {
+ super(pkgItem, titleSectionName, items);
+ widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
+ shortcutsCount = Math.max(0, items.size() - widgetsCount);
+ mIsWidgetListShown = isWidgetListShown;
+ }
+
+ /** Returns {@code true} if the widgets list associated with this header is shown. */
+ @Override
+ public boolean isWidgetListShown() {
+ return mIsWidgetListShown;
+ }
+
+ @Override
+ public String toString() {
+ return "Header:" + mPkgItem.packageName + ":" + mWidgets.size();
+ }
+
+ @Override
+ @Rank
+ public int getRank() {
+ return RANK_WIDGETS_LIST_HEADER;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof WidgetsListHeaderEntry)) return false;
+ WidgetsListHeaderEntry otherEntry = (WidgetsListHeaderEntry) obj;
+ return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName)
+ && mIsWidgetListShown == otherEntry.mIsWidgetListShown;
+ }
+
+ /** Returns a copy of this {@link WidgetsListHeaderEntry} with the widget list shown. */
+ @Override
+ public WidgetsListHeaderEntry withWidgetListShown() {
+ if (mIsWidgetListShown) return this;
+ return new WidgetsListHeaderEntry(
+ mPkgItem,
+ mTitleSectionName,
+ mWidgets,
+ /* isWidgetListShown= */ true);
+ }
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
new file mode 100644
index 0000000..055e4ec
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.List;
+
+/** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */
+public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry
+ implements WidgetsListBaseEntry.Header<WidgetsListSearchHeaderEntry> {
+
+ private final boolean mIsWidgetListShown;
+
+ public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false);
+ }
+
+ private WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items, boolean isWidgetListShown) {
+ super(pkgItem, titleSectionName, items);
+ mIsWidgetListShown = isWidgetListShown;
+ }
+
+ /** Returns {@code true} if the widgets list associated with this header is shown. */
+ @Override
+ public boolean isWidgetListShown() {
+ return mIsWidgetListShown;
+ }
+
+ @Override
+ public String toString() {
+ return "SearchHeader:" + mPkgItem.packageName + ":" + mWidgets.size();
+ }
+
+ @Override
+ @Rank
+ public int getRank() {
+ return RANK_WIDGETS_LIST_SEARCH_HEADER;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof WidgetsListSearchHeaderEntry)) return false;
+ WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj;
+ return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName)
+ && mIsWidgetListShown == otherEntry.mIsWidgetListShown;
+ }
+
+ /** Returns a copy of this {@link WidgetsListSearchHeaderEntry} with the widget list shown. */
+ @Override
+ public WidgetsListSearchHeaderEntry withWidgetListShown() {
+ if (mIsWidgetListShown) return this;
+ return new WidgetsListSearchHeaderEntry(
+ mPkgItem,
+ mTitleSectionName,
+ mWidgets,
+ /* isWidgetListShown= */ true);
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java
new file mode 100644
index 0000000..7372751
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import com.android.launcher3.util.PackageUserKey;
+
+/**
+ * A listener to be invoked when a header is clicked.
+ */
+public interface OnHeaderClickListener {
+ /**
+ * Calls when a header is clicked to show / hide widgets for a package.
+ */
+ void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey);
+}
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
new file mode 100644
index 0000000..6643779
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.animation.ValueAnimator;
+import android.graphics.Point;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.views.RecyclerViewFastScroller;
+import com.android.launcher3.widget.picker.WidgetsFullSheet.SearchAndRecommendationViewHolder;
+import com.android.launcher3.workprofile.PersonalWorkPagedView;
+
+/**
+ * A controller which measures & updates {@link WidgetsFullSheet}'s views padding, margin and
+ * vertical displacement upon scrolling.
+ */
+final class SearchAndRecommendationsScrollController implements
+ RecyclerViewFastScroller.OnFastScrollChangeListener, ValueAnimator.AnimatorUpdateListener {
+ private final boolean mHasWorkProfile;
+ private final SearchAndRecommendationViewHolder mViewHolder;
+ private final View mSearchAndRecommendationViewParent;
+ private final WidgetsRecyclerView mPrimaryRecyclerView;
+ private final WidgetsRecyclerView mSearchRecyclerView;
+ private final TextView mNoWidgetsView;
+ private final int mTabsHeight;
+ private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
+ private final Point mTempOffset = new Point();
+ private int mBottomInset;
+
+ // The following are only non null if mHasWorkProfile is true.
+ @Nullable private final WidgetsRecyclerView mWorkRecyclerView;
+ @Nullable private final View mPrimaryWorkTabsView;
+ @Nullable private final PersonalWorkPagedView mPrimaryWorkViewPager;
+
+ private WidgetsRecyclerView mCurrentRecyclerView;
+ private int mCurrentRecyclerViewScrollY = 0;
+
+ private OnContentChangeListener mOnContentChangeListener = () -> onScrollChanged();
+
+ /**
+ * The vertical distance, in pixels, until the search is pinned at the top of the screen when
+ * the user scrolls down the recycler view.
+ */
+ private int mCollapsibleHeightForSearch = 0;
+ /**
+ * The vertical distance, in pixels, until the recommendation table disappears from the top of
+ * the screen when the user scrolls down the recycler view.
+ */
+ private int mCollapsibleHeightForRecommendation = 0;
+ /**
+ * The vertical distance, in pixels, until the tabs is pinned at the top of the screen when the
+ * user scrolls down the recycler view.
+ *
+ * <p>Always 0 if there is no work profile.
+ */
+ private int mCollapsibleHeightForTabs = 0;
+
+ private boolean mShouldForwardToRecyclerView = false;
+
+ SearchAndRecommendationsScrollController(
+ boolean hasWorkProfile,
+ int tabsHeight,
+ SearchAndRecommendationViewHolder viewHolder,
+ WidgetsRecyclerView primaryRecyclerView,
+ @Nullable WidgetsRecyclerView workRecyclerView,
+ WidgetsRecyclerView searchRecyclerView,
+ @Nullable View personalWorkTabsView,
+ @Nullable PersonalWorkPagedView primaryWorkViewPager,
+ TextView noWidgetsView) {
+ mHasWorkProfile = hasWorkProfile;
+ mViewHolder = viewHolder;
+ mViewHolder.mContainer.setSearchAndRecommendationScrollController(this);
+ mSearchAndRecommendationViewParent = (View) mViewHolder.mContainer.getParent();
+ mPrimaryRecyclerView = primaryRecyclerView;
+ mWorkRecyclerView = workRecyclerView;
+ mSearchRecyclerView = searchRecyclerView;
+ mPrimaryWorkTabsView = personalWorkTabsView;
+ mPrimaryWorkViewPager = primaryWorkViewPager;
+ mTabsHeight = tabsHeight;
+ mNoWidgetsView = noWidgetsView;
+ setCurrentRecyclerView(mPrimaryRecyclerView, /* animateReset= */ false);
+ }
+
+ public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
+ setCurrentRecyclerView(currentRecyclerView, /* animateReset= */ true);
+ }
+
+ /** Sets the current active {@link WidgetsRecyclerView}. */
+ private void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView,
+ boolean animateReset) {
+ if (mCurrentRecyclerView == currentRecyclerView) {
+ return;
+ }
+ if (mCurrentRecyclerView != null) {
+ mCurrentRecyclerView.setOnContentChangeListener(null);
+ }
+ mCurrentRecyclerView = currentRecyclerView;
+ mCurrentRecyclerView.setOnContentChangeListener(mOnContentChangeListener);
+ reset(animateReset);
+ }
+
+ /**
+ * Updates padding of {@link WidgetsFullSheet} contents to include {@code bottomInset} wherever
+ * necessary.
+ */
+ public boolean updateBottomInset(int bottomInset) {
+ mBottomInset = bottomInset;
+ return updateMarginAndPadding();
+ }
+
+ /**
+ * Updates the margin and padding of {@link WidgetsFullSheet} to accumulate collapsible views.
+ *
+ * @return {@code true} if margins or/and padding of views in the search and recommendations
+ * container have been updated.
+ */
+ public boolean updateMarginAndPadding() {
+ boolean hasMarginOrPaddingUpdated = false;
+ mCollapsibleHeightForSearch = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle);
+ mCollapsibleHeightForRecommendation =
+ measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle)
+ + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle)
+ + measureHeightWithVerticalMargins((View) mViewHolder.mSearchBarContainer)
+ + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable);
+
+ int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer);
+ int noWidgetsViewHeight = topContainerHeight - mBottomInset;
+
+ if (mHasWorkProfile) {
+ mCollapsibleHeightForTabs = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle)
+ + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable);
+ // In a work profile setup, the full widget sheet contains the following views:
+ // ------- (pinned) -|
+ // Widgets (collapsible) -|---> LinearLayout for search & recommendations
+ // Search bar (pinned) -|
+ // Widgets recommendation (collapsible)-|
+ // Personal | Work (pinned)
+ // View Pager
+ //
+ // Views after the search & recommendations are not bound by RelativelyLayout param.
+ // To position them on the expected location, padding & margin are added to these views
+
+ // Tabs should have a padding of the height of the search & recommendations container.
+ RelativeLayout.LayoutParams tabsLayoutParams =
+ (RelativeLayout.LayoutParams) mPrimaryWorkTabsView.getLayoutParams();
+ tabsLayoutParams.topMargin = topContainerHeight;
+ mPrimaryWorkTabsView.setLayoutParams(tabsLayoutParams);
+
+ // Instead of setting the top offset directly, we split the top offset into two values:
+ // 1. topOffsetAfterAllViewsCollapsed: this is the top offset after all collapsible
+ // views are no longer visible on the screen.
+ // This value is set as the margin for the view pager.
+ // 2. mMaxCollapsibleDistance
+ // This value is set as the padding for the recycler views in order to work with
+ // clipToPadding="false", which is an attribute for not showing top / bottom padding
+ // when a recycler view has not reached the top or bottom of the list.
+ // e.g. a list of 10 entries, only 3 entries are visible at a time.
+ // case 1: recycler view is scrolled to the top. Top padding is visible/
+ // (top padding)
+ // item 1
+ // item 2
+ // item 3
+ //
+ // case 2: recycler view is scrolled to the middle. No padding is visible.
+ // item 4
+ // item 5
+ // item 6
+ //
+ // case 3: recycler view is scrolled to the end. bottom padding is visible.
+ // item 8
+ // item 9
+ // item 10
+ // (bottom padding): not set in this case.
+ //
+ // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and
+ // mMaxCollapsibleDistance should equal to the top container height.
+ int topOffsetAfterAllViewsCollapsed =
+ topContainerHeight + mTabsHeight - mCollapsibleHeightForTabs;
+
+ if (mPrimaryWorkTabsView.getVisibility() == View.VISIBLE) {
+ noWidgetsViewHeight += mTabsHeight;
+ }
+
+ RelativeLayout.LayoutParams viewPagerLayoutParams =
+ (RelativeLayout.LayoutParams) mPrimaryWorkViewPager.getLayoutParams();
+ if (viewPagerLayoutParams.topMargin != topOffsetAfterAllViewsCollapsed) {
+ viewPagerLayoutParams.topMargin = topOffsetAfterAllViewsCollapsed;
+ mPrimaryWorkViewPager.setLayoutParams(viewPagerLayoutParams);
+ hasMarginOrPaddingUpdated = true;
+ }
+
+ if (mPrimaryRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) {
+ mPrimaryRecyclerView.setPadding(
+ mPrimaryRecyclerView.getPaddingLeft(),
+ mCollapsibleHeightForTabs,
+ mPrimaryRecyclerView.getPaddingRight(),
+ mPrimaryRecyclerView.getPaddingBottom());
+ hasMarginOrPaddingUpdated = true;
+ }
+ if (mWorkRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) {
+ mWorkRecyclerView.setPadding(
+ mWorkRecyclerView.getPaddingLeft(),
+ mCollapsibleHeightForTabs,
+ mWorkRecyclerView.getPaddingRight(),
+ mWorkRecyclerView.getPaddingBottom());
+ hasMarginOrPaddingUpdated = true;
+ }
+ } else {
+ if (mPrimaryRecyclerView.getPaddingTop() != topContainerHeight) {
+ mPrimaryRecyclerView.setPadding(
+ mPrimaryRecyclerView.getPaddingLeft(),
+ topContainerHeight,
+ mPrimaryRecyclerView.getPaddingRight(),
+ mPrimaryRecyclerView.getPaddingBottom());
+ hasMarginOrPaddingUpdated = true;
+ }
+ }
+ if (mSearchRecyclerView.getPaddingTop() != topContainerHeight) {
+ mSearchRecyclerView.setPadding(
+ mSearchRecyclerView.getPaddingLeft(),
+ topContainerHeight,
+ mSearchRecyclerView.getPaddingRight(),
+ mSearchRecyclerView.getPaddingBottom());
+ hasMarginOrPaddingUpdated = true;
+ }
+ if (mNoWidgetsView.getPaddingTop() != noWidgetsViewHeight) {
+ mNoWidgetsView.setPadding(
+ mNoWidgetsView.getPaddingLeft(),
+ noWidgetsViewHeight,
+ mNoWidgetsView.getPaddingRight(),
+ mNoWidgetsView.getPaddingBottom());
+ hasMarginOrPaddingUpdated = true;
+ }
+ return hasMarginOrPaddingUpdated;
+ }
+
+ @Override
+ public void onScrollChanged() {
+ int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY();
+ if (recyclerViewYOffset < 0) return;
+ mCurrentRecyclerViewScrollY = recyclerViewYOffset;
+ if (mAnimator.isStarted()) {
+ mAnimator.cancel();
+ }
+ applyVerticalTransition();
+ }
+
+ /**
+ * Changes the displacement of collapsible views (e.g. title & widget recommendations) and fixed
+ * views (e.g. recycler views, tabs) upon scrolling / content changes in the recycler view.
+ */
+ private void applyVerticalTransition() {
+ if (mCollapsibleHeightForRecommendation > 0) {
+ int yDisplacement = Math.max(-mCurrentRecyclerViewScrollY,
+ -mCollapsibleHeightForRecommendation);
+ mViewHolder.mHeaderTitle.setTranslationY(yDisplacement);
+ mViewHolder.mRecommendedWidgetsTable.setTranslationY(yDisplacement);
+ }
+
+ if (mCollapsibleHeightForSearch > 0) {
+ int searchYDisplacement = Math.max(-mCurrentRecyclerViewScrollY,
+ -mCollapsibleHeightForSearch);
+ mViewHolder.mSearchBarContainer.setTranslationY(searchYDisplacement);
+ }
+
+ if (mHasWorkProfile && mCollapsibleHeightForTabs > 0) {
+ int yDisplacementForTabs = Math.max(-mCurrentRecyclerViewScrollY,
+ -mCollapsibleHeightForTabs);
+ mPrimaryWorkTabsView.setTranslationY(yDisplacementForTabs);
+ }
+ }
+
+ /** Resets any previous view translation. */
+ public void reset(boolean animate) {
+ if (mCurrentRecyclerViewScrollY == 0) {
+ return;
+ }
+ if (mAnimator.isStarted()) {
+ mAnimator.cancel();
+ }
+
+ if (animate) {
+ mAnimator.setIntValues(mCurrentRecyclerViewScrollY, 0);
+ mAnimator.addUpdateListener(this);
+ mAnimator.setDuration(300);
+ mAnimator.start();
+ } else {
+ mCurrentRecyclerViewScrollY = 0;
+ applyVerticalTransition();
+ }
+ }
+
+ /**
+ * Returns {@code true} if a touch event should be intercepted by this controller.
+ */
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ calculateMotionEventOffset(mTempOffset);
+ event.offsetLocation(mTempOffset.x, mTempOffset.y);
+ try {
+ mShouldForwardToRecyclerView = mCurrentRecyclerView.onInterceptTouchEvent(event);
+ return mShouldForwardToRecyclerView;
+ } finally {
+ event.offsetLocation(-mTempOffset.x, -mTempOffset.y);
+ }
+ }
+
+ /**
+ * Returns {@code true} if this controller has intercepted and consumed a touch event.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mShouldForwardToRecyclerView) {
+ calculateMotionEventOffset(mTempOffset);
+ event.offsetLocation(mTempOffset.x, mTempOffset.y);
+ try {
+ return mCurrentRecyclerView.onTouchEvent(event);
+ } finally {
+ event.offsetLocation(-mTempOffset.x, -mTempOffset.y);
+ }
+ }
+ return false;
+ }
+
+ private void calculateMotionEventOffset(Point p) {
+ p.x = mViewHolder.mContainer.getLeft() - mCurrentRecyclerView.getLeft()
+ - mSearchAndRecommendationViewParent.getLeft();
+ p.y = mViewHolder.mContainer.getTop() - mCurrentRecyclerView.getTop()
+ - mSearchAndRecommendationViewParent.getTop();
+ }
+
+ /** private the height, in pixel, + the vertical margins of a given view. */
+ private static int measureHeightWithVerticalMargins(View view) {
+ if (view.getVisibility() != View.VISIBLE) {
+ return 0;
+ }
+ MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
+ return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+ + marginLayoutParams.topMargin;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mCurrentRecyclerViewScrollY = (Integer) animation.getAnimatedValue();
+ applyVerticalTransition();
+ }
+
+ /**
+ * A listener to be notified when there is a content change in the recycler view that may affect
+ * the relative position of the search and recommendation container.
+ */
+ public interface OnContentChangeListener {
+ /** Notifies a content change in the recycler view. */
+ void onContentChanged();
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsView.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsView.java
new file mode 100644
index 0000000..0d7d2b5
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsView.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+
+/**
+ * A {@link LinearLayout} container for holding search and widgets recommendation.
+ *
+ * <p>This class intercepts touch events and dispatch them to the right view.
+ */
+public class SearchAndRecommendationsView extends LinearLayout {
+ private SearchAndRecommendationsScrollController mController;
+
+ public SearchAndRecommendationsView(Context context) {
+ this(context, /* attrs= */ null);
+ }
+
+ public SearchAndRecommendationsView(Context context, AttributeSet attrs) {
+ this(context, attrs, /* defStyleAttr= */ 0);
+ }
+
+ public SearchAndRecommendationsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
+ }
+
+ public SearchAndRecommendationsView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void setSearchAndRecommendationScrollController(
+ SearchAndRecommendationsScrollController controller) {
+ mController = controller;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return mController.onInterceptTouchEvent(event) || super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return mController.onTouchEvent(event) || super.onTouchEvent(event);
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
new file mode 100644
index 0000000..99374f5
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -0,0 +1,192 @@
+/*
+ * 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.widget.picker;
+
+import android.util.Log;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
+ * methods accordingly.
+ */
+public class WidgetsDiffReporter {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "WidgetsDiffReporter";
+
+ private final IconCache mIconCache;
+ private final RecyclerView.Adapter mListener;
+
+ public WidgetsDiffReporter(IconCache iconCache, RecyclerView.Adapter listener) {
+ mIconCache = iconCache;
+ mListener = listener;
+ }
+
+ /**
+ * Notifies the difference between {@code currentEntries} & {@code newEntries} by calling the
+ * relevant {@link androidx.recyclerview.widget.RecyclerView.RecyclerViewDataObserver} methods.
+ */
+ public void process(ArrayList<WidgetsListBaseEntry> currentEntries,
+ List<WidgetsListBaseEntry> newEntries,
+ WidgetListBaseRowEntryComparator comparator) {
+ if (DEBUG) {
+ Log.d(TAG, "process oldEntries#=" + currentEntries.size()
+ + " newEntries#=" + newEntries.size());
+ }
+ // Early exit if either of the list is empty
+ if (currentEntries.isEmpty() || newEntries.isEmpty()) {
+ // Skip if both list are empty.
+ // On rotation, we open the widget tray with empty. Then try to fetch the list again
+ // when the animation completes (which still gives empty). And we get the final result
+ // when the bind actually completes.
+ if (currentEntries.size() != newEntries.size()) {
+ currentEntries.clear();
+ currentEntries.addAll(newEntries);
+ mListener.notifyDataSetChanged();
+ }
+ return;
+ }
+ ArrayList<WidgetsListBaseEntry> orgEntries =
+ (ArrayList<WidgetsListBaseEntry>) currentEntries.clone();
+ Iterator<WidgetsListBaseEntry> orgIter = orgEntries.iterator();
+ Iterator<WidgetsListBaseEntry> newIter = newEntries.iterator();
+
+ WidgetsListBaseEntry orgRowEntry = orgIter.next();
+ WidgetsListBaseEntry newRowEntry = newIter.next();
+
+ do {
+ int diff = compareAppNameAndType(orgRowEntry, newRowEntry, comparator);
+ if (DEBUG) {
+ Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
+ diff, orgRowEntry != null ? orgRowEntry.toString() : null,
+ newRowEntry != null ? newRowEntry.toString() : null));
+ }
+ int index = -1;
+ if (diff < 0) {
+ index = currentEntries.indexOf(orgRowEntry);
+ mListener.notifyItemRemoved(index);
+ if (DEBUG) {
+ Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
+ orgRowEntry.mTitleSectionName));
+ }
+ currentEntries.remove(index);
+ orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
+ } else if (diff > 0) {
+ index = orgRowEntry != null ? currentEntries.indexOf(orgRowEntry)
+ : currentEntries.size();
+ currentEntries.add(index, newRowEntry);
+ if (DEBUG) {
+ Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
+ newRowEntry.mTitleSectionName));
+ }
+ newRowEntry = newIter.hasNext() ? newIter.next() : null;
+ mListener.notifyItemInserted(index);
+
+ } else {
+ // same app name & type but,
+ // did the icon, title, etc, change?
+ // or did the header view changed due to user interactions?
+ // or did the widget size and desc, span, etc change?
+ if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem)
+ || hasHeaderUpdated(orgRowEntry, newRowEntry)
+ || hasWidgetsListContentChanged(orgRowEntry, newRowEntry)) {
+ index = currentEntries.indexOf(orgRowEntry);
+ currentEntries.set(index, newRowEntry);
+ mListener.notifyItemChanged(index);
+ if (DEBUG) {
+ Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
+ newRowEntry.mTitleSectionName));
+ }
+ }
+ orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
+ newRowEntry = newIter.hasNext() ? newIter.next() : null;
+ }
+ } while(orgRowEntry != null || newRowEntry != null);
+ }
+
+ /**
+ * Compares the app name and then entry type for the given {@link WidgetsListBaseEntry}s.
+ *
+ * @Return 0 if both entries' order is the same. Negative integer if {@code newRowEntry} should
+ * order before {@code orgRowEntry}. Positive integer if {@code orgRowEntry} should
+ * order before {@code newRowEntry}.
+ */
+ private int compareAppNameAndType(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow,
+ WidgetListBaseRowEntryComparator comparator) {
+ if (curRow == null && newRow == null) {
+ throw new IllegalStateException(
+ "Cannot compare PackageItemInfo if both rows are null.");
+ }
+
+ if (curRow == null && newRow != null) {
+ return 1; // new row needs to be inserted
+ } else if (curRow != null && newRow == null) {
+ return -1; // old row needs to be deleted
+ }
+ int diff = comparator.compare(curRow, newRow);
+ if (diff == 0) {
+ return newRow.getRank() - curRow.getRank();
+ }
+ return diff;
+ }
+
+ /**
+ * Returns {@code true} if both {@code curRow} & {@code newRow} are
+ * {@link WidgetsListContentEntry}s with a different list or arrangement of widgets.
+ */
+ private boolean hasWidgetsListContentChanged(WidgetsListBaseEntry curRow,
+ WidgetsListBaseEntry newRow) {
+ if (!(curRow instanceof WidgetsListContentEntry)
+ || !(newRow instanceof WidgetsListContentEntry)) {
+ return false;
+ }
+ return !curRow.equals(newRow);
+ }
+
+ /**
+ * Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has
+ * been changed due to user interactions.
+ */
+ private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) {
+ if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) {
+ return !curRow.equals(newRow);
+ }
+ if (newRow instanceof WidgetsListSearchHeaderEntry
+ && curRow instanceof WidgetsListSearchHeaderEntry) {
+ // Always refresh search header entries to reset rounded corners in their view holder.
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
+ return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
+ && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
new file mode 100644
index 0000000..f2fee0a
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -0,0 +1,819 @@
+/*
+ * 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.widget.picker;
+
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
+import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.views.ArrowTipView;
+import com.android.launcher3.views.RecyclerViewFastScroller;
+import com.android.launcher3.views.TopRoundedCornerView;
+import com.android.launcher3.views.WidgetsEduView;
+import com.android.launcher3.widget.BaseWidgetSheet;
+import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.search.SearchModeListener;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
+import com.android.launcher3.workprofile.PersonalWorkPagedView;
+import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.IntStream;
+
+/**
+ * Popup for showing the full list of available widgets
+ */
+public class WidgetsFullSheet extends BaseWidgetSheet
+ implements ProviderChangedListener, OnActivePageChangedListener,
+ WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
+ private static final String TAG = WidgetsFullSheet.class.getSimpleName();
+
+ private static final long DEFAULT_OPEN_DURATION = 267;
+ private static final long FADE_IN_DURATION = 150;
+ private static final long EDUCATION_TIP_DELAY_MS = 200;
+ private static final long EDUCATION_DIALOG_DELAY_MS = 500;
+ private static final float VERTICAL_START_POSITION = 0.3f;
+ // The widget recommendation table can easily take over the entire screen on devices with small
+ // resolution or landscape on phone. This ratio defines the max percentage of content area that
+ // the table can display.
+ private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
+
+ private static final String KEY_WIDGETS_EDUCATION_DIALOG_SEEN =
+ "launcher.widgets_education_dialog_seen";
+
+ private final Rect mInsets = new Rect();
+ private final boolean mHasWorkProfile;
+ private final SparseArray<AdapterHolder> mAdapters = new SparseArray();
+ private final UserHandle mCurrentUser = Process.myUserHandle();
+ private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter =
+ entry -> mCurrentUser.equals(entry.mPkgItem.user);
+ private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
+ mPrimaryWidgetsFilter.negate();
+ @Nullable private ArrowTipView mLatestEducationalTip;
+ private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
+ new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if (hasSeenEducationTip()) {
+ removeOnLayoutChangeListener(this);
+ return;
+ }
+
+ // Widgets are loaded asynchronously, We are adding a delay because we only want
+ // to show the tip when the widget preview has finished loading and rendering in
+ // this view.
+ removeCallbacks(mShowEducationTipTask);
+ postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS);
+ }
+ };
+
+ private final Runnable mShowEducationTipTask = () -> {
+ if (hasSeenEducationTip()) {
+ removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ return;
+ }
+ mLatestEducationalTip = showEducationTipOnViewIfPossible(getViewToShowEducationTip());
+ if (mLatestEducationalTip != null) {
+ removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ }
+ };
+
+ private final OnAttachStateChangeListener mBindScrollbarInSearchMode =
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ WidgetsRecyclerView searchRecyclerView =
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
+ if (mIsInSearchMode && searchRecyclerView != null) {
+ searchRecyclerView.bindFastScrollbar();
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ }
+ };
+
+ private final int mTabsHeight;
+ private final int mViewPagerTopPadding;
+ private final int mSearchAndRecommendationContainerBottomMargin;
+ private final int mWidgetCellHorizontalPadding;
+
+ @Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
+ @Nullable private PersonalWorkPagedView mViewPager;
+ private boolean mIsInSearchMode;
+ private boolean mIsNoWidgetsViewNeeded;
+ private int mMaxSpansPerRow = 4;
+ private View mTabsView;
+ private TextView mNoWidgetsView;
+ private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
+ private SearchAndRecommendationsScrollController mSearchAndRecommendationsScrollController;
+
+ public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
+ mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
+ mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
+ mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
+ mTabsHeight = mHasWorkProfile
+ ? getContext().getResources()
+ .getDimensionPixelSize(R.dimen.all_apps_header_pill_height)
+ : 0;
+ mViewPagerTopPadding = mHasWorkProfile
+ ? getContext().getResources()
+ .getDimensionPixelSize(R.dimen.widget_picker_view_pager_top_padding)
+ : 0;
+ mSearchAndRecommendationContainerBottomMargin = getContext().getResources()
+ .getDimensionPixelSize(mHasWorkProfile
+ ? R.dimen.search_and_recommended_widgets_container_small_bottom_margin
+ : R.dimen.search_and_recommended_widgets_container_bottom_margin);
+ mWidgetCellHorizontalPadding = 2 * getResources().getDimensionPixelOffset(
+ R.dimen.widget_cell_horizontal_padding);
+ }
+
+ public WidgetsFullSheet(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContent = findViewById(R.id.container);
+ TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent;
+
+ LayoutInflater layoutInflater = LayoutInflater.from(getContext());
+ int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
+ : R.layout.widgets_full_sheet_recyclerview;
+ layoutInflater.inflate(contentLayoutRes, springLayout, true);
+
+ RecyclerViewFastScroller fastScroller = findViewById(R.id.fast_scroller);
+ mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view));
+ mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view));
+ if (mHasWorkProfile) {
+ mViewPager = findViewById(R.id.widgets_view_pager);
+ mViewPager.initParentViews(this);
+ mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
+ mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY);
+ mTabsView = findViewById(R.id.tabs);
+ findViewById(R.id.tab_personal)
+ .setOnClickListener((View view) -> mViewPager.snapToPage(0));
+ findViewById(R.id.tab_work)
+ .setOnClickListener((View view) -> mViewPager.snapToPage(1));
+ fastScroller.setIsRecyclerViewFirstChildInParent(false);
+ mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view));
+ } else {
+ mViewPager = null;
+ }
+
+ layoutInflater.inflate(R.layout.widgets_full_sheet_search_and_recommendations, springLayout,
+ true);
+ mNoWidgetsView = findViewById(R.id.no_widgets_text);
+ mSearchAndRecommendationViewHolder = new SearchAndRecommendationViewHolder(
+ findViewById(R.id.search_and_recommendations_container));
+ TopRoundedCornerView.LayoutParams layoutParams =
+ (TopRoundedCornerView.LayoutParams)
+ mSearchAndRecommendationViewHolder.mContainer.getLayoutParams();
+ layoutParams.bottomMargin = mSearchAndRecommendationContainerBottomMargin;
+ mSearchAndRecommendationViewHolder.mContainer.setLayoutParams(layoutParams);
+ mSearchAndRecommendationsScrollController = new SearchAndRecommendationsScrollController(
+ mHasWorkProfile,
+ mTabsHeight,
+ mSearchAndRecommendationViewHolder,
+ findViewById(R.id.primary_widgets_list_view),
+ mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null,
+ findViewById(R.id.search_widgets_list_view),
+ mTabsView,
+ mViewPager,
+ mNoWidgetsView);
+ fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController);
+
+
+ onRecommendedWidgetsBound();
+ onWidgetsBound();
+
+ mSearchAndRecommendationViewHolder.mSearchBar.initialize(
+ mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
+
+ setUpEducationViewsIfNeeded();
+ }
+
+ @Override
+ public void onActivePageChanged(int currentActivePage) {
+ AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
+ WidgetsRecyclerView currentRecyclerView =
+ mAdapters.get(currentActivePage).mWidgetsRecyclerView;
+
+ updateRecyclerViewVisibility(currentAdapterHolder);
+ attachScrollbarToRecyclerView(currentRecyclerView);
+ }
+
+ private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
+ recyclerView.bindFastScrollbar();
+ if (mCurrentWidgetsRecyclerView != recyclerView) {
+ // Only reset the scroll position & expanded apps if the currently shown recycler view
+ // has been updated.
+ reset();
+ resetExpandedHeaders();
+ mCurrentWidgetsRecyclerView = recyclerView;
+ mSearchAndRecommendationsScrollController.setCurrentRecyclerView(recyclerView);
+ }
+ }
+
+ private void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
+ boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.getItemCount() > 0;
+ adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
+
+ mNoWidgetsView.setText(
+ adapterHolder.mAdapterType == AdapterHolder.SEARCH
+ ? R.string.no_search_results
+ : R.string.no_widgets_available);
+ mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE);
+ }
+
+ private void reset() {
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
+ if (mHasWorkProfile) {
+ mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
+ }
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
+ mSearchAndRecommendationsScrollController.reset(/* animate= */ true);
+ }
+
+ @VisibleForTesting
+ public WidgetsRecyclerView getRecyclerView() {
+ if (mIsInSearchMode) {
+ return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
+ }
+ if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
+ return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
+ }
+ return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView;
+ }
+
+ @Override
+ protected Pair<View, String> getAccessibilityTarget() {
+ return Pair.create(getRecyclerView(), getContext().getString(
+ mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mActivityContext.getAppWidgetHost().addProviderChangeListener(this);
+ notifyWidgetProvidersChanged();
+ onRecommendedWidgetsBound();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mActivityContext.getAppWidgetHost().removeProviderChangeListener(this);
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView
+ .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
+ if (mHasWorkProfile) {
+ mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView
+ .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
+ }
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ super.setInsets(insets);
+
+ setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, insets.bottom);
+ setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, insets.bottom);
+ if (mHasWorkProfile) {
+ setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, insets.bottom);
+ }
+ mSearchAndRecommendationsScrollController.updateBottomInset(insets.bottom);
+ if (insets.bottom > 0) {
+ setupNavBarColor();
+ } else {
+ clearNavBarColor();
+ }
+
+ requestLayout();
+ }
+
+ private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) {
+ recyclerView.setPadding(
+ recyclerView.getPaddingLeft(),
+ recyclerView.getPaddingTop(),
+ recyclerView.getPaddingRight(),
+ bottomPadding);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ doMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) {
+ doMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ if (updateMaxSpansPerRow()) {
+ doMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) {
+ doMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+
+ /** Returns {@code true} if the max spans have been updated. */
+ private boolean updateMaxSpansPerRow() {
+ if (getMeasuredWidth() == 0) return false;
+
+ int previousMaxSpansPerRow = mMaxSpansPerRow;
+ mMaxSpansPerRow = getMeasuredWidth()
+ / (mActivityContext.getDeviceProfile().cellWidthPx + mWidgetCellHorizontalPadding);
+
+ if (previousMaxSpansPerRow != mMaxSpansPerRow) {
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+ mMaxSpansPerRow);
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+ mMaxSpansPerRow);
+ if (mHasWorkProfile) {
+ mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+ mMaxSpansPerRow);
+ }
+ onRecommendedWidgetsBound();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = r - l;
+ int height = b - t;
+
+ // Content is laid out as center bottom aligned
+ int contentWidth = mContent.getMeasuredWidth();
+ int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
+ mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
+ contentLeft + contentWidth, height);
+
+ setTranslationShift(mTranslationShift);
+ }
+
+ @Override
+ public void notifyWidgetProvidersChanged() {
+ mActivityContext.refreshAndBindWidgetsForPackageUser(null);
+ }
+
+ @Override
+ public void onWidgetsBound() {
+ if (mIsInSearchMode) {
+ return;
+ }
+ List<WidgetsListBaseEntry> allWidgets =
+ mActivityContext.getPopupDataProvider().getAllWidgets();
+
+ AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
+ primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+
+ if (mHasWorkProfile) {
+ mViewPager.setVisibility(VISIBLE);
+ mTabsView.setVisibility(VISIBLE);
+ AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
+ workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+ onActivePageChanged(mViewPager.getCurrentPage());
+ } else {
+ updateRecyclerViewVisibility(primaryUserAdapterHolder);
+ }
+ // Update recommended widgets section so that it occupies appropriate space on screen to
+ // leave enough space for presence/absence of mNoWidgetsView.
+ boolean isNoWidgetsViewNeeded =
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.getItemCount() == 0
+ || (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK)
+ .mWidgetsListAdapter.getItemCount() == 0);
+ if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) {
+ mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded;
+ onRecommendedWidgetsBound();
+ }
+ }
+
+ @Override
+ public void enterSearchMode() {
+ if (mIsInSearchMode) return;
+ setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
+ attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
+ mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED);
+ }
+
+ @Override
+ public void exitSearchMode() {
+ if (!mIsInSearchMode) return;
+ onSearchResults(new ArrayList<>());
+ setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false);
+ if (mHasWorkProfile) {
+ mViewPager.snapToPage(AdapterHolder.PRIMARY);
+ }
+ attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView);
+
+ mSearchAndRecommendationsScrollController.updateMarginAndPadding();
+ }
+
+ @Override
+ public void onSearchResults(List<WidgetsListBaseEntry> entries) {
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries);
+ updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
+ }
+
+ private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
+ mIsInSearchMode = isInSearchMode;
+ if (isInSearchMode) {
+ mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.setVisibility(GONE);
+ if (mHasWorkProfile) {
+ mViewPager.setVisibility(GONE);
+ mTabsView.setVisibility(GONE);
+ } else {
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE);
+ }
+ updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
+ // Hide no search results view to prevent it from flashing on enter search.
+ mNoWidgetsView.setVisibility(GONE);
+ } else {
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
+ // Visibility of recommended widgets, recycler views and headers are handled in methods
+ // below.
+ onRecommendedWidgetsBound();
+ onWidgetsBound();
+ }
+ }
+
+ private void resetExpandedHeaders() {
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader();
+ mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader();
+ }
+
+ @Override
+ public void onRecommendedWidgetsBound() {
+ if (mIsInSearchMode) {
+ return;
+ }
+ List<WidgetItem> recommendedWidgets =
+ mActivityContext.getPopupDataProvider().getRecommendedWidgets();
+ WidgetsRecommendationTableLayout table =
+ mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable;
+ if (recommendedWidgets.size() > 0) {
+ float noWidgetsViewHeight = 0;
+ if (mIsNoWidgetsViewNeeded) {
+ // Make sure recommended section leaves enough space for noWidgetsView.
+ Rect noWidgetsViewTextBounds = new Rect();
+ mNoWidgetsView.getPaint()
+ .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
+ mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
+ noWidgetsViewHeight = noWidgetsViewTextBounds.height();
+ }
+ doMeasure(
+ makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
+ MeasureSpec.EXACTLY),
+ makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
+ MeasureSpec.EXACTLY));
+ float maxTableHeight = (mContent.getMeasuredHeight()
+ - mTabsHeight - mViewPagerTopPadding - getHeaderViewHeight()
+ - noWidgetsViewHeight) * RECOMMENDATION_TABLE_HEIGHT_RATIO;
+
+ List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
+ WidgetsTableUtils.groupWidgetItemsIntoTable(recommendedWidgets,
+ mMaxSpansPerRow);
+ table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight);
+ } else {
+ table.setVisibility(GONE);
+ }
+ }
+
+ private void open(boolean animate) {
+ if (animate) {
+ if (getPopupContainer().getInsets().bottom > 0) {
+ mContent.setAlpha(0);
+ setTranslationShift(VERTICAL_START_POSITION);
+ }
+ mOpenCloseAnimator.setValues(
+ PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+ mOpenCloseAnimator
+ .setDuration(DEFAULT_OPEN_DURATION)
+ .setInterpolator(AnimationUtils.loadInterpolator(
+ getContext(), android.R.interpolator.linear_out_slow_in));
+ mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mOpenCloseAnimator.removeListener(this);
+ }
+ });
+ post(() -> {
+ mOpenCloseAnimator.start();
+ mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
+ });
+ } else {
+ setTranslationShift(TRANSLATION_SHIFT_OPENED);
+ post(this::announceAccessibilityChanges);
+ }
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ handleClose(animate, DEFAULT_OPEN_DURATION);
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ // Disable swipe down when recycler view is scrolling
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mNoIntercept = false;
+ RecyclerViewFastScroller scroller = getRecyclerView().getScrollbar();
+ if (scroller.getThumbOffsetY() >= 0
+ && getPopupContainer().isEventOverView(scroller, ev)) {
+ mNoIntercept = true;
+ } else if (getPopupContainer().isEventOverView(mContent, ev)) {
+ mNoIntercept = !getRecyclerView().shouldContainerScroll(ev, getPopupContainer());
+ }
+
+ if (mSearchAndRecommendationViewHolder.mSearchBar.isSearchBarFocused()
+ && !getPopupContainer().isEventOverView(
+ mSearchAndRecommendationViewHolder.mSearchBarContainer, ev)) {
+ mSearchAndRecommendationViewHolder.mSearchBar.clearSearchBarFocus();
+ }
+ }
+ return super.onControllerInterceptTouchEvent(ev);
+ }
+
+ /** Shows the {@link WidgetsFullSheet} on the launcher. */
+ public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
+ WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
+ .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
+ sheet.attachToContainer();
+ sheet.mIsOpen = true;
+ sheet.open(animate);
+ return sheet;
+ }
+
+ /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */
+ @VisibleForTesting
+ public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
+ return launcher.findViewById(R.id.primary_widgets_list_view);
+ }
+
+ @Override
+ public void addHintCloseAnim(
+ float distanceToMove, Interpolator interpolator, PendingAnimation target) {
+ target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
+ target.setViewAlpha(getRecyclerView(), 0.5f, interpolator);
+ }
+
+ @Override
+ protected void onCloseComplete() {
+ super.onCloseComplete();
+ removeCallbacks(mShowEducationTipTask);
+ if (mLatestEducationalTip != null) {
+ mLatestEducationalTip.close(false);
+ }
+ AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
+ }
+
+ @Override
+ public int getHeaderViewHeight() {
+ return measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mCollapseHandle)
+ + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mHeaderTitle)
+ + measureHeightWithVerticalMargins(
+ (View) mSearchAndRecommendationViewHolder.mSearchBarContainer);
+ }
+
+ /** private the height, in pixel, + the vertical margins of a given view. */
+ private static int measureHeightWithVerticalMargins(View view) {
+ if (view.getVisibility() != VISIBLE) {
+ return 0;
+ }
+ MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
+ return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+ + marginLayoutParams.topMargin;
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mIsInSearchMode) {
+ mSearchAndRecommendationViewHolder.mSearchBar.reset();
+ }
+ }
+
+ @Override
+ public boolean onBackPressed() {
+ if (mIsInSearchMode) {
+ mSearchAndRecommendationViewHolder.mSearchBar.reset();
+ return true;
+ }
+ return super.onBackPressed();
+ }
+
+ @Override
+ public void onDragStart(boolean start, float startDisplacement) {
+ super.onDragStart(start, startDisplacement);
+ getWindowInsetsController().hide(WindowInsets.Type.ime());
+ }
+
+ @Nullable private View getViewToShowEducationTip() {
+ if (mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getVisibility() == VISIBLE
+ && mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getChildCount() > 0
+ ) {
+ return ((ViewGroup) mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable
+ .getChildAt(0)).getChildAt(0);
+ }
+
+ AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
+ ? AdapterHolder.SEARCH
+ : mViewPager == null
+ ? AdapterHolder.PRIMARY
+ : mViewPager.getCurrentPage());
+ WidgetsRowViewHolder viewHolderForTip =
+ (WidgetsRowViewHolder) IntStream.range(
+ 0, adapterHolder.mWidgetsListAdapter.getItemCount())
+ .mapToObj(adapterHolder.mWidgetsRecyclerView::
+ findViewHolderForAdapterPosition)
+ .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder)
+ .findFirst()
+ .orElse(null);
+ if (viewHolderForTip != null) {
+ return ((ViewGroup) viewHolderForTip.mTableContainer.getChildAt(0)).getChildAt(0);
+ }
+
+ return null;
+ }
+
+ /** Shows education dialog for widgets. */
+ private WidgetsEduView showEducationDialog() {
+ mActivityContext.getSharedPrefs().edit()
+ .putBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, true).apply();
+ return WidgetsEduView.showEducationDialog(mActivityContext);
+ }
+
+ /** Returns {@code true} if education dialog has previously been shown. */
+ protected boolean hasSeenEducationDialog() {
+ return mActivityContext.getSharedPrefs()
+ .getBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, false)
+ || Utilities.IS_RUNNING_IN_TEST_HARNESS;
+ }
+
+ private void setUpEducationViewsIfNeeded() {
+ if (!hasSeenEducationDialog()) {
+ postDelayed(() -> {
+ WidgetsEduView eduDialog = showEducationDialog();
+ eduDialog.addOnCloseListener(() -> {
+ if (!hasSeenEducationTip()) {
+ addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ // Call #requestLayout() to trigger layout change listener in order to show
+ // arrow tip immediately if there is a widget to show it on.
+ requestLayout();
+ }
+ });
+ }, EDUCATION_DIALOG_DELAY_MS);
+ } else if (!hasSeenEducationTip()) {
+ addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+ }
+ }
+
+ /** A holder class for holding adapters & their corresponding recycler view. */
+ private final class AdapterHolder {
+ static final int PRIMARY = 0;
+ static final int WORK = 1;
+ static final int SEARCH = 2;
+
+ private final int mAdapterType;
+ private final WidgetsListAdapter mWidgetsListAdapter;
+ private final DefaultItemAnimator mWidgetsListItemAnimator;
+
+ private WidgetsRecyclerView mWidgetsRecyclerView;
+
+ AdapterHolder(int adapterType) {
+ mAdapterType = adapterType;
+
+ Context context = getContext();
+ LauncherAppState apps = LauncherAppState.getInstance(context);
+ mWidgetsListAdapter = new WidgetsListAdapter(
+ context,
+ LayoutInflater.from(context),
+ apps.getWidgetCache(),
+ apps.getIconCache(),
+ /* iconClickListener= */ WidgetsFullSheet.this,
+ /* iconLongClickListener= */ WidgetsFullSheet.this);
+ mWidgetsListAdapter.setHasStableIds(true);
+ switch (mAdapterType) {
+ case PRIMARY:
+ mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
+ break;
+ case WORK:
+ mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
+ break;
+ default:
+ break;
+ }
+ mWidgetsListItemAnimator = new DefaultItemAnimator();
+ // Disable change animations because it disrupts the item focus upon adapter item
+ // change.
+ mWidgetsListItemAnimator.setSupportsChangeAnimations(false);
+ }
+
+ void setup(WidgetsRecyclerView recyclerView) {
+ mWidgetsRecyclerView = recyclerView;
+ mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
+ mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator);
+ mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
+ mWidgetsRecyclerView.setEdgeEffectFactory(
+ ((TopRoundedCornerView) mContent).createEdgeEffectFactory());
+ // Recycler view binds to fast scroller when it is attached to screen. Make sure
+ // search recycler view is bound to fast scroller if user is in search mode at the time
+ // of attachment.
+ if (mAdapterType == PRIMARY || mAdapterType == WORK) {
+ mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode);
+ }
+ mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView);
+ mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow);
+ }
+ }
+
+ final class SearchAndRecommendationViewHolder {
+ final SearchAndRecommendationsView mContainer;
+ final View mCollapseHandle;
+ final View mSearchBarContainer;
+ final WidgetsSearchBar mSearchBar;
+ final TextView mHeaderTitle;
+ final WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
+
+ SearchAndRecommendationViewHolder(
+ SearchAndRecommendationsView searchAndRecommendationContainer) {
+ mContainer = searchAndRecommendationContainer;
+ mCollapseHandle = mContainer.findViewById(R.id.collapse_handle);
+ mSearchBarContainer = mContainer.findViewById(R.id.search_bar_container);
+ mSearchBar = mContainer.findViewById(R.id.widgets_search_bar);
+ mHeaderTitle = mContainer.findViewById(R.id.title);
+ mRecommendedWidgetsTable = mContainer.findViewById(R.id.recommended_widget_table);
+ mRecommendedWidgetsTable.setWidgetCellOnTouchListener((view, event) -> {
+ getRecyclerView().onTouchEvent(event);
+ return false;
+ });
+ mRecommendedWidgetsTable.setWidgetCellLongClickListener(WidgetsFullSheet.this);
+ mRecommendedWidgetsTable.setWidgetCellOnClickListener(WidgetsFullSheet.this);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
new file mode 100644
index 0000000..1125b82
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_APP_EXPANDED;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Process;
+import android.util.Log;
+import android.util.Size;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.TableRow;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.LayoutParams;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.LabelComparator;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+import com.android.launcher3.widget.util.WidgetSizes;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalInt;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * Recycler view adapter for the widget tray.
+ *
+ * <p>This adapter supports view binding of subclasses of {@link WidgetsListBaseEntry}. There are 2
+ * subclasses: {@link WidgetsListHeader} & {@link WidgetsListContentEntry}.
+ * {@link WidgetsListHeader} entries are always visible in the recycler view. At most one
+ * {@link WidgetsListContentEntry} is shown in the recycler view at any time. Clicking a
+ * {@link WidgetsListHeader} will result in expanding / collapsing a corresponding
+ * {@link WidgetsListContentEntry} of the same app.
+ */
+public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderClickListener {
+
+ private static final String TAG = "WidgetsListAdapter";
+ private static final boolean DEBUG = false;
+
+ /** Uniquely identifies widgets list view type within the app. */
+ private static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
+ private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
+ private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;
+
+ private final Context mContext;
+ private final Launcher mLauncher;
+ private final CachingWidgetPreviewLoader mCachingPreviewLoader;
+ private final WidgetsDiffReporter mDiffReporter;
+ private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
+ private final WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
+ private final WidgetListBaseRowEntryComparator mRowComparator =
+ new WidgetListBaseRowEntryComparator();
+
+ private List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
+ private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
+ @Nullable private PackageUserKey mWidgetsContentVisiblePackageUserKey = null;
+
+ private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
+ entry instanceof WidgetsListHeaderEntry
+ || entry instanceof WidgetsListSearchHeaderEntry
+ || new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+ .equals(mWidgetsContentVisiblePackageUserKey);
+ @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
+ @Nullable private RecyclerView mRecyclerView;
+ @Nullable private PackageUserKey mPendingClickHeader;
+ private final int mShortcutPreviewPadding;
+ private final int mSpacingBetweenEntries;
+ private int mMaxSpanSize = 4;
+
+ private final WidgetPreviewLoadedCallback mPreviewLoadedCallback =
+ ignored -> updateVisibleEntries();
+
+ public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
+ DatabaseWidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
+ OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
+ mContext = context;
+ mLauncher = Launcher.getLauncher(context);
+ mCachingPreviewLoader = new CachingWidgetPreviewLoader(widgetPreviewLoader);
+ mDiffReporter = new WidgetsDiffReporter(iconCache, this);
+ WidgetsListDrawableFactory listDrawableFactory = new WidgetsListDrawableFactory(context);
+ mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(
+ layoutInflater, iconClickListener, iconLongClickListener,
+ mCachingPreviewLoader, listDrawableFactory, /* listAdapter= */ this);
+ mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
+ mViewHolderBinders.put(
+ VIEW_TYPE_WIDGETS_HEADER,
+ new WidgetsListHeaderViewHolderBinder(
+ layoutInflater,
+ /* onHeaderClickListener= */ this,
+ listDrawableFactory,
+ /* listAdapter= */ this));
+ mViewHolderBinders.put(
+ VIEW_TYPE_WIDGETS_SEARCH_HEADER,
+ new WidgetsListSearchHeaderViewHolderBinder(
+ layoutInflater,
+ /* onHeaderClickListener= */ this,
+ listDrawableFactory,
+ /* listAdapter= */ this));
+ mShortcutPreviewPadding =
+ 2 * context.getResources()
+ .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
+ mSpacingBetweenEntries =
+ context.getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+
+ mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
+ @Override
+ public void getItemOffsets(
+ @NonNull Rect outRect,
+ @NonNull View view,
+ @NonNull RecyclerView parent,
+ @NonNull RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ int position = ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
+ boolean isHeader =
+ view.getTag(R.id.tag_widget_entry) instanceof WidgetsListBaseEntry.Header;
+ outRect.top += position > 0 && isHeader ? mSpacingBetweenEntries : 0;
+ }
+ });
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = null;
+ }
+
+ public void setFilter(Predicate<WidgetsListBaseEntry> filter) {
+ mFilter = filter;
+ }
+
+ /**
+ * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}.
+ *
+ * @see WidgetCell#setApplyBitmapDeferred(boolean)
+ */
+ public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
+ mWidgetsListTableViewHolderBinder.setApplyBitmapDeferred(isDeferred);
+
+ for (int i = rv.getChildCount() - 1; i >= 0; i--) {
+ ViewHolder viewHolder = rv.getChildViewHolder(rv.getChildAt(i));
+ if (viewHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
+ WidgetsRowViewHolder holder = (WidgetsRowViewHolder) viewHolder;
+ for (int j = holder.mTableContainer.getChildCount() - 1; j >= 0; j--) {
+ TableRow row = (TableRow) holder.mTableContainer.getChildAt(j);
+ for (int k = row.getChildCount() - 1; k >= 0; k--) {
+ ((WidgetCell) row.getChildAt(k)).setApplyBitmapDeferred(isDeferred);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mVisibleEntries.size();
+ }
+
+ /** Returns all items that will be drawn in a recycler view. */
+ public List<WidgetsListBaseEntry> getItems() {
+ return mVisibleEntries;
+ }
+
+ /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
+ public String getSectionName(int pos) {
+ return mVisibleEntries.get(pos).mTitleSectionName;
+ }
+
+ /** Updates the widget list based on {@code tempEntries}. */
+ public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
+ mCachingPreviewLoader.clearAll();
+ mAllEntries = tempEntries.stream().sorted(mRowComparator)
+ .collect(Collectors.toList());
+ if (shouldClearVisibleEntries()) {
+ mVisibleEntries.clear();
+ }
+ updateVisibleEntries();
+ }
+
+ /** Updates the widget list based on {@code searchResults}. */
+ public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
+ // Forget the expanded package every time widget list is refreshed in search mode.
+ mWidgetsContentVisiblePackageUserKey = null;
+ cancelLoadingPreviews();
+ setWidgets(searchResults);
+ }
+
+ private void updateVisibleEntries() {
+ // If not all previews are ready, then defer this update and try again after the preview
+ // loads.
+ if (!ensureAllPreviewsReady()) return;
+
+ // Get the current top of the header with the matching key before adjusting the visible
+ // entries.
+ OptionalInt previousPositionForPackageUserKey =
+ getPositionForPackageUserKey(mPendingClickHeader);
+ OptionalInt topForPackageUserKey =
+ getOffsetForPosition(previousPositionForPackageUserKey);
+
+ List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
+ .filter(entry -> (mFilter == null || mFilter.test(entry))
+ && mHeaderAndSelectedContentFilter.test(entry))
+ .map(entry -> {
+ if (entry instanceof WidgetsListBaseEntry.Header<?>
+ && matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
+ // Adjust the original entries to expand headers for the selected content.
+ return ((WidgetsListBaseEntry.Header<?>) entry).withWidgetListShown();
+ } else if (entry instanceof WidgetsListContentEntry) {
+ // Adjust the original content entries to accommodate for the current
+ // maxSpanSize.
+ return ((WidgetsListContentEntry) entry).withMaxSpanSize(mMaxSpanSize);
+ }
+ return entry;
+ })
+ .collect(Collectors.toList());
+
+ mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
+
+ if (mPendingClickHeader != null) {
+ // Get the position for the clicked header after adjusting the visible entries. The
+ // position may have changed if another header had previously been expanded.
+ OptionalInt positionForPackageUserKey =
+ getPositionForPackageUserKey(mPendingClickHeader);
+ scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
+ mPendingClickHeader = null;
+ }
+ }
+
+ /**
+ * Checks that all preview images are loaded and starts loading for those that aren't ready.
+ *
+ * @return true if all previews are ready and the data can be updated, false otherwise.
+ */
+ private boolean ensureAllPreviewsReady() {
+ boolean allReady = true;
+ BaseActivity activity = BaseActivity.fromContext(mContext);
+ for (WidgetsListBaseEntry entry : mAllEntries) {
+ if (!(entry instanceof WidgetsListContentEntry)) continue;
+
+ WidgetsListContentEntry contentEntry = (WidgetsListContentEntry) entry;
+ if (!matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
+ // If the entry isn't visible, clear any loaded previews.
+ mCachingPreviewLoader.clearPreviews(contentEntry.mWidgets);
+ continue;
+ }
+
+ for (int i = 0; i < entry.mWidgets.size(); i++) {
+ WidgetItem widgetItem = entry.mWidgets.get(i);
+ DeviceProfile deviceProfile = activity.getDeviceProfile();
+ Size widgetSize = WidgetSizes.getWidgetItemSizePx(mContext, deviceProfile,
+ widgetItem);
+ if (widgetItem.isShortcut()) {
+ widgetSize =
+ new Size(
+ widgetSize.getWidth() + mShortcutPreviewPadding,
+ widgetSize.getHeight() + mShortcutPreviewPadding);
+ }
+
+ if (widgetItem.hasPreviewLayout()
+ || mCachingPreviewLoader.isPreviewLoaded(widgetItem, widgetSize)) {
+ // The widget is ready if it can be rendered with a preview layout or if its
+ // preview bitmap is in the cache.
+ continue;
+ }
+
+ // If we've reached this point, we should load the preview for the widget.
+ allReady = false;
+ mCachingPreviewLoader.loadPreview(
+ activity,
+ widgetItem,
+ widgetSize,
+ mPreviewLoadedCallback);
+ }
+ }
+ return allReady;
+ }
+
+ /** Returns whether {@code entry} matches {@code key}. */
+ private static boolean isHeaderForPackageUserKey(
+ @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
+ return entry instanceof WidgetsListBaseEntry.Header && matchesKey(entry, key);
+ }
+
+ private static boolean matchesKey(
+ @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
+ if (key == null) return false;
+ return entry.mPkgItem.packageName.equals(key.mPackageName)
+ && entry.mPkgItem.user.equals(key.mUser);
+ }
+
+ /**
+ * Resets any expanded widget header.
+ */
+ public void resetExpandedHeader() {
+ if (mWidgetsContentVisiblePackageUserKey != null) {
+ mWidgetsContentVisiblePackageUserKey = null;
+ cancelLoadingPreviews();
+ updateVisibleEntries();
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int pos) {
+ ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
+ WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
+ viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), pos);
+ holder.itemView.setTag(R.id.tag_widget_entry, entry);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (DEBUG) {
+ Log.v(TAG, "\nonCreateViewHolder");
+ }
+
+ return mViewHolderBinders.get(viewType).newViewHolder(parent);
+ }
+
+ @Override
+ public void onViewRecycled(ViewHolder holder) {
+ mViewHolderBinders.get(holder.getItemViewType()).unbindViewHolder(holder);
+ }
+
+ @Override
+ public boolean onFailedToRecycleView(ViewHolder holder) {
+ // If child views are animating, then the RecyclerView may choose not to recycle the view,
+ // causing extraneous onCreateViewHolder() calls. It is safe in this case to continue
+ // recycling this view, and take care in onViewRecycled() to cancel any existing
+ // animations.
+ return true;
+ }
+
+ @Override
+ public long getItemId(int pos) {
+ return Arrays.hashCode(new Object[]{
+ mVisibleEntries.get(pos).mPkgItem.hashCode(),
+ getItemViewType(pos)});
+ }
+
+ @Override
+ public int getItemViewType(int pos) {
+ WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
+ if (entry instanceof WidgetsListContentEntry) {
+ return VIEW_TYPE_WIDGETS_LIST;
+ } else if (entry instanceof WidgetsListHeaderEntry) {
+ return VIEW_TYPE_WIDGETS_HEADER;
+ } else if (entry instanceof WidgetsListSearchHeaderEntry) {
+ return VIEW_TYPE_WIDGETS_SEARCH_HEADER;
+ }
+ throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
+ }
+
+ @Override
+ public void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey) {
+ // Ignore invalid clicks, such as collapsing a package that isn't currently expanded.
+ if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;
+
+ cancelLoadingPreviews();
+
+ if (showWidgets) {
+ mWidgetsContentVisiblePackageUserKey = packageUserKey;
+ mLauncher.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_APP_EXPANDED);
+ } else {
+ mWidgetsContentVisiblePackageUserKey = null;
+ }
+
+ // Store the header that was clicked so that its position will be maintained the next time
+ // we update the entries.
+ mPendingClickHeader = packageUserKey;
+
+ updateVisibleEntries();
+ }
+
+ private void cancelLoadingPreviews() {
+ mCachingPreviewLoader.clearAll();
+ }
+
+ /** Returns the position of the currently expanded header, or empty if it's not present. */
+ public OptionalInt getSelectedHeaderPosition() {
+ if (mWidgetsContentVisiblePackageUserKey == null) return OptionalInt.empty();
+ return getPositionForPackageUserKey(mWidgetsContentVisiblePackageUserKey);
+ }
+
+ /**
+ * Returns the position of {@code key} in {@link #mVisibleEntries}, or empty if it's not
+ * present.
+ */
+ @NonNull
+ private OptionalInt getPositionForPackageUserKey(@Nullable PackageUserKey key) {
+ return IntStream.range(0, mVisibleEntries.size())
+ .filter(index -> isHeaderForPackageUserKey(mVisibleEntries.get(index), key))
+ .findFirst();
+ }
+
+ /**
+ * Returns the top of {@code positionOptional} in the recycler view, or empty if its view
+ * can't be found for any reason, including the position not being currently visible. The
+ * returned value does not include the top padding of the recycler view.
+ */
+ private OptionalInt getOffsetForPosition(OptionalInt positionOptional) {
+ if (!positionOptional.isPresent() || mRecyclerView == null) return OptionalInt.empty();
+
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager == null) return OptionalInt.empty();
+
+ View view = layoutManager.findViewByPosition(positionOptional.getAsInt());
+ if (view == null) return OptionalInt.empty();
+
+ return OptionalInt.of(layoutManager.getDecoratedTop(view));
+ }
+
+ /**
+ * Scrolls to the selected header position with the provided offset. LinearLayoutManager
+ * scrolls the minimum distance necessary, so this will keep the selected header in place during
+ * clicks, without interrupting the animation.
+ *
+ * @param positionOptional The position too scroll to. No scrolling will be done if empty.
+ * @param offsetOptional The offset from the top to maintain. If empty, then the list will
+ * scroll to the top of the position.
+ */
+ private void scrollToPositionAndMaintainOffset(
+ OptionalInt positionOptional,
+ OptionalInt offsetOptional) {
+ if (!positionOptional.isPresent() || mRecyclerView == null) return;
+ int position = positionOptional.getAsInt();
+
+ LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
+ if (layoutManager == null) return;
+
+ if (position == mVisibleEntries.size() - 2
+ && mVisibleEntries.get(mVisibleEntries.size() - 1)
+ instanceof WidgetsListContentEntry) {
+ // If the selected header is in the last position and its content is showing, then
+ // scroll to the final position so the last list of widgets will show.
+ layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
+ return;
+ }
+
+ // Scroll to the header view's current offset, accounting for the recycler view's padding.
+ // If the header view couldn't be found, then it will appear at the top of the list.
+ layoutManager.scrollToPositionWithOffset(
+ position,
+ offsetOptional.orElse(0) - mRecyclerView.getPaddingTop());
+ }
+
+ /**
+ * Sets the max horizontal span in cells that is allowed for grouping more than one widget in a
+ * table row.
+ */
+ public void setMaxHorizontalSpansPerRow(int maxHorizontalSpans) {
+ mMaxSpanSize = maxHorizontalSpans;
+ updateVisibleEntries();
+ }
+
+ /**
+ * Returns {@code true} if there is a change in {@link #mAllEntries} that results in an
+ * invalidation of {@link #mVisibleEntries}. e.g. there is change in the device language.
+ */
+ private boolean shouldClearVisibleEntries() {
+ Map<PackageUserKey, PackageItemInfo> packagesInfo =
+ mAllEntries.stream()
+ .filter(entry -> entry instanceof WidgetsListHeaderEntry)
+ .map(entry -> entry.mPkgItem)
+ .collect(Collectors.toMap(
+ entry -> new PackageUserKey(entry.packageName, entry.user),
+ entry -> entry));
+ for (WidgetsListBaseEntry visibleEntry: mVisibleEntries) {
+ PackageUserKey key = new PackageUserKey(visibleEntry.mPkgItem.packageName,
+ visibleEntry.mPkgItem.user);
+ PackageItemInfo packageItemInfo = packagesInfo.get(key);
+ if (packageItemInfo != null
+ && !visibleEntry.mPkgItem.title.equals(packageItemInfo.title)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Comparator for sorting WidgetListRowEntry based on package title. */
+ public static class WidgetListBaseRowEntryComparator implements
+ Comparator<WidgetsListBaseEntry> {
+
+ private final LabelComparator mComparator = new LabelComparator();
+
+ @Override
+ public int compare(WidgetsListBaseEntry a, WidgetsListBaseEntry b) {
+ int i = mComparator.compare(a.mPkgItem.title.toString(), b.mPkgItem.title.toString());
+ if (i != 0) {
+ return i;
+ }
+ // Prioritize entries from current user over other users if the entries are same.
+ if (a.mPkgItem.user.equals(b.mPkgItem.user)) return 0;
+ if (a.mPkgItem.user.equals(Process.myUserHandle())) return -1;
+ return 1;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListDrawableFactory.java b/src/com/android/launcher3/widget/picker/WidgetsListDrawableFactory.java
new file mode 100644
index 0000000..c61e3a4
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListDrawableFactory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.FIRST;
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.FIRST_EXPANDED;
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.LAST;
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE;
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE_EXPANDED;
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.SINGLE;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.RippleDrawable;
+import android.graphics.drawable.StateListDrawable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+/** Factory for creating drawables to use as background for list elements. */
+final class WidgetsListDrawableFactory {
+
+ private final float mTopBottomCornerRadius;
+ private final float mMiddleCornerRadius;
+ private final ColorStateList mSurfaceColor;
+ private final ColorStateList mRippleColor;
+
+ WidgetsListDrawableFactory(Context context) {
+ Resources res = context.getResources();
+ mTopBottomCornerRadius = res.getDimension(R.dimen.widget_list_top_bottom_corner_radius);
+ mMiddleCornerRadius = res.getDimension(R.dimen.widget_list_content_corner_radius);
+ mSurfaceColor = context.getColorStateList(R.color.surface);
+ mRippleColor = ColorStateList.valueOf(
+ Themes.getAttrColor(context, android.R.attr.colorControlHighlight));
+ }
+
+ /**
+ * Creates a drawable for widget header list items. This drawable supports all positions
+ * in {@link WidgetsListDrawableState}.
+ */
+ Drawable createHeaderBackgroundDrawable() {
+ StateListDrawable stateList = new StateListDrawable();
+ stateList.addState(
+ SINGLE.mStateSet,
+ createRoundedRectDrawable(mTopBottomCornerRadius, mTopBottomCornerRadius));
+ stateList.addState(
+ FIRST_EXPANDED.mStateSet,
+ createRoundedRectDrawable(mTopBottomCornerRadius, 0));
+ stateList.addState(
+ FIRST.mStateSet,
+ createRoundedRectDrawable(mTopBottomCornerRadius, mMiddleCornerRadius));
+ stateList.addState(
+ MIDDLE_EXPANDED.mStateSet,
+ createRoundedRectDrawable(mMiddleCornerRadius, 0));
+ stateList.addState(
+ MIDDLE.mStateSet,
+ createRoundedRectDrawable(mMiddleCornerRadius, mMiddleCornerRadius));
+ stateList.addState(
+ LAST.mStateSet,
+ createRoundedRectDrawable(mMiddleCornerRadius, mTopBottomCornerRadius));
+ return new RippleDrawable(mRippleColor, /* content= */ stateList, /* mask= */ stateList);
+ }
+
+ /**
+ * Creates a drawable for widget content list items. This state list supports the middle and
+ * last states.
+ */
+ Drawable createContentBackgroundDrawable() {
+ StateListDrawable stateList = new StateListDrawable();
+ stateList.addState(
+ MIDDLE.mStateSet,
+ createRoundedRectDrawable(0, mMiddleCornerRadius));
+ stateList.addState(
+ LAST.mStateSet,
+ createRoundedRectDrawable(0, mTopBottomCornerRadius));
+ return new RippleDrawable(mRippleColor, /* content= */ stateList, /* mask= */ stateList);
+ }
+
+ /** Creates a rounded-rect drawable with the specified radii. */
+ private Drawable createRoundedRectDrawable(float topRadius, float bottomRadius) {
+ GradientDrawable backgroundMask = new GradientDrawable();
+ backgroundMask.setColor(mSurfaceColor);
+ backgroundMask.setShape(GradientDrawable.RECTANGLE);
+ backgroundMask.setCornerRadii(
+ new float[]{
+ topRadius,
+ topRadius,
+ topRadius,
+ topRadius,
+ bottomRadius,
+ bottomRadius,
+ bottomRadius,
+ bottomRadius
+ });
+ return backgroundMask;
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListDrawableState.java b/src/com/android/launcher3/widget/picker/WidgetsListDrawableState.java
new file mode 100644
index 0000000..94f292b
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListDrawableState.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+/**
+ * Different possible list position states for an item in the widgets list to have. Note that only
+ * headers use the expanded state.
+ */
+enum WidgetsListDrawableState {
+ FIRST(new int[]{android.R.attr.state_first}),
+ FIRST_EXPANDED(new int[]{android.R.attr.state_first, android.R.attr.state_expanded}),
+ MIDDLE(new int[]{android.R.attr.state_middle}),
+ MIDDLE_EXPANDED(new int[]{android.R.attr.state_middle, android.R.attr.state_expanded}),
+ LAST(new int[]{android.R.attr.state_last}),
+ SINGLE(new int[]{android.R.attr.state_single});
+
+ final int[] mStateSet;
+
+ WidgetsListDrawableState(int[] stateSet) {
+ mStateSet = stateSet;
+ }
+
+ static WidgetsListDrawableState obtain(boolean isFirst, boolean isLast, boolean isExpanded) {
+ if (isFirst && isLast) return SINGLE;
+ if (isFirst && isExpanded) return FIRST_EXPANDED;
+ if (isFirst) return FIRST;
+ if (isLast) return LAST;
+ if (isExpanded) return MIDDLE_EXPANDED;
+ return MIDDLE;
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
new file mode 100644
index 0000000..ef2adbb
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.icons.PlaceHolderIconDrawable;
+import com.android.launcher3.icons.cache.HandlerRunnable;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import java.util.stream.Collectors;
+
+/**
+ * A UI represents a header of an app shown in the full widgets tray.
+ *
+ * It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox
+ * which indicates if the widgets content view underneath this header should be shown.
+ */
+public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver {
+
+ private boolean mEnableIconUpdateAnimation = false;
+
+ @Nullable private HandlerRunnable mIconLoadRequest;
+ @Nullable private Drawable mIconDrawable;
+ private final int mIconSize;
+
+ private ImageView mAppIcon;
+ private TextView mTitle;
+ private TextView mSubtitle;
+
+ private CheckBox mExpandToggle;
+ private boolean mIsExpanded = false;
+ @Nullable private WidgetsListDrawableState mListDrawableState;
+
+ public WidgetsListHeader(Context context) {
+ this(context, /* attrs= */ null);
+ }
+
+ public WidgetsListHeader(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, /* defStyle= */ 0);
+ }
+
+ public WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ ActivityContext activity = ActivityContext.lookupContext(context);
+ DeviceProfile grid = activity.getDeviceProfile();
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
+ mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
+ grid.iconSizePx);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mAppIcon = findViewById(R.id.app_icon);
+ mTitle = findViewById(R.id.app_title);
+ mSubtitle = findViewById(R.id.app_subtitle);
+ mExpandToggle = findViewById(R.id.toggle);
+ findViewById(R.id.app_container).setAccessibilityDelegate(new AccessibilityDelegate() {
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ if (mIsExpanded) {
+ info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
+ info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+ } else {
+ info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+ info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
+ }
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_EXPAND:
+ case AccessibilityNodeInfo.ACTION_COLLAPSE:
+ callOnClick();
+ return true;
+ default:
+ return super.performAccessibilityAction(host, action, args);
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets a {@link OnExpansionChangeListener} to get a callback when this app widgets section
+ * expands / collapses.
+ */
+ @UiThread
+ public void setOnExpandChangeListener(
+ @Nullable OnExpansionChangeListener onExpandChangeListener) {
+ // Use the entire touch area of this view to expand / collapse an app widgets section.
+ setOnClickListener(view -> {
+ setExpanded(!mIsExpanded);
+ if (onExpandChangeListener != null) {
+ onExpandChangeListener.onExpansionChange(mIsExpanded);
+ }
+ });
+ }
+
+ /** Sets the expand toggle to expand / collapse. */
+ @UiThread
+ public void setExpanded(boolean isExpanded) {
+ this.mIsExpanded = isExpanded;
+ mExpandToggle.setChecked(isExpanded);
+ }
+
+ /** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
+ @UiThread
+ public void setListDrawableState(WidgetsListDrawableState state) {
+ if (state == mListDrawableState) return;
+ this.mListDrawableState = state;
+ refreshDrawableState();
+ }
+
+ /** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */
+ @UiThread
+ public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) {
+ applyIconAndLabel(entry);
+ }
+
+ @UiThread
+ private void applyIconAndLabel(WidgetsListHeaderEntry entry) {
+ PackageItemInfo info = entry.mPkgItem;
+ setIcon(info);
+ setTitles(entry);
+ setExpanded(entry.isWidgetListShown());
+
+ super.setTag(info);
+
+ verifyHighRes();
+ }
+
+ private void setIcon(PackageItemInfo info) {
+ Drawable icon;
+ switch (info.category) {
+ case PackageItemInfo.CONVERSATIONS:
+ icon = getContext().getDrawable(R.drawable.ic_conversations_widget_category);
+ break;
+ default:
+ icon = info.newIcon(getContext());
+ }
+ applyDrawables(icon);
+ mIconDrawable = icon;
+ if (mIconDrawable != null) {
+ mIconDrawable.setVisible(
+ /* visible= */ getWindowVisibility() == VISIBLE && isShown(),
+ /* restart= */ false);
+ }
+ }
+
+ private void applyDrawables(Drawable icon) {
+ icon.setBounds(0, 0, mIconSize, mIconSize);
+
+ LinearLayout.LayoutParams layoutParams =
+ (LinearLayout.LayoutParams) mAppIcon.getLayoutParams();
+ layoutParams.width = mIconSize;
+ layoutParams.height = mIconSize;
+ mAppIcon.setLayoutParams(layoutParams);
+ mAppIcon.setImageDrawable(icon);
+
+ // If the current icon is a placeholder color, animate its update.
+ if (mIconDrawable != null
+ && mIconDrawable instanceof PlaceHolderIconDrawable
+ && mEnableIconUpdateAnimation) {
+ ((PlaceHolderIconDrawable) mIconDrawable).animateIconUpdate(icon);
+ }
+ }
+
+ private void setTitles(WidgetsListHeaderEntry entry) {
+ mTitle.setText(entry.mPkgItem.title);
+
+ Resources resources = getContext().getResources();
+ if (entry.widgetsCount == 0 && entry.shortcutsCount == 0) {
+ mSubtitle.setVisibility(GONE);
+ return;
+ }
+
+ String subtitle;
+ if (entry.widgetsCount > 0 && entry.shortcutsCount > 0) {
+ String widgetsCount = resources.getQuantityString(R.plurals.widgets_count,
+ entry.widgetsCount, entry.widgetsCount);
+ String shortcutsCount = resources.getQuantityString(R.plurals.shortcuts_count,
+ entry.shortcutsCount, entry.shortcutsCount);
+ subtitle = resources.getString(R.string.widgets_and_shortcuts_count, widgetsCount,
+ shortcutsCount);
+ } else if (entry.widgetsCount > 0) {
+ subtitle = resources.getQuantityString(R.plurals.widgets_count,
+ entry.widgetsCount, entry.widgetsCount);
+ } else {
+ subtitle = resources.getQuantityString(R.plurals.shortcuts_count,
+ entry.shortcutsCount, entry.shortcutsCount);
+ }
+ mSubtitle.setText(subtitle);
+ mSubtitle.setVisibility(VISIBLE);
+ }
+
+ /** Apply app icon, labels and tag using a generic {@link WidgetsListSearchHeaderEntry}. */
+ @UiThread
+ public void applyFromItemInfoWithIcon(WidgetsListSearchHeaderEntry entry) {
+ applyIconAndLabel(entry);
+ }
+
+ @UiThread
+ private void applyIconAndLabel(WidgetsListSearchHeaderEntry entry) {
+ PackageItemInfo info = entry.mPkgItem;
+ setIcon(info);
+ setTitles(entry);
+ setExpanded(entry.isWidgetListShown());
+
+ super.setTag(info);
+
+ verifyHighRes();
+ }
+
+ private void setTitles(WidgetsListSearchHeaderEntry entry) {
+ mTitle.setText(entry.mPkgItem.title);
+
+ mSubtitle.setText(entry.mWidgets.stream()
+ .map(item -> item.label).sorted().collect(Collectors.joining(", ")));
+ mSubtitle.setVisibility(VISIBLE);
+ }
+
+ @Override
+ public void reapplyItemInfo(ItemInfoWithIcon info) {
+ if (getTag() == info) {
+ mIconLoadRequest = null;
+ mEnableIconUpdateAnimation = true;
+
+ // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
+ info.bitmap.icon.prepareToDraw();
+
+ setIcon((PackageItemInfo) info);
+
+ mEnableIconUpdateAnimation = false;
+ }
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if (mListDrawableState == null) return super.onCreateDrawableState(extraSpace);
+ // Augment the state set from the super implementation with the custom states from
+ // mListDrawableState.
+ int[] drawableState =
+ super.onCreateDrawableState(extraSpace + mListDrawableState.mStateSet.length);
+ mergeDrawableStates(drawableState, mListDrawableState.mStateSet);
+ return drawableState;
+ }
+
+ /** Verifies that the current icon is high-res otherwise posts a request to load the icon. */
+ public void verifyHighRes() {
+ if (mIconLoadRequest != null) {
+ mIconLoadRequest.cancel();
+ mIconLoadRequest = null;
+ }
+ if (getTag() instanceof ItemInfoWithIcon) {
+ ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
+ if (info.usingLowResIcon()) {
+ mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
+ .updateIconInBackground(this, info);
+ }
+ }
+ }
+
+ /** A listener for the widget section expansion / collapse events. */
+ public interface OnExpansionChangeListener {
+ /** Notifies that the widget section is expanded or collapsed. */
+ void onExpansionChange(boolean isExpanded);
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java
new file mode 100644
index 0000000..d4e1b1c
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * A {@link ViewHolder} for {@link WidgetsListHeader} of an app, which renders the app icon, the app
+ * name, label and a button for showing / hiding widgets.
+ */
+public final class WidgetsListHeaderHolder extends ViewHolder {
+ final WidgetsListHeader mWidgetsListHeader;
+
+ public WidgetsListHeaderHolder(WidgetsListHeader view) {
+ super(view);
+
+ mWidgetsListHeader = view;
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
new file mode 100644
index 0000000..2f8f1ba
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+/**
+ * Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
+ */
+public final class WidgetsListHeaderViewHolderBinder implements
+ ViewHolderBinder<WidgetsListHeaderEntry, WidgetsListHeaderHolder> {
+ private final LayoutInflater mLayoutInflater;
+ private final OnHeaderClickListener mOnHeaderClickListener;
+ private final WidgetsListDrawableFactory mListDrawableFactory;
+ private final WidgetsListAdapter mWidgetsListAdapter;
+
+ public WidgetsListHeaderViewHolderBinder(LayoutInflater layoutInflater,
+ OnHeaderClickListener onHeaderClickListener,
+ WidgetsListDrawableFactory listDrawableFactory,
+ WidgetsListAdapter listAdapter) {
+ mLayoutInflater = layoutInflater;
+ mOnHeaderClickListener = onHeaderClickListener;
+ mListDrawableFactory = listDrawableFactory;
+ mWidgetsListAdapter = listAdapter;
+ }
+
+ @Override
+ public WidgetsListHeaderHolder newViewHolder(ViewGroup parent) {
+ WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+ R.layout.widgets_list_row_header, parent, false);
+ header.setBackground(mListDrawableFactory.createHeaderBackgroundDrawable());
+ return new WidgetsListHeaderHolder(header);
+ }
+
+ @Override
+ public void bindViewHolder(WidgetsListHeaderHolder viewHolder, WidgetsListHeaderEntry data,
+ int position) {
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ widgetsListHeader.applyFromItemInfoWithIcon(data);
+ widgetsListHeader.setExpanded(data.isWidgetListShown());
+ widgetsListHeader.setListDrawableState(
+ WidgetsListDrawableState.obtain(
+ /* isFirst= */ position == 0,
+ /* isLast= */ position == mWidgetsListAdapter.getItemCount() - 1,
+ /* isExpanded= */ data.isWidgetListShown()));
+ widgetsListHeader.setOnExpandChangeListener(isExpanded ->
+ mOnHeaderClickListener.onHeaderClicked(
+ isExpanded,
+ new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)
+ ));
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListLayoutManager.java b/src/com/android/launcher3/widget/picker/WidgetsListLayoutManager.java
new file mode 100644
index 0000000..2b7f544
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListLayoutManager.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.widget.picker.SearchAndRecommendationsScrollController.OnContentChangeListener;
+
+/**
+ * A layout manager for the {@link WidgetsRecyclerView}.
+ *
+ * {@link #setOnContentChangeListener(OnContentChangeListener)} can be used to register a callback
+ * for when the content of the layout manager has changed, following measurement and animation.
+ */
+public final class WidgetsListLayoutManager extends LinearLayoutManager {
+ @Nullable
+ private OnContentChangeListener mOnContentChangeListener;
+
+ public WidgetsListLayoutManager(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ super.onLayoutCompleted(state);
+ if (mOnContentChangeListener != null) {
+ mOnContentChangeListener.onContentChanged();
+ }
+ }
+
+ public void setOnContentChangeListener(@Nullable OnContentChangeListener listener) {
+ mOnContentChangeListener = listener;
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java
new file mode 100644
index 0000000..9562af3
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * A {@link ViewHolder} for {@link WidgetsListHeader} of an app, which renders the app icon, the app
+ * name, label and a button for showing / hiding widgets.
+ */
+public final class WidgetsListSearchHeaderHolder extends ViewHolder {
+ final WidgetsListHeader mWidgetsListHeader;
+
+ public WidgetsListSearchHeaderHolder(WidgetsListHeader view) {
+ super(view);
+
+ mWidgetsListHeader = view;
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java
new file mode 100644
index 0000000..31dd9ee
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+/**
+ * Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
+ */
+public final class WidgetsListSearchHeaderViewHolderBinder implements
+ ViewHolderBinder<WidgetsListSearchHeaderEntry, WidgetsListSearchHeaderHolder> {
+ private final LayoutInflater mLayoutInflater;
+ private final OnHeaderClickListener mOnHeaderClickListener;
+ private final WidgetsListDrawableFactory mListDrawableFactory;
+ private final WidgetsListAdapter mWidgetsListAdapter;
+
+ public WidgetsListSearchHeaderViewHolderBinder(LayoutInflater layoutInflater,
+ OnHeaderClickListener onHeaderClickListener,
+ WidgetsListDrawableFactory listDrawableFactory,
+ WidgetsListAdapter listAdapter) {
+ mLayoutInflater = layoutInflater;
+ mOnHeaderClickListener = onHeaderClickListener;
+ mListDrawableFactory = listDrawableFactory;
+ mWidgetsListAdapter = listAdapter;
+ }
+
+ @Override
+ public WidgetsListSearchHeaderHolder newViewHolder(ViewGroup parent) {
+ WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+ R.layout.widgets_list_row_header, parent, false);
+ header.setBackground(mListDrawableFactory.createHeaderBackgroundDrawable());
+ return new WidgetsListSearchHeaderHolder(header);
+ }
+
+ @Override
+ public void bindViewHolder(WidgetsListSearchHeaderHolder viewHolder,
+ WidgetsListSearchHeaderEntry data, int position) {
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ widgetsListHeader.applyFromItemInfoWithIcon(data);
+ widgetsListHeader.setExpanded(data.isWidgetListShown());
+ widgetsListHeader.setListDrawableState(
+ WidgetsListDrawableState.obtain(
+ /* isFirst= */ position == 0,
+ /* isLast= */ position == mWidgetsListAdapter.getItemCount() - 1,
+ /* isExpanded= */ data.isWidgetListShown()));
+ widgetsListHeader.setOnExpandChangeListener(isExpanded ->
+ mOnHeaderClickListener.onHeaderClicked(isExpanded,
+ new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)));
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableView.java b/src/com/android/launcher3/widget/picker/WidgetsListTableView.java
new file mode 100644
index 0000000..d30e7b6
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableView.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TableLayout;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+/**
+ * Extension of {@link TableLayout} to support the drawable states used by
+ * {@link WidgetsListDrawableState}.
+ */
+public class WidgetsListTableView extends TableLayout {
+
+ @Nullable private WidgetsListDrawableState mListDrawableState;
+
+ public WidgetsListTableView(Context context) {
+ super(context);
+ }
+
+ public WidgetsListTableView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
+ @UiThread
+ public void setListDrawableState(WidgetsListDrawableState state) {
+ if (state == mListDrawableState) return;
+ mListDrawableState = state;
+ refreshDrawableState();
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if (mListDrawableState == null) return super.onCreateDrawableState(extraSpace);
+ // Augment the state set from the super implementation with the custom states from
+ // mListDrawableState.
+ int[] drawableState =
+ super.onCreateDrawableState(extraSpace + mListDrawableState.mStateSet.length);
+ mergeDrawableStates(drawableState, mListDrawableState.mStateSet);
+ return drawableState;
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
new file mode 100644
index 0000000..9c06558
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.LAST;
+import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import android.util.Size;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+
+import com.android.launcher3.R;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Binds data from {@link WidgetsListContentEntry} to UI elements in {@link WidgetsRowViewHolder}.
+ */
+public final class WidgetsListTableViewHolderBinder
+ implements ViewHolderBinder<WidgetsListContentEntry, WidgetsRowViewHolder> {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "WidgetsListRowViewHolderBinder";
+
+ private final LayoutInflater mLayoutInflater;
+ private final OnClickListener mIconClickListener;
+ private final OnLongClickListener mIconLongClickListener;
+ private final WidgetsListDrawableFactory mListDrawableFactory;
+ private final CachingWidgetPreviewLoader mWidgetPreviewLoader;
+ private final WidgetsListAdapter mWidgetsListAdapter;
+ private boolean mApplyBitmapDeferred = false;
+
+ public WidgetsListTableViewHolderBinder(
+ LayoutInflater layoutInflater,
+ OnClickListener iconClickListener,
+ OnLongClickListener iconLongClickListener,
+ CachingWidgetPreviewLoader widgetPreviewLoader,
+ WidgetsListDrawableFactory listDrawableFactory,
+ WidgetsListAdapter listAdapter) {
+ mLayoutInflater = layoutInflater;
+ mIconClickListener = iconClickListener;
+ mIconLongClickListener = iconLongClickListener;
+ mWidgetPreviewLoader = widgetPreviewLoader;
+ mListDrawableFactory = listDrawableFactory;
+ mWidgetsListAdapter = listAdapter;
+ }
+
+ /**
+ * Defers applying bitmap on all the {@link WidgetCell} at
+ * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry, int)} if
+ * {@code applyBitmapDeferred} is {@code true}.
+ */
+ public void setApplyBitmapDeferred(boolean applyBitmapDeferred) {
+ mApplyBitmapDeferred = applyBitmapDeferred;
+ }
+
+ @Override
+ public WidgetsRowViewHolder newViewHolder(ViewGroup parent) {
+ if (DEBUG) {
+ Log.v(TAG, "\nonCreateViewHolder");
+ }
+
+ WidgetsRowViewHolder viewHolder =
+ new WidgetsRowViewHolder(mLayoutInflater.inflate(
+ R.layout.widgets_table_container, parent, false));
+ viewHolder.mTableContainer.setBackgroundDrawable(
+ mListDrawableFactory.createContentBackgroundDrawable());
+ return viewHolder;
+ }
+
+ @Override
+ public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry,
+ int position) {
+ WidgetsListTableView table = holder.mTableContainer;
+ if (DEBUG) {
+ Log.d(TAG, String.format("onBindViewHolder [widget#=%d, table.getChildCount=%d]",
+ entry.mWidgets.size(), table.getChildCount()));
+ }
+
+ table.setListDrawableState(
+ position == mWidgetsListAdapter.getItemCount() - 1 ? LAST : MIDDLE);
+
+ List<ArrayList<WidgetItem>> widgetItemsTable =
+ WidgetsTableUtils.groupWidgetItemsIntoTable(
+ entry.mWidgets, entry.getMaxSpanSizeInCells());
+ recycleTableBeforeBinding(table, widgetItemsTable);
+ // Bind the widget items.
+ for (int i = 0; i < widgetItemsTable.size(); i++) {
+ List<WidgetItem> widgetItemsPerRow = widgetItemsTable.get(i);
+ for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+ TableRow row = (TableRow) table.getChildAt(i);
+ row.setVisibility(View.VISIBLE);
+ WidgetCell widget = (WidgetCell) row.getChildAt(j);
+ widget.clear();
+ WidgetItem widgetItem = widgetItemsPerRow.get(j);
+ Size previewSize = widget.setPreviewSize(widgetItem);
+ widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
+ widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
+ Bitmap preview = mWidgetPreviewLoader.getPreview(widgetItem, previewSize);
+ if (preview == null) {
+ widget.ensurePreview();
+ } else {
+ widget.applyPreview(preview);
+ }
+ widget.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ /**
+ * Adds and hides table rows and columns from {@code table} to ensure there is sufficient room
+ * to display {@code widgetItemsTable}.
+ *
+ * <p>Instead of recreating all UI elements in {@code table}, this function recycles all
+ * existing UI elements. Instead of deleting excessive elements, it hides them.
+ */
+ private void recycleTableBeforeBinding(TableLayout table,
+ List<ArrayList<WidgetItem>> widgetItemsTable) {
+ // Hide extra table rows.
+ for (int i = widgetItemsTable.size(); i < table.getChildCount(); i++) {
+ table.getChildAt(i).setVisibility(View.GONE);
+ }
+
+ for (int i = 0; i < widgetItemsTable.size(); i++) {
+ List<WidgetItem> widgetItems = widgetItemsTable.get(i);
+ TableRow tableRow;
+ if (i < table.getChildCount()) {
+ tableRow = (TableRow) table.getChildAt(i);
+ } else {
+ tableRow = new TableRow(table.getContext());
+ tableRow.setGravity(Gravity.TOP);
+ table.addView(tableRow);
+ }
+ if (tableRow.getChildCount() > widgetItems.size()) {
+ for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
+ tableRow.getChildAt(j).setVisibility(View.GONE);
+ }
+ } else {
+ for (int j = tableRow.getChildCount(); j < widgetItems.size(); j++) {
+ WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
+ R.layout.widget_cell, tableRow, false);
+ // set up touch.
+ View preview = widget.findViewById(R.id.widget_preview_container);
+ preview.setOnClickListener(mIconClickListener);
+ preview.setOnLongClickListener(mIconLongClickListener);
+ tableRow.addView(widget);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void unbindViewHolder(WidgetsRowViewHolder holder) {
+ int numOfRows = holder.mTableContainer.getChildCount();
+ for (int i = 0; i < numOfRows; i++) {
+ TableRow tableRow = (TableRow) holder.mTableContainer.getChildAt(i);
+ int numOfCols = tableRow.getChildCount();
+ for (int j = 0; j < numOfCols; j++) {
+ WidgetCell widget = (WidgetCell) tableRow.getChildAt(j);
+ widget.clear();
+ }
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
new file mode 100644
index 0000000..0b8ca34
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Size;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.util.WidgetSizes;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A {@link TableLayout} for showing recommended widgets. */
+public final class WidgetsRecommendationTableLayout extends TableLayout {
+ private static final String TAG = "WidgetsRecommendationTableLayout";
+ private static final float DOWN_SCALE_RATIO = 0.9f;
+ private static final float MAX_DOWN_SCALE_RATIO = 0.5f;
+ private final float mWidgetsRecommendationTableVerticalPadding;
+ private final float mWidgetCellVerticalPadding;
+ private final float mWidgetCellTextViewsHeight;
+
+ private float mRecommendationTableMaxHeight = Float.MAX_VALUE;
+ @Nullable private OnLongClickListener mWidgetCellOnLongClickListener;
+ @Nullable private OnClickListener mWidgetCellOnClickListener;
+ @Nullable private OnTouchListener mWidgetCellOnTouchListener;
+
+ public WidgetsRecommendationTableLayout(Context context) {
+ this(context, /* attrs= */ null);
+ }
+
+ public WidgetsRecommendationTableLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // There are 1 row for title, 1 row for dimension and 2 rows for description.
+ mWidgetsRecommendationTableVerticalPadding = 2 * getResources()
+ .getDimensionPixelSize(R.dimen.recommended_widgets_table_vertical_padding);
+ mWidgetCellVerticalPadding = 2 * getResources()
+ .getDimensionPixelSize(R.dimen.widget_cell_vertical_padding);
+ mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size);
+ }
+
+ /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */
+ public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) {
+ mWidgetCellOnLongClickListener = onLongClickListener;
+ }
+
+ /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */
+ public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) {
+ mWidgetCellOnClickListener = widgetCellOnClickListener;
+ }
+
+ /** Sets a {@link android.view.View.OnTouchListener} for all widget cells in this table. */
+ public void setWidgetCellOnTouchListener(OnTouchListener widgetCellOnTouchListener) {
+ mWidgetCellOnTouchListener = widgetCellOnTouchListener;
+ }
+
+ /**
+ * Sets a list of recommended widgets that would like to be displayed in this table within the
+ * desired {@code recommendationTableMaxHeight}.
+ *
+ * <p>If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a
+ * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only
+ * row still doesn't fit, we scale down the preview image.
+ */
+ public void setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
+ float recommendationTableMaxHeight) {
+ mRecommendationTableMaxHeight = recommendationTableMaxHeight;
+ RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f,
+ recommendedWidgets);
+ bindData(data);
+ }
+
+ private void bindData(RecommendationTableData data) {
+ if (data.mRecommendationTable.size() == 0) {
+ setVisibility(GONE);
+ return;
+ }
+
+ removeAllViews();
+
+ for (int i = 0; i < data.mRecommendationTable.size(); i++) {
+ List<WidgetItem> widgetItems = data.mRecommendationTable.get(i);
+ TableRow tableRow = new TableRow(getContext());
+ tableRow.setGravity(Gravity.TOP);
+
+ for (WidgetItem widgetItem : widgetItems) {
+ WidgetCell widgetCell = addItemCell(tableRow);
+ widgetCell.setPreviewSize(widgetItem, data.mPreviewScale);
+ widgetCell.applyFromCellItem(widgetItem,
+ LauncherAppState.getInstance(getContext()).getWidgetCache());
+ widgetCell.ensurePreview();
+ }
+ addView(tableRow);
+ }
+ setVisibility(VISIBLE);
+ }
+
+ private WidgetCell addItemCell(ViewGroup parent) {
+ WidgetCell widget = (WidgetCell) LayoutInflater.from(
+ getContext()).inflate(R.layout.widget_cell, parent, false);
+
+ widget.setOnTouchListener(mWidgetCellOnTouchListener);
+ View previewContainer = widget.findViewById(R.id.widget_preview_container);
+ previewContainer.setOnClickListener(mWidgetCellOnClickListener);
+ previewContainer.setOnLongClickListener(mWidgetCellOnLongClickListener);
+ widget.setAnimatePreview(false);
+ widget.setSourceContainer(CONTAINER_WIDGETS_PREDICTION);
+
+ parent.addView(widget);
+ return widget;
+ }
+
+ private RecommendationTableData fitRecommendedWidgetsToTableSpace(
+ float previewScale,
+ List<ArrayList<WidgetItem>> recommendedWidgetsInTable) {
+ if (previewScale < MAX_DOWN_SCALE_RATIO) {
+ Log.w(TAG, "Hide recommended widgets. Can't down scale previews to " + previewScale);
+ return new RecommendationTableData(List.of(), previewScale);
+ }
+ // A naive estimation of the widgets recommendation table height without inflation.
+ float totalHeight = mWidgetsRecommendationTableVerticalPadding;
+ DeviceProfile deviceProfile = Launcher.getLauncher(getContext()).getDeviceProfile();
+ for (int i = 0; i < recommendedWidgetsInTable.size(); i++) {
+ List<WidgetItem> widgetItems = recommendedWidgetsInTable.get(i);
+ float rowHeight = 0;
+ for (int j = 0; j < widgetItems.size(); j++) {
+ WidgetItem widgetItem = widgetItems.get(j);
+ Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile,
+ widgetItem);
+ float previewHeight = widgetSize.getHeight() * previewScale;
+ rowHeight = Math.max(rowHeight,
+ previewHeight + mWidgetCellTextViewsHeight + mWidgetCellVerticalPadding);
+ }
+ totalHeight += rowHeight;
+ }
+
+ if (totalHeight < mRecommendationTableMaxHeight) {
+ return new RecommendationTableData(recommendedWidgetsInTable, previewScale);
+ }
+
+ if (recommendedWidgetsInTable.size() > 1) {
+ // We don't want to scale down widgets preview unless we really need to. Reduce the
+ // num of row by 1 to see if it fits.
+ return fitRecommendedWidgetsToTableSpace(
+ previewScale,
+ recommendedWidgetsInTable.subList(/* fromIndex= */0,
+ /* toIndex= */recommendedWidgetsInTable.size() - 1));
+ }
+
+ float nextPreviewScale = previewScale * DOWN_SCALE_RATIO;
+ return fitRecommendedWidgetsToTableSpace(nextPreviewScale, recommendedWidgetsInTable);
+ }
+
+ /** Data class for the widgets recommendation table and widgets preview scaling. */
+ private class RecommendationTableData {
+ private final List<ArrayList<WidgetItem>> mRecommendationTable;
+ private final float mPreviewScale;
+
+ RecommendationTableData(List<ArrayList<WidgetItem>> recommendationTable,
+ float previewScale) {
+ mRecommendationTable = recommendationTable;
+ mPreviewScale = previewScale;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
new file mode 100644
index 0000000..7671841
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TableLayout;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+
+import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+import com.android.launcher3.widget.picker.SearchAndRecommendationsScrollController.OnContentChangeListener;
+
+/**
+ * The widgets recycler view.
+ */
+public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouchListener {
+
+ private WidgetsListAdapter mAdapter;
+
+ private final int mScrollbarTop;
+
+ private final Point mFastScrollerOffset = new Point();
+ private boolean mTouchDownOnScroller;
+ private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
+ private int mLastVisibleWidgetContentTableHeight = 0;
+ private int mWidgetHeaderHeight = 0;
+ private final int mSpacingBetweenEntries;
+ @Nullable private OnContentChangeListener mOnContentChangeListener;
+
+ public WidgetsRecyclerView(Context context) {
+ this(context, null);
+ }
+
+ public WidgetsRecyclerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ // API 21 and below only support 3 parameter ctor.
+ super(context, attrs, defStyleAttr);
+ mScrollbarTop = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
+ addOnItemTouchListener(this);
+
+ ActivityContext activity = ActivityContext.lookupContext(getContext());
+ DeviceProfile grid = activity.getDeviceProfile();
+
+ // The spacing used between entries.
+ mSpacingBetweenEntries =
+ getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ // create a layout manager with Launcher's context so that scroll position
+ // can be preserved during screen rotation.
+ WidgetsListLayoutManager layoutManager = new WidgetsListLayoutManager(getContext());
+ layoutManager.setOnContentChangeListener(mOnContentChangeListener);
+ setLayoutManager(layoutManager);
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+ super.setAdapter(adapter);
+ mAdapter = (WidgetsListAdapter) adapter;
+ }
+
+ /**
+ * Maps the touch (from 0..1) to the adapter position that should be visible.
+ */
+ @Override
+ public String scrollToPositionAtProgress(float touchFraction) {
+ // Skip early if widgets are not bound.
+ if (isModelNotReady()) {
+ return "";
+ }
+
+ // Stop the scroller if it is scrolling
+ stopScroll();
+
+ int rowCount = mAdapter.getItemCount();
+ float pos = rowCount * touchFraction;
+ int availableScrollHeight = getAvailableScrollHeight();
+ LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
+ layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
+
+ int posInt = (int) ((touchFraction == 1) ? pos - 1 : pos);
+ return mAdapter.getSectionName(posInt);
+ }
+
+ /**
+ * Updates the bounds for the scrollbar.
+ */
+ @Override
+ public void onUpdateScrollbar(int dy) {
+ // Skip early if widgets are not bound.
+ if (isModelNotReady()) {
+ mScrollbar.setThumbOffsetY(-1);
+ return;
+ }
+
+ // Skip early if, there no child laid out in the container.
+ int scrollY = getCurrentScrollY();
+ if (scrollY < 0) {
+ mScrollbar.setThumbOffsetY(-1);
+ return;
+ }
+
+ synchronizeScrollBarThumbOffsetToViewScroll(scrollY, getAvailableScrollHeight());
+ }
+
+ @Override
+ public int getCurrentScrollY() {
+ // Skip early if widgets are not bound.
+ if (isModelNotReady() || getChildCount() == 0) {
+ return -1;
+ }
+
+ int rowIndex = -1;
+ View child = null;
+
+ LayoutManager layoutManager = getLayoutManager();
+ if (layoutManager instanceof LinearLayoutManager) {
+ // Use the LayoutManager as the source of truth for visible positions. During
+ // animations, the view group child may not correspond to the visible views that appear
+ // at the top.
+ rowIndex = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
+ child = layoutManager.findViewByPosition(rowIndex);
+ }
+
+ if (child == null) {
+ // If the layout manager returns null for any reason, which can happen before layout
+ // has occurred for the position, then look at the child of this view as a ViewGroup.
+ child = getChildAt(0);
+ rowIndex = getChildPosition(child);
+ }
+
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ if (view instanceof TableLayout) {
+ // This assumes there is ever only one content shown in this recycler view.
+ mLastVisibleWidgetContentTableHeight = view.getMeasuredHeight();
+ } else if (view instanceof WidgetsListHeader
+ && mLastVisibleWidgetContentTableHeight == 0
+ && view.getMeasuredHeight() > 0) {
+ // This assumes all header views are of the same height.
+ mWidgetHeaderHeight = view.getMeasuredHeight();
+ }
+ }
+
+ int scrollPosition = getItemsHeight(rowIndex);
+ int offset = getLayoutManager().getDecoratedTop(child);
+
+ return getPaddingTop() + scrollPosition - offset;
+ }
+
+ /**
+ * Returns the available scroll height, in pixel.
+ *
+ * <p>If the recycler view can't be scrolled, returns 0.
+ */
+ @Override
+ protected int getAvailableScrollHeight() {
+ // AvailableScrollHeight = Total height of the all items - first page height
+ int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+ int totalHeightOfAllItems = getItemsHeight(/* untilIndex= */ mAdapter.getItemCount());
+ int availableScrollHeight = totalHeightOfAllItems - firstPageHeight;
+ return Math.max(0, availableScrollHeight);
+ }
+
+ private boolean isModelNotReady() {
+ return mAdapter.getItemCount() == 0;
+ }
+
+ @Override
+ public int getScrollBarTop() {
+ return mHeaderViewDimensionsProvider == null
+ ? mScrollbarTop
+ : mHeaderViewDimensionsProvider.getHeaderViewHeight() + mScrollbarTop;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (e.getAction() == MotionEvent.ACTION_DOWN) {
+ mTouchDownOnScroller =
+ mScrollbar.isHitInParent(e.getX(), e.getY(), mFastScrollerOffset);
+ }
+ if (mTouchDownOnScroller) {
+ final boolean result = mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
+ return result;
+ }
+ return false;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (mTouchDownOnScroller) {
+ mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
+ }
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+
+ public void setHeaderViewDimensionsProvider(
+ HeaderViewDimensionsProvider headerViewDimensionsProvider) {
+ mHeaderViewDimensionsProvider = headerViewDimensionsProvider;
+ }
+
+ @Override
+ public void scrollToTop() {
+ if (mScrollbar != null) {
+ mScrollbar.reattachThumbToScroll();
+ }
+
+ if (getLayoutManager() instanceof LinearLayoutManager) {
+ if (getCurrentScrollY() == 0) {
+ // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
+ return;
+ }
+ }
+ scrollToPosition(0);
+ }
+
+ public void setOnContentChangeListener(@Nullable OnContentChangeListener listener) {
+ mOnContentChangeListener = listener;
+ WidgetsListLayoutManager layoutManager = (WidgetsListLayoutManager) getLayoutManager();
+ if (layoutManager != null) {
+ layoutManager.setOnContentChangeListener(listener);
+ }
+ }
+
+ /**
+ * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until
+ * {@code untilIndex}.
+ *
+ * <p>If the untilIndex is larger than the total number of items in this adapter, returns the
+ * sum of all items' height.
+ */
+ private int getItemsHeight(int untilIndex) {
+ if (untilIndex > mAdapter.getItems().size()) {
+ untilIndex = mAdapter.getItems().size();
+ }
+ int totalItemsHeight = 0;
+ for (int i = 0; i < untilIndex; i++) {
+ WidgetsListBaseEntry entry = mAdapter.getItems().get(i);
+ if (entry instanceof WidgetsListHeaderEntry
+ || entry instanceof WidgetsListSearchHeaderEntry) {
+ totalItemsHeight += mWidgetHeaderHeight;
+ if (i > 0) {
+ // Each header contains the spacing between entries as top decoration, except
+ // the first one.
+ totalItemsHeight += mSpacingBetweenEntries;
+ }
+ } else if (entry instanceof WidgetsListContentEntry) {
+ totalItemsHeight += mLastVisibleWidgetContentTableHeight;
+ } else {
+ throw new UnsupportedOperationException("Can't estimate height for " + entry);
+ }
+ }
+ return totalItemsHeight;
+ }
+
+ /**
+ * Provides dimensions of the header view that is shown at the top of a
+ * {@link WidgetsRecyclerView}.
+ */
+ public interface HeaderViewDimensionsProvider {
+ /**
+ * Returns the height, in pixels, of the header view that is shown at the top of a
+ * {@link WidgetsRecyclerView}.
+ */
+ int getHeaderViewHeight();
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
new file mode 100644
index 0000000..618e2cb
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+import com.android.launcher3.R;
+
+/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
+public final class WidgetsRowViewHolder extends ViewHolder {
+
+ public final WidgetsListTableView mTableContainer;
+
+ public WidgetsRowViewHolder(View v) {
+ super(v);
+
+ mTableContainer = v.findViewById(R.id.widgets_table);
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
new file mode 100644
index 0000000..65937b6
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.R;
+import com.android.launcher3.popup.PopupDataProvider;
+
+/**
+ * View for a search bar with an edit text with a cancel button.
+ */
+public class LauncherWidgetsSearchBar extends LinearLayout implements WidgetsSearchBar {
+ private WidgetsSearchBarController mController;
+ private ExtendedEditText mEditText;
+ private ImageButton mCancelButton;
+
+ public LauncherWidgetsSearchBar(Context context) {
+ this(context, null, 0);
+ }
+
+ public LauncherWidgetsSearchBar(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public LauncherWidgetsSearchBar(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener) {
+ mController = new WidgetsSearchBarController(
+ new SimpleWidgetsSearchAlgorithm(dataProvider),
+ mEditText, mCancelButton, searchModeListener);
+ }
+
+ @Override
+ public void reset() {
+ mController.clearSearchResult();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mEditText = findViewById(R.id.widgets_search_bar_edit_text);
+ mCancelButton = findViewById(R.id.widgets_search_cancel_button);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mController.onDestroy();
+ }
+
+ @Override
+ public boolean isSearchBarFocused() {
+ return mEditText.isFocused();
+ }
+
+ @Override
+ public void clearSearchBarFocus() {
+ mController.clearFocus();
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/SearchModeListener.java b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java
new file mode 100644
index 0000000..cee7d67
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker.search;
+
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
+
+/**
+ * A listener to help with widgets picker search.
+ */
+public interface SearchModeListener {
+ /**
+ * Notifies the subscriber when user enters widget picker search mode.
+ */
+ void enterSearchMode();
+
+ /**
+ * Notifies the subscriber when user exits widget picker search mode.
+ */
+ void exitSearchMode();
+
+ /**
+ * Notifies the subscriber with search results.
+ */
+ void onSearchResults(List<WidgetsListBaseEntry> entries);
+}
diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
new file mode 100644
index 0000000..9be3b5f
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import static com.android.launcher3.search.StringMatcherUtility.matches;
+
+import android.os.Handler;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of {@link SearchAlgorithm} that posts a task to query on the main thread.
+ */
+public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm<WidgetsListBaseEntry> {
+
+ private final Handler mResultHandler;
+ private final PopupDataProvider mDataProvider;
+
+ public SimpleWidgetsSearchAlgorithm(PopupDataProvider dataProvider) {
+ mResultHandler = new Handler();
+ mDataProvider = dataProvider;
+ }
+
+ @Override
+ public void doSearch(String query, SearchCallback<WidgetsListBaseEntry> callback) {
+ ArrayList<WidgetsListBaseEntry> result = getFilteredWidgets(mDataProvider, query);
+ mResultHandler.post(() -> callback.onSearchResult(query, result));
+ }
+
+ @Override
+ public void cancel(boolean interruptActiveRequests) {
+ if (interruptActiveRequests) {
+ mResultHandler.removeCallbacksAndMessages(/*token= */null);
+ }
+ }
+
+ /**
+ * Returns entries for all matched widgets
+ */
+ public static ArrayList<WidgetsListBaseEntry> getFilteredWidgets(
+ PopupDataProvider dataProvider, String input) {
+ ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
+ dataProvider.getAllWidgets().stream()
+ .filter(entry -> entry instanceof WidgetsListHeaderEntry)
+ .forEach(headerEntry -> {
+ List<WidgetItem> matchedWidgetItems = filterWidgetItems(
+ input, headerEntry.mPkgItem.title.toString(), headerEntry.mWidgets);
+ if (matchedWidgetItems.size() > 0) {
+ results.add(new WidgetsListSearchHeaderEntry(headerEntry.mPkgItem,
+ headerEntry.mTitleSectionName, matchedWidgetItems));
+ results.add(new WidgetsListContentEntry(headerEntry.mPkgItem,
+ headerEntry.mTitleSectionName, matchedWidgetItems));
+ }
+ });
+ return results;
+ }
+
+ private static List<WidgetItem> filterWidgetItems(String query, String packageTitle,
+ List<WidgetItem> items) {
+ StringMatcher matcher = StringMatcher.getInstance();
+ if (matches(query, packageTitle, matcher)) {
+ return items;
+ }
+ return items.stream()
+ .filter(item -> matches(query, item.label, matcher))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
new file mode 100644
index 0000000..44a5e80
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import com.android.launcher3.popup.PopupDataProvider;
+
+/**
+ * Interface for a widgets picker search bar.
+ */
+public interface WidgetsSearchBar {
+ /**
+ * Attaches a controller to the search bar which interacts with {@code searchModeListener}.
+ */
+ void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener);
+
+ /**
+ * Clears search bar.
+ */
+ void reset();
+
+ /** Returns {@code true} if the search bar is in focus. */
+ boolean isSearchBarFocused();
+
+ /**
+ * Clears focus from search bar.
+ */
+ void clearSearchBarFocus();
+
+ /**
+ * Sets the vertical location, in pixels, of this search bar relative to its top position.
+ */
+ void setTranslationY(float translationY);
+}
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
new file mode 100644
index 0000000..2751a52
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.search;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.ArrayList;
+
+/**
+ * Controller for a search bar with an edit text and a cancel button.
+ */
+public class WidgetsSearchBarController implements TextWatcher,
+ SearchCallback<WidgetsListBaseEntry>, ExtendedEditText.OnBackKeyListener,
+ View.OnKeyListener {
+ private static final String TAG = "WidgetsSearchBarController";
+ private static final boolean DEBUG = false;
+
+ protected SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
+ protected ExtendedEditText mInput;
+ protected ImageButton mCancelButton;
+ protected SearchModeListener mSearchModeListener;
+ protected String mQuery;
+
+ public WidgetsSearchBarController(
+ SearchAlgorithm<WidgetsListBaseEntry> algo, ExtendedEditText editText,
+ ImageButton cancelButton, SearchModeListener searchModeListener) {
+ mSearchAlgorithm = algo;
+ mInput = editText;
+ mInput.addTextChangedListener(this);
+ mInput.setOnBackKeyListener(this);
+ mInput.setOnKeyListener(this);
+ mCancelButton = cancelButton;
+ mCancelButton.setOnClickListener(v -> clearSearchResult());
+ mSearchModeListener = searchModeListener;
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ mQuery = s.toString();
+ if (mQuery.isEmpty()) {
+ mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
+ mSearchModeListener.exitSearchMode();
+ mCancelButton.setVisibility(GONE);
+ } else {
+ mSearchAlgorithm.cancel(/* interruptActiveRequests= */ false);
+ mSearchModeListener.enterSearchMode();
+ mSearchAlgorithm.doSearch(mQuery, this);
+ mCancelButton.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
+ if (DEBUG) {
+ Log.d(TAG, "onSearchResult query: " + query + " items: " + items);
+ }
+ mSearchModeListener.onSearchResults(items);
+ }
+
+ @Override
+ public void onAppendSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
+ // Not needed.
+ }
+
+ @Override
+ public void clearSearchResult() {
+ // Any existing search session will be cancelled by setting text to empty.
+ mInput.setText("");
+ }
+
+ /**
+ * Cleans up after search is no longer needed.
+ */
+ public void onDestroy() {
+ mSearchAlgorithm.destroy();
+ }
+
+ @Override
+ public boolean onBackKey() {
+ clearFocus();
+ return true;
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
+ clearFocus();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Clears focus from edit text.
+ */
+ public void clearFocus() {
+ mInput.clearFocus();
+ mInput.hideKeyboard();
+ }
+}
diff --git a/src/com/android/launcher3/widget/util/WidgetSizes.java b/src/com/android/launcher3/widget/util/WidgetSizes.java
new file mode 100644
index 0000000..451ed6e
--- /dev/null
+++ b/src/com/android/launcher3/widget/util/WidgetSizes.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.util;
+
+import static android.appwidget.AppWidgetHostView.getDefaultPaddingForWidget;
+
+
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Size;
+import android.util.SizeF;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.model.WidgetItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A utility class for widget sizes related calculations. */
+public final class WidgetSizes {
+
+ /**
+ * Returns the list of all possible sizes, in dp, for a widget of given spans on this device.
+ *
+ * <p>The returned sizes already take into account the system padding, and whether it is applied
+ * or not in that specific configuration.
+ */
+ public static ArrayList<SizeF> getWidgetPaddedSizes(Context context, ComponentName provider,
+ int spanX, int spanY) {
+ Rect padding = getDefaultPaddingForWidget(context, provider, /* padding= */ null);
+
+ ArrayList<SizeF> sizes = new ArrayList<>(2);
+ final float density = context.getResources().getDisplayMetrics().density;
+ final Point cellSize = new Point();
+
+ for (DeviceProfile profile : LauncherAppState.getIDP(context).supportedProfiles) {
+ Size widgetSizePx = getWidgetSizePx(profile, spanX, spanY, cellSize);
+ if (!profile.shouldInsetWidgets()) {
+ widgetSizePx = new Size(widgetSizePx.getWidth() - padding.left - padding.right,
+ widgetSizePx.getHeight() - padding.top - padding.bottom);
+ }
+ sizes.add(new SizeF(widgetSizePx.getWidth() / density,
+ widgetSizePx.getHeight() / density));
+ }
+ return sizes;
+ }
+
+ /** Returns the size, in pixels, a widget of given spans & {@code profile}. */
+ public static Size getWidgetSizePx(DeviceProfile profile, int spanX, int spanY) {
+ return getWidgetSizePx(profile, spanX, spanY, /* recycledCellSize= */ null);
+ }
+
+ /**
+ * Returns the size, in pixels and removing padding, a widget of given spans & {@code profile}.
+ */
+ public static Size getWidgetPaddedSizePx(Context context, ComponentName component,
+ DeviceProfile profile, int spanX, int spanY) {
+ Size size = getWidgetSizePx(profile, spanX, spanY);
+ if (profile.shouldInsetWidgets()) {
+ return size;
+ }
+ Rect padding = getDefaultPaddingForWidget(context, component, /* padding= */ null);
+ return new Size(size.getWidth() - padding.left - padding.right,
+ size.getHeight() - padding.top - padding.bottom);
+ }
+
+ /**
+ * Returns the size of a WidgetItem.
+ */
+ public static Size getWidgetItemSizePx(Context context, DeviceProfile profile,
+ WidgetItem widgetItem) {
+ if (widgetItem.isShortcut()) {
+ int dimension = profile.allAppsIconSizePx + 2 * context.getResources()
+ .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
+ return new Size(dimension, dimension);
+ }
+ return getWidgetPaddedSizePx(context, widgetItem.componentName, profile, widgetItem.spanX,
+ widgetItem.spanY);
+ }
+
+ private static Size getWidgetSizePx(DeviceProfile profile, int spanX, int spanY,
+ @Nullable Point recycledCellSize) {
+ final int hBorderSpacing = (spanX - 1) * profile.cellLayoutBorderSpacingPx;
+ final int vBorderSpacing = (spanY - 1) * profile.cellLayoutBorderSpacingPx;
+ if (recycledCellSize == null) {
+ recycledCellSize = new Point();
+ }
+ profile.getCellSize(recycledCellSize);
+ return new Size(((spanX * recycledCellSize.x) + hBorderSpacing),
+ ((spanY * recycledCellSize.y) + vBorderSpacing));
+ }
+
+ /**
+ * Updates a given {@code widgetView} with size, {@code spanX}, {@code spanY}.
+ *
+ * <p>On Android S+, it also updates the given {@code widgetView} with a list of sizes derived
+ * from {@code spanX}, {@code spanY} in all supported device profiles.
+ */
+ public static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Context context,
+ int spanX, int spanY) {
+ AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
+ int widgetId = widgetView.getAppWidgetId();
+ if (widgetId <= 0) {
+ return;
+ }
+ Bundle sizeOptions = getWidgetSizeOptions(context, widgetView.getAppWidgetInfo().provider,
+ spanX, spanY);
+ if (sizeOptions.<SizeF>getParcelableArrayList(
+ AppWidgetManager.OPTION_APPWIDGET_SIZES).equals(
+ widgetManager.getAppWidgetOptions(widgetId).<SizeF>getParcelableArrayList(
+ AppWidgetManager.OPTION_APPWIDGET_SIZES))) {
+ return;
+ }
+ widgetManager.updateAppWidgetOptions(widgetId, sizeOptions);
+ }
+
+ /**
+ * Returns the bundle to be used as the default options for a widget with provided size.
+ */
+ public static Bundle getWidgetSizeOptions(Context context, ComponentName provider, int spanX,
+ int spanY) {
+ ArrayList<SizeF> paddedSizes = getWidgetPaddedSizes(context, provider, spanX, spanY);
+
+ Rect rect = getMinMaxSizes(paddedSizes);
+ Bundle options = new Bundle();
+ options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, rect.left);
+ options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, rect.top);
+ options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, rect.right);
+ options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, rect.bottom);
+ options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, paddedSizes);
+ return options;
+ }
+
+ /**
+ * Returns the min and max widths and heights given a list of sizes, in dp.
+ *
+ * @param sizes List of sizes to get the min/max from.
+ * @return A rectangle with the left (resp. top) is used for the min width (resp. height) and
+ * the right (resp. bottom) for the max. The returned rectangle is set with 0s if the list is
+ * empty.
+ */
+ private static Rect getMinMaxSizes(List<SizeF> sizes) {
+ if (sizes.isEmpty()) {
+ return new Rect();
+ } else {
+ SizeF first = sizes.get(0);
+ Rect result = new Rect((int) first.getWidth(), (int) first.getHeight(),
+ (int) first.getWidth(), (int) first.getHeight());
+ for (int i = 1; i < sizes.size(); i++) {
+ result.union((int) sizes.get(i).getWidth(), (int) sizes.get(i).getHeight());
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
new file mode 100644
index 0000000..54aaf93
--- /dev/null
+++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.util;
+
+import com.android.launcher3.model.WidgetItem;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** An utility class which groups {@link WidgetItem}s into a table. */
+public final class WidgetsTableUtils {
+
+ /**
+ * Groups widgets in the following order:
+ * 1. Widgets always go before shortcuts.
+ * 2. Widgets with smaller horizontal spans will be shown first.
+ * 3. If widgets have the same horizontal spans, then widgets with a smaller vertical spans will
+ * go first.
+ * 4. If both widgets have the same horizontal and vertical spans, they will use the same order
+ * from the given {@code widgetItems}.
+ */
+ private static final Comparator<WidgetItem> WIDGET_SHORTCUT_COMPARATOR = (item, otherItem) -> {
+ if (item.widgetInfo != null && otherItem.widgetInfo == null) return -1;
+
+ if (item.widgetInfo == null && otherItem.widgetInfo != null) return 1;
+ if (item.spanX == otherItem.spanX) {
+ if (item.spanY == otherItem.spanY) return 0;
+ return item.spanY > otherItem.spanY ? 1 : -1;
+ }
+ return item.spanX > otherItem.spanX ? 1 : -1;
+ };
+
+
+ /**
+ * Groups widgets items into a 2D array which matches their appearance in a UI table.
+ *
+ * <p>Grouping:
+ * 1. Widgets and shortcuts never group together in the same row.
+ * 2. The ordered widgets are grouped together in the same row until their total horizontal
+ * spans exceed the {@code maxSpansPerRow} - 1.
+ * 3. The order shortcuts are grouped together in the same row until their total horizontal
+ * spans exceed the {@code maxSpansPerRow} - 1.
+ * 4. If there is only one widget in a row, its width may exceed the {@code maxSpansPerRow}.
+ *
+ * <p>Let's say the {@code maxSpansPerRow} is set to 6. Widgets can be grouped in the same row
+ * if their total horizontal spans added don't exceed 5.
+ * Example 1: Row 1: 2x2, 2x3, 1x1. Total horizontal spans is 5. This is okay.
+ * Example 2: Row 1: 2x2, 4x3, 1x1. the total horizontal spans is 7. This is wrong. 4x3 and 1x1
+ * should be moved to a new row.
+ * Example 3: Row 1: 6x4. This is okay because this is the only item in the row.
+ */
+ public static List<ArrayList<WidgetItem>> groupWidgetItemsIntoTable(
+ List<WidgetItem> widgetItems, final int maxSpansPerRow) {
+ List<WidgetItem> sortedWidgetItems = widgetItems.stream().sorted(WIDGET_SHORTCUT_COMPARATOR)
+ .collect(Collectors.toList());
+ List<ArrayList<WidgetItem>> widgetItemsTable = new ArrayList<>();
+ ArrayList<WidgetItem> widgetItemsAtRow = null;
+ for (WidgetItem widgetItem : sortedWidgetItems) {
+ if (widgetItemsAtRow == null) {
+ widgetItemsAtRow = new ArrayList<>();
+ widgetItemsTable.add(widgetItemsAtRow);
+ }
+ int numOfWidgetItems = widgetItemsAtRow.size();
+ int totalHorizontalSpan = widgetItemsAtRow.stream().map(item -> item.spanX)
+ .reduce(/* default= */ 0, Integer::sum);
+ int totalHorizontalSpanAfterAddingWidget = widgetItem.spanX + totalHorizontalSpan;
+ if (numOfWidgetItems == 0) {
+ widgetItemsAtRow.add(widgetItem);
+ } else if (
+ // The max spans per row is reduced by 1 to ensure we don't pack too many
+ // 1xn widgets on the same row, which may reduce the space for rendering a
+ // widget's description.
+ totalHorizontalSpanAfterAddingWidget <= maxSpansPerRow - 1
+ && widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1))) {
+ // Group items in the same row if
+ // 1. they are with the same type, i.e. a row can only have widgets or shortcuts but
+ // never a mix of both.
+ // 2. the total number of horizontal spans are smaller than or equal to
+ // MAX_SPAN_PER_ROW. If an item has a horizontal span > MAX_SPAN_PER_ROW, we just
+ // place it in its own row regardless of the horizontal span limit.
+ widgetItemsAtRow.add(widgetItem);
+ } else {
+ widgetItemsAtRow = new ArrayList<>();
+ widgetItemsTable.add(widgetItemsAtRow);
+ widgetItemsAtRow.add(widgetItem);
+ }
+ }
+ return widgetItemsTable;
+ }
+}
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkPagedView.java b/src/com/android/launcher3/workprofile/PersonalWorkPagedView.java
new file mode 100644
index 0000000..8b05a0d
--- /dev/null
+++ b/src/com/android/launcher3/workprofile/PersonalWorkPagedView.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.workprofile;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.launcher3.PagedView;
+
+/**
+ * A {@link PagedView} for showing different views for the personal and work profile respectively.
+ */
+public class PersonalWorkPagedView extends PagedView<PersonalWorkSlidingTabStrip> {
+
+ static final float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
+ static final float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
+ static final float TOUCH_SLOP_DAMPING_FACTOR = 4;
+
+ public PersonalWorkPagedView(Context context) {
+ this(context, null);
+ }
+
+ public PersonalWorkPagedView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PersonalWorkPagedView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected String getCurrentPageDescription() {
+ // Not necessary, tab-bar already has two tabs with their own descriptions.
+ return "";
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ mPageIndicator.setScroll(l, mMaxScroll);
+ }
+
+ @Override
+ protected void determineScrollingStart(MotionEvent ev) {
+ float absDeltaX = Math.abs(ev.getX() - getDownMotionX());
+ float absDeltaY = Math.abs(ev.getY() - getDownMotionY());
+
+ if (Float.compare(absDeltaX, 0f) == 0) return;
+
+ float slope = absDeltaY / absDeltaX;
+ float theta = (float) Math.atan(slope);
+
+ if (absDeltaX > mTouchSlop || absDeltaY > mTouchSlop) {
+ cancelCurrentPageLongPress();
+ }
+
+ if (theta > MAX_SWIPE_ANGLE) {
+ return;
+ } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
+ theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
+ float extraRatio = (float)
+ Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
+ super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
+ } else {
+ super.determineScrollingStart(ev);
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ @Override
+ protected boolean canScroll(float absVScroll, float absHScroll) {
+ return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
+ }
+}
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
new file mode 100644
index 0000000..11185fb
--- /dev/null
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -0,0 +1,82 @@
+/*
+ * 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.workprofile;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.pageindicators.PageIndicator;
+
+/**
+ * Supports two indicator colors, dedicated for personal and work tabs.
+ */
+public class PersonalWorkSlidingTabStrip extends LinearLayout implements PageIndicator {
+ private OnActivePageChangedListener mOnActivePageChangedListener;
+ private int mLastActivePage = 0;
+
+ public PersonalWorkSlidingTabStrip(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Highlights tab with index pos
+ */
+ public void updateTabTextColor(int pos) {
+ for (int i = 0; i < getChildCount(); i++) {
+ Button tab = (Button) getChildAt(i);
+ tab.setSelected(i == pos);
+ }
+ }
+
+ @Override
+ public void setScroll(int currentScroll, int totalScroll) {
+ }
+
+ @Override
+ public void setActiveMarker(int activePage) {
+ updateTabTextColor(activePage);
+ if (mOnActivePageChangedListener != null && mLastActivePage != activePage) {
+ mOnActivePageChangedListener.onActivePageChanged(activePage);
+ }
+ mLastActivePage = activePage;
+ }
+
+ public void setOnActivePageChangedListener(OnActivePageChangedListener listener) {
+ mOnActivePageChangedListener = listener;
+ }
+
+ @Override
+ public void setMarkersCount(int numMarkers) {
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when an active page has been changed.
+ */
+ public interface OnActivePageChangedListener {
+ /** Called when the active page has been changed. */
+ void onActivePageChanged(int currentActivePage);
+ }
+}
diff --git a/src_build_config/BuildConfig.java b/src_build_config/com/android/launcher3/BuildConfig.java
similarity index 100%
rename from src_build_config/BuildConfig.java
rename to src_build_config/com/android/launcher3/BuildConfig.java
diff --git a/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java b/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java
deleted file mode 100644
index c57f07d..0000000
--- a/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import android.app.Activity;
-import android.view.ViewGroup;
-import android.widget.EditText;
-
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-
-/**
- * Implement this plugin interface to replace the all apps recycler view of the all apps drawer.
- */
-@ProvidesInterface(action = AllAppsSearchPlugin.ACTION, version = AllAppsSearchPlugin.VERSION)
-public interface AllAppsSearchPlugin extends Plugin {
- String ACTION = "com.android.systemui.action.PLUGIN_ALL_APPS_SEARCH_ACTIONS";
- int VERSION = 3;
-
- /** Following are the order that these methods should be called. */
- void setup(ViewGroup parent, Activity activity, float allAppsContainerHeight);
-
- /**
- * When drag starts, pass window inset related fields and the progress to indicate
- * whether user is swiping down or swiping up
- */
- void onDragStart(float progress);
-
- /** progress is between [0, 1] 1: down, 0: up */
- void setProgress(float progress);
-
- /** Called when container animation stops, so that plugin can perform cleanups */
- void onAnimationEnd(float progress);
-
- /** pass over the search box object */
- void setEditText(EditText editText);
-}
diff --git a/src_plugins/com/android/systemui/plugins/AppLaunchEventsPlugin.java b/src_plugins/com/android/systemui/plugins/AppLaunchEventsPlugin.java
deleted file mode 100644
index 15a0ffa..0000000
--- a/src_plugins/com/android/systemui/plugins/AppLaunchEventsPlugin.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.plugins;
-
-import android.content.ComponentName;
-import android.os.UserHandle;
-
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-
-/**
- * Plugin interface which sends app launch events.
- */
-@ProvidesInterface(action = AppLaunchEventsPlugin.ACTION, version = AppLaunchEventsPlugin.VERSION)
-public interface AppLaunchEventsPlugin extends Plugin {
- String ACTION = "com.android.systemui.action.PLUGIN_APP_EVENTS";
- int VERSION = 1;
-
- /**
- * Receives onStartShortcut event from
- * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
- */
- void onStartShortcut(String packageName, String shortcutId, UserHandle user, String container);
-
- /**
- * Receives onStartApp event from
- * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
- */
- void onStartApp(ComponentName componentName, UserHandle user, String container);
-
- /**
- * Receives onDismissApp event from
- * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
- */
- void onDismissApp(ComponentName componentName, UserHandle user, String container);
-
- /**
- * Receives onReturnedToHome event from
- * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
- */
- void onReturnedToHome();
-}
diff --git a/src_plugins/com/android/systemui/plugins/OWNERS b/src_plugins/com/android/systemui/plugins/OWNERS
deleted file mode 100644
index 0514999..0000000
--- a/src_plugins/com/android/systemui/plugins/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-# When changing interface for this plugin OR when increasing version code, please add Alex
-# Only add other owners if Alex is not available
-per-file AllAppsSearchPlugin.java, globs = set noparent
-per-file AllAppsSearchPlugin.java = alexmang@google.com, hyunyoungs@google.com, sunnygoyal@google.com, twickham@google.com
diff --git a/src_plugins/com/android/systemui/plugins/UserEventPlugin.java b/src_plugins/com/android/systemui/plugins/UserEventPlugin.java
deleted file mode 100644
index 0e3664a..0000000
--- a/src_plugins/com/android/systemui/plugins/UserEventPlugin.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-
-/**
- * Implement this plugin interface to access user event log on the device for prototype purpose.
- * NOTE: plugin is for internal prototype only and is not visible in production environment.
- */
-@ProvidesInterface(action = UserEventPlugin.ACTION, version = UserEventPlugin.VERSION)
-public interface UserEventPlugin extends Plugin {
- String ACTION = "com.android.launcher3.action.PLUGIN_USER_EVENT_LOG";
- int VERSION = 1;
-
- /**
- * Callback to be triggered whenever an user event occurs.
- */
- void onUserEvent(Object event);
-}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index dcb4636..abce2a2 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -21,11 +21,10 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
/**
* Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -34,12 +33,7 @@
public LoaderResults(LauncherAppState app, BgDataModel dataModel,
AllAppsList allAppsList, Callbacks[] callbacks) {
- this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
- }
-
- public LoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
- super(app, dataModel, allAppsList, callbacks, executor);
+ super(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
}
@Override
@@ -53,8 +47,8 @@
@Override
public void bindWidgets() {
- final ArrayList<WidgetListRowEntry> widgets =
- mBgDataModel.widgetsModel.getWidgetsList(mApp.getContext());
+ final List<WidgetsListBaseEntry> widgets =
+ mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
}
}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 9d87788..631067b 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -5,20 +5,21 @@
import static com.android.launcher3.pm.ShortcutConfigActivityInfo.queryList;
+import static java.util.stream.Collectors.toList;
+
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
import com.android.launcher3.AppFilter;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.Utilities;
import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.config.FeatureFlags;
@@ -26,21 +27,25 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
-import com.android.launcher3.util.MultiHashMap;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.widget.WidgetItemComparator;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsDiffReporter;
import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
/**
* Widgets data model that is used by the adapters of the widget views and controllers.
@@ -51,38 +56,56 @@
// True is the widget support is disabled.
public static final boolean GO_DISABLE_WIDGETS = false;
+ public static final boolean GO_DISABLE_NOTIFICATION_DOTS = false;
private static final String TAG = "WidgetsModel";
private static final boolean DEBUG = false;
- /* Map of widgets and shortcuts that are tracked per package. */
- private final MultiHashMap<PackageItemInfo, WidgetItem> mWidgetsList = new MultiHashMap<>();
+ private static final ComponentName CONVERSATION_WIDGET = ComponentName.createRelative(
+ "com.android.systemui", ".people.widget.PeopleSpaceWidgetProvider");
- private AppFilter mAppFilter;
+ /* Map of widgets and shortcuts that are tracked per package. */
+ private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
/**
- * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row
- * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s
- * is not sorted. This list is sorted at the UI when using
- * {@link com.android.launcher3.widget.WidgetsDiffReporter}
+ * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
+ * are sorted (based on label and user), but the overall list of
+ * {@link WidgetsListBaseEntry}s is not sorted. This list is sorted at the UI when using
+ * {@link WidgetsDiffReporter}
*
- * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList)
+ * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
*/
- public synchronized ArrayList<WidgetListRowEntry> getWidgetsList(Context context) {
- ArrayList<WidgetListRowEntry> result = new ArrayList<>();
+ public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
+ ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
- WidgetItemComparator widgetComparator = new WidgetItemComparator();
- for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : mWidgetsList.entrySet()) {
- WidgetListRowEntry row = new WidgetListRowEntry(entry.getKey(), entry.getValue());
- row.titleSectionName = (row.pkgItem.title == null) ? "" :
- indexer.computeSectionName(row.pkgItem.title);
- Collections.sort(row.widgets, widgetComparator);
- result.add(row);
+ for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
+ PackageItemInfo pkgItem = entry.getKey();
+ List<WidgetItem> widgetItems = entry.getValue();
+ String sectionName = (pkgItem.title == null) ? "" :
+ indexer.computeSectionName(pkgItem.title);
+ result.add(new WidgetsListHeaderEntry(pkgItem, sectionName, widgetItems));
+ result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
}
return result;
}
+ /** Returns a mapping of packages to their widgets without static shortcuts. */
+ public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
+ Map<PackageUserKey, List<WidgetItem>> packagesToWidgets = new HashMap<>();
+ mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> {
+ List<WidgetItem> widgets = widgetsAndShortcuts.stream()
+ .filter(item -> item.widgetInfo != null)
+ .collect(toList());
+ if (widgets.size() > 0) {
+ packagesToWidgets.put(
+ new PackageUserKey(packageItemInfo.packageName, packageItemInfo.user),
+ widgets);
+ }
+ });
+ return packagesToWidgets;
+ }
+
/**
* @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
* only widgets and shortcuts associated with the package/user are.
@@ -137,98 +160,38 @@
Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
}
- // Temporary list for {@link PackageItemInfos} to avoid having to go through
+ // Temporary cache for {@link PackageItemInfos} to avoid having to go through
// {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
- HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
+ PackageItemInfoCache packageItemInfoCache = new PackageItemInfoCache();
- // clear the lists.
if (packageUser == null) {
+ // Clear the list if this is an update on all widgets and shortcuts.
mWidgetsList.clear();
} else {
- // Only clear the widgets for the given package/user.
- PackageItemInfo packageItem = null;
- for (PackageItemInfo item : mWidgetsList.keySet()) {
- if (item.packageName.equals(packageUser.mPackageName)) {
- packageItem = item;
- break;
- }
- }
- if (packageItem != null) {
- // We want to preserve the user that was on the packageItem previously,
- // so add it to tmpPackageItemInfos here to avoid creating a new entry.
- tmpPackageItemInfos.put(packageItem.packageName, packageItem);
-
- Iterator<WidgetItem> widgetItemIterator = mWidgetsList.get(packageItem).iterator();
- while (widgetItemIterator.hasNext()) {
- WidgetItem nextWidget = widgetItemIterator.next();
- if (nextWidget.componentName.getPackageName().equals(packageUser.mPackageName)
- && nextWidget.user.equals(packageUser.mUser)) {
- widgetItemIterator.remove();
- }
- }
- }
+ // Otherwise, only clear the widgets and shortcuts for the changed package.
+ mWidgetsList.remove(
+ packageItemInfoCache.getOrCreate(new WidgetPackageOrCategoryKey(packageUser)));
}
- InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
- UserHandle myUser = Process.myUserHandle();
-
// add and update.
- for (WidgetItem item : rawWidgetsShortcuts) {
- if (item.widgetInfo != null) {
- if ((item.widgetInfo.getWidgetFeatures() & WIDGET_FEATURE_HIDE_FROM_PICKER) != 0) {
- // Widget is hidden from picker
- continue;
- }
-
- // Ensure that all widgets we show can be added on a workspace of this size
- int minSpanX = Math.min(item.widgetInfo.spanX, item.widgetInfo.minSpanX);
- int minSpanY = Math.min(item.widgetInfo.spanY, item.widgetInfo.minSpanY);
- if (minSpanX > idp.numColumns || minSpanY > idp.numRows) {
- if (DEBUG) {
- Log.d(TAG, String.format(
- "Widget %s : (%d X %d) can't fit on this device",
- item.componentName, minSpanX, minSpanY));
- }
- continue;
- }
- }
-
- if (mAppFilter == null) {
- mAppFilter = AppFilter.newInstance(app.getContext());
- }
- if (!mAppFilter.shouldShowApp(item.componentName)) {
- if (DEBUG) {
- Log.d(TAG, String.format("%s is filtered and not added to the widget tray.",
- item.componentName));
- }
- continue;
- }
-
- String packageName = item.componentName.getPackageName();
- PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName);
- if (pInfo == null) {
- pInfo = new PackageItemInfo(packageName);
- pInfo.user = item.user;
- tmpPackageItemInfos.put(packageName, pInfo);
- } else if (!myUser.equals(pInfo.user)) {
- // Keep updating the user, until we get the primary user.
- pInfo.user = item.user;
- }
- mWidgetsList.addToList(pInfo, item);
- }
+ mWidgetsList.putAll(rawWidgetsShortcuts.stream()
+ .filter(new WidgetValidityCheck(app))
+ .collect(Collectors.groupingBy(item ->
+ packageItemInfoCache.getOrCreate(getWidgetPackageOrCategoryKey(item))
+ )));
// Update each package entry
IconCache iconCache = app.getIconCache();
- for (PackageItemInfo p : tmpPackageItemInfos.values()) {
+ for (PackageItemInfo p : packageItemInfoCache.values()) {
iconCache.getTitleAndIconForApp(p, true /* userLowResIcon */);
}
}
public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user,
LauncherAppState app) {
- for (Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : mWidgetsList.entrySet()) {
+ for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
if (packageNames.contains(entry.getKey().packageName)) {
- ArrayList<WidgetItem> items = entry.getValue();
+ List<WidgetItem> items = entry.getValue();
int count = items.size();
for (int i = 0; i < count; i++) {
WidgetItem item = items.get(i);
@@ -248,7 +211,7 @@
public WidgetItem getWidgetProviderInfoByProviderName(
ComponentName providerName) {
- ArrayList<WidgetItem> widgetsList = mWidgetsList.get(
+ List<WidgetItem> widgetsList = mWidgetsList.get(
new PackageItemInfo(providerName.getPackageName()));
if (widgetsList == null) {
return null;
@@ -261,4 +224,117 @@
}
return null;
}
+
+ /** Returns {@link PackageItemInfo} of a pending widget. */
+ public static PackageItemInfo newPendingItemInfo(ComponentName provider) {
+ if (CONVERSATION_WIDGET.equals(provider)) {
+ return new PackageItemInfo(provider.getPackageName(), PackageItemInfo.CONVERSATIONS);
+ }
+ return new PackageItemInfo(provider.getPackageName());
+ }
+
+ private WidgetPackageOrCategoryKey getWidgetPackageOrCategoryKey(WidgetItem item) {
+ if (CONVERSATION_WIDGET.equals(item.componentName)) {
+ return new WidgetPackageOrCategoryKey(PackageItemInfo.CONVERSATIONS, item.user);
+ }
+ return new WidgetPackageOrCategoryKey(item.componentName.getPackageName(), item.user);
+ }
+
+ private static class WidgetValidityCheck implements Predicate<WidgetItem> {
+
+ private final InvariantDeviceProfile mIdp;
+ private final AppFilter mAppFilter;
+
+ WidgetValidityCheck(LauncherAppState app) {
+ mIdp = app.getInvariantDeviceProfile();
+ mAppFilter = new AppFilter(app.getContext());
+ }
+
+ @Override
+ public boolean test(WidgetItem item) {
+ if (item.widgetInfo != null) {
+ if ((item.widgetInfo.getWidgetFeatures() & WIDGET_FEATURE_HIDE_FROM_PICKER) != 0) {
+ // Widget is hidden from picker
+ return false;
+ }
+
+ // Ensure that all widgets we show can be added on a workspace of this size
+ if (!item.widgetInfo.isMinSizeFulfilled()) {
+ if (DEBUG) {
+ Log.d(TAG, String.format(
+ "Widget %s : can't fit on this device with a grid size: %dx%d",
+ item.componentName, mIdp.numColumns, mIdp.numRows));
+ }
+ return false;
+ }
+ }
+ if (!mAppFilter.shouldShowApp(item.componentName)) {
+ if (DEBUG) {
+ Log.d(TAG, String.format("%s is filtered and not added to the widget tray.",
+ item.componentName));
+ }
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /** A hash key for grouping widgets by package name or category. */
+ private static class WidgetPackageOrCategoryKey {
+ /**
+ * The package name of the widget provider.
+ *
+ * <p>This shouldn't be empty if {@link #mCategory} has a value,
+ * {@link PackageItemInfo#NO_CATEGORY}.
+ */
+ public final String mPackage;
+ /** A widget category. */
+ @PackageItemInfo.Category public final int mCategory;
+ public final UserHandle mUser;
+ private final int mHashCode;
+
+ WidgetPackageOrCategoryKey(PackageUserKey key) {
+ this(key.mPackageName, key.mUser);
+ }
+
+ WidgetPackageOrCategoryKey(String packageName, UserHandle user) {
+ this(packageName, PackageItemInfo.NO_CATEGORY, user);
+ }
+
+ WidgetPackageOrCategoryKey(@PackageItemInfo.Category int category, UserHandle user) {
+ this("", category, user);
+ }
+
+ private WidgetPackageOrCategoryKey(String packageName,
+ @PackageItemInfo.Category int category, UserHandle user) {
+ mPackage = packageName;
+ mCategory = category;
+ mUser = user;
+ mHashCode = Arrays.hashCode(new Object[]{mPackage, mCategory, mUser});
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+ }
+
+ private static final class PackageItemInfoCache {
+ private final Map<WidgetPackageOrCategoryKey, PackageItemInfo> mMap = new ArrayMap<>();
+
+ PackageItemInfo getOrCreate(WidgetPackageOrCategoryKey key) {
+ PackageItemInfo pInfo = mMap.get(key);
+ if (pInfo == null) {
+ pInfo = new PackageItemInfo(key.mPackage, key.mCategory);
+ pInfo.user = key.mUser;
+ mMap.put(key, pInfo);
+ }
+ return pInfo;
+ }
+
+ Collection<PackageItemInfo> values() {
+ return mMap.values();
+ }
+ }
}
\ No newline at end of file
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
index 6fd147a..4407fe1 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -18,12 +18,22 @@
import android.app.Person;
import android.content.pm.ShortcutInfo;
+import android.view.Display;
import com.android.launcher3.Utilities;
public class ApiWrapper {
+ public static final boolean TASKBAR_DRAWN_IN_PROCESS = false;
+
public static Person[] getPersons(ShortcutInfo si) {
return Utilities.EMPTY_PERSON_ARRAY;
}
+
+ /**
+ * Returns true if the display is an internal displays
+ */
+ public static boolean isInternalDisplay(Display display) {
+ return display.getDisplayId() == Display.DEFAULT_DISPLAY;
+ }
}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/WallpaperColorInfo.java b/src_ui_overrides/com/android/launcher3/uioverrides/WallpaperColorInfo.java
deleted file mode 100644
index b3aa365..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/WallpaperColorInfo.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import static android.app.WallpaperManager.FLAG_SYSTEM;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.util.Pair;
-
-import com.android.launcher3.uioverrides.dynamicui.ColorExtractionAlgorithm;
-import com.android.launcher3.uioverrides.dynamicui.WallpaperColorsCompat;
-import com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompat;
-import com.android.launcher3.util.MainThreadInitializedObject;
-
-import java.util.ArrayList;
-
-public class WallpaperColorInfo implements WallpaperManagerCompat.OnColorsChangedListenerCompat {
-
- private static final int MAIN_COLOR_LIGHT = 0xffdadce0;
- private static final int MAIN_COLOR_DARK = 0xff202124;
- private static final int MAIN_COLOR_REGULAR = 0xff000000;
-
- private static final int FALLBACK_COLOR = Color.WHITE;
-
- public static final MainThreadInitializedObject<WallpaperColorInfo> INSTANCE =
- new MainThreadInitializedObject<>(WallpaperColorInfo::new);
-
- private final ArrayList<OnChangeListener> mListeners = new ArrayList<>();
- private final WallpaperManagerCompat mWallpaperManager;
- private final ColorExtractionAlgorithm mExtractionType;
- private int mMainColor;
- private int mSecondaryColor;
- private boolean mIsDark;
- private boolean mSupportsDarkText;
-
- private OnChangeListener[] mTempListeners;
-
- private WallpaperColorInfo(Context context) {
- mWallpaperManager = WallpaperManagerCompat.getInstance(context);
- mWallpaperManager.addOnColorsChangedListener(this);
- mExtractionType = new ColorExtractionAlgorithm();
- update(mWallpaperManager.getWallpaperColors(FLAG_SYSTEM));
- }
-
- public int getMainColor() {
- return mMainColor;
- }
-
- public int getSecondaryColor() {
- return mSecondaryColor;
- }
-
- public boolean isDark() {
- return mIsDark;
- }
-
- public boolean supportsDarkText() {
- return mSupportsDarkText;
- }
-
- public boolean isMainColorDark() {
- return mMainColor == MAIN_COLOR_DARK;
- }
-
- @Override
- public void onColorsChanged(WallpaperColorsCompat colors, int which) {
- if ((which & FLAG_SYSTEM) != 0) {
- update(colors);
- notifyChange();
- }
- }
-
- private void update(WallpaperColorsCompat wallpaperColors) {
- Pair<Integer, Integer> colors = mExtractionType.extractInto(wallpaperColors);
- if (colors != null) {
- mMainColor = colors.first;
- mSecondaryColor = colors.second;
- } else {
- mMainColor = FALLBACK_COLOR;
- mSecondaryColor = FALLBACK_COLOR;
- }
- mSupportsDarkText = wallpaperColors != null
- ? (wallpaperColors.getColorHints()
- & WallpaperColorsCompat.HINT_SUPPORTS_DARK_TEXT) > 0 : false;
- mIsDark = wallpaperColors != null
- ? (wallpaperColors.getColorHints()
- & WallpaperColorsCompat.HINT_SUPPORTS_DARK_THEME) > 0 : false;
- }
-
- public void addOnChangeListener(OnChangeListener listener) {
- mListeners.add(listener);
- }
-
- public void removeOnChangeListener(OnChangeListener listener) {
- mListeners.remove(listener);
- }
-
- private void notifyChange() {
- OnChangeListener[] copy =
- mTempListeners != null && mTempListeners.length == mListeners.size() ?
- mTempListeners : new OnChangeListener[mListeners.size()];
-
- // Create a new array to avoid concurrent modification when the activity destroys itself.
- mTempListeners = mListeners.toArray(copy);
- for (OnChangeListener listener : mTempListeners) {
- listener.onExtractedColorsChanged(this);
- }
- }
-
- public interface OnChangeListener {
- void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo);
- }
-}
\ No newline at end of file
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java
deleted file mode 100644
index 5a1f9ca..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java
+++ /dev/null
@@ -1,795 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides.dynamicui;
-
-import android.graphics.Color;
-import android.util.Log;
-import android.util.Pair;
-import android.util.Range;
-
-import com.android.launcher3.Utilities;
-
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.graphics.ColorUtils;
-
-/**
- * Implementation of tonal color extraction
- **/
-public class ColorExtractionAlgorithm {
-
- private static final String TAG = "Tonal";
-
- // Used for tonal palette fitting
- private static final float FIT_WEIGHT_H = 1.0f;
- private static final float FIT_WEIGHT_S = 1.0f;
- private static final float FIT_WEIGHT_L = 10.0f;
-
- public static final int MAIN_COLOR_LIGHT = 0xffb0b0b0;
- public static final int SECONDARY_COLOR_LIGHT = 0xff9e9e9e;
- public static final int MAIN_COLOR_DARK = 0xff212121;
- public static final int SECONDARY_COLOR_DARK = 0xff000000;
-
- // Temporary variable to avoid allocations
- private float[] mTmpHSL = new float[3];
-
- public Pair<Integer, Integer> extractInto(WallpaperColorsCompat inWallpaperColors) {
- if (inWallpaperColors == null) {
- return applyFallback(inWallpaperColors);
- }
-
- final List<Integer> mainColors = getMainColors(inWallpaperColors);
- final int mainColorsSize = mainColors.size();
- final boolean supportsDarkText = (inWallpaperColors.getColorHints() &
- WallpaperColorsCompat.HINT_SUPPORTS_DARK_TEXT) != 0;
-
- if (mainColorsSize == 0) {
- return applyFallback(inWallpaperColors);
- }
- // Tonal is not really a sort, it takes a color from the extracted
- // palette and finds a best fit amongst a collection of pre-defined
- // palettes. The best fit is tweaked to be closer to the source color
- // and replaces the original palette
-
- // Get the most preeminent, non-blacklisted color.
- Integer bestColor = 0;
- final float[] hsl = new float[3];
- for (int i = 0; i < mainColorsSize; i++) {
- final int colorValue = mainColors.get(i);
- ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue),
- Color.blue(colorValue), hsl);
-
- // Stop when we find a color that meets our criteria
- if (!isBlacklisted(hsl)) {
- bestColor = colorValue;
- break;
- }
- }
-
- // Fail if not found
- if (bestColor == null) {
- return applyFallback(inWallpaperColors);
- }
-
- int colorValue = bestColor;
- ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue),
- hsl);
-
- // The Android HSL definition requires the hue to go from 0 to 360 but
- // the Material Tonal Palette defines hues from 0 to 1.
- hsl[0] /= 360f;
-
- // Find the palette that contains the closest color
- TonalPalette palette = findTonalPalette(hsl[0], hsl[1]);
- if (palette == null) {
- Log.w(TAG, "Could not find a tonal palette!");
- return applyFallback(inWallpaperColors);
- }
-
- // Figure out what's the main color index in the optimal palette
- int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]);
- if (fitIndex == -1) {
- Log.w(TAG, "Could not find best fit!");
- return applyFallback(inWallpaperColors);
- }
-
- // Generate the 10 colors palette by offsetting each one of them
- float[] h = fit(palette.h, hsl[0], fitIndex,
- Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
- float[] s = fit(palette.s, hsl[1], fitIndex, 0.0f, 1.0f);
- float[] l = fit(palette.l, hsl[2], fitIndex, 0.0f, 1.0f);
-
- int primaryIndex = fitIndex;
- int mainColor = getColorInt(primaryIndex, h, s, l);
-
- // We might want use the fallback in case the extracted color is brighter than our
- // light fallback or darker than our dark fallback.
- ColorUtils.colorToHSL(mainColor, mTmpHSL);
- final float mainLuminosity = mTmpHSL[2];
- ColorUtils.colorToHSL(MAIN_COLOR_LIGHT, mTmpHSL);
- final float lightLuminosity = mTmpHSL[2];
- if (mainLuminosity > lightLuminosity) {
- return applyFallback(inWallpaperColors);
- }
- ColorUtils.colorToHSL(MAIN_COLOR_DARK, mTmpHSL);
- final float darkLuminosity = mTmpHSL[2];
- if (mainLuminosity < darkLuminosity) {
- return applyFallback(inWallpaperColors);
- }
-
- // Dark colors:
- // Stops at 4th color, only lighter if dark text is supported
- if (supportsDarkText) {
- primaryIndex = h.length - 1;
- } else if (fitIndex < 2) {
- primaryIndex = 0;
- } else {
- primaryIndex = Math.min(fitIndex, 3);
- }
- int secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
- int secondaryColor = getColorInt(secondaryIndex, h, s, l);
-
- return new Pair<>(mainColor, secondaryColor);
- }
-
- public static Pair<Integer, Integer> applyFallback(WallpaperColorsCompat inWallpaperColors) {
- boolean light = inWallpaperColors != null
- && (inWallpaperColors.getColorHints()
- & WallpaperColorsCompat.HINT_SUPPORTS_DARK_TEXT)!= 0;
- int innerColor = light ? MAIN_COLOR_LIGHT : MAIN_COLOR_DARK;
- int outerColor = light ? SECONDARY_COLOR_LIGHT : SECONDARY_COLOR_DARK;
- return new Pair<>(innerColor, outerColor);
- }
-
- private int getColorInt(int fitIndex, float[] h, float[] s, float[] l) {
- mTmpHSL[0] = fract(h[fitIndex]) * 360.0f;
- mTmpHSL[1] = s[fitIndex];
- mTmpHSL[2] = l[fitIndex];
- return ColorUtils.HSLToColor(mTmpHSL);
- }
-
- /**
- * Checks if a given color exists in the blacklist
- * @param hsl float array with 3 components (H 0..360, S 0..1 and L 0..1)
- * @return true if color should be avoided
- */
- private boolean isBlacklisted(float[] hsl) {
- for (ColorRange badRange: BLACKLISTED_COLORS) {
- if (badRange.containsColor(hsl[0], hsl[1], hsl[2])) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Offsets all colors by a delta, clamping values that go beyond what's
- * supported on the color space.
- * @param data what you want to fit
- * @param v how big should be the offset
- * @param index which index to calculate the delta against
- * @param min minimum accepted value (clamp)
- * @param max maximum accepted value (clamp)
- * @return new shifted palette
- */
- private static float[] fit(float[] data, float v, int index, float min, float max) {
- float[] fitData = new float[data.length];
- float delta = v - data[index];
-
- for (int i = 0; i < data.length; i++) {
- fitData[i] = Utilities.boundToRange(data[i] + delta, min, max);
- }
-
- return fitData;
- }
-
- /**
- * Finds the closest color in a palette, given another HSL color
- *
- * @param palette where to search
- * @param h hue
- * @param s saturation
- * @param l lightness
- * @return closest index or -1 if palette is empty.
- */
- private static int bestFit(@NonNull TonalPalette palette, float h, float s, float l) {
- int minErrorIndex = -1;
- float minError = Float.POSITIVE_INFINITY;
-
- for (int i = 0; i < palette.h.length; i++) {
- float error =
- FIT_WEIGHT_H * Math.abs(h - palette.h[i])
- + FIT_WEIGHT_S * Math.abs(s - palette.s[i])
- + FIT_WEIGHT_L * Math.abs(l - palette.l[i]);
- if (error < minError) {
- minError = error;
- minErrorIndex = i;
- }
- }
-
- return minErrorIndex;
- }
-
- @Nullable
- private static TonalPalette findTonalPalette(float h, float s) {
- // Fallback to a grey palette if the color is too desaturated.
- // This avoids hue shifts.
- if (s < 0.05f) {
- return GREY_PALETTE;
- }
-
- TonalPalette best = null;
- float error = Float.POSITIVE_INFINITY;
-
- for (int i = 0; i < TONAL_PALETTES.length; i++) {
- final TonalPalette candidate = TONAL_PALETTES[i];
-
- if (h >= candidate.minHue && h <= candidate.maxHue) {
- best = candidate;
- break;
- }
-
- if (candidate.maxHue > 1.0f && h >= 0.0f && h <= fract(candidate.maxHue)) {
- best = candidate;
- break;
- }
-
- if (candidate.minHue < 0.0f && h >= fract(candidate.minHue) && h <= 1.0f) {
- best = candidate;
- break;
- }
-
- if (h <= candidate.minHue && candidate.minHue - h < error) {
- best = candidate;
- error = candidate.minHue - h;
- } else if (h >= candidate.maxHue && h - candidate.maxHue < error) {
- best = candidate;
- error = h - candidate.maxHue;
- } else if (candidate.maxHue > 1.0f && h >= fract(candidate.maxHue)
- && h - fract(candidate.maxHue) < error) {
- best = candidate;
- error = h - fract(candidate.maxHue);
- } else if (candidate.minHue < 0.0f && h <= fract(candidate.minHue)
- && fract(candidate.minHue) - h < error) {
- best = candidate;
- error = fract(candidate.minHue) - h;
- }
- }
-
- return best;
- }
-
- private static float fract(float v) {
- return v - (float) Math.floor(v);
- }
-
- static class TonalPalette {
- final float[] h;
- final float[] s;
- final float[] l;
- final float minHue;
- final float maxHue;
-
- TonalPalette(float[] h, float[] s, float[] l) {
- if (h.length != s.length || s.length != l.length) {
- throw new IllegalArgumentException("All arrays should have the same size. h: "
- + Arrays.toString(h) + " s: " + Arrays.toString(s) + " l: "
- + Arrays.toString(l));
- }
-
- this.h = h;
- this.s = s;
- this.l = l;
-
- float minHue = Float.POSITIVE_INFINITY;
- float maxHue = Float.NEGATIVE_INFINITY;
-
- for (float v : h) {
- minHue = Math.min(v, minHue);
- maxHue = Math.max(v, maxHue);
- }
-
- this.minHue = minHue;
- this.maxHue = maxHue;
- }
- }
-
- // Data definition of Material Design tonal palettes
- // When the sort type is set to TONAL, these palettes are used to find
- // a best fit. Each palette is defined as 22 HSL colors
- private static final TonalPalette[] TONAL_PALETTES = {
- new TonalPalette(
- new float[] {1f, 1f, 0.991f, 0.991f, 0.9833333333333333f, 0f, 0f, 0f,
- 0.01134380453752181f, 0.015625000000000003f, 0.024193548387096798f,
- 0.027397260273972573f, 0.017543859649122865f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 1f, 0.8434782608695652f, 1f, 1f, 1f, 1f,
- 1f},
- new float[] {0.04f, 0.09f, 0.14f, 0.2f, 0.27450980392156865f,
- 0.34901960784313724f, 0.4235294117647059f, 0.5490196078431373f,
- 0.6254901960784314f, 0.6862745098039216f, 0.7568627450980392f,
- 0.8568627450980393f, 0.9254901960784314f}
- ),
- new TonalPalette(
- new float[] {0.638f, 0.638f, 0.6385767790262171f, 0.6301169590643275f,
- 0.6223958333333334f, 0.6151079136690647f, 0.6065400843881856f,
- 0.5986964618249534f, 0.5910746812386157f, 0.5833333333333334f,
- 0.5748031496062993f, 0.5582010582010583f},
- new float[] {1f, 1f, 1f, 1f, 0.9014084507042253f, 0.8128654970760234f,
- 0.7979797979797981f, 0.7816593886462883f, 0.778723404255319f, 1f, 1f,
- 1f},
- new float[] {0.05f, 0.12f, 0.17450980392156862f, 0.2235294117647059f,
- 0.2784313725490196f, 0.3352941176470588f, 0.388235294117647f,
- 0.44901960784313727f, 0.5392156862745098f, 0.6509803921568628f,
- 0.7509803921568627f, 0.8764705882352941f}
- ),
- new TonalPalette(
- new float[] {0.563f, 0.569f, 0.5666f, 0.5669934640522876f, 0.5748031496062993f,
- 0.5595238095238095f, 0.5473118279569893f, 0.5393258426966292f,
- 0.5315955766192734f, 0.524031007751938f, 0.5154711673699016f,
- 0.508080808080808f, 0.5f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 0.8847736625514403f, 1f, 1f,
- 1f},
- new float[] {0.07f, 0.12f, 0.16f, 0.2f, 0.24901960784313726f,
- 0.27450980392156865f, 0.30392156862745096f, 0.34901960784313724f,
- 0.4137254901960784f, 0.47647058823529415f, 0.5352941176470588f,
- 0.6764705882352942f, 0.8f}
- ),
- new TonalPalette(
- new float[] {0.508f, 0.511f, 0.508f, 0.508f, 0.5082304526748972f,
- 0.5069444444444444f, 0.5f, 0.5f, 0.5f, 0.48724954462659376f,
- 0.4800347222222222f, 0.4755134281200632f, 0.4724409448818897f,
- 0.4671052631578947f},
- new float[] {1f, 1f, 1f, 1f, 1f, 0.8888888888888887f, 0.9242424242424242f, 1f,
- 1f, 0.8133333333333332f, 0.7868852459016393f, 1f, 1f, 1f},
- new float[] {0.04f, 0.06f, 0.08f, 0.12f, 0.1588235294117647f,
- 0.21176470588235297f, 0.25882352941176473f, 0.3f, 0.34901960784313724f,
- 0.44117647058823534f, 0.5215686274509804f, 0.5862745098039216f,
- 0.7509803921568627f, 0.8509803921568627f}
- ),
- new TonalPalette(
- new float[] {0.333f, 0.333f, 0.333f, 0.3333333333333333f, 0.3333333333333333f,
- 0.34006734006734f, 0.34006734006734f, 0.34006734006734f,
- 0.34259259259259256f, 0.3475783475783476f, 0.34767025089605735f,
- 0.3467741935483871f, 0.3703703703703704f},
- new float[] {0.70f, 0.72f, 0.69f, 0.6703296703296703f, 0.728813559322034f,
- 0.5657142857142856f, 0.5076923076923077f, 0.3944223107569721f,
- 0.6206896551724138f, 0.8931297709923666f, 1f, 1f, 1f},
- new float[] {0.05f, 0.08f, 0.14f, 0.1784313725490196f, 0.23137254901960785f,
- 0.3431372549019608f, 0.38235294117647056f, 0.49215686274509807f,
- 0.6588235294117647f, 0.7431372549019608f, 0.8176470588235294f,
- 0.8784313725490196f, 0.9294117647058824f}
- ),
- new TonalPalette(
- new float[] {0.161f, 0.163f, 0.163f, 0.162280701754386f, 0.15032679738562088f,
- 0.15879265091863518f, 0.16236559139784948f, 0.17443868739205526f,
- 0.17824074074074076f, 0.18674698795180725f, 0.18692449355432778f,
- 0.1946778711484594f, 0.18604651162790695f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f},
- new float[] {0.05f, 0.08f, 0.11f, 0.14901960784313725f, 0.2f,
- 0.24901960784313726f, 0.30392156862745096f, 0.3784313725490196f,
- 0.4235294117647059f, 0.48823529411764705f, 0.6450980392156863f,
- 0.7666666666666666f, 0.8313725490196078f}
- ),
- new TonalPalette(
- new float[] {0.108f, 0.105f, 0.105f, 0.105f, 0.10619469026548674f,
- 0.11924686192468618f, 0.13046448087431692f, 0.14248366013071895f,
- 0.1506024096385542f, 0.16220238095238093f, 0.16666666666666666f,
- 0.16666666666666666f, 0.162280701754386f, 0.15686274509803924f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f},
- new float[] {0.17f, 0.22f, 0.28f, 0.35f, 0.44313725490196076f,
- 0.46862745098039216f, 0.47843137254901963f, 0.5f, 0.5117647058823529f,
- 0.5607843137254902f, 0.6509803921568628f, 0.7509803921568627f,
- 0.8509803921568627f, 0.9f}
- ),
- new TonalPalette(
- new float[] {0.036f, 0.036f, 0.036f, 0.036f, 0.03561253561253561f,
- 0.05098039215686275f, 0.07516339869281045f, 0.09477124183006536f,
- 0.1150326797385621f, 0.134640522875817f, 0.14640522875816991f,
- 0.1582397003745319f, 0.15773809523809523f, 0.15359477124183002f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f},
- new float[] {0.19f, 0.26f, 0.34f, 0.39f, 0.4588235294117647f, 0.5f, 0.5f, 0.5f,
- 0.5f, 0.5f, 0.5f, 0.6509803921568628f, 0.7803921568627451f, 0.9f}
- ),
- new TonalPalette(
- new float[] {0.955f, 0.961f, 0.958f, 0.9596491228070175f, 0.9593837535014005f,
- 0.9514767932489452f, 0.943859649122807f, 0.9396825396825397f,
- 0.9395424836601307f, 0.9393939393939394f, 0.9362745098039216f,
- 0.9754098360655739f, 0.9824561403508771f},
- new float[] {0.87f, 0.85f, 0.85f, 0.84070796460177f, 0.8206896551724138f,
- 0.7979797979797981f, 0.7661290322580644f, 0.9051724137931036f,
- 1f, 1f, 1f, 1f, 1f},
- new float[] {0.06f, 0.11f, 0.16f, 0.22156862745098038f, 0.2843137254901961f,
- 0.388235294117647f, 0.48627450980392156f, 0.5450980392156863f,
- 0.6f, 0.6764705882352942f, 0.8f, 0.8803921568627451f,
- 0.9254901960784314f}
- ),
- new TonalPalette(
- new float[] {0.866f, 0.855f, 0.841025641025641f, 0.8333333333333334f,
- 0.8285256410256411f, 0.821522309711286f, 0.8083333333333333f,
- 0.8046594982078853f, 0.8005822416302766f, 0.7842377260981912f,
- 0.7771084337349398f, 0.7747747747747749f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f,
- 0.737142857142857f, 0.6434108527131781f, 0.46835443037974644f},
- new float[] {0.05f, 0.08f, 0.12745098039215685f, 0.15490196078431373f,
- 0.20392156862745098f, 0.24901960784313726f, 0.3137254901960784f,
- 0.36470588235294116f, 0.44901960784313727f, 0.6568627450980392f,
- 0.7470588235294118f, 0.8450980392156863f}
- ),
- new TonalPalette(
- new float[] {0.925f, 0.93f, 0.938f, 0.947f, 0.955952380952381f,
- 0.9681069958847737f, 0.9760479041916167f, 0.9873563218390804f, 0f, 0f,
- 0.009057971014492771f, 0.026748971193415648f,
- 0.041666666666666616f, 0.05303030303030304f},
- new float[] {1f, 1f, 1f, 1f, 1f, 0.8350515463917526f, 0.6929460580912863f,
- 0.6387665198237885f, 0.6914893617021276f, 0.7583892617449666f,
- 0.8070175438596495f, 0.9310344827586209f, 1f, 1f},
- new float[] {0.10f, 0.13f, 0.17f, 0.2f, 0.27450980392156865f,
- 0.3803921568627451f, 0.4725490196078432f, 0.5549019607843138f,
- 0.6313725490196078f, 0.707843137254902f, 0.7764705882352941f,
- 0.8294117647058823f, 0.9058823529411765f, 0.9568627450980391f}
- ),
- new TonalPalette(
- new float[] {0.733f, 0.736f, 0.744f, 0.7514619883040936f, 0.7679738562091503f,
- 0.7802083333333333f, 0.7844311377245509f, 0.796875f,
- 0.8165618448637316f, 0.8487179487179487f, 0.8582375478927203f,
- 0.8562091503267975f, 0.8666666666666667f},
- new float[] {1f, 1f, 1f, 1f, 1f, 0.8163265306122449f, 0.6653386454183268f,
- 0.7547169811320753f, 0.929824561403509f, 0.9558823529411766f,
- 0.9560439560439562f, 1f, 1f},
- new float[] {0.07f, 0.12f, 0.17f, 0.2235294117647059f, 0.3f,
- 0.38431372549019605f, 0.492156862745098f, 0.5843137254901961f,
- 0.6647058823529411f, 0.7333333333333334f, 0.8215686274509804f, 0.9f,
- 0.9411764705882353f}
- ),
- new TonalPalette(
- new float[] {0.6666666666666666f, 0.6666666666666666f, 0.6666666666666666f,
- 0.6666666666666666f, 0.6666666666666666f, 0.6666666666666666f,
- 0.6666666666666666f, 0.6666666666666666f, 0.6666666666666666f,
- 0.6666666666666666f, 0.6666666666666666f},
- new float[] {0.25f, 0.24590163934426232f, 0.17880794701986752f,
- 0.14606741573033713f, 0.13761467889908252f, 0.14893617021276592f,
- 0.16756756756756758f, 0.20312500000000017f, 0.26086956521739135f,
- 0.29999999999999966f, 0.5000000000000004f},
- new float[] {0.18f, 0.2392156862745098f, 0.296078431372549f,
- 0.34901960784313724f, 0.4274509803921569f, 0.5392156862745098f,
- 0.6372549019607843f, 0.7490196078431373f, 0.8196078431372549f,
- 0.8823529411764706f, 0.9372549019607843f}
- ),
- new TonalPalette(
- new float[] {0.938f, 0.944f, 0.952f, 0.961f, 0.9678571428571429f,
- 0.9944812362030905f, 0f, 0f,
- 0.0047348484848484815f, 0.00316455696202532f, 0f,
- 0.9980392156862745f, 0.9814814814814816f, 0.9722222222222221f},
- new float[] {1f, 1f, 1f, 1f, 1f, 0.7023255813953488f, 0.6638655462184874f,
- 0.6521739130434782f, 0.7719298245614035f, 0.8315789473684211f,
- 0.6867469879518071f, 0.7264957264957265f, 0.8181818181818182f,
- 0.8181818181818189f},
- new float[] {0.08f, 0.13f, 0.18f, 0.23f, 0.27450980392156865f,
- 0.4215686274509804f,
- 0.4666666666666667f, 0.503921568627451f, 0.5529411764705883f,
- 0.6274509803921569f, 0.6745098039215687f, 0.7705882352941176f,
- 0.892156862745098f, 0.9568627450980391f}
- ),
- new TonalPalette(
- new float[] {0.88f, 0.888f, 0.897f, 0.9052287581699346f, 0.9112021857923498f,
- 0.9270152505446624f, 0.9343137254901961f, 0.9391534391534391f,
- 0.9437984496124031f, 0.943661971830986f, 0.9438943894389439f,
- 0.9426229508196722f, 0.9444444444444444f},
- new float[] {1f, 1f, 1f, 1f, 0.8133333333333332f, 0.7927461139896375f,
- 0.7798165137614679f, 0.7777777777777779f, 0.8190476190476191f,
- 0.8255813953488372f, 0.8211382113821142f, 0.8133333333333336f,
- 0.8000000000000006f},
- new float[] {0.08f, 0.12f, 0.16f, 0.2f, 0.29411764705882354f,
- 0.3784313725490196f, 0.42745098039215684f, 0.4764705882352941f,
- 0.5882352941176471f, 0.6627450980392157f, 0.7588235294117647f,
- 0.8529411764705882f, 0.9411764705882353f}
- ),
- new TonalPalette(
- new float[] {0.669f, 0.680f, 0.6884057971014492f, 0.6974789915966387f,
- 0.7079889807162534f, 0.7154471544715447f, 0.7217741935483872f,
- 0.7274143302180687f, 0.7272727272727273f, 0.7258064516129031f,
- 0.7252252252252251f, 0.7333333333333333f},
- new float[] {0.81f, 0.81f, 0.8214285714285715f, 0.6878612716763006f,
- 0.6080402010050251f, 0.5774647887323943f, 0.5391304347826086f,
- 0.46724890829694316f, 0.4680851063829788f, 0.462686567164179f,
- 0.45679012345678977f, 0.4545454545454551f},
- new float[] {0.12f, 0.16f, 0.2196078431372549f, 0.33921568627450976f,
- 0.39019607843137255f, 0.4176470588235294f, 0.45098039215686275f,
- 0.5509803921568628f, 0.6313725490196078f, 0.7372549019607844f,
- 0.8411764705882353f, 0.9352941176470588f}
- ),
- new TonalPalette(
- new float[] {0.6470588235294118f, 0.6516666666666667f, 0.6464174454828661f,
- 0.6441441441441442f, 0.6432748538011696f, 0.6416666666666667f,
- 0.6402439024390243f, 0.6412429378531074f, 0.6435185185185186f,
- 0.6428571428571429f},
- new float[] {0.8095238095238095f, 0.6578947368421053f, 0.5721925133689839f,
- 0.5362318840579711f, 0.5f, 0.4424778761061947f, 0.44086021505376327f,
- 0.44360902255639095f, 0.4499999999999997f, 0.4375000000000006f},
- new float[] {0.16470588235294117f, 0.2980392156862745f, 0.36666666666666664f,
- 0.40588235294117647f, 0.44705882352941173f,
- 0.5568627450980392f, 0.6352941176470588f, 0.7392156862745098f,
- 0.8431372549019608f, 0.9372549019607843f}
- ),
- new TonalPalette(
- new float[] {0.469f, 0.46732026143790845f, 0.4718614718614719f,
- 0.4793650793650794f, 0.48071625344352614f, 0.4829683698296837f,
- 0.484375f, 0.4841269841269842f, 0.48444444444444457f,
- 0.48518518518518516f, 0.4907407407407408f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 0.6274509803921569f, 0.41832669322709176f,
- 0.41899441340782106f, 0.4128440366972478f, 0.4090909090909088f},
- new float[] {0.07f, 0.1f, 0.15098039215686274f, 0.20588235294117646f,
- 0.2372549019607843f, 0.26862745098039215f, 0.4f, 0.5078431372549019f,
- 0.6490196078431372f, 0.7862745098039216f, 0.9137254901960784f}
- ),
- new TonalPalette(
- new float[] {0.542f, 0.5444444444444444f, 0.5555555555555556f,
- 0.5555555555555556f, 0.553763440860215f, 0.5526315789473684f,
- 0.5555555555555556f, 0.5555555555555555f, 0.5555555555555556f,
- 0.5512820512820514f, 0.5666666666666667f},
- new float[] {0.25f, 0.24590163934426232f, 0.19148936170212766f,
- 0.1791044776119403f, 0.18343195266272191f, 0.18446601941747576f,
- 0.1538461538461539f, 0.15625000000000003f, 0.15328467153284678f,
- 0.15662650602409653f, 0.151515151515151f},
- new float[] {0.05f, 0.1196078431372549f, 0.1843137254901961f,
- 0.2627450980392157f,
- 0.33137254901960783f, 0.403921568627451f, 0.5411764705882354f,
- 0.6235294117647059f, 0.7313725490196079f, 0.8372549019607843f,
- 0.9352941176470588f}
- ),
- new TonalPalette(
- new float[] {0.022222222222222223f, 0.02469135802469136f, 0.031249999999999997f,
- 0.03947368421052631f, 0.04166666666666668f,
- 0.043650793650793655f, 0.04411764705882352f, 0.04166666666666652f,
- 0.04444444444444459f, 0.05555555555555529f},
- new float[] {0.33333333333333337f, 0.2783505154639175f, 0.2580645161290323f,
- 0.25675675675675674f, 0.2528735632183908f, 0.17500000000000002f,
- 0.15315315315315312f, 0.15189873417721522f,
- 0.15789473684210534f, 0.15789473684210542f},
- new float[] {0.08823529411764705f, 0.19019607843137254f, 0.2431372549019608f,
- 0.2901960784313725f, 0.3411764705882353f, 0.47058823529411764f,
- 0.5647058823529412f, 0.6901960784313725f, 0.8137254901960784f,
- 0.9254901960784314f}
- ),
- new TonalPalette(
- new float[] {0.027f, 0.03f, 0.038f, 0.044f, 0.050884955752212385f,
- 0.07254901960784313f, 0.0934640522875817f,
- 0.10457516339869281f, 0.11699346405228758f,
- 0.1255813953488372f, 0.1268939393939394f, 0.12533333333333332f,
- 0.12500000000000003f, 0.12777777777777777f},
- new float[] {1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f},
- new float[] {0.25f, 0.3f, 0.35f, 0.4f, 0.44313725490196076f, 0.5f, 0.5f, 0.5f,
- 0.5f, 0.5784313725490196f,
- 0.6549019607843137f, 0.7549019607843137f, 0.8509803921568627f,
- 0.9411764705882353f}
- )
- };
-
- private static final TonalPalette GREY_PALETTE = new TonalPalette(
- new float[]{0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f},
- new float[]{0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f},
- new float[]{0.08f, 0.11f, 0.14901960784313725f, 0.2f, 0.2980392156862745f, 0.4f,
- 0.4980392156862745f, 0.6196078431372549f, 0.7176470588235294f,
- 0.8196078431372549f, 0.9176470588235294f, 0.9490196078431372f}
- );
-
- @SuppressWarnings("WeakerAccess")
- static final ColorRange[] BLACKLISTED_COLORS = new ColorRange[] {
-
- // Red
- new ColorRange(
- new Range<>(0f, 20f) /* H */,
- new Range<>(0.7f, 1f) /* S */,
- new Range<>(0.21f, 0.79f)) /* L */,
- new ColorRange(
- new Range<>(0f, 20f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.355f, 0.653f)),
-
- // Red Orange
- new ColorRange(
- new Range<>(20f, 40f),
- new Range<>(0.7f, 1f),
- new Range<>(0.28f, 0.643f)),
- new ColorRange(
- new Range<>(20f, 40f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.414f, 0.561f)),
- new ColorRange(
- new Range<>(20f, 40f),
- new Range<>(0f, 3f),
- new Range<>(0.343f, 0.584f)),
-
- // Orange
- new ColorRange(
- new Range<>(40f, 60f),
- new Range<>(0.7f, 1f),
- new Range<>(0.173f, 0.349f)),
- new ColorRange(
- new Range<>(40f, 60f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.233f, 0.427f)),
- new ColorRange(
- new Range<>(40f, 60f),
- new Range<>(0f, 0.3f),
- new Range<>(0.231f, 0.484f)),
-
- // Yellow 60
- new ColorRange(
- new Range<>(60f, 80f),
- new Range<>(0.7f, 1f),
- new Range<>(0.488f, 0.737f)),
- new ColorRange(
- new Range<>(60f, 80f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.673f, 0.837f)),
-
- // Yellow Green 80
- new ColorRange(
- new Range<>(80f, 100f),
- new Range<>(0.7f, 1f),
- new Range<>(0.469f, 0.61f)),
-
- // Yellow green 100
- new ColorRange(
- new Range<>(100f, 120f),
- new Range<>(0.7f, 1f),
- new Range<>(0.388f, 0.612f)),
- new ColorRange(
- new Range<>(100f, 120f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.424f, 0.541f)),
-
- // Green
- new ColorRange(
- new Range<>(120f, 140f),
- new Range<>(0.7f, 1f),
- new Range<>(0.375f, 0.52f)),
- new ColorRange(
- new Range<>(120f, 140f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.435f, 0.524f)),
-
- // Green Blue 140
- new ColorRange(
- new Range<>(140f, 160f),
- new Range<>(0.7f, 1f),
- new Range<>(0.496f, 0.641f)),
-
- // Seafoam
- new ColorRange(
- new Range<>(160f, 180f),
- new Range<>(0.7f, 1f),
- new Range<>(0.496f, 0.567f)),
-
- // Cyan
- new ColorRange(
- new Range<>(180f, 200f),
- new Range<>(0.7f, 1f),
- new Range<>(0.52f, 0.729f)),
-
- // Blue
- new ColorRange(
- new Range<>(220f, 240f),
- new Range<>(0.7f, 1f),
- new Range<>(0.396f, 0.571f)),
- new ColorRange(
- new Range<>(220f, 240f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.425f, 0.551f)),
-
- // Blue Purple 240
- new ColorRange(
- new Range<>(240f, 260f),
- new Range<>(0.7f, 1f),
- new Range<>(0.418f, 0.639f)),
- new ColorRange(
- new Range<>(220f, 240f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.441f, 0.576f)),
-
- // Blue Purple 260
- new ColorRange(
- new Range<>(260f, 280f),
- new Range<>(0.3f, 1f), // Bigger range
- new Range<>(0.461f, 0.553f)),
-
- // Fuchsia
- new ColorRange(
- new Range<>(300f, 320f),
- new Range<>(0.7f, 1f),
- new Range<>(0.484f, 0.588f)),
- new ColorRange(
- new Range<>(300f, 320f),
- new Range<>(0.3f, 0.7f),
- new Range<>(0.48f, 0.592f)),
-
- // Pink
- new ColorRange(
- new Range<>(320f, 340f),
- new Range<>(0.7f, 1f),
- new Range<>(0.466f, 0.629f)),
-
- // Soft red
- new ColorRange(
- new Range<>(340f, 360f),
- new Range<>(0.7f, 1f),
- new Range<>(0.437f, 0.596f))
- };
-
- /**
- * Representation of an HSL color range.
- * <ul>
- * <li>hsl[0] is Hue [0 .. 360)</li>
- * <li>hsl[1] is Saturation [0...1]</li>
- * <li>hsl[2] is Lightness [0...1]</li>
- * </ul>
- */
- static class ColorRange {
- private Range<Float> mHue;
- private Range<Float> mSaturation;
- private Range<Float> mLightness;
-
- ColorRange(Range<Float> hue, Range<Float> saturation, Range<Float> lightness) {
- mHue = hue;
- mSaturation = saturation;
- mLightness = lightness;
- }
-
- boolean containsColor(float h, float s, float l) {
- if (!mHue.contains(h)) {
- return false;
- } else if (!mSaturation.contains(s)) {
- return false;
- } else if (!mLightness.contains(l)) {
- return false;
- }
- return true;
- }
-
- float[] getCenter() {
- return new float[] {
- mHue.getLower() + (mHue.getUpper() - mHue.getLower()) / 2f,
- mSaturation.getLower() + (mSaturation.getUpper() - mSaturation.getLower()) / 2f,
- mLightness.getLower() + (mLightness.getUpper() - mLightness.getLower()) / 2f
- };
- }
-
- @Override
- public String toString() {
- return String.format("H: %s, S: %s, L %s", mHue, mSaturation, mLightness);
- }
- }
-
- private static List<Integer> getMainColors(WallpaperColorsCompat wallpaperColors) {
- LinkedList<Integer> colors = new LinkedList<>();
- if (wallpaperColors.getPrimaryColor() != 0) {
- colors.add(wallpaperColors.getPrimaryColor());
- }
- if (wallpaperColors.getSecondaryColor() != 0) {
- colors.add(wallpaperColors.getSecondaryColor());
- }
- if (wallpaperColors.getTertiaryColor() != 0) {
- colors.add(wallpaperColors.getTertiaryColor());
- }
- return colors;
- }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperColorsCompat.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperColorsCompat.java
deleted file mode 100644
index d984a84..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperColorsCompat.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.dynamicui;
-
-/**
- * A compatibility layer around platform implementation of WallpaperColors
- */
-public class WallpaperColorsCompat {
-
- public static final int HINT_SUPPORTS_DARK_TEXT = 0x1;
- public static final int HINT_SUPPORTS_DARK_THEME = 0x2;
-
- private final int mPrimaryColor;
- private final int mSecondaryColor;
- private final int mTertiaryColor;
- private final int mColorHints;
-
- public WallpaperColorsCompat(int primaryColor, int secondaryColor, int tertiaryColor,
- int colorHints) {
- mPrimaryColor = primaryColor;
- mSecondaryColor = secondaryColor;
- mTertiaryColor = tertiaryColor;
- mColorHints = colorHints;
- }
-
- public int getPrimaryColor() {
- return mPrimaryColor;
- }
-
- public int getSecondaryColor() {
- return mSecondaryColor;
- }
-
- public int getTertiaryColor() {
- return mTertiaryColor;
- }
-
- public int getColorHints() {
- return mColorHints;
- }
-
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
deleted file mode 100644
index 0fd0a35..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides.dynamicui;
-
-import android.content.Context;
-
-import com.android.launcher3.Utilities;
-
-import androidx.annotation.Nullable;
-
-public abstract class WallpaperManagerCompat {
-
- private static final Object sInstanceLock = new Object();
- private static WallpaperManagerCompat sInstance;
-
- public static WallpaperManagerCompat getInstance(Context context) {
- synchronized (sInstanceLock) {
- if (sInstance == null) {
- context = context.getApplicationContext();
-
- if (Utilities.ATLEAST_OREO_MR1) {
- try {
- sInstance = new WallpaperManagerCompatVOMR1(context);
- } catch (Throwable e) {
- // The wallpaper APIs do not yet exist
- }
- }
- if (sInstance == null) {
- sInstance = new WallpaperManagerCompatVL(context);
- }
- }
- return sInstance;
- }
- }
-
-
- public abstract @Nullable WallpaperColorsCompat getWallpaperColors(int which);
-
- public abstract void addOnColorsChangedListener(OnColorsChangedListenerCompat listener);
-
- /**
- * Interface definition for a callback to be invoked when colors change on a wallpaper.
- */
- public interface OnColorsChangedListenerCompat {
-
- void onColorsChanged(WallpaperColorsCompat colors, int which);
- }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompatVL.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompatVL.java
deleted file mode 100644
index 500fdc3..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompatVL.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.dynamicui;
-
-import static android.app.WallpaperManager.FLAG_SYSTEM;
-
-import static com.android.launcher3.Utilities.getDevicePrefs;
-
-import android.app.WallpaperInfo;
-import android.app.WallpaperManager;
-import android.app.job.JobInfo;
-import android.app.job.JobParameters;
-import android.app.job.JobScheduler;
-import android.app.job.JobService;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.pm.PermissionInfo;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.BitmapRegionDecoder;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-import android.util.Pair;
-
-import com.android.launcher3.icons.ColorExtractor;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-import androidx.annotation.Nullable;
-
-public class WallpaperManagerCompatVL extends WallpaperManagerCompat {
-
- private static final String TAG = "WMCompatVL";
-
- private static final String VERSION_PREFIX = "1,";
- private static final String KEY_COLORS = "wallpaper_parsed_colors";
- private static final String ACTION_EXTRACTION_COMPLETE =
- "com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompatVL.EXTRACTION_COMPLETE";
-
- public static final int WALLPAPER_COMPAT_JOB_ID = 1;
-
- private final ArrayList<OnColorsChangedListenerCompat> mListeners = new ArrayList<>();
-
- private final Context mContext;
- private WallpaperColorsCompat mColorsCompat;
-
- WallpaperManagerCompatVL(Context context) {
- mContext = context;
-
- String colors = getDevicePrefs(mContext).getString(KEY_COLORS, "");
- int wallpaperId = -1;
- if (colors.startsWith(VERSION_PREFIX)) {
- Pair<Integer, WallpaperColorsCompat> storedValue = parseValue(colors);
- wallpaperId = storedValue.first;
- mColorsCompat = storedValue.second;
- }
-
- if (wallpaperId == -1 || wallpaperId != getWallpaperId(context)) {
- reloadColors();
- }
- context.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- reloadColors();
- }
- }, new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED));
-
- // Register a receiver for results
- String permission = null;
- // Find a permission which only we can use.
- try {
- for (PermissionInfo info : context.getPackageManager().getPackageInfo(
- context.getPackageName(),
- PackageManager.GET_PERMISSIONS).permissions) {
- if ((info.protectionLevel & PermissionInfo.PROTECTION_SIGNATURE) != 0) {
- permission = info.name;
- }
- }
- } catch (PackageManager.NameNotFoundException e) {
- // Something went wrong. ignore
- Log.d(TAG, "Unable to get permission info", e);
- }
- mContext.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- handleResult(intent.getStringExtra(KEY_COLORS));
- }
- }, new IntentFilter(ACTION_EXTRACTION_COMPLETE), permission, new Handler());
- }
-
- @Nullable
- @Override
- public WallpaperColorsCompat getWallpaperColors(int which) {
- return which == FLAG_SYSTEM ? mColorsCompat : null;
- }
-
- @Override
- public void addOnColorsChangedListener(OnColorsChangedListenerCompat listener) {
- mListeners.add(listener);
- }
-
- private void reloadColors() {
- JobInfo job = new JobInfo.Builder(WALLPAPER_COMPAT_JOB_ID,
- new ComponentName(mContext, ColorExtractionService.class))
- .setMinimumLatency(0).build();
- ((JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(job);
- }
-
- private void handleResult(String result) {
- getDevicePrefs(mContext).edit().putString(KEY_COLORS, result).apply();
- mColorsCompat = parseValue(result).second;
- for (OnColorsChangedListenerCompat listener : mListeners) {
- listener.onColorsChanged(mColorsCompat, FLAG_SYSTEM);
- }
- }
-
- private static final int getWallpaperId(Context context) {
- return context.getSystemService(WallpaperManager.class).getWallpaperId(FLAG_SYSTEM);
- }
-
- /**
- * Parses the stored value and returns the wallpaper id and wallpaper colors.
- */
- private static Pair<Integer, WallpaperColorsCompat> parseValue(String value) {
- String[] parts = value.split(",");
- Integer wallpaperId = Integer.parseInt(parts[1]);
- if (parts.length == 2) {
- // There is no wallpaper color info present, eg when live wallpaper has no preview.
- return Pair.create(wallpaperId, null);
- }
-
- int primary = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
- int secondary = parts.length > 3 ? Integer.parseInt(parts[3]) : 0;
- int tertiary = parts.length > 4 ? Integer.parseInt(parts[4]) : 0;
-
- return Pair.create(wallpaperId, new WallpaperColorsCompat(primary, secondary, tertiary,
- 0 /* hints */));
- }
-
- /**
- * Intent service to handle color extraction
- */
- public static class ColorExtractionService extends JobService implements Runnable {
- private static final int MAX_WALLPAPER_EXTRACTION_AREA = 112 * 112;
-
- private HandlerThread mWorkerThread;
- private Handler mWorkerHandler;
- private ColorExtractor mColorExtractor;
-
- @Override
- public void onCreate() {
- super.onCreate();
- mWorkerThread = new HandlerThread("ColorExtractionService");
- mWorkerThread.start();
- mWorkerHandler = new Handler(mWorkerThread.getLooper());
- mColorExtractor = new ColorExtractor();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mWorkerThread.quit();
- }
-
- @Override
- public boolean onStartJob(final JobParameters jobParameters) {
- mWorkerHandler.post(this);
- return true;
- }
-
- @Override
- public boolean onStopJob(JobParameters jobParameters) {
- mWorkerHandler.removeCallbacksAndMessages(null);
- return true;
- }
-
- /**
- * Extracts the wallpaper colors and sends the result back through the receiver.
- */
- @Override
- public void run() {
- int wallpaperId = getWallpaperId(this);
-
- Bitmap bitmap = null;
- Drawable drawable = null;
-
- WallpaperManager wm = WallpaperManager.getInstance(this);
- WallpaperInfo info = wm.getWallpaperInfo();
- if (info != null) {
- // For live wallpaper, extract colors from thumbnail
- drawable = info.loadThumbnail(getPackageManager());
- } else {
- try (ParcelFileDescriptor fd = wm.getWallpaperFile(FLAG_SYSTEM)) {
- BitmapRegionDecoder decoder = BitmapRegionDecoder
- .newInstance(fd.getFileDescriptor(), false);
-
- int requestedArea = decoder.getWidth() * decoder.getHeight();
- BitmapFactory.Options options = new BitmapFactory.Options();
-
- if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
- double areaRatio =
- (double) requestedArea / MAX_WALLPAPER_EXTRACTION_AREA;
- double nearestPowOf2 =
- Math.floor(Math.log(areaRatio) / (2 * Math.log(2)));
- options.inSampleSize = (int) Math.pow(2, nearestPowOf2);
- }
- Rect region = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
- bitmap = decoder.decodeRegion(region, options);
- decoder.recycle();
- } catch (IOException | NullPointerException e) {
- Log.e(TAG, "Fetching partial bitmap failed, trying old method", e);
- }
- if (bitmap == null) {
- drawable = wm.getDrawable();
- }
- }
-
- if (drawable != null) {
- // Calculate how big the bitmap needs to be.
- // This avoids unnecessary processing and allocation inside Palette.
- final int requestedArea = drawable.getIntrinsicWidth() *
- drawable.getIntrinsicHeight();
- double scale = 1;
- if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
- scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea);
- }
- bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * scale),
- (int) (drawable.getIntrinsicHeight() * scale), Bitmap.Config.ARGB_8888);
- final Canvas bmpCanvas = new Canvas(bitmap);
- drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
- drawable.draw(bmpCanvas);
- }
-
- String value = VERSION_PREFIX + wallpaperId;
-
- if (bitmap != null) {
- int color = mColorExtractor.findDominantColorByHue(bitmap,
- MAX_WALLPAPER_EXTRACTION_AREA);
- value += "," + color;
- }
-
- // Send the result
- sendBroadcast(new Intent(ACTION_EXTRACTION_COMPLETE)
- .setPackage(getPackageName())
- .putExtra(KEY_COLORS, value));
- }
- }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompatVOMR1.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompatVOMR1.java
deleted file mode 100644
index f34497d..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompatVOMR1.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides.dynamicui;
-
-import android.annotation.TargetApi;
-import android.app.WallpaperColors;
-import android.app.WallpaperManager;
-import android.app.WallpaperManager.OnColorsChangedListener;
-import android.content.Context;
-import android.graphics.Color;
-import android.util.Log;
-
-import java.lang.reflect.Method;
-
-import androidx.annotation.Nullable;
-
-@TargetApi(27)
-public class WallpaperManagerCompatVOMR1 extends WallpaperManagerCompat {
-
- private static final String TAG = "WMCompatVOMR1";
-
- private final WallpaperManager mWm;
- private Method mWCColorHintsMethod;
-
- WallpaperManagerCompatVOMR1(Context context) throws Throwable {
- mWm = context.getSystemService(WallpaperManager.class);
- String className = WallpaperColors.class.getName();
- try {
- mWCColorHintsMethod = WallpaperColors.class.getDeclaredMethod("getColorHints");
- } catch (Exception exc) {
- Log.e(TAG, "getColorHints not available", exc);
- }
- }
-
- @Nullable
- @Override
- public WallpaperColorsCompat getWallpaperColors(int which) {
- return convertColorsObject(mWm.getWallpaperColors(which));
- }
-
- @Override
- public void addOnColorsChangedListener(final OnColorsChangedListenerCompat listener) {
- OnColorsChangedListener onChangeListener = new OnColorsChangedListener() {
- @Override
- public void onColorsChanged(WallpaperColors colors, int which) {
- listener.onColorsChanged(convertColorsObject(colors), which);
- }
- };
- mWm.addOnColorsChangedListener(onChangeListener, null);
- }
-
- private WallpaperColorsCompat convertColorsObject(WallpaperColors colors) {
- if (colors == null) {
- return null;
- }
- Color primary = colors.getPrimaryColor();
- Color secondary = colors.getSecondaryColor();
- Color tertiary = colors.getTertiaryColor();
- int primaryVal = primary != null ? primary.toArgb() : 0;
- int secondaryVal = secondary != null ? secondary.toArgb() : 0;
- int tertiaryVal = tertiary != null ? tertiary.toArgb() : 0;
- int colorHints = 0;
- try {
- if (mWCColorHintsMethod != null) {
- colorHints = (Integer) mWCColorHintsMethod.invoke(colors);
- }
- } catch (Exception exc) {
- Log.e(TAG, "error calling color hints", exc);
- }
- return new WallpaperColorsCompat(primaryVal, secondaryVal, tertiaryVal, colorHints);
- }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
index ec3f93f..978c321 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -16,13 +16,14 @@
package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
import android.content.Context;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.util.Themes;
/**
* Definition for AllApps state
@@ -41,7 +42,7 @@
};
public AllAppsState(int id) {
- super(id, ContainerType.ALLAPPS, STATE_FLAGS);
+ super(id, LAUNCHER_STATE_ALLAPPS, STATE_FLAGS);
}
@Override
@@ -56,7 +57,7 @@
@Override
public int getVisibleElements(Launcher launcher) {
- return ALL_APPS_HEADER | ALL_APPS_CONTENT;
+ return ALL_APPS_CONTENT;
}
@Override
@@ -74,4 +75,9 @@
public float getVerticalProgress(Launcher launcher) {
return 0f;
}
+
+ @Override
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return Themes.getAttrColor(launcher, R.attr.allAppsScrimColor);
+ }
}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
index 7a6332c..d154317 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -15,10 +15,14 @@
*/
package com.android.launcher3.uioverrides.states;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
+
import android.content.Context;
+import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
/**
* Definition for overview state
@@ -26,7 +30,7 @@
public class OverviewState extends LauncherState {
public OverviewState(int id) {
- super(id, ContainerType.WORKSPACE, FLAG_DISABLE_RESTORE);
+ super(id, LAUNCHER_STATE_OVERVIEW, FLAG_DISABLE_RESTORE);
}
@Override
@@ -38,10 +42,6 @@
return new OverviewState(id);
}
- public static OverviewState newPeekState(int id) {
- return new OverviewState(id);
- }
-
public static OverviewState newSwitchState(int id) {
return new OverviewState(id);
}
@@ -52,4 +52,16 @@
public static OverviewState newModalTaskState(int id) {
return new OverviewState(id);
}
+
+ /**
+ * New Overview substate that represents the overview in modal mode (one task shown on its own)
+ */
+ public static OverviewState newSplitSelectState(int id) {
+ return new OverviewState(id);
+ }
+
+ @Override
+ public int getWorkspaceScrimColor(Launcher launcher) {
+ return Themes.getAttrColor(launcher, R.attr.overviewScrimColor);
+ }
}
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 0000000..da55c28
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,26 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "packages_apps_Launcher3_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["packages_apps_Launcher3_license"],
+}
+
+filegroup {
+ name: "launcher3-test-src-common",
+ srcs: ["src_common/**/*.java"],
+}
diff --git a/tests/Android.mk b/tests/Android.mk
index 4d1bfa6..6adc685 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -16,31 +16,6 @@
LOCAL_PATH := $(call my-dir)
#
-# Build rule for Tapl library.
-#
-include $(CLEAR_VARS)
-LOCAL_STATIC_JAVA_LIBRARIES := \
- androidx.annotation_annotation \
- androidx.test.runner \
- androidx.test.rules \
- androidx.test.uiautomator_uiautomator
-
-ifneq (,$(wildcard frameworks/base))
-else
- LOCAL_STATIC_JAVA_LIBRARIES += SystemUISharedLib
-
- LOCAL_SRC_FILES := $(call all-java-files-under, tapl) \
- ../src/com/android/launcher3/ResourceUtils.java \
- ../src/com/android/launcher3/util/SecureSettingsObserver.java \
- ../src/com/android/launcher3/testing/TestProtocol.java
-endif
-
-LOCAL_MODULE := ub-launcher-aosp-tapl
-LOCAL_SDK_VERSION := system_current
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#
# Build rule for Launcher3Tests
#
include $(CLEAR_VARS)
@@ -53,14 +28,8 @@
mockito-target-minus-junit4 \
launcher_log_protos_lite
-ifneq (,$(wildcard frameworks/base))
- LOCAL_PRIVATE_PLATFORM_APIS := true
- LOCAL_STATIC_JAVA_LIBRARIES += launcher-aosp-tapl
-else
- LOCAL_SDK_VERSION := system_28
- LOCAL_MIN_SDK_VERSION := 21
- LOCAL_STATIC_JAVA_LIBRARIES += ub-launcher-aosp-tapl
-endif
+LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_STATIC_JAVA_LIBRARIES += launcher-aosp-tapl
LOCAL_SRC_FILES := \
$(call all-java-files-under, src) \
@@ -73,6 +42,13 @@
LOCAL_INSTRUMENTATION_FOR := Launcher3
+LOCAL_TEST_CONFIG := Launcher3Tests.xml
+
+LOCAL_COMPATIBILITY_SUPPORT_FILES := $(call intermediates-dir-for,APPS,Launcher3)/package.apk:Launcher3.apk
+
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../NOTICE
include $(BUILD_PACKAGE)
include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 1c8f095..918ec4a 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -29,6 +29,7 @@
<receiver
android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
+ android:exported="true"
android:label="No Config">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -39,6 +40,7 @@
<receiver
android:name="com.android.launcher3.testcomponent.AppWdigetHidden"
+ android:exported="true"
android:label="Hidden widget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -49,6 +51,7 @@
<receiver
android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
+ android:exported="true"
android:label="With Config">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -57,13 +60,26 @@
android:resource="@xml/appwidget_with_config"/>
</receiver>
+ <receiver
+ android:name="com.android.launcher3.testcomponent.AppWidgetDynamicColors"
+ android:exported="true"
+ android:label="Dynamic Colors">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider"
+ android:resource="@xml/appwidget_dynamic_colors"/>
+ </receiver>
+
<activity
- android:name="com.android.launcher3.testcomponent.WidgetConfigActivity">
+ android:name="com.android.launcher3.testcomponent.WidgetConfigActivity"
+ android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
- <activity android:name="com.android.launcher3.testcomponent.CustomShortcutConfigActivity">
+ <activity android:name="com.android.launcher3.testcomponent.CustomShortcutConfigActivity"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
@@ -72,6 +88,7 @@
<activity
android:name="com.android.launcher3.testcomponent.RequestPinItemActivity"
android:icon="@drawable/test_drawable_pin_item"
+ android:exported="true"
android:label="Test Pin Item">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -92,7 +109,7 @@
<activity
android:name="com.android.launcher3.testcomponent.TestLauncherActivity"
android:clearTaskOnLaunch="true"
- android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|density"
android:enabled="false"
android:label="Test launcher"
android:launchMode="singleTask"
@@ -102,6 +119,7 @@
android:stateNotNeeded="true"
android:taskAffinity=""
android:theme="@android:style/Theme.DeviceDefault.Light"
+ android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -114,6 +132,7 @@
<activity
android:name="com.android.launcher3.testcomponent.BaseTestingActivity"
android:label="LauncherTestApp"
+ android:exported="true"
android:taskAffinity="com.android.launcher3.testcomponent.Affinity1">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -128,6 +147,7 @@
</activity>
<activity-alias android:name="Activity2"
android:label="TestActivity2"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -136,6 +156,7 @@
</activity-alias>
<activity-alias android:name="Activity3"
android:label="TestActivity3"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -144,6 +165,7 @@
</activity-alias>
<activity-alias android:name="Activity4"
android:label="TestActivity4"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -152,6 +174,7 @@
</activity-alias>
<activity-alias android:name="Activity5"
android:label="TestActivity5"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -160,6 +183,7 @@
</activity-alias>
<activity-alias android:name="Activity6"
android:label="TestActivity6"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -168,6 +192,7 @@
</activity-alias>
<activity-alias android:name="Activity7"
android:label="TestActivity7"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -176,6 +201,7 @@
</activity-alias>
<activity-alias android:name="Activity8"
android:label="TestActivity8"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -184,6 +210,7 @@
</activity-alias>
<activity-alias android:name="Activity9"
android:label="TestActivity9"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -192,6 +219,7 @@
</activity-alias>
<activity-alias android:name="Activity10"
android:label="TestActivity10"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -200,6 +228,7 @@
</activity-alias>
<activity-alias android:name="Activity11"
android:label="TestActivity11"
+ android:exported="true"
android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
diff --git a/tests/Launcher3Tests.xml b/tests/Launcher3Tests.xml
new file mode 100644
index 0000000..3fff622
--- /dev/null
+++ b/tests/Launcher3Tests.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- This test config file is auto-generated. -->
+<configuration description="Runs Launcher3 tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="set-test-harness" value="true" />
+ <option name="run-command" value="am force-stop com.android.launcher3" />
+ <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher" />
+ <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher.out_of_proc_tests" />
+ <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher.tests" />
+ <option name="run-command" value="pm disable com.google.android.googlequicksearchbox" />
+
+ <option name="run-command" value="input keyevent 82" />
+ <option name="run-command" value="settings delete secure assistant" />
+ <option name="run-command" value="settings put global airplane_mode_on 1" />
+ <option name="run-command" value="am broadcast -a android.intent.action.AIRPLANE_MODE" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="Launcher3Tests.apk" />
+ <option name="test-file-name" value="Launcher3.apk" />
+ </target_preparer>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/data/user/0/com.android.launcher3/files" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.launcher3.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/tests/dummy_app/Android.mk b/tests/dummy_app/Android.mk
index f4ab582..3472079 100644
--- a/tests/dummy_app/Android.mk
+++ b/tests/dummy_app/Android.mk
@@ -7,6 +7,9 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := Aardwolf
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../NOTICE
LOCAL_SDK_VERSION := current
diff --git a/tests/dummy_app/AndroidManifest.xml b/tests/dummy_app/AndroidManifest.xml
index f00138c..d5e2320 100644
--- a/tests/dummy_app/AndroidManifest.xml
+++ b/tests/dummy_app/AndroidManifest.xml
@@ -26,6 +26,7 @@
<activity
android:name="Activity1"
android:icon="@mipmap/ic_launcher1"
+ android:exported="true"
android:label="Aardwolf">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
diff --git a/tests/res/layout/test_layout_appwidget_dynamic_colors.xml b/tests/res/layout/test_layout_appwidget_dynamic_colors.xml
new file mode 100644
index 0000000..56c343e
--- /dev/null
+++ b/tests/res/layout/test_layout_appwidget_dynamic_colors.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:background="?android:attr/colorBackground"
+ android:padding="8dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:orientation = "horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="neut1"/>
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:background="@android:color/system_neutral1_500"/>
+ </LinearLayout>
+ <LinearLayout
+ android:orientation = "horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="accent1"/>
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:background="@android:color/system_accent1_500"/>
+ </LinearLayout>
+ <LinearLayout
+ android:orientation = "horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="accent2"/>
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:background="@android:color/system_accent2_500"/>
+ </LinearLayout>
+
+ </LinearLayout>
\ No newline at end of file
diff --git a/tests/res/xml/appwidget_dynamic_colors.xml b/tests/res/xml/appwidget_dynamic_colors.xml
new file mode 100644
index 0000000..f6b9a04
--- /dev/null
+++ b/tests/res/xml/appwidget_dynamic_colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="1dp"
+ android:minHeight="1dp"
+ android:updatePeriodMillis="0"
+ android:initialLayout="@layout/test_layout_appwidget_dynamic_colors"
+ android:resizeMode="horizontal|vertical"
+ android:widgetCategory="home_screen">
+</appwidget-provider>
\ No newline at end of file
diff --git a/tests/res/xml/appwidget_hidden.xml b/tests/res/xml/appwidget_hidden.xml
index 6f0e006..f6cffb5 100644
--- a/tests/res/xml/appwidget_hidden.xml
+++ b/tests/res/xml/appwidget_hidden.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
- android:minWidth="180dp"
- android:minHeight="110dp"
+ android:minWidth="1dp"
+ android:minHeight="1dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/test_layout_appwidget_blue"
android:resizeMode="horizontal|vertical"
diff --git a/tests/res/xml/appwidget_no_config.xml b/tests/res/xml/appwidget_no_config.xml
index d24dfe3..0d932dc 100644
--- a/tests/res/xml/appwidget_no_config.xml
+++ b/tests/res/xml/appwidget_no_config.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
- android:minWidth="180dp"
- android:minHeight="110dp"
+ android:minWidth="1dp"
+ android:minHeight="1dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/test_layout_appwidget_red"
android:resizeMode="horizontal|vertical"
diff --git a/tests/res/xml/appwidget_with_config.xml b/tests/res/xml/appwidget_with_config.xml
index 8403689..813e6df 100644
--- a/tests/res/xml/appwidget_with_config.xml
+++ b/tests/res/xml/appwidget_with_config.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
- android:minWidth="180dp"
- android:minHeight="110dp"
+ android:minWidth="1dp"
+ android:minHeight="1dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/test_layout_appwidget_blue"
android:configure="com.android.launcher3.testcomponent.WidgetConfigActivity"
diff --git a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
deleted file mode 100644
index bdf01f3..0000000
--- a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.allapps.search;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ComponentName;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.model.data.AppInfo;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link DefaultAppSearchAlgorithm}
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class DefaultAppSearchAlgorithmTest {
- private static final DefaultAppSearchAlgorithm.StringMatcher MATCHER =
- DefaultAppSearchAlgorithm.StringMatcher.getInstance();
-
- @Test
- public void testMatches() {
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white cow"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCow"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCOW"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCOW"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white2cow"), "cow", MATCHER));
-
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecow"), "cow", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitEcow"), "cow", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCow"), "cow", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecow cow"), "cow", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecowcow"), "cow", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whit ecowcow"), "cow", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&dogs"), "dog", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "dog", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "&", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "43", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "3", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Q"), "q", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo(" Q"), "q", MATCHER));
-
- // match lower case words
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("elephant"), "e", MATCHER));
-
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电子", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "子", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "邮件", MATCHER));
-
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("Bot"), "ba", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("bot"), "ba", MATCHER));
- }
-
- @Test
- public void testMatchesVN() {
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드"), "다", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("드라이브"), "드", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷ", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("운로 드라이브"), "ㄷ", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åbç", MATCHER));
- assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Alpha"), "ål", MATCHER));
-
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷㄷ", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("로드라이브"), "ㄷ", MATCHER));
- assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åç", MATCHER));
- }
-
- private AppInfo getInfo(String title) {
- AppInfo info = new AppInfo();
- info.title = title;
- info.componentName = new ComponentName("Test", title);
- return info;
- }
-}
diff --git a/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java
new file mode 100644
index 0000000..413f404
--- /dev/null
+++ b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.search;
+
+import static com.android.launcher3.search.StringMatcherUtility.matches;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link StringMatcherUtility}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StringMatcherUtilityTest {
+ private static final StringMatcher MATCHER =
+ StringMatcher.getInstance();
+
+ @Test
+ public void testMatches() {
+ assertTrue(matches("white ", "white cow", MATCHER));
+ assertTrue(matches("white c", "white cow", MATCHER));
+ assertTrue(matches("cow", "white cow", MATCHER));
+ assertTrue(matches("cow", "whiteCow", MATCHER));
+ assertTrue(matches("cow", "whiteCOW", MATCHER));
+ assertTrue(matches("cow", "whitecowCOW", MATCHER));
+ assertTrue(matches("cow", "white2cow", MATCHER));
+
+ assertFalse(matches("cow", "whitecow", MATCHER));
+ assertFalse(matches("cow", "whitEcow", MATCHER));
+
+ assertTrue(matches("cow", "whitecowCow", MATCHER));
+ assertTrue(matches("cow", "whitecow cow", MATCHER));
+ assertFalse(matches("cow", "whitecowcow", MATCHER));
+ assertFalse(matches("cow", "whit ecowcow", MATCHER));
+
+ assertTrue(matches("dog", "cats&dogs", MATCHER));
+ assertTrue(matches("dog", "cats&Dogs", MATCHER));
+ assertTrue(matches("&", "cats&Dogs", MATCHER));
+
+ assertTrue(matches("43", "2+43", MATCHER));
+ assertFalse(matches("3", "2+43", MATCHER));
+
+ assertTrue(matches("q", "Q", MATCHER));
+ assertTrue(matches("q", " Q", MATCHER));
+
+ // match lower case words
+ assertTrue(matches("e", "elephant", MATCHER));
+ assertTrue(matches("eL", "Elephant", MATCHER));
+
+ assertTrue(matches("电", "电子邮件", MATCHER));
+ assertTrue(matches("电子", "电子邮件", MATCHER));
+ assertTrue(matches("子", "电子邮件", MATCHER));
+ assertTrue(matches("邮件", "电子邮件", MATCHER));
+
+ assertFalse(matches("ba", "Bot", MATCHER));
+ assertFalse(matches("ba", "bot", MATCHER));
+ assertFalse(matches("phant", "elephant", MATCHER));
+ assertFalse(matches("elephants", "elephant", MATCHER));
+ }
+
+ @Test
+ public void testMatchesVN() {
+ assertTrue(matches("다", "다운로드", MATCHER));
+ assertTrue(matches("드", "드라이브", MATCHER));
+ assertTrue(matches("ㄷ", "다운로드 드라이브", MATCHER));
+ assertTrue(matches("ㄷ", "운로 드라이브", MATCHER));
+ assertTrue(matches("åbç", "abc", MATCHER));
+ assertTrue(matches("ål", "Alpha", MATCHER));
+
+ assertFalse(matches("ㄷㄷ", "다운로드 드라이브", MATCHER));
+ assertFalse(matches("ㄷ", "로드라이브", MATCHER));
+ assertFalse(matches("åç", "abc", MATCHER));
+ }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java b/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java
new file mode 100644
index 0000000..5fb3454
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.testcomponent;
+
+import android.appwidget.AppWidgetProvider;
+
+/**
+ * A simple app widget showing a primary, secondary and neutral color.
+ */
+public class AppWidgetDynamicColors extends AppWidgetProvider {
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/ListViewService.java b/tests/src/com/android/launcher3/testcomponent/ListViewService.java
index 3da20e0..9e3a492 100644
--- a/tests/src/com/android/launcher3/testcomponent/ListViewService.java
+++ b/tests/src/com/android/launcher3/testcomponent/ListViewService.java
@@ -89,7 +89,7 @@
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return SimpleViewsFactory.this;
}
- }.onBind(new Intent("dummy_intent"));
+ }.onBind(new Intent("stub_intent"));
}
}
}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 858e183..dcb6dc1 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -53,7 +53,6 @@
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.common.WidgetUtils;
-import com.android.launcher3.model.AppLaunchTracker;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.tapl.LauncherInstrumentation;
@@ -64,9 +63,9 @@
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.rule.FailureRewriterRule;
import com.android.launcher3.util.rule.FailureWatcher;
import com.android.launcher3.util.rule.LauncherActivityRule;
+import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.ShellCommandRule;
import com.android.launcher3.util.rule.TestStabilityRule;
@@ -101,6 +100,7 @@
private static final String TAG = "AbstractLauncherUiTest";
private static String sStrictmodeDetectedActivityLeak;
+ private static boolean sDumpWasGenerated = false;
private static boolean sActivityLeakReported;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
protected static final ActivityLeakTracker ACTIVITY_LEAK_TRACKER = new ActivityLeakTracker();
@@ -116,8 +116,6 @@
if (TestHelpers.isInLauncherProcess()) {
StrictMode.VmPolicy.Builder builder =
new StrictMode.VmPolicy.Builder()
-// b/154772063
-// .detectActivityLeaks()
.penaltyLog()
.penaltyListener(Runnable::run, violation -> {
if (sStrictmodeDetectedActivityLeak == null) {
@@ -141,7 +139,7 @@
// Check whether activity leak detector has found leaked activities.
Wait.atMost(AbstractLauncherUiTest::getActivityLeakErrorMessage,
() -> {
- launcher.getTotalPssKb(); // Triggers GC
+ launcher.forceGc();
return MAIN_EXECUTOR.submit(
() -> ACTIVITY_LEAK_TRACKER.noLeakedActivities()).get();
}, DEFAULT_UI_TIMEOUT, launcher);
@@ -152,16 +150,34 @@
return "Activity leak detector has found leaked activities, " + dumpHprofData() + ".";
}
- private static String dumpHprofData() {
- try {
- final String fileName = getInstrumentation().getTargetContext().getFilesDir().getPath()
- + "/ActivityLeakHeapDump.hprof";
- Debug.dumpHprofData(fileName);
- return "memory dump filename: " + fileName;
- } catch (Throwable e) {
- Log.e(TAG, "dumpHprofData failed", e);
- return "failed to save memory dump";
+ public static String dumpHprofData() {
+ String result;
+ if (sDumpWasGenerated) {
+ Log.d("b/195319692", "dump has already been generated by another test",
+ new Exception());
+ result = "dump has already been generated by another test";
+ } else {
+ try {
+ final String fileName =
+ getInstrumentation().getTargetContext().getFilesDir().getPath()
+ + "/ActivityLeakHeapDump.hprof";
+ if (TestHelpers.isInLauncherProcess()) {
+ Debug.dumpHprofData(fileName);
+ } else {
+ final UiDevice device = UiDevice.getInstance(getInstrumentation());
+ device.executeShellCommand(
+ "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
+ }
+ sDumpWasGenerated = true;
+ Log.d("b/195319692", "sDumpWasGenerated := true", new Exception());
+ result = "memory dump filename: " + fileName;
+ } catch (Throwable e) {
+ Log.e(TAG, "dumpHprofData failed", e);
+ result = "failed to save memory dump";
+ }
}
+ return result
+ + ". Full list of activities: " + ACTIVITY_LEAK_TRACKER.getActivitiesList();
}
protected AbstractLauncherUiTest() {
@@ -192,6 +208,9 @@
public ShellCommandRule mDisableHeadsUpNotification =
ShellCommandRule.disableHeadsUpNotification();
+ @Rule
+ public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
+
protected void clearPackageData(String pkg) throws IOException, InterruptedException {
final CountDownLatch count = new CountDownLatch(2);
final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@@ -217,7 +236,7 @@
protected TestRule getRulesInsideActivityMonitor() {
final RuleChain inner = RuleChain.outerRule(new PortraitLandscapeRunner(this))
- .around(new FailureWatcher(mDevice));
+ .around(new FailureWatcher(mDevice, mLauncher));
return TestHelpers.isInLauncherProcess()
? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher())
@@ -226,9 +245,8 @@
}
@Rule
- public TestRule mOrderSensitiveRules = RuleChain.
- outerRule(new FailureRewriterRule())
- .around(new TestStabilityRule())
+ public TestRule mOrderSensitiveRules = RuleChain
+ .outerRule(new TestStabilityRule())
.around(mActivityMonitor)
.around(getRulesInsideActivityMonitor());
@@ -242,6 +260,7 @@
@Before
public void setUp() throws Exception {
+ mLauncher.onTestStart();
Log.d(TAG, "Before disabling battery defender");
mDevice.executeShellCommand("setprop vendor.battery.defender.disable 1");
Log.d(TAG, "Before enabling stay awake");
@@ -252,7 +271,7 @@
mDevice.waitForIdle();
}
Assert.assertTrue("Keyguard still visible",
- mDevice.wait(
+ TestHelpers.wait(
Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
Log.d(TAG, "Keyguard is not visible");
@@ -272,8 +291,6 @@
}
mLauncherPid = 0;
- // Disable app tracker
- AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
mTargetContext = InstrumentationRegistry.getTargetContext();
mTargetPackage = mTargetContext.getPackageName();
@@ -283,6 +300,8 @@
if (userManager != null) {
for (UserHandle userHandle : userManager.getUserProfiles()) {
if (!userHandle.isSystem()) {
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED,
+ "removing user " + userHandle.getIdentifier());
mDevice.executeShellCommand("pm remove-user " + userHandle.getIdentifier());
}
}
@@ -291,13 +310,16 @@
@After
public void verifyLauncherState() {
- // Limits UI tests affecting tests running after them.
- mLauncher.waitForLauncherInitialized();
- if (mLauncherPid != 0) {
- assertEquals("Launcher crashed, pid mismatch:",
- mLauncherPid, mLauncher.getPid().intValue());
+ try {
+ // Limits UI tests affecting tests running after them.
+ mLauncher.waitForLauncherInitialized();
+ if (mLauncherPid != 0) {
+ assertEquals("Launcher crashed, pid mismatch:",
+ mLauncherPid, mLauncher.getPid().intValue());
+ }
+ } finally {
+ mLauncher.onTestFinish();
}
- checkDetectedLeaks(mLauncher);
}
protected void clearLauncherData() {
@@ -486,8 +508,7 @@
}
getInstrumentation().getTargetContext().startActivity(intent);
assertTrue("App didn't start: " + selector,
- UiDevice.getInstance(getInstrumentation())
- .wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
+ TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
}
public static ActivityInfo resolveSystemAppInfo(String category) {
@@ -506,6 +527,7 @@
// Destroy Launcher activity.
executeOnLauncher(launcher -> {
if (launcher != null) {
+ onLauncherActivityClose(launcher);
launcher.finish();
}
});
@@ -514,7 +536,7 @@
}
protected boolean isInBackground(Launcher launcher) {
- return !launcher.hasBeenResumed();
+ return launcher == null || !launcher.hasBeenResumed();
}
protected boolean isInState(Supplier<LauncherState> state) {
@@ -527,7 +549,7 @@
return launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
}
- private static void checkLauncherIntegrity(
+ private void checkLauncherIntegrity(
Launcher launcher, ContainerType expectedContainerType) {
if (launcher != null) {
final StateManager<LauncherState> stateManager = launcher.getStateManager();
@@ -538,10 +560,8 @@
stableState == stateManager.getState());
final boolean isResumed = launcher.hasBeenResumed();
- assertTrue("hasBeenResumed() != isStarted(), hasBeenResumed(): " + isResumed,
- isResumed == launcher.isStarted());
- assertTrue("hasBeenResumed() != isUserActive(), hasBeenResumed(): " + isResumed,
- isResumed == launcher.isUserActive());
+ final boolean isStarted = launcher.isStarted();
+ checkLauncherState(launcher, expectedContainerType, isResumed, isStarted);
final int ordinal = stableState.ordinal;
@@ -564,8 +584,7 @@
break;
}
case OVERVIEW: {
- assertTrue(
- "Launcher is not resumed in state: " + expectedContainerType,
+ checkLauncherStateInOverview(launcher, expectedContainerType, isStarted,
isResumed);
assertTrue(TestProtocol.stateOrdinalToString(ordinal),
ordinal == TestProtocol.OVERVIEW_STATE_ORDINAL);
@@ -590,4 +609,21 @@
expectedContainerType == ContainerType.FALLBACK_OVERVIEW);
}
}
+
+ protected void checkLauncherState(Launcher launcher, ContainerType expectedContainerType,
+ boolean isResumed, boolean isStarted) {
+ assertTrue("hasBeenResumed() != isStarted(), hasBeenResumed(): " + isResumed,
+ isResumed == isStarted);
+ assertTrue("hasBeenResumed() != isUserActive(), hasBeenResumed(): " + isResumed,
+ isResumed == launcher.isUserActive());
+ }
+
+ protected void checkLauncherStateInOverview(Launcher launcher,
+ ContainerType expectedContainerType, boolean isStarted, boolean isResumed) {
+ assertTrue("Launcher is not resumed in state: " + expectedContainerType,
+ isResumed);
+ }
+
+ protected void onLauncherActivityClose(Launcher launcher) {
+ }
}
diff --git a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
index 202dcb1..2db7472 100644
--- a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
+++ b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
@@ -25,6 +25,7 @@
import com.android.launcher3.tapl.TestHelpers;
import java.util.WeakHashMap;
+import java.util.stream.Collectors;
public class ActivityLeakTracker implements Application.ActivityLifecycleCallbacks {
private final WeakHashMap<Activity, Boolean> mActivities = new WeakHashMap<>();
@@ -73,20 +74,17 @@
}
public boolean noLeakedActivities() {
- int liveActivities = 0;
- int destroyedActivities = 0;
-
for (Activity activity : mActivities.keySet()) {
if (activity.isDestroyed()) {
- ++destroyedActivities;
- } else {
- ++liveActivities;
+ return false;
}
}
- if (liveActivities > 2) return false;
+ return mActivities.size() <= 2;
+ }
- // It's OK to have 1 leaked activity if no active activities exist.
- return liveActivities == 0 ? destroyedActivities <= 1 : destroyedActivities == 0;
+ public String getActivitiesList() {
+ return mActivities.keySet().stream().map(a -> a.getClass().getSimpleName())
+ .collect(Collectors.joining(","));
}
}
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 266f0ae..8f2d528 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -4,6 +4,7 @@
import android.view.Surface;
import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.testing.TestProtocol;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 34e425d..4dd44f4 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -36,11 +36,13 @@
import com.android.launcher3.tapl.AppIconMenuItem;
import com.android.launcher3.tapl.Widgets;
import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
import com.android.launcher3.views.OptionsPopupView;
-import com.android.launcher3.widget.WidgetsFullSheet;
-import com.android.launcher3.widget.WidgetsRecyclerView;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsRecyclerView;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -91,6 +93,7 @@
}
@Test
+ @ScreenRecord //b/187080582
public void testDevicePressMenu() throws Exception {
mDevice.pressMenu();
mDevice.waitForIdle();
@@ -101,7 +104,7 @@
mLauncher.pressHome();
}
- @Test
+ @Ignore
public void testOpenHomeSettingsFromWorkspace() {
mDevice.pressMenu();
mDevice.waitForIdle();
@@ -292,13 +295,15 @@
try {
final AppIconMenu menu = allApps.
getAppIcon(APP_NAME).
- openMenu();
+ openDeepShortcutMenu();
executeOnLauncher(
launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
isOptionsPopupVisible(launcher)));
- menu.getMenuItem(1).launch(getAppPackageName());
+ final AppIconMenuItem menuItem = menu.getMenuItem(1);
+ assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
+ menuItem.launch(getAppPackageName());
} finally {
allApps.unfreeze();
}
@@ -331,19 +336,31 @@
// 1. Open all apps and wait for load complete.
// 2. Find the app and long press it to show shortcuts.
// 3. Press icon center until shortcuts appear
- final AllApps allApps = mLauncher.
- getWorkspace().
- switchToAllApps();
+ final AllApps allApps = mLauncher
+ .getWorkspace()
+ .switchToAllApps();
allApps.freeze();
try {
- final AppIconMenuItem menuItem = allApps.
- getAppIcon(APP_NAME).
- openMenu().
- getMenuItem(0);
- final String shortcutName = menuItem.getText();
+ final AppIconMenu menu = allApps
+ .getAppIcon(APP_NAME)
+ .openDeepShortcutMenu();
+ final AppIconMenuItem menuItem0 = menu.getMenuItem(0);
+ final AppIconMenuItem menuItem2 = menu.getMenuItem(2);
+
+ final AppIconMenuItem menuItem;
+
+ final String expectedShortcutName = "Shortcut 3";
+ if (menuItem0.getText().equals(expectedShortcutName)) {
+ menuItem = menuItem0;
+ } else {
+ final String shortcutName2 = menuItem2.getText();
+ assertEquals("Wrong menu item", expectedShortcutName, shortcutName2);
+ menuItem = menuItem2;
+ }
menuItem.dragToWorkspace(false, false);
- mLauncher.getWorkspace().getWorkspaceAppIcon(shortcutName).launch(getAppPackageName());
+ mLauncher.getWorkspace().getWorkspaceAppIcon(expectedShortcutName)
+ .launch(getAppPackageName());
} finally {
allApps.unfreeze();
}
diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
index eceff34..083f580 100644
--- a/tests/src/com/android/launcher3/ui/TestViewHelpers.java
+++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
@@ -24,9 +24,9 @@
import android.view.View;
import android.view.ViewGroup;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.testcomponent.AppWidgetNoConfig;
import com.android.launcher3.testcomponent.AppWidgetWithConfig;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import java.util.concurrent.Callable;
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
deleted file mode 100644
index 8d571ff..0000000
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright 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.ui;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.widget.TextView;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsPagedView;
-import com.android.launcher3.allapps.WorkModeSwitch;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.views.WorkEduView;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-import java.util.Objects;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class WorkTabTest extends AbstractLauncherUiTest {
-
- private int mProfileUserId;
-
- private static final int WORK_PAGE = AllAppsContainerView.AdapterHolder.WORK;
-
- @Before
- public void createWorkProfile() throws Exception {
- String output =
- mDevice.executeShellCommand(
- "pm create-user --profileOf 0 --managed TestProfile");
- assertTrue("Failed to create work profile", output.startsWith("Success"));
-
- String[] tokens = output.split("\\s+");
- mProfileUserId = Integer.parseInt(tokens[tokens.length - 1]);
-
- mDevice.executeShellCommand("am start-user " + mProfileUserId);
- }
-
- @After
- public void removeWorkProfile() throws Exception {
- mDevice.executeShellCommand("pm remove-user " + mProfileUserId);
- }
-
- @Test
- public void workTabExists() {
- mDevice.pressHome();
- waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
- executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
- waitForLauncherCondition("Personal tab is missing",
- launcher -> launcher.getAppsView().isPersonalTabVisible(), 60000);
- waitForLauncherCondition("Work tab is missing",
- launcher -> launcher.getAppsView().isWorkTabVisible(), 60000);
- }
-
- @Test
- public void toggleWorks() {
- mDevice.pressHome();
- waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
- executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
- waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
- getOnceNotNull("Apps view did not bind",
- launcher -> launcher.getAppsView().getWorkModeSwitch(), 60000);
-
- UserManager userManager = getFromLauncher(l -> l.getSystemService(UserManager.class));
- assertEquals(2, userManager.getUserProfiles().size());
- UserHandle workProfile = getFromLauncher(l -> {
- UserHandle myHandle = Process.myUserHandle();
- List<UserHandle> userProfiles = userManager.getUserProfiles();
- return userProfiles.get(0) == myHandle ? userProfiles.get(1) : userProfiles.get(0);
- });
-
- waitForLauncherCondition("work profile can't be turned off",
- l -> userManager.requestQuietModeEnabled(true, workProfile));
-
- assertTrue(userManager.isQuietModeEnabled(workProfile));
- executeOnLauncher(launcher -> {
- WorkModeSwitch wf = launcher.getAppsView().getWorkModeSwitch();
- ((AllAppsPagedView) launcher.getAppsView().getContentView()).snapToPageImmediately(
- AllAppsContainerView.AdapterHolder.WORK);
- wf.toggle();
- });
- waitForLauncherCondition("Work toggle did not work",
- l -> l.getSystemService(UserManager.class).isQuietModeEnabled(workProfile));
- }
-
- @Test
- public void testWorkEduFlow() {
- mDevice.pressHome();
- waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
- executeOnLauncher(launcher -> launcher.getSharedPrefs().edit().remove(
- WorkEduView.KEY_WORK_EDU_STEP).remove(
- WorkEduView.KEY_LEGACY_WORK_EDU_SEEN).commit());
-
- waitForLauncherCondition("Work tab not setup",
- launcher -> launcher.getAppsView().getContentView() instanceof AllAppsPagedView,
- 60000);
-
- executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
- WorkEduView workEduView = getEduView();
- // verify personal app edu is seen first and click "next"
- executeOnLauncher(l -> {
- assertEquals(((TextView) workEduView.findViewById(R.id.content_text)).getText(),
- l.getResources().getString(R.string.work_profile_edu_personal_apps));
- workEduView.findViewById(R.id.proceed).callOnClick();
- });
- // verify work edu is seen next
- waitForLauncherCondition("Launcher did not show the next edu screen", l ->
- ((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage() == WORK_PAGE
- && ((TextView) workEduView.findViewById(
- R.id.content_text)).getText().equals(
- l.getResources().getString(R.string.work_profile_edu_work_apps)));
- }
-
- @Test
- public void testWorkEduIntermittent() {
- mDevice.pressHome();
- waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
- executeOnLauncher(launcher -> launcher.getSharedPrefs().edit().remove(
- WorkEduView.KEY_WORK_EDU_STEP).remove(
- WorkEduView.KEY_LEGACY_WORK_EDU_SEEN).commit());
-
-
- waitForLauncherCondition("Work tab not setup",
- launcher -> launcher.getAppsView().getContentView() instanceof AllAppsPagedView,
- 60000);
- executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
-
- // verify personal app edu is seen
- getEduView();
-
- // dismiss personal edu
- mDevice.pressHome();
- waitForState("Launcher did not go home", () -> NORMAL);
-
- // open work tab
- executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
- waitForState("Launcher did not switch to all apps", () -> ALL_APPS);
- executeOnLauncher(launcher -> {
- AllAppsPagedView pagedView = (AllAppsPagedView) launcher.getAppsView().getContentView();
- pagedView.setCurrentPage(WORK_PAGE);
- });
-
- WorkEduView workEduView = getEduView();
-
- // verify work tab edu is shown
- waitForLauncherCondition("Launcher did not show the next edu screen",
- l -> ((TextView) workEduView.findViewById(R.id.content_text)).getText().equals(
- l.getResources().getString(R.string.work_profile_edu_work_apps)));
- }
-
-
- private WorkEduView getEduView() {
- waitForLauncherCondition("Edu did not show", l -> {
- DragLayer dragLayer = l.getDragLayer();
- return dragLayer.getChildCount() > 0 && dragLayer.getChildAt(
- dragLayer.getChildCount() - 1) instanceof WorkEduView;
- });
- return getFromLauncher(launcher -> (WorkEduView) launcher.getDragLayer().getChildAt(
- launcher.getDragLayer().getChildCount() - 1));
- }
-
-}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 9d4ccff..4978c01 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -28,7 +28,6 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.Workspace;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -39,6 +38,7 @@
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.Wait.Condition;
import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import org.junit.Before;
import org.junit.Rule;
@@ -92,9 +92,8 @@
// Drag widget to homescreen
WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
- widgets.
- getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager())).
- dragToWorkspace(true, false);
+ widgets.getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))
+ .dragToWorkspace(true, false);
// Widget id for which the config activity was opened
mWidgetId = monitor.getWidgetId();
@@ -103,6 +102,7 @@
setResult(acceptConfig);
if (acceptConfig) {
+ // TODO(b/192655785) Assert widget resize frame is shown and then dismiss it.
Wait.atMost("", new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
} else {
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index f146db5..dad4f2b 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -23,12 +23,12 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.tapl.Widget;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TestViewHelpers;
import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import org.junit.Rule;
import org.junit.Test;
@@ -58,6 +58,8 @@
openAllWidgets().
getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager())).
dragToWorkspace(false, false);
+ // Dismiss widget resize frame.
+ mDevice.pressHome();
assertTrue(mActivityMonitor.itemExists(
(info, view) -> info instanceof LauncherAppWidgetInfo &&
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index a4aa9f2..9c6c317 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -37,7 +37,6 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -47,6 +46,7 @@
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TestViewHelpers;
import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import org.junit.After;
@@ -192,7 +192,7 @@
WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
executeOnLauncher(l -> l.getAppWidgetHost().startListening());
verifyWidgetPresent(info);
- assertNull(mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT));
+ assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100));
}
@Test
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 0e43d81..745dc22 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -30,7 +30,6 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -43,6 +42,7 @@
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.Wait.Condition;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
import com.android.launcher3.util.rule.ShellCommandRule;
import org.junit.Before;
@@ -78,6 +78,7 @@
public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ }
@Test
+ @ScreenRecord //b/192010616
public void testPinWidgetNoConfig() throws Throwable {
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
@@ -86,6 +87,7 @@
}
@Test
+ @ScreenRecord //b/192005114
public void testPinWidgetNoConfig_customPreview() throws Throwable {
// Command to set custom preview
Intent command = RequestPinItemActivity.getCommandIntent(
@@ -126,9 +128,6 @@
private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
Intent... commandIntents) throws Throwable {
- if (!Utilities.ATLEAST_OREO) {
- return;
- }
clearHomescreen();
mDevice.pressHome();
diff --git a/tests/src/com/android/launcher3/util/TestUtil.java b/tests/src/com/android/launcher3/util/TestUtil.java
index 55e5744..67f3902 100644
--- a/tests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/src/com/android/launcher3/util/TestUtil.java
@@ -22,6 +22,8 @@
import androidx.test.uiautomator.UiDevice;
+import org.junit.Assert;
+
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -48,7 +50,10 @@
in.close();
out.close();
- UiDevice.getInstance(getInstrumentation()).executeShellCommand("pm install " + apkFilename);
+ final String result = UiDevice.getInstance(getInstrumentation())
+ .executeShellCommand("pm install " + apkFilename);
+ Assert.assertTrue("Failed to install wellbeing test apk; make sure the device is rooted",
+ "Success".equals(result.replaceAll("\\s+", "")));
}
public static void uninstallDummyApp() throws IOException {
diff --git a/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java b/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
deleted file mode 100644
index e4f520f..0000000
--- a/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util.rule;
-
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-
-import android.os.SystemClock;
-
-import androidx.test.uiautomator.UiDevice;
-
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.regex.Pattern;
-
-class FailureInvestigator {
- private static boolean matches(String regex, CharSequence string) {
- return Pattern.compile(regex).matcher(string).find();
- }
-
- static class LogcatMatch {
- String logcatPattern;
- int bug;
-
- LogcatMatch(String logcatPattern, int bug) {
- this.logcatPattern = logcatPattern;
- this.bug = bug;
- }
- }
-
- static class ExceptionMatch {
- String exceptionPattern;
- LogcatMatch[] logcatMatches;
-
- ExceptionMatch(String exceptionPattern, LogcatMatch[] logcatMatches) {
- this.exceptionPattern = exceptionPattern;
- this.logcatMatches = logcatMatches;
- }
- }
-
- private static final ExceptionMatch[] EXCEPTION_MATCHES = {
- new ExceptionMatch(
- "java.lang.AssertionError: http://go/tapl : Tests are broken by a "
- + "non-Launcher system error: (Phone is locked|Screen is empty)",
- new LogcatMatch[]{
- new LogcatMatch(
- "BroadcastQueue: Can't deliver broadcast to com.android"
- + ".systemui.*Crashing it",
- 147845913),
- new LogcatMatch(
- "Attempt to invoke virtual method 'boolean android\\"
- + ".graphics\\.Bitmap\\.isRecycled\\(\\)' on a null "
- + "object reference",
- 148424291),
- new LogcatMatch(
- "java\\.lang\\.IllegalArgumentException\\: Ranking map "
- + "doesn't contain key",
- 148570537),
- }),
- new ExceptionMatch("Launcher didn't initialize",
- new LogcatMatch[]{
- new LogcatMatch(
- "ActivityManager: Reason: executing service com.google"
- + ".android.apps.nexuslauncher/com.android.launcher3"
- + ".notification.NotificationListener",
- 148238677),
- }),
- };
-
- static int getBugForFailure(CharSequence exception) {
- if ("com.google.android.setupwizard".equals(
- UiDevice.getInstance(getInstrumentation()).getLauncherPackageName())) {
- return 145935261;
- }
-
- if (matches("java\\.lang\\.AssertionError\\: http\\:\\/\\/go\\/tapl \\: want to get "
- + "workspace object; Presence of recents button doesn't match the interaction "
- + "mode, mode\\=ZERO_BUTTON, hasRecents\\=true", exception)) {
- return 148422894;
- }
-
- final String logSinceBoot;
- try {
- final String systemBootTime =
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(
- new Date(System.currentTimeMillis() - SystemClock.elapsedRealtime()));
-
- logSinceBoot =
- UiDevice.getInstance(getInstrumentation())
- .executeShellCommand("logcat -d -t " + systemBootTime.replace(" ", ""));
- } catch (IOException | OutOfMemoryError e) {
- return 0;
- }
-
- if (matches("android\\:\\:uirenderer\\:\\:renderthread\\:\\:EglManager\\:\\:swapBuffers",
- logSinceBoot)) {
- return 148529608;
- }
-
- for (ExceptionMatch exceptionMatch : EXCEPTION_MATCHES) {
- if (matches(exceptionMatch.exceptionPattern, exception)) {
- for (LogcatMatch logcatMatch : exceptionMatch.logcatMatches) {
- if (matches(logcatMatch.logcatPattern, logSinceBoot)) {
- return logcatMatch.bug;
- }
- }
- break;
- }
- }
-
- return 0;
- }
-}
diff --git a/tests/src/com/android/launcher3/util/rule/FailureRewriterRule.java b/tests/src/com/android/launcher3/util/rule/FailureRewriterRule.java
deleted file mode 100644
index 99ddee4..0000000
--- a/tests/src/com/android/launcher3/util/rule/FailureRewriterRule.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util.rule;
-
-import android.util.Log;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-public class FailureRewriterRule implements TestRule {
- private static final String TAG = "FailureRewriter";
-
- @Override
- public Statement apply(Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- try {
- base.evaluate();
- } catch (Throwable e) {
- final int bug = FailureInvestigator.getBugForFailure(e.toString());
- if (bug == 0) throw e;
-
- Log.e(TAG, "Known bug found for the original failure "
- + android.util.Log.getStackTraceString(e));
- throw new AssertionError(
- "Detected a failure that matches a known bug b/" + bug);
- }
- }
- };
- }
-}
diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
index cdda0f0..dc59bdd 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -2,37 +2,39 @@
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.util.Log;
import androidx.test.uiautomator.UiDevice;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
-import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
public class FailureWatcher extends TestWatcher {
private static final String TAG = "FailureWatcher";
final private UiDevice mDevice;
+ private final LauncherInstrumentation mLauncher;
- public FailureWatcher(UiDevice device) {
+ public FailureWatcher(UiDevice device, LauncherInstrumentation launcher) {
mDevice = device;
+ mLauncher = launcher;
}
- private static void dumpViewHierarchy(UiDevice device) {
- final ByteArrayOutputStream stream = new ByteArrayOutputStream();
- try {
- device.dumpWindowHierarchy(stream);
- stream.flush();
- stream.close();
- for (String line : stream.toString().split("\\r?\\n")) {
- Log.e(TAG, line.trim());
- }
- } catch (IOException e) {
- Log.e(TAG, "error dumping XML to logcat", e);
- }
+ @Override
+ protected void succeeded(Description description) {
+ super.succeeded(description);
+ AbstractLauncherUiTest.checkDetectedLeaks(mLauncher);
}
@Override
@@ -42,22 +44,41 @@
public static void onError(UiDevice device, Description description, Throwable e) {
if (device == null) return;
- final String pathname = getInstrumentation().getTargetContext().
- getFilesDir().getPath() + "/TestScreenshot-" + description.getMethodName()
- + ".png";
- Log.e(TAG, "Failed test " + description.getMethodName() +
- ", screenshot will be saved to " + pathname +
- ", track trace is below, UI object dump is further below:\n" +
- Log.getStackTraceString(e));
- dumpViewHierarchy(device);
+ final File parentFile = getInstrumentation().getTargetContext().getFilesDir();
+ final File sceenshot = new File(parentFile,
+ "TestScreenshot-" + description.getMethodName() + ".png");
+ final File hierarchy = new File(parentFile,
+ "Hierarchy-" + description.getMethodName() + ".zip");
- try {
- final String dumpsysResult = device.executeShellCommand(
- "dumpsys activity service TouchInteractionService");
- Log.d(TAG, "TouchInteractionService: " + dumpsysResult);
- } catch (IOException ex) {
+ // Dump window hierarchy
+ try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) {
+ out.putNextEntry(new ZipEntry("bugreport.txt"));
+ dumpStringCommand("dumpsys window windows", out);
+ dumpStringCommand("dumpsys package", out);
+ dumpStringCommand("dumpsys activity service TouchInteractionService", out);
+ out.closeEntry();
+
+ out.putNextEntry(new ZipEntry("visible_windows.zip"));
+ dumpCommand("cmd window dump-visible-window-views", out);
+ out.closeEntry();
+ } catch (IOException ex) { }
+
+ Log.e(TAG, "Failed test " + description.getMethodName()
+ + ",\nscreenshot will be saved to " + sceenshot
+ + ",\nUI dump at: " + hierarchy
+ + " (use go/web-hv to open the dump file)", e);
+ device.takeScreenshot(sceenshot);
+ }
+
+ private static void dumpStringCommand(String cmd, OutputStream out) throws IOException {
+ out.write(("\n\n" + cmd + "\n").getBytes());
+ dumpCommand(cmd, out);
+ }
+
+ private static void dumpCommand(String cmd, OutputStream out) throws IOException {
+ try (AutoCloseInputStream in = new AutoCloseInputStream(getInstrumentation()
+ .getUiAutomation().executeShellCommand(cmd))) {
+ FileUtils.copy(in, out);
}
-
- device.takeScreenshot(new File(pathname));
}
}
diff --git a/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
new file mode 100644
index 0000000..7a5cf2c
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util.rule;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Rule which captures a screen record for a test.
+ * After adding this rule to the test class, apply the annotation @ScreenRecord to individual tests
+ */
+public class ScreenRecordRule implements TestRule {
+
+ private static final String TAG = "ScreenRecordRule";
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ if (description.getAnnotation(ScreenRecord.class) == null) {
+ return base;
+ }
+
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ Instrumentation inst = getInstrumentation();
+ UiAutomation automation = inst.getUiAutomation();
+ UiDevice device = UiDevice.getInstance(inst);
+
+ File outputFile = new File(inst.getTargetContext().getFilesDir(),
+ "screenrecord-" + description.getMethodName() + ".mp4");
+ device.executeShellCommand("killall screenrecord");
+ ParcelFileDescriptor output =
+ automation.executeShellCommand("screenrecord " + outputFile);
+ String screenRecordPid = device.executeShellCommand("pidof screenrecord");
+ boolean success = false;
+ try {
+ base.evaluate();
+ success = true;
+ } finally {
+ device.executeShellCommand("kill -INT " + screenRecordPid);
+ Log.e(TAG, "Screenrecord captured at: " + outputFile);
+ output.close();
+ if (success) {
+ automation.executeShellCommand("rm " + outputFile);
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Interface to indicate that the test should capture screenrecord
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.METHOD)
+ public @interface ScreenRecord {
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
index 0ec0f02..2b2fef4 100644
--- a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
+++ b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
@@ -15,21 +15,27 @@
*/
package com.android.launcher3.util.rule;
-import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
-
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
+
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
+import android.util.Log;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
+import com.android.systemui.shared.system.PackageManagerWrapper;
+
+import org.junit.Assert;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+
/**
* Test rule which executes a shell command at the start of the test.
*/
@@ -37,10 +43,19 @@
private final String mCmd;
private final String mRevertCommand;
+ private final boolean mCheckSuccess;
+ private final Runnable mAdditionalChecks;
- public ShellCommandRule(String cmd, @Nullable String revertCommand) {
+ public ShellCommandRule(String cmd, @Nullable String revertCommand, boolean checkSuccess,
+ Runnable additionalChecks) {
mCmd = cmd;
mRevertCommand = revertCommand;
+ mCheckSuccess = checkSuccess;
+ mAdditionalChecks = additionalChecks;
+ }
+
+ public ShellCommandRule(String cmd, @Nullable String revertCommand) {
+ this(cmd, revertCommand, false, null);
}
@Override
@@ -48,12 +63,27 @@
return new Statement() {
@Override
public void evaluate() throws Throwable {
- UiDevice.getInstance(getInstrumentation()).executeShellCommand(mCmd);
+ final String result =
+ UiDevice.getInstance(getInstrumentation()).executeShellCommand(mCmd);
+ if (mCheckSuccess) {
+ Assert.assertTrue(
+ "Failed command: " + mCmd + ", result: " + result,
+ "Success".equals(result.replaceAll("\\s", "")));
+ }
+ if (mAdditionalChecks != null) mAdditionalChecks.run();
try {
base.evaluate();
} finally {
if (mRevertCommand != null) {
- UiDevice.getInstance(getInstrumentation()).executeShellCommand(mRevertCommand);
+ final String revertResult = UiDevice.getInstance(
+ getInstrumentation()).executeShellCommand(
+ mRevertCommand);
+ if (mCheckSuccess) {
+ Assert.assertTrue(
+ "Failed command: " + mRevertCommand
+ + ", result: " + revertResult,
+ "Success".equals(result.replaceAll("\\s", "")));
+ }
}
}
}
@@ -72,7 +102,15 @@
* Sets the target launcher as default launcher.
*/
public static ShellCommandRule setDefaultLauncher() {
- return new ShellCommandRule(getLauncherCommand(getLauncherInMyProcess()), null);
+ final ActivityInfo launcher = getLauncherInMyProcess();
+ Log.d("b/187080582", "Launcher: " + new ComponentName(launcher.packageName, launcher.name)
+ .flattenToString());
+ return new ShellCommandRule(getLauncherCommand(launcher), null, true, () ->
+ Assert.assertEquals("Setting default launcher failed",
+ new ComponentName(launcher.packageName, launcher.name)
+ .flattenToString(),
+ PackageManagerWrapper.getInstance().getHomeActivities(new ArrayList<>())
+ .flattenToString()));
}
public static String getLauncherCommand(ActivityInfo launcher) {
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index 61f5e05..0e27b61 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -53,8 +53,6 @@
+ ")$");
public static final int LOCAL = 0x1;
- public static final int UNBUNDLED_PRESUBMIT = 0x2;
- public static final int UNBUNDLED_POSTSUBMIT = 0x4;
public static final int PLATFORM_PRESUBMIT = 0x8;
public static final int PLATFORM_POSTSUBMIT = 0x10;
@@ -136,14 +134,6 @@
platformBuildMatcher.group("postsubmit") != null)) {
Log.d(TAG, "LOCAL RUN");
sRunFlavor = LOCAL;
- } else if (launcherBuildMatcher.group("presubmit") != null
- && platformBuildMatcher.group("postsubmit") != null) {
- Log.d(TAG, "UNBUNDLED PRESUBMIT");
- sRunFlavor = UNBUNDLED_PRESUBMIT;
- } else if (launcherBuildMatcher.group("postsubmit") != null
- && platformBuildMatcher.group("postsubmit") != null) {
- Log.d(TAG, "UNBUNDLED POSTSUBMIT");
- sRunFlavor = UNBUNDLED_POSTSUBMIT;
} else if (launcherBuildMatcher.group("platform") != null
&& platformBuildMatcher.group("presubmit") != null) {
Log.d(TAG, "PLATFORM PRESUBMIT");
diff --git a/tests/src_common/com/android/launcher3/common/WidgetUtils.java b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
index c0913bf..97500e3 100644
--- a/tests/src_common/com/android/launcher3/common/WidgetUtils.java
+++ b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
@@ -16,19 +16,18 @@
package com.android.launcher3.common;
import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
-import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
import android.appwidget.AppWidgetHost;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
-import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
@@ -57,12 +56,14 @@
item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
if (bindWidget) {
- PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
+ PendingAddWidgetInfo pendingInfo =
+ new PendingAddWidgetInfo(
+ info, LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY);
pendingInfo.spanX = item.spanX;
pendingInfo.spanY = item.spanY;
pendingInfo.minSpanX = item.minSpanX;
pendingInfo.minSpanY = item.minSpanY;
- Bundle options = getDefaultOptionsForWidget(targetContext, pendingInfo);
+ Bundle options = pendingInfo.getDefaultSizeOptions(targetContext);
AppWidgetHost host = new LauncherAppWidgetHost(targetContext);
int widgetId = host.allocateAppWidgetId();
diff --git a/tests/src_disabled/WorkTabTest.java b/tests/src_disabled/WorkTabTest.java
new file mode 100644
index 0000000..bfacc74
--- /dev/null
+++ b/tests/src_disabled/WorkTabTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 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.ui;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.widget.TextView;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsPagedView;
+import com.android.launcher3.allapps.WorkModeSwitch;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.views.WorkEduView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class WorkTabTest extends AbstractLauncherUiTest {
+
+ private int mProfileUserId;
+
+ private static final int WORK_PAGE = AllAppsContainerView.AdapterHolder.WORK;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ String output =
+ mDevice.executeShellCommand(
+ "pm create-user --profileOf 0 --managed TestProfile");
+ assertTrue("Failed to create work profile", output.startsWith("Success"));
+
+ String[] tokens = output.split("\\s+");
+ mProfileUserId = Integer.parseInt(tokens[tokens.length - 1]);
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Created new user uid" + mProfileUserId);
+ mDevice.executeShellCommand("am start-user " + mProfileUserId);
+ }
+
+ @After
+ public void removeWorkProfile() throws Exception {
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED, "(teardown) removing uid" + mProfileUserId,
+ new Exception());
+ mDevice.executeShellCommand("pm remove-user " + mProfileUserId);
+ }
+
+ @After
+ public void resumeAppStoreUpdate() {
+ executeOnLauncher(launcher -> {
+ if (launcher == null || launcher.getAppsView() == null) {
+ return;
+ }
+ launcher.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED, "resuming AppStore updates");
+ });
+ }
+
+ @Ignore("b/182844465")
+ @Test
+ public void workTabExists() {
+ mDevice.pressHome();
+ waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+ executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+ waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
+ waitForLauncherCondition("Personal tab is missing",
+ launcher -> launcher.getAppsView().isPersonalTabVisible(), 60000);
+ waitForLauncherCondition("Work tab is missing",
+ launcher -> launcher.getAppsView().isWorkTabVisible(), 60000);
+ }
+
+ @Ignore("b/182844465")
+ @Test
+ public void toggleWorks() {
+ mDevice.pressHome();
+ waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+ executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+ waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
+ getOnceNotNull("Apps view did not bind",
+ launcher -> launcher.getAppsView().getWorkModeSwitch(), 60000);
+
+ UserManager userManager = getFromLauncher(l -> l.getSystemService(UserManager.class));
+ assertEquals(2, userManager.getUserProfiles().size());
+ UserHandle workProfile = getFromLauncher(l -> {
+ UserHandle myHandle = Process.myUserHandle();
+ List<UserHandle> userProfiles = userManager.getUserProfiles();
+ return userProfiles.get(0) == myHandle ? userProfiles.get(1) : userProfiles.get(0);
+ });
+
+ waitForLauncherCondition("work profile can't be turned off",
+ l -> userManager.requestQuietModeEnabled(true, workProfile));
+
+ assertTrue(userManager.isQuietModeEnabled(workProfile));
+ executeOnLauncher(launcher -> {
+ WorkModeSwitch wf = launcher.getAppsView().getWorkModeSwitch();
+ ((AllAppsPagedView) launcher.getAppsView().getContentView()).snapToPageImmediately(
+ AllAppsContainerView.AdapterHolder.WORK);
+ wf.toggle();
+ });
+ waitForLauncherCondition("Work toggle did not work",
+ l -> l.getSystemService(UserManager.class).isQuietModeEnabled(workProfile));
+ }
+
+ @Ignore("b/182844465")
+ @Test
+ public void testWorkEduFlow() {
+ mDevice.pressHome();
+ waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+ executeOnLauncher(launcher -> launcher.getSharedPrefs().edit().remove(
+ WorkEduView.KEY_WORK_EDU_STEP).remove(
+ WorkEduView.KEY_LEGACY_WORK_EDU_SEEN).commit());
+
+ waitForLauncherCondition("Work tab not setup", launcher -> {
+ if (launcher.getAppsView().getContentView() instanceof AllAppsPagedView) {
+ launcher.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
+ return true;
+ }
+ return false;
+ }, LauncherInstrumentation.WAIT_TIME_MS);
+
+ executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+ WorkEduView workEduView = getEduView();
+ // verify personal app edu is seen first and click "next"
+ executeOnLauncher(l -> {
+ assertEquals(((TextView) workEduView.findViewById(R.id.content_text)).getText(),
+ l.getResources().getString(R.string.work_profile_edu_personal_apps));
+ workEduView.findViewById(R.id.proceed).callOnClick();
+ });
+
+ AtomicInteger attempt = new AtomicInteger(0);
+ // verify work edu is seen next
+ waitForLauncherCondition("Launcher did not show the next edu screen", l -> {
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED,
+ "running test attempt" + attempt.getAndIncrement());
+ if (!(l.getAppsView().getContentView() instanceof AllAppsPagedView)) {
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work tab not setup. Skipping test");
+ return false;
+ }
+ if (((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage()
+ != WORK_PAGE) {
+ Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work page not highlighted");
+ }
+ return ((TextView) workEduView.findViewById(R.id.content_text)).getText().equals(
+ l.getResources().getString(R.string.work_profile_edu_work_apps));
+ });
+ }
+
+ @Ignore("b/182844465")
+ @Test
+ public void testWorkEduIntermittent() {
+ mDevice.pressHome();
+ waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+ executeOnLauncher(launcher -> launcher.getSharedPrefs().edit().remove(
+ WorkEduView.KEY_WORK_EDU_STEP).remove(
+ WorkEduView.KEY_LEGACY_WORK_EDU_SEEN).commit());
+
+
+ waitForLauncherCondition("Work tab not setup",
+ launcher -> launcher.getAppsView().getContentView() instanceof AllAppsPagedView,
+ 60000);
+ executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+
+ // verify personal app edu is seen
+ getEduView();
+
+ // dismiss personal edu
+ mDevice.pressHome();
+ waitForState("Launcher did not go home", () -> NORMAL);
+
+ // open work tab
+ executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+ waitForState("Launcher did not switch to all apps", () -> ALL_APPS);
+ waitForLauncherCondition("Work tab not setup",
+ launcher -> launcher.getAppsView().getContentView() instanceof AllAppsPagedView,
+ 60000);
+
+ executeOnLauncher(launcher -> {
+ AllAppsPagedView pagedView = (AllAppsPagedView) launcher.getAppsView().getContentView();
+ pagedView.setCurrentPage(WORK_PAGE);
+ });
+
+ WorkEduView workEduView = getEduView();
+
+ // verify work tab edu is shown
+ waitForLauncherCondition("Launcher did not show the next edu screen",
+ l -> ((TextView) workEduView.findViewById(R.id.content_text)).getText().equals(
+ l.getResources().getString(R.string.work_profile_edu_work_apps)));
+ }
+
+
+ private WorkEduView getEduView() {
+ waitForLauncherCondition("Edu did not show", l -> {
+ DragLayer dragLayer = l.getDragLayer();
+ return dragLayer.getChildCount() > 0 && dragLayer.getChildAt(
+ dragLayer.getChildCount() - 1) instanceof WorkEduView;
+ }, 6000);
+ return getFromLauncher(launcher -> (WorkEduView) launcher.getDragLayer().getChildAt(
+ launcher.getDragLayer().getChildCount() - 1));
+ }
+
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
index e1fde3b..0582bc9 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -28,7 +28,7 @@
public class AddToHomeScreenPrompt {
private static final Pattern ADD_AUTOMATICALLY =
- Pattern.compile("^Add automatically$", CASE_INSENSITIVE);
+ Pattern.compile("^Add to Home screen$", CASE_INSENSITIVE);
private final LauncherInstrumentation mLauncher;
private final UiObject2 mWidgetCell;
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index b6c17df..1cb6b2d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -25,9 +25,9 @@
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.StaleObjectException;
import androidx.test.uiautomator.UiObject2;
-import com.android.launcher3.ResourceUtils;
import com.android.launcher3.testing.TestProtocol;
import java.util.stream.Collectors;
@@ -62,7 +62,13 @@
private boolean hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler,
BySelector appIconSelector, int displayBottom) {
- final UiObject2 icon = appListRecycler.findObject(appIconSelector);
+ final UiObject2 icon;
+ try {
+ icon = appListRecycler.findObject(appIconSelector);
+ } catch (StaleObjectException e) {
+ mLauncher.fail("All apps recycler disappeared from screen");
+ return false;
+ }
if (icon == null) {
LauncherInstrumentation.log("hasClickableIcon: icon not visible");
return false;
@@ -108,26 +114,24 @@
"apps_list_view");
final UiObject2 searchBox = getSearchBox(allAppsContainer);
- int bottomGestureMargin = ResourceUtils.getNavbarSize(
- ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1;
- int deviceHeight = mLauncher.getDevice().getDisplayHeight();
- int displayBottom = deviceHeight - bottomGestureMargin;
+ int deviceHeight = mLauncher.getRealDisplaySize().y;
+ int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen();
final BySelector appIconSelector = AppIcon.getAppIconSelector(appName, mLauncher);
if (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
- displayBottom)) {
+ bottomGestureStartOnScreen)) {
scrollBackToBeginning();
int attempts = 0;
int scroll = getAllAppsScroll();
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled")) {
while (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
- displayBottom)) {
+ bottomGestureStartOnScreen)) {
mLauncher.scrollToLastVisibleRow(
allAppsContainer,
mLauncher.getObjectsInContainer(allAppsContainer, "icon")
.stream()
.filter(icon ->
- mLauncher.getVisibleBounds(icon).bottom
- <= displayBottom)
+ mLauncher.getVisibleBounds(icon).top
+ < bottomGestureStartOnScreen)
.collect(Collectors.toList()),
mLauncher.getVisibleBounds(searchBox).bottom
- mLauncher.getVisibleBounds(allAppsContainer).top);
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 5de5b4a..21099b4 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -47,6 +47,16 @@
public AppIconMenu openMenu() {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
+ mObject, "popup_container", LONG_CLICK_EVENT));
+ }
+ }
+
+ /**
+ * Long-clicks the icon to open its menu, and looks at the deep shortcuts container only.
+ */
+ public AppIconMenu openDeepShortcutMenu() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
mObject, "deep_shortcuts_container", LONG_CLICK_EVENT));
}
}
@@ -58,11 +68,16 @@
@Override
protected String getLongPressIndicator() {
- return "deep_shortcuts_container";
+ return "popup_container";
}
@Override
protected void expectActivityStartEvents() {
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
}
+
+ @Override
+ protected String launchableType() {
+ return "app icon";
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
index a40919b..ac0db08 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -20,8 +20,6 @@
import com.android.launcher3.testing.TestProtocol;
-import java.util.regex.Pattern;
-
/**
* Menu item in an app icon menu.
*/
@@ -51,4 +49,9 @@
protected void expectActivityStartEvents() {
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
}
+
+ @Override
+ protected String launchableType() {
+ return "app icon menu item";
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index ce94a3e..4b1610e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,8 +16,9 @@
package com.android.launcher3.tapl;
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
+
import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
-import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
import android.graphics.Point;
@@ -85,15 +86,11 @@
final LauncherInstrumentation.GestureScope gestureScope =
zeroButtonToOverviewGestureStartsInLauncher()
? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
- : LauncherInstrumentation.GestureScope.OUTSIDE;
-
- // b/156044202
- mLauncher.log("Hierarchy before swiping up to overview:");
- mLauncher.dumpViewHierarchy();
+ : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
mLauncher.sendPointer(
downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
- mLauncher.executeAndWaitForEvent(
+ mLauncher.executeAndWaitForLauncherEvent(
() -> mLauncher.movePointer(
downTime,
downTime,
@@ -102,12 +99,12 @@
end,
gestureScope),
event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(event.getClassName()),
- () -> "Pause wasn't detected");
+ () -> "Pause wasn't detected", "swiping and holding");
mLauncher.runToState(
() -> mLauncher.sendPointer(
downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end,
gestureScope),
- OVERVIEW_STATE_ORDINAL);
+ OVERVIEW_STATE_ORDINAL, "sending UP event");
break;
}
@@ -125,12 +122,14 @@
endY = startY - swipeLength;
} else {
startX = getSwipeStartX();
- endX = startX - swipeLength;
+ // TODO(b/184059820) make horizontal swipe use swipe width not height, for the
+ // moment just double the swipe length.
+ endX = startX - swipeLength * 2;
startY = endY = mLauncher.getDevice().getDisplayHeight() / 2;
}
mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL,
- LauncherInstrumentation.GestureScope.OUTSIDE);
+ LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
break;
}
@@ -138,7 +137,7 @@
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
mLauncher.runToState(
() -> mLauncher.waitForSystemUiObject("recent_apps").click(),
- OVERVIEW_STATE_ORDINAL);
+ OVERVIEW_STATE_ORDINAL, "clicking Recents button");
break;
}
expectSwitchToOverviewEvents();
@@ -147,68 +146,100 @@
private void expectSwitchToOverviewEvents() {
}
- /**
- * Swipes right or double presses the square button to switch to the previous app.
- */
+ @NonNull
public Background quickSwitchToPreviousApp() {
+ boolean toRight = true;
+ quickSwitch(toRight);
+ return new Background(mLauncher);
+ }
+
+ @NonNull
+ public Background quickSwitchToPreviousAppSwipeLeft() {
+ boolean toRight = false;
+ quickSwitch(toRight);
+ return new Background(mLauncher);
+ }
+
+ @NonNull
+ private void quickSwitch(boolean toRight) {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to quick switch to the previous app")) {
verifyActiveContainer();
- quickSwitchToPreviousApp(getExpectedStateForQuickSwitch());
- return new Background(mLauncher);
- }
- }
+ final boolean launcherWasVisible = mLauncher.isLauncherVisible();
+ boolean transposeInLandscape = false;
+ switch (mLauncher.getNavigationModel()) {
+ case TWO_BUTTON:
+ transposeInLandscape = true;
+ // Fall through, zero button and two button modes behave the same.
+ case ZERO_BUTTON: {
+ final int startX;
+ final int startY;
+ final int endX;
+ final int endY;
+ final int cornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
+ if (toRight) {
+ if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
+ // Swipe from the bottom left to the bottom right of the screen.
+ startX = cornerRadius;
+ startY = getSwipeStartY();
+ endX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
+ endY = startY;
+ } else {
+ // Swipe from the bottom right to the top right of the screen.
+ startX = getSwipeStartX();
+ startY = mLauncher.getRealDisplaySize().y - 1 - cornerRadius;
+ endX = startX;
+ endY = cornerRadius;
+ }
+ } else {
+ if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
+ // Swipe from the bottom right to the bottom left of the screen.
+ startX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
+ startY = getSwipeStartY();
+ endX = cornerRadius;
+ endY = startY;
+ } else {
+ // Swipe from the bottom left to the top left of the screen.
+ startX = getSwipeStartX();
+ startY = cornerRadius;
+ endX = startX;
+ endY = mLauncher.getRealDisplaySize().y - 1 - cornerRadius;
+ }
+ }
- protected int getExpectedStateForQuickSwitch() {
- return BACKGROUND_APP_STATE_ORDINAL;
- }
-
- protected void quickSwitchToPreviousApp(int expectedState) {
- final boolean launcherWasVisible = mLauncher.isLauncherVisible();
- boolean transposeInLandscape = false;
- switch (mLauncher.getNavigationModel()) {
- case TWO_BUTTON:
- transposeInLandscape = true;
- // Fall through, zero button and two button modes behave the same.
- case ZERO_BUTTON: {
- final int startX;
- final int startY;
- final int endX;
- final int endY;
- if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
- // Swipe from the bottom left to the bottom right of the screen.
- startX = 0;
- startY = getSwipeStartY();
- endX = mLauncher.getDevice().getDisplayWidth();
- endY = startY;
- } else {
- // Swipe from the bottom right to the top right of the screen.
- startX = getSwipeStartX();
- startY = mLauncher.getRealDisplaySize().y - 1;
- endX = startX;
- endY = 0;
+ final boolean isZeroButton = mLauncher.getNavigationModel()
+ == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
+ LauncherInstrumentation.GestureScope gestureScope =
+ launcherWasVisible && isZeroButton
+ ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+ : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
+ mLauncher.executeAndWaitForEvent(
+ () -> mLauncher.linearGesture(
+ startX, startY, endX, endY, 20, false, gestureScope),
+ event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+ () -> "Quick switch gesture didn't change window state", "swiping");
+ break;
}
- final boolean isZeroButton = mLauncher.getNavigationModel()
- == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
- mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
- launcherWasVisible && isZeroButton
- ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
- : LauncherInstrumentation.GestureScope.OUTSIDE);
- break;
- }
- case THREE_BUTTON:
- // Double press the recents button.
- UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
- mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
- mLauncher.getOverview();
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
- recentsButton.click();
- break;
+ case THREE_BUTTON:
+ // Double press the recents button.
+ UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+ mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL,
+ "clicking Recents button for the first time");
+ mLauncher.getOverview();
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+ mLauncher.executeAndWaitForEvent(
+ () -> recentsButton.click(),
+ event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+ () -> "Pressing recents button didn't change window state",
+ "clicking Recents button for the second time");
+ break;
+ }
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+ return;
}
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
}
protected String getSwipeHeightRequestName() {
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 223ae29..588b6b8 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -27,7 +27,7 @@
import java.util.List;
/**
- * Common overview pane for both Launcher and fallback recents
+ * Common overview panel for both Launcher and fallback recents
*/
public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
@@ -135,4 +135,19 @@
public boolean hasTasks() {
return getTasks().size() > 0;
}
+
+ /**
+ * Gets Overview Actions.
+ *
+ * @return The Overview Actions
+ */
+ @NonNull
+ public OverviewActions getOverviewActions() {
+ try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to get overview actions")) {
+ verifyActiveContainer();
+ UiObject2 overviewActions = mLauncher.waitForLauncherObject("action_buttons");
+ return new OverviewActions(overviewActions, mLauncher);
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index c06e254..0060844 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -16,8 +16,6 @@
package com.android.launcher3.tapl;
-import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
-
import androidx.annotation.NonNull;
/**
@@ -65,8 +63,4 @@
return true;
}
- @Override
- protected int getExpectedStateForQuickSwitch() {
- return QUICK_SWITCH_STATE_ORDINAL;
- }
}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 13ecfb8..a15131d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -53,19 +53,25 @@
protected abstract void expectActivityStartEvents();
+ protected abstract String launchableType();
+
private Background launch(BySelector selector) {
- LauncherInstrumentation.log("Launchable.launch before click " +
- mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
+ LauncherInstrumentation.log("Launchable.launch before click "
+ + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
+ final String label = mObject.getText();
mLauncher.executeAndWaitForEvent(
- () -> mLauncher.clickLauncherObject(mObject),
+ () -> {
+ mLauncher.clickLauncherObject(mObject);
+ expectActivityStartEvents();
+ },
event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
- () -> "Launching an app didn't open a new window: " + mObject.getText());
- expectActivityStartEvents();
+ () -> "Launching an app didn't open a new window: " + label,
+ "clicking " + launchableType());
mLauncher.assertTrue(
- "App didn't start: " + selector,
- mLauncher.getDevice().wait(Until.hasObject(selector),
+ "App didn't start: " + label + " (" + selector + ")",
+ TestHelpers.wait(Until.hasObject(selector),
LauncherInstrumentation.WAIT_TIME_MS));
return new Background(mLauncher);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f4274a8..19094f8 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -39,6 +39,7 @@
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
+import android.os.DeadObjectException;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -52,6 +53,7 @@
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
@@ -64,6 +66,7 @@
import com.android.launcher3.ResourceUtils;
import com.android.launcher3.testing.TestProtocol;
+import com.android.systemui.shared.system.ContextUtils;
import com.android.systemui.shared.system.QuickStepContract;
import org.junit.Assert;
@@ -93,7 +96,6 @@
private static final String TAG = "Tapl";
private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
private static final int GESTURE_STEP_MS = 16;
- private static long START_TIME = System.currentTimeMillis();
private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
private static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
@@ -103,6 +105,9 @@
static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
+ private final String mLauncherPackage;
+ private Boolean mIsLauncher3;
+ private long mTestStartTime = -1;
// Types for launcher containers that the user is interacting with. "Background" is a
// pseudo-container corresponding to inactive launcher covered by another app.
@@ -112,9 +117,10 @@
public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
- // Where the gesture happens: outside of Launcher, inside or from inside to outside.
+ // Where the gesture happens: outside of Launcher, inside or from inside to outside and
+ // whether the gesture recognition triggers pilfer.
public enum GestureScope {
- OUTSIDE, INSIDE, INSIDE_TO_OUTSIDE
+ OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE
}
;
@@ -149,10 +155,11 @@
private static final String WORKSPACE_RES_ID = "workspace";
private static final String APPS_RES_ID = "apps_view";
private static final String OVERVIEW_RES_ID = "overview_panel";
- private static final String WIDGETS_RES_ID = "widgets_list_view";
- private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
- public static final int WAIT_TIME_MS = 10000;
+ private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
+ private static final String CONTEXT_MENU_RES_ID = "popup_container";
+ public static final int WAIT_TIME_MS = 60000;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+ private static final String ANDROID_PACKAGE = "android";
private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
@@ -171,8 +178,7 @@
private Runnable mOnLauncherCrashed;
private static Pattern getTouchEventPattern(String prefix, String action) {
- // The pattern includes sanity checks that we don't get a multi-touch events or other
- // surprises.
+ // The pattern includes checks that we don't get a multi-touch events or other surprises.
return Pattern.compile(
prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+ ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
@@ -214,11 +220,11 @@
// Launcher package. As during inproc tests the tested launcher may not be selected as the
// current launcher, choosing target package for inproc. For out-of-proc, use the installed
// launcher package.
- final String authorityPackage = testPackage.equals(targetPackage) ?
- getLauncherPackageName() :
- targetPackage;
+ mLauncherPackage = testPackage.equals(targetPackage)
+ ? getLauncherPackageName()
+ : targetPackage;
- String testProviderAuthority = authorityPackage + ".TestInfo";
+ String testProviderAuthority = mLauncherPackage + ".TestInfo";
mTestProviderUri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(testProviderAuthority)
@@ -235,11 +241,12 @@
if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
if (TestHelpers.isInLauncherProcess()) {
- getContext().getPackageManager().setComponentEnabledSetting(
- cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
+ pm.setComponentEnabledSetting(cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
} else {
try {
- mDevice.executeShellCommand("pm enable " + cn.flattenToString());
+ final int userId = ContextUtils.getUserId(getContext());
+ mDevice.executeShellCommand(
+ "pm enable --user " + userId + " " + cn.flattenToString());
} catch (IOException e) {
fail(e.toString());
}
@@ -263,6 +270,9 @@
try (ContentProviderClient client = getContext().getContentResolver()
.acquireContentProviderClient(mTestProviderUri)) {
return client.call(request, null, null);
+ } catch (DeadObjectException e) {
+ fail("Launcher crashed");
+ return null;
} catch (RemoteException e) {
throw new RuntimeException(e);
}
@@ -358,9 +368,11 @@
if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
- if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
+ if (!mDevice.wait(Until.hasObject(getAnyObjectSelector()), WAIT_TIME_MS)) {
+ return "Screen is empty";
+ }
- final String navigationModeError = getNavigationModeMismatchError();
+ final String navigationModeError = getNavigationModeMismatchError(true);
if (navigationModeError != null) return navigationModeError;
} catch (Throwable e) {
Log.w(TAG, "getSystemAnomalyMessage failed", e);
@@ -379,7 +391,7 @@
}
private String getVisiblePackages() {
- return mDevice.findObjects(By.textStartsWith(""))
+ return mDevice.findObjects(getAnyObjectSelector())
.stream()
.map(LauncherInstrumentation::getApplicationPackageSafe)
.distinct()
@@ -413,6 +425,14 @@
mOnSettledStateAction = onSettledStateAction;
}
+ public void onTestStart() {
+ mTestStartTime = System.currentTimeMillis();
+ }
+
+ public void onTestFinish() {
+ mTestStartTime = -1;
+ }
+
private String formatSystemHealthMessage(String message) {
final String testPackage = getContext().getPackageName();
@@ -421,16 +441,17 @@
mInstrumentation.getUiAutomation().grantRuntimePermission(
testPackage, "android.permission.PACKAGE_USAGE_STATS");
- final String systemHealth = mSystemHealthSupplier != null
- ? mSystemHealthSupplier.apply(START_TIME)
- : TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
+ if (mTestStartTime > 0) {
+ final String systemHealth = mSystemHealthSupplier != null
+ ? mSystemHealthSupplier.apply(mTestStartTime)
+ : TestHelpers.getSystemHealthMessage(getContext(), mTestStartTime);
- if (systemHealth != null) {
- return message
- + ",\nperhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
- + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+ if (systemHealth != null) {
+ return message
+ + ";\nPerhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+ + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+ }
}
-
return message;
}
@@ -441,14 +462,14 @@
if (checkEvents) {
final String eventMismatch = eventChecker.verify(0, false);
if (eventMismatch != null) {
- message = message + ", having produced " + eventMismatch;
+ message = message + ";\n" + eventMismatch;
}
} else {
eventChecker.finishNoWait();
}
}
- dumpDiagnostics();
+ dumpDiagnostics(message);
log("Hierarchy dump for: " + message);
dumpViewHierarchy();
@@ -456,10 +477,11 @@
return message;
}
- private void dumpDiagnostics() {
- Log.e("b/156287114", "Input:");
+ private void dumpDiagnostics(String message) {
+ log("Diagnostics for failure: " + message);
+ log("Input:");
logShellCommand("dumpsys input");
- Log.e("b/156287114", "TIS:");
+ log("TIS:");
logShellCommand("dumpsys activity service TouchInteractionService");
}
@@ -467,22 +489,24 @@
try {
for (String line : mDevice.executeShellCommand(command).split("\\n")) {
SystemClock.sleep(10);
- Log.d("b/156287114", line);
+ log(line);
}
} catch (IOException e) {
- Log.d("b/156287114", "Failed to execute " + command);
+ log("Failed to execute " + command);
}
}
- private void fail(String message) {
+ void fail(String message) {
checkForAnomaly();
Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
- "http://go/tapl : " + getContextDescription() + message
- + " (visible state: " + getVisibleStateMessage() + ")", true)));
+ "http://go/tapl test failure:\nContext: " + getContextDescription()
+ + " - visible state is " + getVisibleStateMessage()
+ + ";\nDetails: " + message, true)));
}
private String getContextDescription() {
- return mDiagnosticContext.isEmpty() ? "" : String.join(", ", mDiagnosticContext) + "; ";
+ return mDiagnosticContext.isEmpty()
+ ? "(no context)" : String.join(", ", mDiagnosticContext);
}
void assertTrue(String message, boolean condition) {
@@ -527,17 +551,28 @@
mExpectedRotation = expectedRotation;
}
- public String getNavigationModeMismatchError() {
+ public String getNavigationModeMismatchError(boolean waitForCorrectState) {
+ final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
final NavigationModel navigationModel = getNavigationModel();
- final boolean hasRecentsButton = hasSystemUiObject("recent_apps");
- final boolean hasHomeButton = hasSystemUiObject("home");
- if ((navigationModel == NavigationModel.THREE_BUTTON) != hasRecentsButton) {
- return "Presence of recents button doesn't match the interaction mode, mode="
- + navigationModel.name() + ", hasRecents=" + hasRecentsButton;
+
+ if (navigationModel == NavigationModel.THREE_BUTTON) {
+ if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+ return "Recents button not present in 3-button mode";
+ }
+ } else {
+ if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+ return "Recents button is present in non-3-button mode";
+ }
}
- if ((navigationModel != NavigationModel.ZERO_BUTTON) != hasHomeButton) {
- return "Presence of home button doesn't match the interaction mode, mode="
- + navigationModel.name() + ", hasHome=" + hasHomeButton;
+
+ if (navigationModel == NavigationModel.ZERO_BUTTON) {
+ if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+ return "Home button is present in gestural mode";
+ }
+ } else {
+ if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+ return "Home button not present in non-gestural mode";
+ }
}
return null;
}
@@ -548,13 +583,7 @@
assertEquals("Unexpected display rotation",
mExpectedRotation, mDevice.getDisplayRotation());
- // b/148422894
- String error = null;
- for (int i = 0; i != 600; ++i) {
- error = getNavigationModeMismatchError();
- if (error == null) break;
- sleep(100);
- }
+ final String error = getNavigationModeMismatchError(true);
assertTrue(error, error == null);
log("verifyContainerType: " + containerType);
@@ -571,11 +600,7 @@
"but the current state is not " + containerType.name())) {
switch (containerType) {
case WORKSPACE: {
- if (mDevice.isNaturalOrientation()) {
- waitForLauncherObject(APPS_RES_ID);
- } else {
- waitUntilLauncherObjectGone(APPS_RES_ID);
- }
+ waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
return waitForLauncherObject(WORKSPACE_RES_ID);
@@ -593,11 +618,7 @@
return waitForLauncherObject(APPS_RES_ID);
}
case OVERVIEW: {
- if (hasAllAppsInOverview()) {
- waitForLauncherObject(APPS_RES_ID);
- } else {
- waitUntilLauncherObjectGone(APPS_RES_ID);
- }
+ waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
@@ -629,22 +650,35 @@
}
SystemClock.sleep(100);
}
+ checkForAnomaly();
fail("Launcher didn't initialize");
}
+ Parcelable executeAndWaitForLauncherEvent(Runnable command,
+ UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
+ String actionName) {
+ return executeAndWaitForEvent(
+ command,
+ e -> mLauncherPackage.equals(e.getPackageName()) && eventFilter.accept(e),
+ message, actionName);
+ }
+
Parcelable executeAndWaitForEvent(Runnable command,
- UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
- try {
- final AccessibilityEvent event =
- mInstrumentation.getUiAutomation().executeAndWaitForEvent(
- command, eventFilter, WAIT_TIME_MS);
- assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
- final Parcelable parcelableData = event.getParcelableData();
- event.recycle();
- return parcelableData;
- } catch (TimeoutException e) {
- fail(message.get());
- return null;
+ UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
+ String actionName) {
+ try (LauncherInstrumentation.Closable c = addContextLayer(actionName)) {
+ try {
+ final AccessibilityEvent event =
+ mInstrumentation.getUiAutomation().executeAndWaitForEvent(
+ command, eventFilter, WAIT_TIME_MS);
+ assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
+ final Parcelable parcelableData = event.getParcelableData();
+ event.recycle();
+ return parcelableData;
+ } catch (TimeoutException e) {
+ fail(message.get());
+ return null;
+ }
}
}
@@ -677,6 +711,10 @@
try (LauncherInstrumentation.Closable c = addContextLayer(
"Swiped up from context menu to home")) {
waitUntilLauncherObjectGone(CONTEXT_MENU_RES_ID);
+ // Swiping up can temporarily bring Nexus Launcher if the current
+ // Launcher is a Launcher3 one. Wait for the current launcher to reappear.
+ SystemClock.sleep(5000); // b/187080582
+ waitForLauncherObject(getAnyObjectSelector());
}
}
if (hasLauncherObject(WORKSPACE_RES_ID)) {
@@ -686,33 +724,30 @@
dumpViewHierarchy();
action = "swiping up to home";
- try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
- swipeToState(
- displaySize.x / 2, displaySize.y - 1,
- displaySize.x / 2, 0,
- ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
- launcherWasVisible
- ? GestureScope.INSIDE_TO_OUTSIDE
- : GestureScope.OUTSIDE);
- }
+ swipeToState(
+ displaySize.x / 2, displaySize.y - 1,
+ displaySize.x / 2, 0,
+ ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
+ launcherWasVisible
+ ? GestureScope.INSIDE_TO_OUTSIDE
+ : GestureScope.OUTSIDE_WITH_PILFER);
}
} else {
log("Hierarchy before clicking home:");
dumpViewHierarchy();
action = "clicking home button";
- try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
- if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) {
- expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
- expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
- }
-
- runToState(
- waitForSystemUiObject("home")::click,
- NORMAL_STATE_ORDINAL,
- !hasLauncherObject(WORKSPACE_RES_ID)
- && (hasLauncherObject(APPS_RES_ID)
- || hasLauncherObject(OVERVIEW_RES_ID)));
+ if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) {
+ expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
+ expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
}
+
+ runToState(
+ waitForSystemUiObject("home")::click,
+ NORMAL_STATE_ORDINAL,
+ !hasLauncherObject(WORKSPACE_RES_ID)
+ && (hasLauncherObject(APPS_RES_ID)
+ || hasLauncherObject(OVERVIEW_RES_ID)),
+ action);
}
try (LauncherInstrumentation.Closable c = addContextLayer(
"performed action to switch to Home - " + action)) {
@@ -721,9 +756,13 @@
}
}
+ private static BySelector getAnyObjectSelector() {
+ return By.textStartsWith("");
+ }
+
boolean isLauncherVisible() {
mDevice.waitForIdle();
- return hasLauncherObject(By.textStartsWith(""));
+ return hasLauncherObject(getAnyObjectSelector());
}
/**
@@ -853,6 +892,16 @@
return object;
}
+ @Nullable
+ UiObject2 findObjectInContainer(UiObject2 container, BySelector selector) {
+ try {
+ return container.findObject(selector);
+ } catch (StaleObjectException e) {
+ fail("The container disappeared from screen");
+ return null;
+ }
+ }
+
@NonNull
List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
try {
@@ -878,6 +927,16 @@
}
}
+ void waitForObjectEnabled(UiObject2 object, String waitReason) {
+ try {
+ assertTrue("Timed out waiting for object to be enabled for " + waitReason + " "
+ + object.getResourceName(),
+ object.wait(Until.enabled(true), WAIT_TIME_MS));
+ } catch (StaleObjectException e) {
+ fail("The object disappeared from screen");
+ }
+ }
+
@NonNull
UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
try {
@@ -925,6 +984,14 @@
return waitForObjectBySelector(getOverviewObjectSelector(resName));
}
+ @NonNull
+ UiObject2 waitForAndroidObject(String resId) {
+ final UiObject2 object = TestHelpers.wait(
+ Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
+ assertNotNull("Can't find a android object with id: " + resId, object);
+ return object;
+ }
+
private UiObject2 waitForObjectBySelector(BySelector selector) {
final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
@@ -966,22 +1033,23 @@
+ "]";
}
- void runToState(Runnable command, int expectedState, boolean requireEvent) {
+ void runToState(Runnable command, int expectedState, boolean requireEvent, String actionName) {
if (requireEvent) {
- runToState(command, expectedState);
+ runToState(command, expectedState, actionName);
} else {
command.run();
}
}
- void runToState(Runnable command, int expectedState) {
+ void runToState(Runnable command, int expectedState, String actionName) {
final List<Integer> actualEvents = new ArrayList<>();
- executeAndWaitForEvent(
+ executeAndWaitForLauncherEvent(
command,
event -> isSwitchToStateEvent(event, expectedState, actualEvents),
() -> "Failed to receive an event for the state change: expected ["
+ TestProtocol.stateOrdinalToString(expectedState)
- + "], actual: " + eventListToString(actualEvents));
+ + "], actual: " + eventListToString(actualEvents),
+ actionName);
}
private boolean isSwitchToStateEvent(
@@ -998,20 +1066,26 @@
GestureScope gestureScope) {
runToState(
() -> linearGesture(startX, startY, endX, endY, steps, false, gestureScope),
- expectedState);
+ expectedState,
+ "swiping");
}
- int getBottomGestureSize() {
+ private int getBottomGestureSize() {
return ResourceUtils.getNavbarSize(
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()) + 1;
}
int getBottomGestureMarginInContainer(UiObject2 container) {
- final int bottomGestureStartOnScreen = getRealDisplaySize().y - getBottomGestureSize();
+ final int bottomGestureStartOnScreen = getBottomGestureStartOnScreen();
return getVisibleBounds(container).bottom - bottomGestureStartOnScreen;
}
+ int getBottomGestureStartOnScreen() {
+ return getRealDisplaySize().y - getBottomGestureSize();
+ }
+
void clickLauncherObject(UiObject2 object) {
+ waitForObjectEnabled(object, "clickLauncherObject");
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);
if (!isLauncher3() && getNavigationModel() != NavigationModel.THREE_BUTTON) {
@@ -1033,6 +1107,11 @@
final int itemRowNewTopOnScreen = containerRect.top + topPaddingInContainer;
final int distance = itemRowCurrentTopOnScreen - itemRowNewTopOnScreen + getTouchSlop();
+ scrollDownByDistance(container, distance);
+ }
+
+ void scrollDownByDistance(UiObject2 container, int distance) {
+ final Rect containerRect = getVisibleBounds(container);
final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
scroll(
container,
@@ -1091,12 +1170,13 @@
return;
}
- executeAndWaitForEvent(
+ executeAndWaitForLauncherEvent(
() -> linearGesture(
startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
() -> "Didn't receive a scroll end message: " + startX + ", " + startY
- + ", " + endX + ", " + endY);
+ + ", " + endX + ", " + endY,
+ "scrolling");
}
// Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
@@ -1159,7 +1239,8 @@
final boolean notLauncher3 = !isLauncher3();
switch (action) {
case MotionEvent.ACTION_DOWN:
- if (gestureScope != GestureScope.OUTSIDE) {
+ if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
+ && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
}
if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
@@ -1167,12 +1248,17 @@
}
break;
case MotionEvent.ACTION_UP:
- if (notLauncher3 && gestureScope != GestureScope.INSIDE) {
+ if (notLauncher3 && gestureScope != GestureScope.INSIDE
+ && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
+ || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
}
- if (gestureScope != GestureScope.OUTSIDE) {
- expectEvent(TestProtocol.SEQUENCE_MAIN, gestureScope == GestureScope.INSIDE
- ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
+ if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
+ && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
+ expectEvent(TestProtocol.SEQUENCE_MAIN,
+ gestureScope == GestureScope.INSIDE
+ || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
+ ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
}
if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
@@ -1181,8 +1267,15 @@
}
final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
+ // b/190748682
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_UP:
+ log("b/190748682: injecting " + event);
+ break;
+ }
assertTrue("injectInputEvent failed",
- mInstrumentation.getUiAutomation().injectInputEvent(event, true));
+ mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
event.recycle();
}
@@ -1273,25 +1366,13 @@
getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
}
- public boolean hasAllAppsInOverview() {
- // Vertical bar layouts don't contain all apps
- if (!mDevice.isNaturalOrientation()) {
- return false;
- }
- // Portrait two button (quickstep) always has all apps.
- if (getNavigationModel() == NavigationModel.TWO_BUTTON) {
- return true;
- }
- // Overview actions hide all apps
- if (overviewActionsEnabled()) {
- return false;
- }
- // ...otherwise there should be all apps
- return true;
+ boolean overviewShareEnabled() {
+ return getTestInfo(TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED).getBoolean(
+ TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
- private boolean overviewActionsEnabled() {
- return getTestInfo(TestProtocol.REQUEST_OVERVIEW_ACTIONS_ENABLED).getBoolean(
+ boolean overviewContentPushEnabled() {
+ return getTestInfo(TestProtocol.REQUEST_OVERVIEW_CONTENT_PUSH_ENABLED).getBoolean(
TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
@@ -1303,9 +1384,10 @@
getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
}
- public int getTotalPssKb() {
- return getTestInfo(TestProtocol.REQUEST_TOTAL_PSS_KB).
- getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ public void forceGc() {
+ // GC the system & sysui first before gc'ing launcher
+ logShellCommand("cmd statusbar run-gc");
+ getTestInfo(TestProtocol.REQUEST_FORCE_GC);
}
public Integer getPid() {
@@ -1313,14 +1395,6 @@
return testInfo != null ? testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) : null;
}
- public void produceJavaLeak() {
- getTestInfo(TestProtocol.REQUEST_JAVA_LEAK);
- }
-
- public void produceNativeLeak() {
- getTestInfo(TestProtocol.REQUEST_NATIVE_LEAK);
- }
-
public void produceViewLeak() {
getTestInfo(TestProtocol.REQUEST_VIEW_LEAK);
}
@@ -1360,7 +1434,7 @@
if (mCheckEventsForSuccessfulGestures) {
final String message = eventChecker.verify(WAIT_TIME_MS, true);
if (message != null) {
- dumpDiagnostics();
+ dumpDiagnostics(message);
checkForAnomaly();
Assert.fail(formatSystemHealthMessage(
"http://go/tapl : successful gesture produced " + message));
@@ -1373,7 +1447,10 @@
}
boolean isLauncher3() {
- return "com.android.launcher3".equals(getLauncherPackageName());
+ if (mIsLauncher3 == null) {
+ mIsLauncher3 = "com.android.launcher3".equals(getLauncherPackageName());
+ }
+ return mIsLauncher3;
}
void expectEvent(String sequence, Pattern expected) {
@@ -1387,9 +1464,41 @@
Rect getVisibleBounds(UiObject2 object) {
try {
return object.getVisibleBounds();
+ } catch (StaleObjectException e) {
+ fail("Object " + object + " disappeared from screen");
+ return null;
} catch (Throwable t) {
fail(t.toString());
return null;
}
}
-}
+
+ float getWindowCornerRadius() {
+ final Resources resources = getResources();
+ if (!supportsRoundedCornersOnWindows(resources)) {
+ return 0f;
+ }
+
+ // Radius that should be used in case top or bottom aren't defined.
+ float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
+
+ float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
+ if (topRadius == 0f) {
+ topRadius = defaultRadius;
+ }
+ float bottomRadius = ResourceUtils.getDimenByName(
+ "rounded_corner_radius_bottom", resources, 0);
+ if (bottomRadius == 0f) {
+ bottomRadius = defaultRadius;
+ }
+
+ // Always use the smallest radius to make sure the rounded corners will
+ // completely cover the display.
+ return Math.min(topRadius, bottomRadius);
+ }
+
+ private static boolean supportsRoundedCornersOnWindows(Resources resources) {
+ return ResourceUtils.getBoolByName(
+ "config_supportsRoundedCornersOnWindows", resources, false);
+ }
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 4440b82..710e3cd 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -15,6 +15,10 @@
*/
package com.android.launcher3.tapl;
+import static com.android.launcher3.testing.TestProtocol.SEQUENCE_MAIN;
+import static com.android.launcher3.testing.TestProtocol.SEQUENCE_PILFER;
+import static com.android.launcher3.testing.TestProtocol.SEQUENCE_TIS;
+
import android.os.SystemClock;
import com.android.launcher3.testing.TestProtocol;
@@ -57,6 +61,8 @@
while (true) {
rawEvents = mLauncher.getTestInfo(TestProtocol.REQUEST_GET_TEST_EVENTS)
.getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ if (rawEvents == null) return null;
+
final int expectedCount = mExpectedEvents.entrySet()
.stream().mapToInt(e -> e.getValue().size()).sum();
if (rawEvents.size() >= expectedCount
@@ -83,7 +89,26 @@
String verify(long waitForExpectedCountMs, boolean successfulGesture) {
final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs);
+ if (actualEvents == null) return "null event sequences because launcher likely died";
+ final String lowLevelDiags = lowLevelMismatchDiagnostics(actualEvents);
+ // If we have a sequence mismatch for a successful gesture, we want to provide all low-level
+ // details.
+ if (successfulGesture) {
+ return lowLevelDiags;
+ }
+
+ final String sequenceMismatchInEnglish = highLevelMismatchDiagnostics(actualEvents);
+
+ if (sequenceMismatchInEnglish != null) {
+ LauncherInstrumentation.log(lowLevelDiags);
+ return "Hint: " + sequenceMismatchInEnglish;
+ } else {
+ return lowLevelDiags;
+ }
+ }
+
+ private String lowLevelMismatchDiagnostics(ListMap<String> actualEvents) {
final StringBuilder sb = new StringBuilder();
boolean hasMismatches = false;
for (Map.Entry<String, List<Pattern>> expectedEvents : mExpectedEvents.entrySet()) {
@@ -91,8 +116,7 @@
List<String> actual = new ArrayList<>(actualEvents.getNonNull(sequence));
final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual);
- hasMismatches = hasMismatches
- || mismatchPosition != -1 && !ignoreMistatch(successfulGesture, sequence);
+ hasMismatches = hasMismatches || mismatchPosition != -1;
formatSequenceWithMismatch(
sb,
sequence,
@@ -103,8 +127,7 @@
// Check for unexpected event sequences in the actual data.
for (String actualNamedSequence : actualEvents.keySet()) {
if (!mExpectedEvents.containsKey(actualNamedSequence)) {
- hasMismatches = hasMismatches
- || !ignoreMistatch(successfulGesture, actualNamedSequence);
+ hasMismatches = true;
formatSequenceWithMismatch(
sb,
actualNamedSequence,
@@ -114,14 +137,43 @@
}
}
- return hasMismatches ? "mismatching events: " + sb.toString() : null;
+ return hasMismatches ? "Mismatching events: " + sb.toString() : null;
}
- // Workaround for b/154157191
- private static boolean ignoreMistatch(boolean successfulGesture, String sequence) {
- // b/156287114
- return false;
-// return TestProtocol.SEQUENCE_TIS.equals(sequence) && successfulGesture;
+ private String highLevelMismatchDiagnostics(ListMap<String> actualEvents) {
+ if (!mExpectedEvents.getNonNull(SEQUENCE_TIS).isEmpty()
+ && actualEvents.getNonNull(SEQUENCE_TIS).isEmpty()) {
+ return "TouchInteractionService didn't receive any of the touch events sent by the "
+ + "test";
+ }
+ if (getMismatchPosition(mExpectedEvents.getNonNull(SEQUENCE_TIS),
+ actualEvents.getNonNull(SEQUENCE_TIS)) != -1) {
+ // If TIS has a mismatch that we can't convert to high-level diags, don't convert
+ // other sequences either.
+ return null;
+ }
+
+ if (mExpectedEvents.getNonNull(SEQUENCE_PILFER).size() == 1
+ && actualEvents.getNonNull(SEQUENCE_PILFER).isEmpty()) {
+ return "Launcher didn't detect the navigation gesture sent by the test";
+ }
+ if (mExpectedEvents.getNonNull(SEQUENCE_PILFER).isEmpty()
+ && actualEvents.getNonNull(SEQUENCE_PILFER).size() == 1) {
+ return "Launcher detected a navigation gesture, but the test didn't send one";
+ }
+ if (getMismatchPosition(mExpectedEvents.getNonNull(SEQUENCE_PILFER),
+ actualEvents.getNonNull(SEQUENCE_PILFER)) != -1) {
+ // If Pilfer has a mismatch that we can't convert to high-level diags, don't analyze
+ // other sequences.
+ return null;
+ }
+
+ if (!mExpectedEvents.getNonNull(SEQUENCE_MAIN).isEmpty()
+ && actualEvents.getNonNull(SEQUENCE_MAIN).isEmpty()) {
+ return "None of the touch or keyboard events sent by the test was received by "
+ + "Launcher's main thread";
+ }
+ return null;
}
// If the list of actual events matches the list of expected events, returns -1, otherwise
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
index 282fca9..787dc70 100644
--- a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
@@ -26,7 +26,7 @@
OptionsPopupMenu(LauncherInstrumentation launcher) {
mLauncher = launcher;
- mDeepShortcutsContainer = launcher.waitForLauncherObject("deep_shortcuts_container");
+ mDeepShortcutsContainer = launcher.waitForLauncherObject("popup_container");
}
/**
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
new file mode 100644
index 0000000..950c052
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+import com.android.launcher3.testing.TestProtocol;
+
+/**
+ * View containing overview actions
+ */
+public class OverviewActions {
+ private final UiObject2 mOverviewActions;
+ private final LauncherInstrumentation mLauncher;
+
+ OverviewActions(UiObject2 overviewActions, LauncherInstrumentation launcherInstrumentation) {
+ this.mOverviewActions = overviewActions;
+ this.mLauncher = launcherInstrumentation;
+ }
+
+ /**
+ * Clicks content push button.
+ */
+ @NonNull
+ public Overview clickAndDismissContentPush() {
+ if (mLauncher.overviewContentPushEnabled()) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to click content push button and exit screenshot ui")) {
+ UiObject2 exo = mLauncher.waitForObjectInContainer(mOverviewActions,
+ "action_content_push");
+ mLauncher.clickLauncherObject(exo);
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "clicked content push button")) {
+ return new Overview(mLauncher);
+ }
+ }
+ }
+ return new Overview(mLauncher);
+ }
+
+ /**
+ * Clicks screenshot button and closes screenshot ui.
+ */
+ @NonNull
+ public Overview clickAndDismissScreenshot() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to click screenshot button and exit screenshot ui")) {
+ UiObject2 screenshot = mLauncher.waitForObjectInContainer(mOverviewActions,
+ "action_screenshot");
+
+ mLauncher.clickLauncherObject(screenshot);
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "clicked screenshot button")) {
+ UiObject2 closeScreenshot = mLauncher.waitForSystemUiObject(
+ "global_screenshot_dismiss_image");
+ if (mLauncher.getNavigationModel()
+ != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+ LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+ LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
+ }
+ closeScreenshot.click();
+ try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+ "dismissed screenshot")) {
+ return new Overview(mLauncher);
+ }
+ }
+ }
+ }
+
+ /**
+ * Click share button, then drags sharesheet down to remove it.
+ *
+ * Share is currently hidden behind flag, test is kept in case share becomes a default feature.
+ * If share is completely removed then remove this test as well.
+ */
+ @NonNull
+ public Overview clickAndDismissShare() {
+ if (mLauncher.overviewShareEnabled()) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to click share button and dismiss sharesheet")) {
+ UiObject2 share = mLauncher.waitForObjectInContainer(mOverviewActions,
+ "action_share");
+ mLauncher.clickLauncherObject(share);
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "clicked share button")) {
+ mLauncher.waitForAndroidObject("contentPanel");
+ mLauncher.getDevice().pressBack();
+ try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+ "dismissed sharesheet")) {
+ return new Overview(mLauncher);
+ }
+ }
+ }
+ }
+ return new Overview(mLauncher);
+ }
+
+ /**
+ * Click select button
+ *
+ * @return The select mode buttons that are now shown instead of action buttons.
+ */
+ @NonNull
+ public SelectModeButtons clickSelect() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c =
+ mLauncher.addContextLayer("want to click select button")) {
+ UiObject2 select = mLauncher.waitForObjectInContainer(mOverviewActions,
+ "action_select");
+ mLauncher.clickLauncherObject(select);
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "clicked select button")) {
+ return getSelectModeButtons();
+ }
+
+ }
+ }
+
+ /**
+ * Gets the Select Mode Buttons.
+ *
+ * @return The Select Mode Buttons.
+ */
+ @NonNull
+ private SelectModeButtons getSelectModeButtons() {
+ try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to get select mode buttons")) {
+ UiObject2 selectModeButtons = mLauncher.waitForLauncherObject("select_mode_buttons");
+ return new SelectModeButtons(selectModeButtons, mLauncher);
+ }
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index b235919..657b74d 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -71,15 +71,13 @@
public Background open() {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
verifyActiveContainer();
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "clicking an overview task")) {
- mLauncher.executeAndWaitForEvent(
- () -> mLauncher.clickLauncherObject(mTask),
- event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
- () -> "Launching task didn't open a new window: "
- + mTask.getParent().getContentDescription());
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
- }
+ mLauncher.executeAndWaitForEvent(
+ () -> mLauncher.clickLauncherObject(mTask),
+ event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+ () -> "Launching task didn't open a new window: "
+ + mTask.getParent().getContentDescription(),
+ "clicking an overview task");
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
return new Background(mLauncher);
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
new file mode 100644
index 0000000..e1b73a4
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * View containing select mode buttons
+ */
+public class SelectModeButtons {
+ private final UiObject2 mSelectModeButtons;
+ private final LauncherInstrumentation mLauncher;
+
+ SelectModeButtons(UiObject2 selectModeButtons,
+ LauncherInstrumentation launcherInstrumentation) {
+ mSelectModeButtons = selectModeButtons;
+ mLauncher = launcherInstrumentation;
+ }
+
+ /**
+ * Click close button.
+ */
+ @NonNull
+ public Overview clickClose() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c =
+ mLauncher.addContextLayer("want to click close button")) {
+ UiObject2 close = mLauncher.waitForObjectInContainer(mSelectModeButtons, "close");
+ mLauncher.clickLauncherObject(close);
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "clicked close button")) {
+ return new Overview(mLauncher);
+ }
+ }
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
index b8791e8..7f6062f 100644
--- a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
+++ b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
@@ -27,6 +27,11 @@
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.DropBoxManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.uiautomator.SearchCondition;
+import androidx.test.uiautomator.UiDevice;
import org.junit.Assert;
@@ -35,6 +40,7 @@
public class TestHelpers {
+ private static final String TAG = "Tapl";
private static Boolean sIsInLauncherProcess;
public static boolean isInLauncherProcess() {
@@ -154,4 +160,12 @@
return null;
}
}
+
+ public static <R> R wait(SearchCondition<R> condition, long timeout) {
+ Log.d(TAG,
+ "TestHelpers.wait, condition=" + timeout + ", time=" + SystemClock.uptimeMillis());
+ final R result = UiDevice.getInstance(getInstrumentation()).wait(condition, timeout);
+ Log.d(TAG, "TestHelpers.wait, result=" + result + ", time=" + SystemClock.uptimeMillis());
+ return result;
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 53ef796..3520318 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -46,4 +46,9 @@
protected void addExpectedEventsForLongClick() {
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT);
}
+
+ @Override
+ protected String launchableType() {
+ return "widget";
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 39ac645..99d9889 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -17,8 +17,8 @@
package com.android.launcher3.tapl;
import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
+import static com.android.launcher3.tapl.LauncherInstrumentation.log;
-import android.graphics.Point;
import android.graphics.Rect;
import androidx.test.uiautomator.By;
@@ -27,7 +27,7 @@
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
-import com.android.launcher3.tapl.LauncherInstrumentation.GestureScope;
+import com.android.launcher3.testing.TestProtocol;
import java.util.Collection;
@@ -36,6 +36,7 @@
*/
public final class Widgets extends LauncherInstrumentation.VisibleContainer {
private static final int FLING_STEPS = 10;
+ private static final int SCROLL_ATTEMPTS = 60;
Widgets(LauncherInstrumentation launcher) {
super(launcher);
@@ -49,7 +50,7 @@
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to fling forward in widgets")) {
- LauncherInstrumentation.log("Widgets.flingForward enter");
+ log("Widgets.flingForward enter");
final UiObject2 widgetsContainer = verifyActiveContainer();
mLauncher.scroll(
widgetsContainer,
@@ -60,7 +61,7 @@
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("flung forward")) {
verifyActiveContainer();
}
- LauncherInstrumentation.log("Widgets.flingForward exit");
+ log("Widgets.flingForward exit");
}
}
@@ -71,7 +72,7 @@
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to fling backwards in widgets")) {
- LauncherInstrumentation.log("Widgets.flingBackward enter");
+ log("Widgets.flingBackward enter");
final UiObject2 widgetsContainer = verifyActiveContainer();
mLauncher.scroll(
widgetsContainer,
@@ -81,7 +82,7 @@
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("flung back")) {
verifyActiveContainer();
}
- LauncherInstrumentation.log("Widgets.flingBackward exit");
+ log("Widgets.flingBackward exit");
}
}
@@ -90,54 +91,123 @@
return LauncherInstrumentation.ContainerType.WIDGETS;
}
+ private int getWidgetsScroll() {
+ return mLauncher.getTestInfo(
+ TestProtocol.REQUEST_WIDGETS_SCROLL_Y)
+ .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
+
public Widget getWidget(String labelText) {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"getting widget " + labelText + " in widgets list")) {
- final UiObject2 widgetsContainer = verifyActiveContainer();
+ final UiObject2 searchBar = findSearchBar();
+ final int searchBarHeight = searchBar.getVisibleBounds().height();
+ final UiObject2 fullWidgetsPicker = verifyActiveContainer();
mLauncher.assertTrue("Widgets container didn't become scrollable",
- widgetsContainer.wait(Until.scrollable(true), WAIT_TIME_MS));
- final Point displaySize = mLauncher.getRealDisplaySize();
- final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText);
+ fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
+ final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer();
+ mLauncher.assertTrue("Can't locate widgets list for the test app: "
+ + mLauncher.getLauncherPackageName(),
+ widgetsContainer != null);
+ final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText);
+ final BySelector previewSelector = By.res(mLauncher.getLauncherPackageName(),
+ "widget_preview");
int i = 0;
for (; ; ) {
- final Collection<UiObject2> cells = mLauncher.getObjectsInContainer(
- widgetsContainer, "widgets_scroll_container");
- mLauncher.assertTrue("Widgets doesn't have 2 rows", cells.size() >= 2);
- for (UiObject2 cell : cells) {
- final UiObject2 label = cell.findObject(labelSelector);
- if (label == null) continue;
-
- final UiObject2 widget = label.getParent().getParent();
- mLauncher.assertEquals(
- "View is not WidgetCell",
- "com.android.launcher3.widget.WidgetCell",
- widget.getClassName());
-
- int maxWidth = 0;
- for (UiObject2 sibling : widget.getParent().getChildren()) {
- maxWidth = Math.max(mLauncher.getVisibleBounds(sibling).width(), maxWidth);
- }
-
- if (mLauncher.getVisibleBounds(widget).bottom
- <= displaySize.y - mLauncher.getBottomGestureSize()) {
- int visibleDelta = maxWidth - mLauncher.getVisibleBounds(widget).width();
- if (visibleDelta > 0) {
- Rect parentBounds = mLauncher.getVisibleBounds(cell);
- mLauncher.linearGesture(parentBounds.centerX() + visibleDelta
- + mLauncher.getTouchSlop(),
- parentBounds.centerY(), parentBounds.centerX(),
- parentBounds.centerY(), 10, true, GestureScope.INSIDE);
+ final Collection<UiObject2> tableRows = widgetsContainer.getChildren();
+ for (UiObject2 row : tableRows) {
+ final Collection<UiObject2> widgetCells = row.getChildren();
+ for (UiObject2 widget : widgetCells) {
+ final UiObject2 label = mLauncher.findObjectInContainer(widget,
+ labelSelector);
+ if (label == null) {
+ continue;
}
-
- return new Widget(mLauncher, widget);
+ mLauncher.assertEquals(
+ "View is not WidgetCell",
+ "com.android.launcher3.widget.WidgetCell",
+ widget.getClassName());
+ UiObject2 preview = mLauncher.waitForObjectInContainer(widget,
+ previewSelector);
+ return new Widget(mLauncher, preview);
}
}
- mLauncher.assertTrue("Too many attempts", ++i <= 40);
- mLauncher.scrollToLastVisibleRow(widgetsContainer, cells, 0);
+ mLauncher.assertTrue("Too many attempts", ++i <= SCROLL_ATTEMPTS);
+ final int scroll = getWidgetsScroll();
+ mLauncher.scrollDownByDistance(fullWidgetsPicker, searchBarHeight);
+ final int newScroll = getWidgetsScroll();
+ mLauncher.assertTrue(
+ "Scrolled in a wrong direction in Widgets: from " + scroll + " to "
+ + newScroll, newScroll >= scroll);
+ mLauncher.assertTrue("Unable to scroll to the widget", newScroll != scroll);
}
}
}
+
+ private UiObject2 findSearchBar() {
+ final BySelector searchBarContainerSelector = By.res(mLauncher.getLauncherPackageName(),
+ "search_and_recommendations_container");
+ final BySelector searchBarSelector = By.res(mLauncher.getLauncherPackageName(),
+ "widgets_search_bar");
+ final UiObject2 searchBarContainer = mLauncher.waitForLauncherObject(
+ searchBarContainerSelector);
+ UiObject2 searchBar = mLauncher.waitForObjectInContainer(searchBarContainer,
+ searchBarSelector);
+ return searchBar;
+ }
+
+ /** Finds the widgets list of this test app from the collapsed full widgets picker. */
+ private UiObject2 findTestAppWidgetsTableContainer() {
+ final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(),
+ "widgets_list_header");
+ final BySelector targetAppSelector = By.clazz("android.widget.TextView").text(
+ mLauncher.getContext().getPackageName());
+ final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(),
+ "widgets_table");
+
+ boolean hasHeaderExpanded = false;
+ int scrollDistance = 0;
+ for (int i = 0; i < SCROLL_ATTEMPTS; i++) {
+ UiObject2 fullWidgetsPicker = verifyActiveContainer();
+
+ UiObject2 header = mLauncher.waitForObjectInContainer(fullWidgetsPicker,
+ headerSelector);
+ // If a header is barely visible in the bottom edge of the screen, its height could be
+ // too small for a scroll gesture. Since all header should have roughly the same height,
+ // let's pick the max height we have seen so far.
+ scrollDistance = Math.max(scrollDistance, header.getVisibleBounds().height());
+
+ // Look for a header that has the test app name.
+ UiObject2 headerTitle = mLauncher.findObjectInContainer(fullWidgetsPicker,
+ targetAppSelector);
+ if (headerTitle != null) {
+ // If we find the header and it has not been expanded, let's click it to see the
+ // widgets list. Note that we wait until the header is out of the gesture region at
+ // the bottom of the screen, because tapping there in Launcher3 causes NexusLauncher
+ // to briefly appear to handle the gesture, which can break our test.
+ boolean isHeaderOutOfGestureRegion = headerTitle.getVisibleCenter().y
+ < mLauncher.getBottomGestureStartOnScreen();
+ if (!hasHeaderExpanded && isHeaderOutOfGestureRegion) {
+ log("Header has not been expanded. Click to expand.");
+ hasHeaderExpanded = true;
+ mLauncher.clickLauncherObject(headerTitle);
+ }
+
+ // Look for a widgets list.
+ UiObject2 widgetsContainer = mLauncher.findObjectInContainer(fullWidgetsPicker,
+ widgetsContainerSelector);
+ if (widgetsContainer != null) {
+ log("Widgets container found.");
+ return widgetsContainer;
+ }
+ }
+ log("Finding test widget package - scroll with distance: " + scrollDistance);
+ mLauncher.scrollDownByDistance(fullWidgetsPicker, scrollDistance);
+ }
+
+ return null;
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index f0e686f..43134d9 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -22,7 +22,6 @@
import static junit.framework.TestCase.assertTrue;
-import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemClock;
@@ -61,34 +60,6 @@
mHotseat = launcher.waitForLauncherObject("hotseat");
}
- private static boolean supportsRoundedCornersOnWindows(Resources resources) {
- return ResourceUtils.getBoolByName(
- "config_supportsRoundedCornersOnWindows", resources, false);
- }
-
- private static float getWindowCornerRadius(Resources resources) {
- if (!supportsRoundedCornersOnWindows(resources)) {
- return 0f;
- }
-
- // Radius that should be used in case top or bottom aren't defined.
- float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
-
- float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
- if (topRadius == 0f) {
- topRadius = defaultRadius;
- }
- float bottomRadius = ResourceUtils.getDimenByName(
- "rounded_corner_radius_bottom", resources, 0);
- if (bottomRadius == 0f) {
- bottomRadius = defaultRadius;
- }
-
- // Always use the smallest radius to make sure the rounded corners will
- // completely cover the display.
- return Math.min(topRadius, bottomRadius);
- }
-
/**
* Swipes up to All Apps.
*
@@ -103,14 +74,14 @@
final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
final int bottomGestureMargin = ResourceUtils.getNavbarSize(
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources());
- final int windowCornerRadius = (int) Math.ceil(getWindowCornerRadius(
- mLauncher.getResources()));
+ final int windowCornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1;
final int swipeHeight = mLauncher.getTestInfo(
TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
LauncherInstrumentation.log(
- "switchToAllApps: swipeHeight = " + swipeHeight + ", slop = "
+ "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY
+ + ", swipeHeight = " + swipeHeight + ", slop = "
+ mLauncher.getTouchSlop());
mLauncher.swipeToState(
@@ -178,7 +149,7 @@
getHotseatAppIcon("Chrome"),
new Point(mLauncher.getDevice().getDisplayWidth(),
mLauncher.getVisibleBounds(workspace).centerY()),
- "deep_shortcuts_container",
+ "popup_container",
false,
false,
() -> mLauncher.expectEvent(
@@ -219,13 +190,15 @@
launcher.movePointer(launchableCenter, dest, 10, downTime, true,
LauncherInstrumentation.GestureScope.INSIDE);
},
- SPRING_LOADED_STATE_ORDINAL);
+ SPRING_LOADED_STATE_ORDINAL,
+ "long-pressing and moving");
LauncherInstrumentation.log("dragIconToWorkspace: moved pointer");
launcher.runToState(
() -> launcher.sendPointer(
downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
LauncherInstrumentation.GestureScope.INSIDE),
- NORMAL_STATE_ORDINAL);
+ NORMAL_STATE_ORDINAL,
+ "sending UP event");
if (startsActivity || isWidgetShortcut) {
launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
}