[automerger skipped] Block recent work profile apps content capture am: 8786016e85 am: 18217e7f94 -s ours
am skip reason: skip tag Change-Id I3e6ba1d8645335785e8e26cdc895ea9c0017a0fd with SHA-1 284aaba153 is already in history
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/13989187
Change-Id: I3b808f2f32a8bb21a19046062824440cb720d555
diff --git a/Android.bp b/Android.bp
index e132854..e30d22f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12,6 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["packages_apps_Launcher3_license"],
+}
+
+// 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 +36,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 +53,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",
@@ -58,3 +94,120 @@
sdk_version: "current",
min_sdk_version: "28",
}
+
+// 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: "26",
+}
+
+//
+// Build rule for Launcher3 dependencies lib.
+//
+android_library {
+ name: "Launcher3CommonDepsLib",
+ srcs: ["src_build_config/**/*.java"],
+ static_libs: ["Launcher3ResLib"],
+ sdk_version: "current",
+ min_sdk_version: "26",
+ manifest: "AndroidManifest-common.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: "26",
+ target_sdk_version: "29",
+ 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",
+ ],
+}
+
+// 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: "28",
+}
+
+
+// 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"],
+}
diff --git a/Android.mk b/Android.mk
index 7805b32..54a80b7 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
@@ -133,9 +59,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 +67,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 +107,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 \
@@ -206,9 +128,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 +140,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,7 +159,7 @@
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
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index dd0fc21..4e72260 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,26 +76,18 @@
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>
@@ -116,7 +102,7 @@
<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" />
@@ -130,7 +116,7 @@
android:theme="@style/AppItemActivityTheme"
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 +140,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 +151,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 +174,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..1fad72d 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"
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..daad057 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,6 +4,13 @@
# People who can approve changes for submission
#
+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 +35,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..aa3710b 100644
--- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -164,6 +164,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..d2575b6
--- /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|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/ic_listen.xml b/go/quickstep/res/drawable/ic_listen.xml
new file mode 100644
index 0000000..a8e6c93
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_listen.xml
@@ -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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28"
+ android:viewportHeight="28">
+ <path
+ android:pathData="M10.5,15.17c2.58,0 4.67,-2.09 4.67,-4.67s-2.09,-4.67 -4.67,-4.67c-2.58,0 -4.67,2.09 -4.67,4.67S7.92,15.17 10.5,15.17zM10.5,8.17c1.28,0 2.33,1.05 2.33,2.33s-1.05,2.33 -2.33,2.33c-1.28,0 -2.33,-1.05 -2.33,-2.33S9.22,8.17 10.5,8.17z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M10.5,17.5c-3.11,0 -9.33,1.56 -9.33,4.67v2.33h18.67v-2.33C19.83,19.06 13.62,17.5 10.5,17.5zM3.5,22.17c0.26,-0.84 3.86,-2.33 7,-2.33c3.15,0 6.77,1.5 7,2.33H3.5z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M25.67,10.5c0,0.36 -0.02,0.71 -0.05,1.05c-0.01,0.15 -0.03,0.29 -0.05,0.43c-0.02,0.18 -0.05,0.36 -0.08,0.54c-0.04,0.2 -0.07,0.39 -0.12,0.58c-0.01,0.06 -0.03,0.11 -0.04,0.17c-0.59,2.34 -1.81,4.01 -2.52,4.82c-0.09,0.1 -0.18,0.2 -0.28,0.3c-0.17,0.18 -0.27,0.27 -0.27,0.27l-1.65,-1.63c1.34,-1.33 2.27,-3.07 2.6,-5.01c0.01,-0.08 0.02,-0.16 0.04,-0.24c0.06,-0.42 0.1,-0.85 0.1,-1.29c0,-0.44 -0.04,-0.88 -0.1,-1.3c-0.01,-0.06 -0.02,-0.13 -0.03,-0.19c-0.32,-1.95 -1.25,-3.7 -2.6,-5.04l1.65,-1.63c0,0 0.11,0.1 0.27,0.27c0.09,0.1 0.19,0.2 0.28,0.3c0.71,0.82 1.93,2.48 2.52,4.82c0.01,0.06 0.03,0.11 0.04,0.17c0.04,0.19 0.08,0.38 0.12,0.58c0.03,0.18 0.06,0.36 0.08,0.54c0.02,0.14 0.04,0.28 0.05,0.43C25.65,9.79 25.67,10.14 25.67,10.5z"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M20.61,8.4C20.85,9.06 21,9.76 21,10.5s-0.15,1.44 -0.39,2.1c-0.28,0.77 -0.71,1.46 -1.25,2.05l-1.66,-1.64c0.56,-0.63 0.91,-1.44 0.95,-2.34c0,-0.06 0.02,-0.11 0.02,-0.17s-0.01,-0.11 -0.02,-0.17c-0.04,-0.9 -0.39,-1.71 -0.95,-2.34l1.66,-1.64C19.9,6.94 20.32,7.63 20.61,8.4z"
+ android:fillColor="#FBBC04"/>
+</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..4307330
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_search.xml
@@ -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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28"
+ android:viewportHeight="28">
+ <path
+ android:pathData="M24.5,22.75l-6.84,-6.84c1,-1.35 1.59,-3.02 1.59,-4.83h-2.33c0,3.22 -2.62,5.83 -5.83,5.83v2.33c1.81,0 3.47,-0.6 4.83,-1.59l6.84,6.84L24.5,22.75z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M11.08,2.92v2.33c3.22,0 5.83,2.62 5.83,5.83h2.33C19.25,6.57 15.59,2.92 11.08,2.92z"
+ android:fillColor="#34A853"/>
+ <path
+ android:pathData="M5.25,11.08H2.92c0,4.51 3.66,8.17 8.17,8.17v-2.33C7.87,16.92 5.25,14.3 5.25,11.08z"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M2.92,11.08h2.33c0,-3.22 2.62,-5.83 5.83,-5.83V2.92C6.57,2.92 2.92,6.57 2.92,11.08z"
+ android:fillColor="#FBBC04"/>
+</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..1247807
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_translate.xml
@@ -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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28"
+ android:viewportHeight="28">
+ <path
+ android:pathData="M12.28,15.19l-0.07,-0.05c-0.61,-0.49 -1.15,-1.05 -1.65,-1.63c-1.05,-1.22 -1.88,-2.63 -2.39,-4.17H5.83c0.54,2.17 1.58,4.16 3.01,5.85l-5.93,5.23l1.75,1.75l5.91,-5.26c0.05,0.04 0.1,0.09 0.15,0.13l3.42,2.79l1.02,-2.33L12.28,15.19z"
+ android:fillColor="#FBBC04"/>
+ <path
+ android:pathData="M21.58,11.67h-2.33l-5.25,14h2.33l1.31,-3.5h5.54l1.32,3.5h2.33L21.58,11.67zM18.53,19.83l1.89,-5.05l1.89,5.05H18.53z"
+ android:fillColor="#4285F4"/>
+ <path
+ android:pathData="M11.67,2.33l-2.34,0l0,2.34l-8.16,0l0,2.33l10.5,0l0,-2.33z"
+ android:fillColor="#EA4335"/>
+ <path
+ android:pathData="M11.67,4.67V7H14c-0.61,2.42 -1.79,4.65 -3.44,6.5c0.5,0.59 1.04,1.15 1.65,1.63l0.07,0.05c2.03,-2.32 3.44,-5.14 4.05,-8.19h3.5V4.67H11.67z"
+ android:fillColor="#34A853"/>
+</vector>
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..866eac6
--- /dev/null
+++ b/go/quickstep/res/layout/overview_actions_container.xml
@@ -0,0 +1,99 @@
+<?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="bottom|center_horizontal"
+ android:orientation="horizontal">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1" />
+
+ <Button
+ android:id="@+id/action_listen"
+ style="@style/OverviewActionButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableTop="@drawable/ic_listen"
+ android:drawablePadding="1dp"
+ android:text="@string/action_listen"
+ android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+ <Button
+ android:id="@+id/action_translate"
+ style="@style/OverviewActionButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableTop="@drawable/ic_translate"
+ android:drawablePadding="1dp"
+ android:text="@string/action_translate"
+ android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+ <Button
+ android:id="@+id/action_search"
+ style="@style/OverviewActionButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableTop="@drawable/ic_search"
+ android:drawablePadding="1dp"
+ android:text="@string/action_search"
+ android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+ <Button
+ android:id="@+id/action_screenshot"
+ style="@style/OverviewActionButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableTop="@drawable/ic_screenshot"
+ android:drawablePadding="1dp"
+ android:text="@string/action_screenshot"
+ android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1" />
+
+ <Button
+ android:id="@+id/action_share"
+ style="@style/OverviewActionButton"
+ 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" />
+
+ <Space
+ android:id="@+id/oav_three_button_space"
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_weight="1"
+ android:visibility="gone" />
+ </LinearLayout>
+
+</com.android.quickstep.views.GoOverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/config.xml b/go/quickstep/res/values/config.xml
similarity index 60%
rename from quickstep/recents_ui_overrides/res/values/config.xml
rename to go/quickstep/res/values/config.xml
index 120e034..402bf9a 100644
--- a/quickstep/recents_ui_overrides/res/values/config.xml
+++ b/go/quickstep/res/values/config.xml
@@ -14,5 +14,13 @@
limitations under the License.
-->
<resources>
- <integer name="max_depth_blur_radius">150</integer>
+ <!-- The component to receive app sharing Intents -->
+ <string name="app_sharing_component" translatable="false"/>
+ <!-- The package to receive Listen, Translate, and Search Intents -->
+ <string name="niu_actions_package" 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/strings.xml b/go/quickstep/res/values/strings.xml
new file mode 100644
index 0000000..61c8cd9
--- /dev/null
+++ b/go/quickstep/res/values/strings.xml
@@ -0,0 +1,14 @@
+<?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>
+</resources>
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..350e0d1
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
@@ -0,0 +1,186 @@
+/*
+ * 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 com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
+
+import android.annotation.SuppressLint;
+import android.app.assist.AssistContent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.R;
+import com.android.quickstep.util.AssistContentRequester;
+import com.android.quickstep.views.OverviewActionsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * 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";
+ private static final String TAG = "TaskOverlayFactoryGo";
+
+ // Empty constructor required for ResourceBasedOverride
+ public TaskOverlayFactoryGo(Context context) {}
+
+ /**
+ * Create a new overlay instance for the given View
+ */
+ public TaskOverlayGo createOverlay(TaskThumbnailView thumbnailView) {
+ return new TaskOverlayGo(thumbnailView);
+ }
+
+ /**
+ * 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 OverviewActionsView> extends TaskOverlay {
+ private String mNIUPackageName;
+ private String mWebUrl;
+
+ private TaskOverlayGo(TaskThumbnailView taskThumbnailView) {
+ super(taskThumbnailView);
+ }
+
+ /**
+ * Called when the current task is interactive for the user
+ */
+ @Override
+ public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+ boolean rotated) {
+ getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
+ mNIUPackageName =
+ mApplicationContext.getResources().getString(R.string.niu_actions_package);
+
+ if (thumbnail == null || TextUtils.isEmpty(mNIUPackageName)) {
+ return;
+ }
+
+ getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+ boolean isAllowedByPolicy = mThumbnailView.isRealSnapshot();
+ getActionsView().setCallbacks(new OverlayUICallbacksGoImpl(isAllowedByPolicy, task));
+
+ int taskId = task.key.id;
+ AssistContentRequester contentRequester =
+ new AssistContentRequester(mApplicationContext);
+ contentRequester.requestAssistContent(taskId, this::onAssistContentReceived);
+ }
+
+ /** 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;
+ }
+
+ /**
+ * Creates and sends an Intent corresponding to the button that was clicked
+ */
+ @VisibleForTesting
+ public void sendNIUIntent(String actionType) {
+ Intent intent = createNIUIntent(actionType);
+ mImageApi.shareAsDataWithExplicitIntent(/* crop */ null, intent);
+ }
+
+ 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(ELAPSED_NANOS, SystemClock.elapsedRealtimeNanos());
+
+ if (mWebUrl != null) {
+ intent.putExtra(ACTIONS_URL, mWebUrl);
+ }
+
+ return intent;
+ }
+
+ protected class OverlayUICallbacksGoImpl extends OverlayUICallbacksImpl
+ implements OverlayUICallbacksGo {
+ public OverlayUICallbacksGoImpl(boolean isAllowedByPolicy, Task task) {
+ super(isAllowedByPolicy, task);
+ }
+
+ @SuppressLint("NewApi")
+ public void onListen() {
+ if (mIsAllowedByPolicy) {
+ sendNIUIntent(ACTION_LISTEN);
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public void onTranslate() {
+ if (mIsAllowedByPolicy) {
+ sendNIUIntent(ACTION_TRANSLATE);
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public void onSearch() {
+ if (mIsAllowedByPolicy) {
+ sendNIUIntent(ACTION_SEARCH);
+ } else {
+ showBlockedByPolicyMessage();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void setImageActionsAPI(ImageActionsApi imageActionsApi) {
+ mImageApi = imageActionsApi;
+ }
+ }
+
+ /**
+ * 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..9997d16
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/GoOverviewActionsView.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.quickstep.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.quickstep.TaskOverlayFactoryGo.OverlayUICallbacksGo;
+
+/**
+ * View for showing Go-specific action buttons in Overview
+ */
+public final class GoOverviewActionsView extends OverviewActionsView<OverlayUICallbacksGo> {
+ 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);
+ findViewById(R.id.action_search).setOnClickListener(this);
+ } else {
+ findViewById(R.id.action_listen).setVisibility(View.GONE);
+ findViewById(R.id.action_translate).setVisibility(View.GONE);
+ findViewById(R.id.action_search).setVisibility(View.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();
+ }
+ }
+}
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..cc5e1cb 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -25,11 +25,12 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.icons.ComponentWithLabelAndIcon;
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 +42,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.
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/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..fe81b4c 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;
}
}
@@ -131,6 +136,7 @@
// Legacy shortcuts and shortcuts handled by ShortcutManager
message Shortcut {
optional string shortcut_name = 1;
+ optional string shortcut_id = 2;
}
// AppWidgets handled by AppWidgetManager
@@ -165,6 +171,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..7fe9b08 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"
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..5e5cf73 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -17,97 +17,103 @@
** 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"
+ 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" />
+ <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"
- android:autoRemoveFromRecents="true"
- android:excludeFromRecents="true"
- android:screenOrientation="portrait">
+ <activity android:name="com.android.quickstep.interaction.GestureSandboxActivity"
+ android:autoRemoveFromRecents="true"
+ android:excludeFromRecents="true"
+ 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
@@ -116,7 +122,8 @@
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..6253b41
--- /dev/null
+++ b/quickstep/protos_overrides/launcher_atom_extension.proto
@@ -0,0 +1,35 @@
+/*
+ * 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;
+ }
+}
+
+// Represents on-device search result container.
+message DeviceSearchResultContainer{
+ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/go/res/values/dimens.xml b/quickstep/res/drawable/default_sandbox_app_icon.xml
similarity index 71%
copy from go/res/values/dimens.xml
copy to quickstep/res/drawable/default_sandbox_app_icon.xml
index f1b1053..e9c9701 100644
--- a/go/res/values/dimens.xml
+++ b/quickstep/res/drawable/default_sandbox_app_icon.xml
@@ -1,20 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
+<!--
+ 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>
- <!-- Dynamic Grid -->
- <dimen name="dynamic_grid_hotseat_size">60dp</dimen>
-</resources>
\ No newline at end of file
+<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/go/res/values/dimens.xml b/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml
similarity index 70%
copy from go/res/values/dimens.xml
copy to quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml
index f1b1053..9c95497 100644
--- a/go/res/values/dimens.xml
+++ b/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml
@@ -1,20 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
+<!--
+ 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>
- <!-- Dynamic Grid -->
- <dimen name="dynamic_grid_hotseat_size">60dp</dimen>
-</resources>
\ No newline at end of file
+<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/go/res/values/dimens.xml b/quickstep/res/drawable/default_sandbox_app_task_thumbnail.xml
similarity index 71%
copy from go/res/values/dimens.xml
copy to quickstep/res/drawable/default_sandbox_app_task_thumbnail.xml
index f1b1053..9d4cc95 100644
--- a/go/res/values/dimens.xml
+++ b/quickstep/res/drawable/default_sandbox_app_task_thumbnail.xml
@@ -1,20 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
+<!--
+ 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>
- <!-- Dynamic Grid -->
- <dimen name="dynamic_grid_hotseat_size">60dp</dimen>
-</resources>
\ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/gesture_tutorial_fake_task_view_color" />
+</shape>
diff --git a/go/res/values/dimens.xml b/quickstep/res/drawable/default_sandbox_wallpaper.xml
similarity index 72%
copy from go/res/values/dimens.xml
copy to quickstep/res/drawable/default_sandbox_wallpaper.xml
index f1b1053..816d1d6 100644
--- a/go/res/values/dimens.xml
+++ b/quickstep/res/drawable/default_sandbox_wallpaper.xml
@@ -1,20 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
+<!--
+ 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>
- <!-- Dynamic Grid -->
- <dimen name="dynamic_grid_hotseat_size">60dp</dimen>
-</resources>
\ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground" />
+</shape>
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/task_menu_bg.xml b/quickstep/res/drawable/task_menu_bg.xml
index 7334d98..a60defc 100644
--- a/quickstep/res/drawable/task_menu_bg.xml
+++ b/quickstep/res/drawable/task_menu_bg.xml
@@ -14,25 +14,7 @@
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="?attr/popupColorPrimary" />
+</shape>
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/recents_ui_overrides/res/layout/fallback_recents_activity.xml b/quickstep/res/layout/fallback_recents_activity.xml
similarity index 84%
rename from quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
rename to quickstep/res/layout/fallback_recents_activity.xml
index cd64a94..55400a7 100644
--- a/quickstep/recents_ui_overrides/res/layout/fallback_recents_activity.xml
+++ b/quickstep/res/layout/fallback_recents_activity.xml
@@ -19,6 +19,14 @@
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"
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_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
index 459d65f..9d06dfb 100644
--- a/quickstep/res/layout/gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -13,10 +13,10 @@
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"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:attr/colorBackground">
+ android:layout_height="match_parent">
<View
android:id="@+id/gesture_tutorial_ripple_view"
@@ -24,11 +24,24 @@
android:layout_height="match_parent"
android:background="@drawable/gesture_tutorial_ripple"/>
+ <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:background="@color/gesture_tutorial_fake_task_view_color"
android:visibility="invisible" />
<ImageView
@@ -41,81 +54,81 @@
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:layout_marginStart="4dp"
+ android:layout_marginTop="30dp"
android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_titles_container"
+ android:background="@android:color/transparent"
android:contentDescription="@string/gesture_tutorial_close_button_content_description"
- android:tint="?android:attr/textColorPrimary"
- android:src="@drawable/gesture_tutorial_close_button"/>
+ android:padding="18dp"
+ android:src="@drawable/gesture_tutorial_close_button"
+ android:tint="?android:attr/textColorPrimary"/>
<LinearLayout
android:id="@+id/gesture_tutorial_fragment_titles_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="70dp"
android:layout_alignParentTop="true"
+ android:layout_marginTop="70dp"
android:focusable="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/gesture_tutorial_fragment_title_view"
+ style="@style/TextAppearance.GestureTutorial.Title"
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"/>
+ android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"/>
<TextView
android:id="@+id/gesture_tutorial_fragment_subtitle_view"
+ style="@style/TextAppearance.GestureTutorial.Subtitle"
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"/>
+ android:layout_marginTop="10dp"
+ android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"/>
</LinearLayout>
<TextView
android:id="@+id/gesture_tutorial_fragment_feedback_view"
+ style="@style/TextAppearance.GestureTutorial.Feedback"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
+ android:layout_below="@id/gesture_tutorial_fragment_titles_container"
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="40dp"/>
<!-- 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"
+ style="@style/TextAppearance.GestureTutorial.ButtonLabel"
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:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
+ android:layout_marginBottom="48dp"
android:background="@drawable/gesture_tutorial_action_button_background"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
- style="@style/TextAppearance.GestureTutorial.ButtonLabel"/>
+ android:stateListAnimator="@null"/>
<Button
android:id="@+id/gesture_tutorial_fragment_action_text_button"
+ style="@style/TextAppearance.GestureTutorial.TextButtonLabel"
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:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
+ android:layout_marginBottom="48dp"
android:background="@null"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
- style="@style/TextAppearance.GestureTutorial.TextButtonLabel"/>
-</RelativeLayout>
\ No newline at end of file
+ android:stateListAnimator="@null"/>
+</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..b652252 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
@@ -58,7 +57,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/recents_ui_overrides/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
similarity index 80%
rename from quickstep/recents_ui_overrides/res/layout/overview_panel.xml
rename to quickstep/res/layout/overview_panel.xml
index fe57e9b..d7bcd9e 100644
--- a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -15,6 +15,13 @@
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <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" />
+
<com.android.quickstep.views.LauncherRecentsView
android:id="@+id/overview_panel"
android:layout_width="match_parent"
@@ -22,7 +29,6 @@
android:accessibilityPaneTitle="@string/accessibility_recent_apps"
android:clipChildren="false"
android:clipToPadding="false"
- android:theme="@style/HomeScreenElementTheme"
android:visibility="invisible" />
<include
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/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..3916ff9 100644
--- a/quickstep/res/layout/task_menu.xml
+++ b/quickstep/res/layout/task_menu.xml
@@ -24,20 +24,12 @@
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
+ android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:textSize="12sp"/>
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
new file mode 100644
index 0000000..84e2304
--- /dev/null
+++ b/quickstep/res/layout/taskbar.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.
+-->
+
+<com.android.launcher3.taskbar.TaskbarContainerView
+ 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="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/taskbar_background"
+ android:gravity="center"/>
+
+</com.android.launcher3.taskbar.TaskbarContainerView>
\ No newline at end of file
diff --git a/go/res/values/dimens.xml b/quickstep/res/layout/taskbar_app_icon.xml
similarity index 78%
rename from go/res/values/dimens.xml
rename to quickstep/res/layout/taskbar_app_icon.xml
index f1b1053..6fefdb6 100644
--- a/go/res/values/dimens.xml
+++ b/quickstep/res/layout/taskbar_app_icon.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -14,7 +14,4 @@
limitations under the License.
-->
-<resources>
- <!-- Dynamic Grid -->
- <dimen name="dynamic_grid_hotseat_size">60dp</dimen>
-</resources>
\ No newline at end of file
+<com.android.launcher3.views.DoubleShadowBubbleTextView style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/layout/scrim_view.xml b/quickstep/res/layout/taskbar_divider.xml
similarity index 74%
rename from quickstep/res/layout/scrim_view.xml
rename to quickstep/res/layout/taskbar_divider.xml
index 2cc37f9..87649f7 100644
--- a/quickstep/res/layout/scrim_view.xml
+++ b/quickstep/res/layout/taskbar_divider.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.quickstep.views.ShelfScrimView
+
+<View
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
+ android:layout_width="@dimen/taskbar_divider_thickness"
+ android:layout_height="@dimen/taskbar_divider_height"
+ android:background="@color/taskbar_divider" />
\ No newline at end of file
diff --git a/go/res/values/dimens.xml b/quickstep/res/layout/taskbar_predicted_app_icon.xml
similarity index 78%
copy from go/res/values/dimens.xml
copy to quickstep/res/layout/taskbar_predicted_app_icon.xml
index f1b1053..211ebc8 100644
--- a/go/res/values/dimens.xml
+++ b/quickstep/res/layout/taskbar_predicted_app_icon.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -14,7 +14,4 @@
limitations under the License.
-->
-<resources>
- <!-- Dynamic Grid -->
- <dimen name="dynamic_grid_hotseat_size">60dp</dimen>
-</resources>
\ No newline at end of file
+<com.android.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/layout/scrim_view.xml b/quickstep/res/layout/taskbar_view.xml
similarity index 68%
copy from quickstep/res/layout/scrim_view.xml
copy to quickstep/res/layout/taskbar_view.xml
index 2cc37f9..34a88ea 100644
--- a/quickstep/res/layout/scrim_view.xml
+++ b/quickstep/res/layout/taskbar_view.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,14 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.quickstep.views.ShelfScrimView
+
+<com.android.launcher3.taskbar.TaskbarView
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/taskbar_view"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:id="@+id/scrim_view" />
\ No newline at end of file
+ android:layout_height="@dimen/taskbar_size"
+ android:background="@android:color/transparent"
+ android:layout_gravity="bottom"
+ android:gravity="center"
+ android:visibility="gone" />
+
diff --git a/quickstep/res/values-land/dimens.xml b/quickstep/res/values-land/dimens.xml
index c03eaa2..7cb01f6 100644
--- a/quickstep/res/values-land/dimens.xml
+++ b/quickstep/res/values-land/dimens.xml
@@ -16,4 +16,6 @@
-->
<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/colors.xml b/quickstep/res/values/colors.xml
index 40da136..54730f1 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_divider">#C0C0C0</color>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 0f2955b..b2ff770 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">150</integer>
</resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9d70316..3e6c78f 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -15,57 +15,59 @@
-->
<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>
+ <dimen name="overview_proactive_row_height">48dp</dimen>
+ <dimen name="overview_proactive_row_bottom_margin">16dp</dimen>
+
+ <dimen name="overview_minimum_next_prev_size">48dp</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">12dp</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">130dp</dimen>
+ <dimen name="split_placeholder_size">110dp</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>
<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">5dp</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_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>
<!-- Copied from framework resource:
docked_stack_divider_thickness - 2 * docked_stack_divider_insets -->
<dimen name="multi_window_task_divider_size">10dp</dimen>
@@ -76,6 +78,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>
@@ -98,4 +104,37 @@
<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_divider_thickness">1dp</dimen>
+ <dimen name="taskbar_divider_height">32dp</dimen>
+ <dimen name="taskbar_folder_margin">16dp</dimen>
</resources>
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/res/values/override.xml
similarity index 74%
rename from quickstep/recents_ui_overrides/res/values/override.xml
rename to quickstep/res/values/override.xml
index 6aa9619..705ec9d 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/res/values/override.xml
@@ -17,16 +17,12 @@
<!-- 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="model_delegate_class" translatable="false">com.android.launcher3.model.QuickstepModelDelegate</string>
- <string name="prediction_model_class" translatable="false">com.android.launcher3.hybridhotseat.HotseatPredictionModel</string>
</resources>
-
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 86120e3..2a06830 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,91 @@
<!-- 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>
- <!-- 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>
+ <string name="back_gesture_feedback_swipe_too_far_from_left_edge">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_left_edge">Make sure you swipe from the left edge to the middle of the screen and let go.</string>
+ <!-- Feedback shown after completing the left back gesture before continuing on to the right edge. [CHAR LIMIT=60] -->
+ <string name="back_gesture_feedback_complete_left_edge">That\'s it! Now try swiping from the right edge.</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">Make sure you swipe from the far-right 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">Make sure you swipe from the right edge to the middle of the screen and let go.</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_complete">You completed the go back gesture. Next up, learn how to go Home.</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=100] -->
+ <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>
+ <string name="home_gesture_feedback_complete">You completed the go Home gesture. Next up, learn how to switch apps.</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>
+ <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. [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_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>
+ <string name="overview_gesture_feedback_complete">You completed the switch apps gesture. You\'re ready to use your phone!</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">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 proceed to the next tutorial step. [CHAR LIMIT=14] -->
+ <string name="gesture_tutorial_action_button_label_next">Next</string>
+ <!-- Button text shown on a button on the feedback popup to complete the tutorial. [CHAR LIMIT=14] -->
+ <string name="gesture_tutorial_action_button_label_done">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>
<!-- ******* Overview ******* -->
<!-- Label for a button that causes the current overview app to be shared. [CHAR_LIMIT=40] -->
@@ -163,4 +184,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. [CHAR_LIMIT=40] -->
+ <string name="skip_tutorial_dialog_subtitle">You can find this later in the Tips 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..e56397a 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -81,8 +81,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/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..70a143e
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -0,0 +1,263 @@
+/*
+ * 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.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.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.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);
+ }
+
+ 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..eca27b5 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -32,7 +32,7 @@
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;
@@ -43,14 +43,24 @@
@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 +73,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 +95,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 +120,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 +152,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 +169,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 +186,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 +224,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,15 +232,16 @@
@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()));
}
@@ -210,9 +253,10 @@
public void applyTransform_taskNotFrozen_90Rotate_inTwoRegions() {
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
@@ -231,18 +275,18 @@
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) {
+ 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 = new Point(screenSize.mHeight, screenSize.mWidth);
}
- return new DefaultDisplay.Info(0, rotation, 0, p, p, p, null);
+ return new DisplayController.Info(0, rotation, 0, p, p, p, null);
}
- 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..88079ae 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.getDefaultDisplay(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..e9ded8a 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -19,19 +19,23 @@
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.util.DisplayController.DisplayHolder.CHANGE_SIZE;
+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.content.Intent;
import android.content.IntentSender;
-import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.view.View;
+
+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.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter;
@@ -39,23 +43,31 @@
import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.statemanager.StateManager.StateHandler;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarController;
+import com.android.launcher3.taskbar.TaskbarStateHandler;
+import com.android.launcher3.taskbar.TaskbarView;
import com.android.launcher3.uioverrides.RecentsViewStateController;
-import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.UiThreadHelper;
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.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 +77,50 @@
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 TaskbarController mTaskbarController;
+ 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);
+ if (mTaskbarController != null) {
+ mTaskbarController.cleanup();
+ }
+
super.onDestroy();
}
+ public QuickstepTransitionManager getAppTransitionManager() {
+ return mAppTransitionManager;
+ }
+
@Override
public void onNavigationModeChanged(Mode newMode) {
getDragLayer().recreateControllers();
- if (mActionsView != null && isOverviewActionsEnabled()) {
+ if (mActionsView != null) {
mActionsView.updateVerticalMargin(newMode);
}
}
@@ -111,6 +136,13 @@
}
@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);
@@ -119,7 +151,8 @@
@Override
protected 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
@@ -157,7 +190,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 +216,106 @@
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(SystemUiProxy.INSTANCE.get(this))
+ );
+ overviewPanel.init(mActionsView, mSplitPlaceholderView);
+ 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();
+
+ addTaskbarIfNecessary();
+ addOnDeviceProfileChangeListener(newDp -> addTaskbarIfNecessary());
+ }
+
+ @Override
+ public void onDisplayInfoChanged(DisplayController.Info info, int flags) {
+ super.onDisplayInfoChanged(info, flags);
+ if ((flags & CHANGE_SIZE) != 0) {
+ addTaskbarIfNecessary();
}
}
- private boolean isOverviewActionsEnabled() {
- return FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(this);
+ private void addTaskbarIfNecessary() {
+ if (mTaskbarController != null) {
+ mTaskbarController.cleanup();
+ mTaskbarController = null;
+ }
+ if (mDeviceProfile.isTaskbarPresent) {
+ TaskbarView taskbarViewOnHome = (TaskbarView) mHotseat.getTaskbarView();
+ TaskbarActivityContext taskbarActivityContext = new TaskbarActivityContext(this);
+ mTaskbarController = new TaskbarController(this,
+ taskbarActivityContext.getTaskbarContainerView(), taskbarViewOnHome);
+ mTaskbarController.init();
+ }
}
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 TaskbarController getTaskbarController() {
+ return mTaskbarController;
+ }
+
+ public TaskbarStateHandler getTaskbarStateHandler() {
+ return mTaskbarStateHandler;
+ }
+
@Override
- protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs) {
- return new QuickstepOnboardingPrefs(this, sharedPrefs);
+ public boolean isViewInTaskbar(View v) {
+ return mTaskbarController != null && mTaskbarController.isViewInTaskbar(v);
+ }
+
+ public boolean supportsAdaptiveIconAnimation(View clickedView) {
+ return mAppTransitionManager.hasControlRemoteAppTransitionPermission()
+ && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get()
+ && !isViewInTaskbar(clickedView);
+ }
+
+ @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,11 +336,19 @@
@Override
public float[] getNormalOverviewScaleAndOffset() {
- return SysUINavigationMode.getMode(this) == Mode.NO_BUTTON
+ return SysUINavigationMode.getMode(this).hasGestures
? new float[] {1, 1} : new float[] {1.1f, 0};
}
@Override
+ public float getNormalTaskbarScale() {
+ if (mTaskbarController != null) {
+ return mTaskbarController.getTaskbarScaleOnHome();
+ }
+ return super.getNormalTaskbarScale();
+ }
+
+ @Override
public void onDragLayerHierarchyChanged() {
onLauncherStateOrFocusChanged();
}
@@ -260,6 +364,12 @@
mDepthController.setActivityStarted(isStarted());
}
+ if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) {
+ if (mTaskbarController != null) {
+ mTaskbarController.onLauncherResumedOrPaused(hasBeenResumed());
+ }
+ }
+
super.onActivityFlagsChanged(changeBits);
}
@@ -282,8 +392,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);
}
@@ -304,15 +416,17 @@
Stream.of(WellbeingModel.SHORTCUT_FACTORY));
}
- public ShelfPeekAnim getShelfPeekAnim() {
- return mShelfPeekAnim;
- }
-
- /**
- * Returns Prediction controller for hybrid hotseat
- */
- public HotseatPredictionController getHotseatPredictionController() {
- return mHotseatPredictionController;
+ @Override
+ public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
+ ActivityOptionsWrapper activityOptions =
+ mAppTransitionManager.hasControlRemoteAppTransitionPermission()
+ ? mAppTransitionManager.getActivityLaunchOptions(this, v)
+ : super.getActivityLaunchOptions(v);
+ if (mLastTouchUpTime > 0) {
+ ActivityOptionsCompat.setLauncherSourceInfo(
+ activityOptions.options, mLastTouchUpTime);
+ }
+ return activityOptions;
}
public void setHintUserWillBeActive() {
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 02769c6..be98157 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,16 +30,14 @@
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 {
-
- private static final String TAG = "LauncherAnimationRunner";
+public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
private final Handler mHandler;
private final boolean mStartAtFrontOfQueue;
@@ -56,17 +56,19 @@
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);
+ onCreateAnimation(transit, appTargets, wallpaperTargets, nonAppTargets,
+ mAnimationResult);
};
if (mStartAtFrontOfQueue) {
postAtFrontOfQueueAsynchronously(mHandler, r);
@@ -75,6 +77,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
@@ -88,8 +98,11 @@
*/
@UiThread
public abstract void onCreateAnimation(
+ int transit,
RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result);
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ AnimationResult result);
@UiThread
private void finishExistingAnimation() {
@@ -110,37 +123,60 @@
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);
+
+ }
+
+ /**
+ * Sets the animation to play for this app launch
+ */
+ @UiThread
+ public void setAnimation(AnimatorSet animation, Context context,
+ @Nullable Runnable onCompleteCallback) {
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() {
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..82a83fc
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -0,0 +1,1209 @@
+/*
+ * 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.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_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.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.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+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.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+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.DynamicResource;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.views.FloatingIconView;
+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.StaggeredWorkspaceAnim;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+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.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.LinkedHashMap;
+
+/**
+ * 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", false);
+
+ /** 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;
+
+ public static final int CONTENT_ALPHA_DURATION = 217;
+ protected static final int CONTENT_TRANSLATION_DURATION = 350;
+
+ private static final int MAX_NUM_TASKS = 5;
+
+ 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 mClosingWindowTransY;
+ private final float mMaxShadowRadius;
+
+ 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 WrappedAnimationRunnerImpl 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);
+ }
+ };
+
+ // Will never be larger than MAX_NUM_TASKS
+ private LinkedHashMap<Integer, Integer> mTypeForTaskId;
+
+ public QuickstepTransitionManager(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);
+ mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
+ mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius);
+
+ mLauncher.addOnDeviceProfileChangeListener(this);
+
+ if (supportsSSplashScreen()) {
+ mTypeForTaskId = new LinkedHashMap<Integer, Integer>(MAX_NUM_TASKS) {
+ @Override
+ protected boolean removeEldestEntry(Entry<Integer, Integer> entry) {
+ return size() > MAX_NUM_TASKS;
+ }
+ };
+
+ SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(
+ new IStartingWindowListener.Stub() {
+ @Override
+ public void onTaskLaunching(int taskId, int supportedType) {
+ mTypeForTaskId.put(taskId, supportedType);
+ }
+ });
+ }
+ }
+
+ @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(Launcher launcher, View v) {
+ boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
+ RunnableList onEndCallback = new RunnableList();
+ mAppLaunchRunner = new AppLaunchAnimationRunner(mHandler, v, onEndCallback);
+ RemoteAnimationRunnerCompat runner = new WrappedLauncherAnimationRunner<>(
+ 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, boolean launcherClosing) {
+ TaskViewUtils.composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets,
+ 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,
+ 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
+ Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange);
+ anim.play(getOpeningWindowAnimators(v, appTargets, wallpaperTargets, windowTargetBounds,
+ areAllTargetsTranslucent(appTargets), rotationChange));
+ 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(@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;
+ }
+
+ private int getOpeningTaskId(RemoteAnimationTargetCompat[] appTargets) {
+ for (RemoteAnimationTargetCompat target : appTargets) {
+ if (target.mode == MODE_OPENING) {
+ return target.taskId;
+ }
+ }
+ return -1;
+ }
+
+ 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)) {
+ 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 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();
+ };
+ }
+
+ /**
+ * @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 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, MODE_OPENING);
+ SurfaceTransactionApplier surfaceApplier =
+ new SurfaceTransactionApplier(floatingView);
+ openingTargets.addReleaseCheck(surfaceApplier);
+
+ int[] dragLayerBounds = new int[2];
+ mDragLayer.getLocationOnScreen(dragLayerBounds);
+
+ final boolean hasSplashScreen;
+ if (supportsSSplashScreen()) {
+ int taskId = getOpeningTaskId(appTargets);
+ int type = mTypeForTaskId.getOrDefault(taskId, STARTING_WINDOW_TYPE_NONE);
+ mTypeForTaskId.remove(taskId);
+ hasSplashScreen = type == STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+ } else {
+ hasSplashScreen = false;
+ }
+
+ AnimOpenProperties prop = new AnimOpenProperties(mLauncher.getResources(), mDeviceProfile,
+ windowTargetBounds, launcherIconBounds, v, dragLayerBounds[0], dragLayerBounds[1],
+ hasSplashScreen);
+ 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 targetBounds = new RectF(windowTargetBounds);
+ 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;
+
+ appAnimator.addUpdateListener(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);
+
+ @Override
+ public void onUpdate(float percent) {
+ // 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;
+ float windowTransY0 = tmpRectF.top - offsetY;
+ if (hasSplashScreen) {
+ windowTransX0 -= crop.left * scale;
+ windowTransY0 -= 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;
+
+ 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);
+ 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(floatingIconBounds, mIconAlpha.value, 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[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.
+ */
+ 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<>(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 WrappedLauncherAnimationRunner<>(
+ 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 WrappedLauncherAnimationRunner<>(mHandler, mWallpaperOpenTransitionRunner,
+ false /* startAtFrontOfQueue */));
+ mLauncherOpenTransition.addHomeOpenCheck();
+ SystemUiProxy.INSTANCE.getNoCreate().registerRemoteTransition(mLauncherOpenTransition);
+ }
+ }
+
+ public void onActivityDestroyed() {
+ unregisterRemoteAnimations();
+ unregisterRemoteTransitions();
+ 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.
+ */
+ 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;
+ }
+
+ 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) {
+ 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) {
+ InteractionJankMonitorWrapper.begin(mDragLayer, cuj);
+ 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 WrappedAnimationRunnerImpl {
+
+ 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 /* 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 velocityPxPerS = DynamicResource.provider(mLauncher)
+ .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
+ 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 static final String TRANSITION_LAUNCH_FROM_RECENTS = "transition:LaunchFromRecents";
+ private static final String TRANSITION_LAUNCH_FROM_ICON = "transition:LaunchFromIcon";
+
+ private final Handler mHandler;
+ private final View mV;
+ private final RunnableList mOnEndCallback;
+
+ AppLaunchAnimationRunner(Handler handler, View v, RunnableList onEndCallback) {
+ mHandler = handler;
+ 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 launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
+ final boolean launchingFromTaskbar = mLauncher.isViewInTaskbar(mV);
+ if (launchingFromRecents) {
+ composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+ launcherClosing);
+ } else if (launchingFromTaskbar) {
+ // TODO
+ } else {
+ composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+ launcherClosing);
+ }
+
+ addCujInstrumentation(anim,
+ launchingFromRecents
+ ? InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_RECENTS
+ : InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_ICON);
+
+ if (launcherClosing) {
+ anim.addListener(mForceInvisibleListener);
+ }
+
+ result.setAnimation(anim, mLauncher, 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) {
+ // 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;
+
+ if (hasSplashScreen) {
+ iconAlphaStart = 0;
+
+ // TOOD: Share value from shell when available.
+ final float windowIconSize = Utilities.pxFromSp(108, r.getDisplayMetrics());
+
+ cropCenterXStart = windowTargetBounds.centerX();
+ cropCenterYStart = windowTargetBounds.centerY();
+
+ cropWidthStart = (int) windowIconSize;
+ cropHeightStart = (int) windowIconSize;
+ } else {
+ iconAlphaStart = 1;
+
+ cropWidthStart = cropHeightStart =
+ Math.min(windowTargetBounds.width(), windowTargetBounds.height());
+ cropCenterXStart = cropCenterYStart =
+ Math.min(windowTargetBounds.centerX(), windowTargetBounds.centerY());
+ }
+
+ cropWidthEnd = windowTargetBounds.width();
+ cropHeightEnd = windowTargetBounds.height();
+
+ cropCenterXEnd = windowTargetBounds.centerX();
+ cropCenterYEnd = windowTargetBounds.centerY();
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
index da2aee4..16727ec 100644
--- a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
+++ b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
@@ -16,8 +16,6 @@
package com.android.launcher3;
-import android.os.Handler;
-
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
/**
@@ -25,8 +23,10 @@
* implementation.
*/
public interface WrappedAnimationRunnerImpl {
- Handler getHandler();
- void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+
+ void onCreateAnimation(int transit,
+ RemoteAnimationTargetCompat[] appTargets,
RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
LauncherAnimationRunner.AnimationResult result);
}
diff --git a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
index 1753b62..e319275 100644
--- a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
@@ -16,6 +16,8 @@
package com.android.launcher3;
+import android.os.Handler;
+
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import java.lang.ref.WeakReference;
@@ -40,17 +42,22 @@
extends LauncherAnimationRunner {
private WeakReference<R> mImpl;
- public WrappedLauncherAnimationRunner(R animationRunnerImpl, boolean startAtFrontOfQueue) {
- super(animationRunnerImpl.getHandler(), startAtFrontOfQueue);
+ public WrappedLauncherAnimationRunner(
+ Handler handler, R animationRunnerImpl, boolean startAtFrontOfQueue) {
+ super(handler, startAtFrontOfQueue);
mImpl = new WeakReference<>(animationRunnerImpl);
}
@Override
- public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+ public void onCreateAnimation(int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ AnimationResult result) {
R animationRunnerImpl = mImpl.get();
if (animationRunnerImpl != null) {
- animationRunnerImpl.onCreateAnimation(appTargets, wallpaperTargets, result);
+ animationRunnerImpl.onCreateAnimation(transit, appTargets, wallpaperTargets,
+ nonAppTargets, result);
}
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/src/com/android/launcher3/appprediction/AllAppsTipView.java
similarity index 91%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
rename to quickstep/src/com/android/launcher3/appprediction/AllAppsTipView.java
index 8477b10..98bf483 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AllAppsTipView.java
@@ -19,7 +19,6 @@
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;
@@ -31,7 +30,6 @@
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
@@ -57,8 +55,7 @@
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);
+ // TODO: add log to WW
});
arrowTipView.show(launcher.getString(R.string.all_apps_prediction_tip), coords[1]);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
similarity index 93%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
rename to quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 914d9e9..d17a5ae 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -1,4 +1,4 @@
-/**
+/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,7 +16,6 @@
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;
@@ -30,7 +29,6 @@
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;
@@ -41,7 +39,6 @@
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;
@@ -90,7 +87,8 @@
mLauncher = Launcher.getLauncher(context);
boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
- mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
+ mPaint.setStrokeWidth(
+ getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
mStrokeColor = ContextCompat.getColor(context, isMainColorDark
? R.color.all_apps_prediction_row_separator_dark
@@ -134,7 +132,7 @@
if (row == this) {
break;
} else if (row.shouldDraw()) {
- sectionCount ++;
+ sectionCount++;
}
}
@@ -286,13 +284,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(this, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, allAppsFade);
- }
-
- @Override
public void setVerticalScroll(int scroll, boolean isScrolledOut) {
setTranslationY(scroll);
mIsScrolledOut = isScrolledOut;
@@ -303,4 +294,9 @@
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..cc3ccea
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -0,0 +1,270 @@
+/*
+ * 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.LauncherAppState;
+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);
+ mNumPredictedAppsPerRow = LauncherAppState.getIDP(context).numAllAppsColumns;
+ mLauncher = Launcher.getLauncher(context);
+ mLauncher.addOnDeviceProfileChangeListener(this);
+ updateVisibility();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ AllAppsTipView.scheduleShowIfNeeded(mLauncher);
+ }
+
+ 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;
+ }
+
+ @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.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(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/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
similarity index 95%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
rename to quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
index c968de9..3a1a2f7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
@@ -29,7 +29,6 @@
*/
public class HotseatEduActivity extends Activity {
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -49,7 +48,7 @@
@Override
public boolean init(BaseActivity activity, boolean alreadyOnHome) {
QuickstepLauncher launcher = (QuickstepLauncher) activity;
- if (launcher != null && launcher.getHotseatPredictionController() != null) {
+ if (launcher != null) {
launcher.getHotseatPredictionController().showEdu();
}
return false;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
similarity index 95%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
rename to quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
index 4f95254..4451e7a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -47,35 +47,27 @@
*/
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) {
+ HotseatEduController(Launcher launcher) {
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();
- }
+ HotseatRestoreHelper.createBackup(mLauncher);
if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
migrateToFolder();
} else {
@@ -227,7 +219,7 @@
}
void finishOnboarding() {
- mOnOnboardingComplete.run();
+ mLauncher.getModel().onWorkspaceUiChanged();
}
void showDimissTip() {
@@ -284,4 +276,3 @@
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/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
similarity index 96%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
rename to quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
index 4b8e434..c3677ea 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -40,7 +40,6 @@
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;
@@ -122,16 +121,6 @@
}
@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;
}
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..f297343
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -0,0 +1,517 @@
+/*
+ * 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.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.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.InvariantDeviceProfile;
+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>, InvariantDeviceProfile.OnIDPChangeListener,
+ 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().inv.numHotseatIcons;
+ mLauncher.getDragController().addDragListener(this);
+
+ launcher.getDeviceProfile().inv.addOnChangeListener(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, () -> {
+ 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(AnimationSuccessListener
+ .forRunnable(this::removeOutlineDrawings));
+ animationSet.start();
+ } else {
+ removeOutlineDrawings();
+ }
+
+ if (mLauncher.getTaskbarController() != null) {
+ mLauncher.getTaskbarController().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.getDeviceProfile().inv.removeOnChangeListener(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 onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+ this.mHotSeatItemsCount = profile.numHotseatIcons;
+ }
+
+ @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/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
similarity index 66%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
rename to quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
index 5a038d2..080633a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
@@ -15,7 +15,7 @@
*/
package com.android.launcher3.hybridhotseat;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
@@ -24,13 +24,10 @@
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.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -38,55 +35,47 @@
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 {
+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";
-
- public HotseatPredictionModel(Context context) { }
-
/**
- * Creates and returns bundle using workspace items and cached items
+ * Creates and returns bundle using workspace 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));
+ 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 AppTarget getAppTargetFromInfo(ItemInfo info) {
+ public static AppTarget getAppTargetFromInfo(Context context, ItemInfo info) {
if (info == null) return null;
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
&& info instanceof LauncherAppWidgetInfo
@@ -107,17 +96,17 @@
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();
+ context.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) {
+ 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,
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
similarity index 79%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
rename to quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
index 8c1db4e..90f762e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
@@ -19,8 +19,10 @@
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.Launcher;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.GridBackupTable;
import com.android.launcher3.provider.LauncherDbUtils;
@@ -29,30 +31,24 @@
* 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() {
+ public static void createBackup(Context context) {
MODEL_EXECUTOR.execute(() -> {
try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
LauncherSettings.Settings.call(
- mLauncher.getContentResolver(),
+ context.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_TRANSACTION)
.getBinder(LauncherSettings.Settings.EXTRA_VALUE)) {
- InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
- GridBackupTable backupTable = new GridBackupTable(mLauncher,
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ GridBackupTable backupTable = new GridBackupTable(context,
transaction.getDb(), idp.numHotseatIcons, idp.numColumns,
idp.numRows);
backupTable.createCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE);
transaction.commit();
- LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+ LauncherSettings.Settings.call(context.getContentResolver(),
LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE);
}
});
@@ -61,25 +57,23 @@
/**
* Finds and restores a previously saved snapshow of Favorites table
*/
- public void restoreBackup() {
- if (mBackupRestored) return;
+ public static void restoreBackup(Context context) {
MODEL_EXECUTOR.execute(() -> {
try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
LauncherSettings.Settings.call(
- mLauncher.getContentResolver(),
+ context.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,
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ GridBackupTable backupTable = new GridBackupTable(context,
transaction.getDb(), idp.numHotseatIcons, idp.numColumns,
idp.numRows);
backupTable.restoreFromCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE, true);
transaction.commit();
- mBackupRestored = true;
- mLauncher.getModel().forceReload();
+ 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..df3657d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -0,0 +1,379 @@
+/*
+ * 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.InvariantDeviceProfile.CHANGE_FLAG_GRID;
+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.numAllAppsColumns);
+ 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.numHotseatIcons);
+ 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.numAllAppsColumns)
+ .build()));
+
+ // TODO: get bundle
+ registerPredictor(mHotseatState, apm.createAppPredictionSession(
+ new AppPredictionContext.Builder(context)
+ .setUiSurface("hotseat")
+ .setPredictedTargetCount(mIDP.numHotseatIcons)
+ .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(int changeFlags, InvariantDeviceProfile profile) {
+ if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
+ // 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..995c4b0 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,51 @@
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;
- }
+ @WorkerThread
+ private void updateActionsWithRetry(int retryCount, @Nullable String packageName) {
+ 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 };
- 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;
- }
+ mWorkerHandler.removeCallbacksAndMessages(packageName);
+ if (updateActions(packageNames)) {
+ return;
}
- return false;
- }
-
- private void scheduleRefreshRetry(Message originalMsg) {
- int retryCount = originalMsg.arg1;
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(
+ () -> updateActionsWithRetry(retryCount + 1, packageName),
+ packageName, RETRY_TIMES_MS[retryCount]);
}
+ @WorkerThread
+ private void 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..a29ac1a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -0,0 +1,82 @@
+/*
+ * 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 android.app.prediction.AppTarget;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.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();
+
+ ArrayList<ItemInfo> recommendedWidgetsInDescendingOrder = new ArrayList<>();
+ 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.
+ recommendedWidgetsInDescendingOrder.add(
+ new PendingAddWidgetInfo(notAddedWidgets.get(0).widgetInfo));
+ }
+ }
+ }
+ FixedContainerItems fixedContainerItems = mPredictorState.items;
+ fixedContainerItems.items.clear();
+ fixedContainerItems.items.addAll(recommendedWidgetsInDescendingOrder);
+ 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..249ef3a 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -193,7 +193,6 @@
public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
PendingAnimation animation) {
if (mSurface == null
- || config.onlyPlayAtomicComponent()
|| config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER)
|| mIgnoreStateChangesDuringMultiWindowAnimation) {
return;
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..5513c16
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.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 android.content.ContextWrapper;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
+
+/**
+ * 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 ContextWrapper implements ActivityContext {
+
+ private final DeviceProfile mDeviceProfile;
+ private final LayoutInflater mLayoutInflater;
+ private final TaskbarContainerView mTaskbarContainerView;
+
+ public TaskbarActivityContext(BaseQuickstepLauncher launcher) {
+ super(launcher);
+ mDeviceProfile = launcher.getDeviceProfile().copy(this);
+ float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
+ float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
+ mDeviceProfile.updateIconSize(iconScale, getResources());
+
+ mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
+
+ mTaskbarContainerView = (TaskbarContainerView) mLayoutInflater
+ .inflate(R.layout.taskbar, null, false);
+ }
+
+ public TaskbarContainerView getTaskbarContainerView() {
+ return mTaskbarContainerView;
+ }
+
+ @Override
+ public LayoutInflater getLayoutInflater() {
+ return mLayoutInflater;
+ }
+
+ @Override
+ public BaseDragLayer<TaskbarActivityContext> getDragLayer() {
+ return mTaskbarContainerView;
+ }
+
+ @Override
+ public DeviceProfile getDeviceProfile() {
+ return mDeviceProfile;
+ }
+
+ @Override
+ public Rect getFolderBoundingBox() {
+ return mTaskbarContainerView.getFolderBoundingBox();
+ }
+}
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..7c54e2d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAnimationController.java
@@ -0,0 +1,126 @@
+/*
+ * 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.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 TaskbarController.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::updateVisibilityAlpha);
+
+ // Scale.
+ private final AnimatedFloat mTaskbarScaleForLauncherState = new AnimatedFloat(
+ this::updateScale);
+
+ public TaskbarAnimationController(BaseQuickstepLauncher launcher,
+ TaskbarController.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 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();
+ }
+
+ 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 alphaDueToLauncher = Math.max(mTaskbarBackgroundAlpha.value,
+ mTaskbarVisibilityAlphaForLauncherState.value);
+ float alphaDueToOther = mTaskbarVisibilityAlphaForIme.value;
+ float taskbarAlpha = alphaDueToLauncher * alphaDueToOther;
+ mTaskbarCallbacks.updateTaskbarVisibilityAlpha(taskbarAlpha);
+
+ // Make the nav bar invisible if taskbar is visible.
+ setNavBarButtonAlpha(1f - taskbarAlpha);
+ }
+
+ 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 setNavBarButtonAlpha(float navBarAlpha) {
+ SystemUiProxy.INSTANCE.get(mLauncher).setNavBarButtonAlpha(navBarAlpha, false);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
new file mode 100644
index 0000000..ccf6b41
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
@@ -0,0 +1,156 @@
+/*
+ * 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.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_FRAME;
+import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION;
+
+import android.content.Context;
+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;
+
+/**
+ * Top-level ViewGroup that hosts the TaskbarView as well as Views created by it such as Folder.
+ */
+public class TaskbarContainerView extends BaseDragLayer<TaskbarActivityContext> {
+
+ private final int[] mTempLoc = new int[2];
+ private final int mFolderMargin;
+
+ // Initialized in TaskbarController constructor.
+ private TaskbarController.TaskbarContainerViewCallbacks mControllerCallbacks;
+
+ // Initialized in init.
+ private TaskbarView mTaskbarView;
+ private ViewTreeObserverWrapper.OnComputeInsetsListener mTaskbarInsetsComputer;
+
+ public TaskbarContainerView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public TaskbarContainerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TaskbarContainerView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TaskbarContainerView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, 1 /* alphaChannelCount */);
+ mFolderMargin = getResources().getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+ }
+
+ protected void construct(TaskbarController.TaskbarContainerViewCallbacks callbacks) {
+ mControllerCallbacks = callbacks;
+ }
+
+ protected void init(TaskbarView taskbarView) {
+ mTaskbarView = taskbarView;
+ mTaskbarInsetsComputer = createTaskbarInsetsComputer();
+ recreateControllers();
+ }
+
+ @Override
+ public void recreateControllers() {
+ mControllers = new TouchController[0];
+ }
+
+ private ViewTreeObserverWrapper.OnComputeInsetsListener createTaskbarInsetsComputer() {
+ return insetsInfo -> {
+ if (mControllerCallbacks.isTaskbarTouchable()) {
+ // Accept touches anywhere in our bounds.
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
+ } else {
+ // Let touches pass through us.
+ insetsInfo.touchableRegion.setEmpty();
+ 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.
+ int[] loc = mTempLoc;
+ float scale = mTaskbarView.getScaleX();
+ mTaskbarView.setScaleX(1);
+ mTaskbarView.setScaleY(1);
+ mTaskbarView.getLocationInWindow(loc);
+ mTaskbarView.setScaleX(scale);
+ mTaskbarView.setScaleY(scale);
+ insetsInfo.contentInsets.left = loc[0];
+ insetsInfo.contentInsets.top = loc[1];
+ insetsInfo.contentInsets.right = getWidth() - (loc[0] + mTaskbarView.getWidth());
+ insetsInfo.contentInsets.bottom = getHeight() - (loc[1] + mTaskbarView.getHeight());
+ };
+ }
+
+ protected void cleanup() {
+ ViewTreeObserverWrapper.removeOnComputeInsetsListener(mTaskbarInsetsComputer);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ ViewTreeObserverWrapper.addOnComputeInsetsListener(getViewTreeObserver(),
+ mTaskbarInsetsComputer);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ cleanup();
+ }
+
+ @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;
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ mControllerCallbacks.onViewRemoved();
+ }
+
+ /**
+ * @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;
+ }
+
+ protected TaskbarActivityContext getTaskbarActivityContext() {
+ return mActivity;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
new file mode 100644
index 0000000..559ede1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -0,0 +1,584 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AbstractFloatingView;
+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.anim.AlphaUpdateListener;
+import com.android.launcher3.anim.PendingAnimation;
+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.states.StateAnimationConfig;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.AnimatedFloat;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Interfaces with Launcher/WindowManager/SystemUI to determine what to show in TaskbarView.
+ */
+public class TaskbarController {
+
+ private static final String WINDOW_TITLE = "Taskbar";
+
+ private final TaskbarContainerView mTaskbarContainerView;
+ private final TaskbarView mTaskbarViewInApp;
+ private final TaskbarView mTaskbarViewOnHome;
+ private final BaseQuickstepLauncher mLauncher;
+ private final WindowManager mWindowManager;
+ // Layout width and height of the Taskbar in the default state.
+ private final Point mTaskbarSize;
+ private final TaskbarStateHandler mTaskbarStateHandler;
+ private final TaskbarAnimationController mTaskbarAnimationController;
+ private final TaskbarHotseatController mHotseatController;
+ private final TaskbarRecentsController mRecentsController;
+ private final TaskbarDragController mDragController;
+
+ // Initialized in init().
+ private WindowManager.LayoutParams mWindowLayoutParams;
+
+ // Contains all loaded Tasks, not yet deduped from Hotseat items.
+ private List<Task> mLatestLoadedRecentTasks;
+ // Contains all loaded Hotseat items.
+ private ItemInfo[] mLatestLoadedHotseatItems;
+
+ private @Nullable Animator mAnimator;
+ private boolean mIsAnimatingToLauncher;
+
+ public TaskbarController(BaseQuickstepLauncher launcher,
+ TaskbarContainerView taskbarContainerView, TaskbarView taskbarViewOnHome) {
+ mLauncher = launcher;
+ mTaskbarContainerView = taskbarContainerView;
+ mTaskbarContainerView.construct(createTaskbarContainerViewCallbacks());
+ mTaskbarViewInApp = mTaskbarContainerView.findViewById(R.id.taskbar_view);
+ mTaskbarViewInApp.construct(createTaskbarViewCallbacks());
+ mTaskbarViewOnHome = taskbarViewOnHome;
+ mTaskbarViewOnHome.construct(createTaskbarViewCallbacks());
+ mWindowManager = mLauncher.getWindowManager();
+ mTaskbarSize = new Point(MATCH_PARENT, mLauncher.getDeviceProfile().taskbarSize);
+ mTaskbarStateHandler = mLauncher.getTaskbarStateHandler();
+ mTaskbarAnimationController = new TaskbarAnimationController(mLauncher,
+ createTaskbarAnimationControllerCallbacks());
+ mHotseatController = new TaskbarHotseatController(mLauncher,
+ createTaskbarHotseatControllerCallbacks());
+ mRecentsController = new TaskbarRecentsController(mLauncher,
+ createTaskbarRecentsControllerCallbacks());
+ mDragController = new TaskbarDragController(mLauncher);
+ }
+
+ private TaskbarAnimationControllerCallbacks createTaskbarAnimationControllerCallbacks() {
+ return new TaskbarAnimationControllerCallbacks() {
+ @Override
+ public void updateTaskbarBackgroundAlpha(float alpha) {
+ mTaskbarViewInApp.setBackgroundAlpha(alpha);
+ }
+
+ @Override
+ public void updateTaskbarVisibilityAlpha(float alpha) {
+ mTaskbarContainerView.setAlpha(alpha);
+ mTaskbarViewOnHome.setAlpha(alpha);
+ }
+
+ @Override
+ public void updateTaskbarScale(float scale) {
+ mTaskbarViewInApp.setScaleX(scale);
+ mTaskbarViewInApp.setScaleY(scale);
+ }
+ };
+ }
+
+ private TaskbarContainerViewCallbacks createTaskbarContainerViewCallbacks() {
+ return new TaskbarContainerViewCallbacks() {
+ @Override
+ public void onViewRemoved() {
+ if (mTaskbarContainerView.getChildCount() == 1) {
+ // Only TaskbarView remains.
+ setTaskbarWindowFullscreen(false);
+ }
+ }
+
+ @Override
+ public boolean isTaskbarTouchable() {
+ return mTaskbarContainerView.getAlpha() > AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD
+ && mTaskbarViewInApp.getVisibility() == View.VISIBLE
+ && !mIsAnimatingToLauncher;
+ }
+ };
+ }
+
+ private TaskbarViewCallbacks createTaskbarViewCallbacks() {
+ return new TaskbarViewCallbacks() {
+ @Override
+ public View.OnClickListener getItemOnClickListener() {
+ return 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);
+
+ mTaskbarContainerView.post(() -> {
+ folder.animateOpen();
+
+ folder.iterateOverItems((itemInfo, itemView) -> {
+ itemView.setOnClickListener(getItemOnClickListener());
+ itemView.setOnLongClickListener(getItemOnLongClickListener());
+ // To play haptic when dragging, like other Taskbar items do.
+ itemView.setHapticFeedbackEnabled(true);
+ return false;
+ });
+ });
+ } else {
+ ItemClickHandler.INSTANCE.onClick(view);
+ }
+
+ AbstractFloatingView.closeAllOpenViews(
+ mTaskbarContainerView.getTaskbarActivityContext());
+ };
+ }
+
+ @Override
+ public View.OnLongClickListener getItemOnLongClickListener() {
+ return mDragController::startSystemDragOnLongClick;
+ }
+
+ @Override
+ public int getEmptyHotseatViewVisibility(TaskbarView taskbarView) {
+ // When on the home screen, we want the empty hotseat views to take up their full
+ // space so that the others line up with the home screen hotseat.
+ boolean isOnHomeScreen = taskbarView == mTaskbarViewOnHome
+ || mLauncher.hasBeenResumed() || mIsAnimatingToLauncher;
+ return isOnHomeScreen ? View.INVISIBLE : View.GONE;
+ }
+
+ @Override
+ public float getNonIconScale(TaskbarView taskbarView) {
+ return taskbarView == mTaskbarViewOnHome ? getTaskbarScaleOnHome() : 1f;
+ }
+
+ @Override
+ public void onItemPositionsChanged(TaskbarView taskbarView) {
+ if (taskbarView == mTaskbarViewOnHome) {
+ alignRealHotseatWithTaskbar();
+ }
+ }
+ };
+ }
+
+ private TaskbarHotseatControllerCallbacks createTaskbarHotseatControllerCallbacks() {
+ return new TaskbarHotseatControllerCallbacks() {
+ @Override
+ public void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+ mTaskbarViewInApp.updateHotseatItems(hotseatItemInfos);
+ mLatestLoadedHotseatItems = hotseatItemInfos;
+ dedupeAndUpdateRecentItems();
+ }
+ };
+ }
+
+ private TaskbarRecentsControllerCallbacks createTaskbarRecentsControllerCallbacks() {
+ return new TaskbarRecentsControllerCallbacks() {
+ @Override
+ public void updateRecentItems(ArrayList<Task> recentTasks) {
+ mLatestLoadedRecentTasks = recentTasks;
+ dedupeAndUpdateRecentItems();
+ }
+
+ @Override
+ public void updateRecentTaskAtIndex(int taskIndex, Task task) {
+ mTaskbarViewInApp.updateRecentTaskAtIndex(taskIndex, task);
+ mTaskbarViewOnHome.updateRecentTaskAtIndex(taskIndex, task);
+ }
+ };
+ }
+
+ /**
+ * Initializes the Taskbar, including adding it to the screen.
+ */
+ public void init() {
+ mTaskbarViewInApp.init(mHotseatController.getNumHotseatIcons(),
+ mRecentsController.getNumRecentIcons());
+ mTaskbarViewOnHome.init(mHotseatController.getNumHotseatIcons(),
+ mRecentsController.getNumRecentIcons());
+ mTaskbarContainerView.init(mTaskbarViewInApp);
+ addToWindowManager();
+ mTaskbarStateHandler.setTaskbarCallbacks(createTaskbarStateHandlerCallbacks());
+ mTaskbarAnimationController.init();
+ mHotseatController.init();
+ mRecentsController.init();
+
+ setWhichTaskbarViewIsVisible(mLauncher.hasBeenResumed()
+ ? mTaskbarViewOnHome
+ : mTaskbarViewInApp);
+ }
+
+ private TaskbarStateHandlerCallbacks createTaskbarStateHandlerCallbacks() {
+ return new TaskbarStateHandlerCallbacks() {
+ @Override
+ public AnimatedFloat getAlphaTarget() {
+ return mTaskbarAnimationController.getTaskbarVisibilityForLauncherState();
+ }
+
+ @Override
+ public AnimatedFloat getScaleTarget() {
+ return mTaskbarAnimationController.getTaskbarScaleForLauncherState();
+ }
+ };
+ }
+
+ /**
+ * Removes the Taskbar from the screen, and removes any obsolete listeners etc.
+ */
+ public void cleanup() {
+ if (mAnimator != null) {
+ // End this first, in case it relies on properties that are about to be cleaned up.
+ mAnimator.end();
+ }
+
+ mTaskbarViewInApp.cleanup();
+ mTaskbarViewOnHome.cleanup();
+ mTaskbarContainerView.cleanup();
+ removeFromWindowManager();
+ mTaskbarStateHandler.setTaskbarCallbacks(null);
+ mTaskbarAnimationController.cleanup();
+ mHotseatController.cleanup();
+ mRecentsController.cleanup();
+
+ setWhichTaskbarViewIsVisible(null);
+ }
+
+ private void removeFromWindowManager() {
+ mWindowManager.removeViewImmediate(mTaskbarContainerView);
+ }
+
+ private void addToWindowManager() {
+ final int gravity = Gravity.BOTTOM;
+
+ mWindowLayoutParams = new WindowManager.LayoutParams(
+ mTaskbarSize.x,
+ mTaskbarSize.y,
+ TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+ mWindowLayoutParams.setTitle(WINDOW_TITLE);
+ mWindowLayoutParams.packageName = mLauncher.getPackageName();
+ mWindowLayoutParams.gravity = gravity;
+ 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 }
+ );
+
+ TaskbarContainerView.LayoutParams taskbarLayoutParams =
+ new TaskbarContainerView.LayoutParams(mTaskbarSize.x, mTaskbarSize.y);
+ taskbarLayoutParams.gravity = gravity;
+ mTaskbarViewInApp.setLayoutParams(taskbarLayoutParams);
+
+ mWindowManager.addView(mTaskbarContainerView, mWindowLayoutParams);
+ }
+
+ /**
+ * 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;
+ mTaskbarViewInApp.updateHotseatItemsVisibility();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIsAnimatingToLauncher = false;
+ setWhichTaskbarViewIsVisible(mTaskbarViewOnHome);
+ }
+ });
+
+ 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) {
+ mTaskbarViewInApp.updateHotseatItemsVisibility();
+ setWhichTaskbarViewIsVisible(mTaskbarViewInApp);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ }
+ });
+ return anim.buildAnim();
+ }
+
+ /**
+ * Should be called when the IME visibility changes, so we can hide/show Taskbar accordingly.
+ */
+ public void setIsImeVisible(boolean isImeVisible) {
+ mTaskbarAnimationController.animateToVisibilityForIme(isImeVisible ? 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 mTaskbarViewInApp.isEventOverAnyItem(ev);
+ }
+
+ public boolean isDraggingItem() {
+ return mTaskbarViewInApp.isDraggingItem() || mTaskbarViewOnHome.isDraggingItem();
+ }
+
+ private void dedupeAndUpdateRecentItems() {
+ if (mLatestLoadedRecentTasks == null || mLatestLoadedHotseatItems == null) {
+ return;
+ }
+
+ final int numRecentIcons = mRecentsController.getNumRecentIcons();
+
+ // From most recent to least recently opened.
+ List<Task> dedupedTasksInDescendingOrder = new ArrayList<>();
+ for (int i = mLatestLoadedRecentTasks.size() - 1; i >= 0; i--) {
+ Task task = mLatestLoadedRecentTasks.get(i);
+ boolean isTaskInHotseat = false;
+ for (ItemInfo hotseatItem : mLatestLoadedHotseatItems) {
+ if (hotseatItem == null) {
+ continue;
+ }
+ ComponentName hotseatActivity = hotseatItem.getTargetComponent();
+ if (hotseatActivity != null && task.key.sourceComponent.getPackageName()
+ .equals(hotseatActivity.getPackageName())) {
+ isTaskInHotseat = true;
+ break;
+ }
+ }
+ if (!isTaskInHotseat) {
+ dedupedTasksInDescendingOrder.add(task);
+ if (dedupedTasksInDescendingOrder.size() == numRecentIcons) {
+ break;
+ }
+ }
+ }
+
+ // TaskbarView expects an array of all the recent tasks to show, in the order to show them.
+ // So we create an array of the proper size, then fill it in such that the most recent items
+ // are at the end. If there aren't enough elements to fill the array, leave them null.
+ Task[] tasksArray = new Task[numRecentIcons];
+ for (int i = 0; i < tasksArray.length; i++) {
+ Task task = i >= dedupedTasksInDescendingOrder.size()
+ ? null
+ : dedupedTasksInDescendingOrder.get(i);
+ tasksArray[tasksArray.length - 1 - i] = task;
+ }
+
+ mTaskbarViewInApp.updateRecentTasks(tasksArray);
+ mTaskbarViewOnHome.updateRecentTasks(tasksArray);
+ mRecentsController.loadIconsForTasks(tasksArray);
+ }
+
+ /**
+ * @return Whether the given View is in the same window as Taskbar.
+ */
+ public boolean isViewInTaskbar(View v) {
+ return mTaskbarContainerView.isAttachedToWindow()
+ && mTaskbarContainerView.getWindowId().equals(v.getWindowId());
+ }
+
+ /**
+ * Pads the Hotseat to line up exactly with Taskbar's copy of the Hotseat.
+ */
+ public void alignRealHotseatWithTaskbar() {
+ Rect hotseatBounds = new Rect();
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ int hotseatHeight = grid.workspacePadding.bottom + grid.taskbarSize;
+ int hotseatTopDiff = hotseatHeight - grid.taskbarSize;
+
+ mTaskbarViewOnHome.getHotseatBounds().roundOut(hotseatBounds);
+ mLauncher.getHotseat().setPadding(hotseatBounds.left,
+ hotseatBounds.top + hotseatTopDiff,
+ mTaskbarViewOnHome.getWidth() - hotseatBounds.right,
+ mTaskbarViewOnHome.getHeight() - hotseatBounds.bottom);
+ }
+
+ private void setWhichTaskbarViewIsVisible(@Nullable TaskbarView visibleTaskbar) {
+ mTaskbarViewInApp.setVisibility(visibleTaskbar == mTaskbarViewInApp
+ ? View.VISIBLE : View.INVISIBLE);
+ mTaskbarViewOnHome.setVisibility(visibleTaskbar == mTaskbarViewOnHome
+ ? View.VISIBLE : View.INVISIBLE);
+ mLauncher.getHotseat().setIconsAlpha(visibleTaskbar != mTaskbarViewInApp ? 1f : 0f);
+ }
+
+ /**
+ * Returns the ratio of the taskbar icon size on home vs in an app.
+ */
+ public float getTaskbarScaleOnHome() {
+ DeviceProfile inAppDp = mTaskbarContainerView.getTaskbarActivityContext()
+ .getDeviceProfile();
+ DeviceProfile onHomeDp = ActivityContext.lookupContext(mTaskbarViewOnHome.getContext())
+ .getDeviceProfile();
+ return (float) onHomeDp.cellWidthPx / inAppDp.cellWidthPx;
+ }
+
+ /**
+ * Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
+ */
+ private void setTaskbarWindowFullscreen(boolean fullscreen) {
+ if (fullscreen) {
+ mWindowLayoutParams.width = MATCH_PARENT;
+ mWindowLayoutParams.height = MATCH_PARENT;
+ } else {
+ mWindowLayoutParams.width = mTaskbarSize.x;
+ mWindowLayoutParams.height = mTaskbarSize.y;
+ }
+ mWindowManager.updateViewLayout(mTaskbarContainerView, mWindowLayoutParams);
+ }
+
+ /**
+ * Contains methods that TaskbarStateHandler can call to interface with TaskbarController.
+ */
+ protected interface TaskbarStateHandlerCallbacks {
+ AnimatedFloat getAlphaTarget();
+ AnimatedFloat getScaleTarget();
+ }
+
+ /**
+ * Contains methods that TaskbarAnimationController can call to interface with
+ * TaskbarController.
+ */
+ protected interface TaskbarAnimationControllerCallbacks {
+ void updateTaskbarBackgroundAlpha(float alpha);
+ void updateTaskbarVisibilityAlpha(float alpha);
+ void updateTaskbarScale(float scale);
+ }
+
+ /**
+ * Contains methods that TaskbarContainerView can call to interface with TaskbarController.
+ */
+ protected interface TaskbarContainerViewCallbacks {
+ void onViewRemoved();
+ boolean isTaskbarTouchable();
+ }
+
+ /**
+ * Contains methods that TaskbarView can call to interface with TaskbarController.
+ */
+ protected interface TaskbarViewCallbacks {
+ View.OnClickListener getItemOnClickListener();
+ View.OnLongClickListener getItemOnLongClickListener();
+ int getEmptyHotseatViewVisibility(TaskbarView taskbarView);
+ /** Returns how much to scale non-icon elements such as spacing and dividers. */
+ float getNonIconScale(TaskbarView taskbarView);
+ void onItemPositionsChanged(TaskbarView taskbarView);
+ }
+
+ /**
+ * Contains methods that TaskbarHotseatController can call to interface with TaskbarController.
+ */
+ protected interface TaskbarHotseatControllerCallbacks {
+ void updateHotseatItems(ItemInfo[] hotseatItemInfos);
+ }
+
+ /**
+ * Contains methods that TaskbarRecentsController can call to interface with TaskbarController.
+ */
+ protected interface TaskbarRecentsControllerCallbacks {
+ void updateRecentItems(ArrayList<Task> recentTasks);
+ void updateRecentTaskAtIndex(int taskIndex, Task task);
+ }
+}
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..5eb34cb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -0,0 +1,144 @@
+/*
+ * 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.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.BaseQuickstepLauncher;
+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 BaseQuickstepLauncher mLauncher;
+ private final int mDragIconSize;
+
+ public TaskbarDragController(BaseQuickstepLauncher launcher) {
+ mLauncher = launcher;
+ Resources resources = mLauncher.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 = mLauncher.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/TaskbarHotseatController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
new file mode 100644
index 0000000..b1bafdb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.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.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;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's Hotseat items.
+ */
+public class TaskbarHotseatController {
+
+ private final BaseQuickstepLauncher mLauncher;
+ private final Hotseat mHotseat;
+ private final TaskbarController.TaskbarHotseatControllerCallbacks 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,
+ TaskbarController.TaskbarHotseatControllerCallbacks taskbarCallbacks) {
+ mLauncher = launcher;
+ mHotseat = mLauncher.getHotseat();
+ mTaskbarCallbacks = taskbarCallbacks;
+ mNumHotseatIcons = mLauncher.getDeviceProfile().inv.numHotseatIcons;
+ }
+
+ 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.updateHotseatItems(hotseatItemInfos);
+ }
+
+ protected int getNumHotseatIcons() {
+ return mNumHotseatIcons;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java
new file mode 100644
index 0000000..4256d2b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.util.CancellableTask;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import java.util.ArrayList;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's Recent items.
+ */
+public class TaskbarRecentsController {
+
+ private final int mNumRecentIcons = 2;
+ private final BaseQuickstepLauncher mLauncher;
+ private final TaskbarController.TaskbarRecentsControllerCallbacks mTaskbarCallbacks;
+ private final RecentsModel mRecentsModel;
+
+ private final TaskStackChangeListener mTaskStackChangeListener = new TaskStackChangeListener() {
+ @Override
+ public void onTaskStackChanged() {
+ reloadRecentTasksIfNeeded();
+ }
+ };
+
+ // TODO: add TaskbarVisualsChangedListener as well (for calendar/clock?)
+
+ // Used to keep track of the last requested task list id, so that we do not request to load the
+ // tasks again if we have already requested it and the task list has not changed
+ private int mTaskListChangeId = -1;
+
+ // The current background requests to load the task icons
+ private CancellableTask[] mIconLoadRequests = new CancellableTask[mNumRecentIcons];
+
+ private boolean mIsAlive;
+
+ public TaskbarRecentsController(BaseQuickstepLauncher launcher,
+ TaskbarController.TaskbarRecentsControllerCallbacks taskbarCallbacks) {
+ mLauncher = launcher;
+ mTaskbarCallbacks = taskbarCallbacks;
+ mRecentsModel = RecentsModel.INSTANCE.get(mLauncher);
+ }
+
+ protected void init() {
+ mIsAlive = true;
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackChangeListener);
+ reloadRecentTasksIfNeeded();
+ }
+
+ protected void cleanup() {
+ mIsAlive = false;
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+ mTaskStackChangeListener);
+ cancelAllPendingIconLoadTasks();
+ }
+
+ private void reloadRecentTasksIfNeeded() {
+ if (!mRecentsModel.isTaskListValid(mTaskListChangeId)) {
+ mTaskListChangeId = mRecentsModel.getTasks(this::onRecentTasksChanged);
+ }
+ }
+
+ private void cancelAllPendingIconLoadTasks() {
+ for (int i = 0; i < mIconLoadRequests.length; i++) {
+ if (mIconLoadRequests[i] != null) {
+ mIconLoadRequests[i].cancel();
+ }
+ mIconLoadRequests[i] = null;
+ }
+ }
+
+ private void onRecentTasksChanged(ArrayList<Task> tasks) {
+ if (mIsAlive) {
+ mTaskbarCallbacks.updateRecentItems(tasks);
+ }
+ }
+
+ /**
+ * For each Task, loads its icon from the cache in the background, then calls
+ * {@link TaskbarController.TaskbarRecentsControllerCallbacks#updateRecentTaskAtIndex}.
+ */
+ protected void loadIconsForTasks(Task[] tasks) {
+ cancelAllPendingIconLoadTasks();
+ for (int i = 0; i < tasks.length; i++) {
+ Task task = tasks[i];
+ if (task == null) {
+ continue;
+ }
+ final int taskIndex = i;
+ mIconLoadRequests[i] = mRecentsModel.getIconCache().updateIconInBackground(
+ task, updatedTask -> onTaskIconLoaded(task, taskIndex));
+ }
+ }
+
+ private void onTaskIconLoaded(Task task, int taskIndex) {
+ mTaskbarCallbacks.updateRecentTaskAtIndex(taskIndex, task);
+ }
+
+ protected int getNumRecentIcons() {
+ return mNumRecentIcons;
+ }
+}
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..9fc7d99
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.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.taskbar;
+
+import static com.android.launcher3.LauncherState.TASKBAR;
+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.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 #setTaskbarCallbacks} 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 TaskbarController.TaskbarStateHandlerCallbacks mTaskbarCallbacks = null;
+
+ public TaskbarStateHandler(BaseQuickstepLauncher launcher) {
+ mLauncher = launcher;
+ }
+
+ public void setTaskbarCallbacks(TaskbarController.TaskbarStateHandlerCallbacks callbacks) {
+ mTaskbarCallbacks = callbacks;
+ }
+
+ @Override
+ public void setState(LauncherState state) {
+ if (mTaskbarCallbacks == null) {
+ return;
+ }
+
+ AnimatedFloat alphaTarget = mTaskbarCallbacks.getAlphaTarget();
+ AnimatedFloat scaleTarget = mTaskbarCallbacks.getScaleTarget();
+ boolean isTaskbarVisible = (state.getVisibleElements(mLauncher) & TASKBAR) != 0;
+ alphaTarget.updateValue(isTaskbarVisible ? 1f : 0f);
+ scaleTarget.updateValue(state.getTaskbarScale(mLauncher));
+ }
+
+ @Override
+ public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
+ PendingAnimation animation) {
+ if (mTaskbarCallbacks == null) {
+ return;
+ }
+
+ AnimatedFloat alphaTarget = mTaskbarCallbacks.getAlphaTarget();
+ AnimatedFloat scaleTarget = mTaskbarCallbacks.getScaleTarget();
+ boolean isTaskbarVisible = (toState.getVisibleElements(mLauncher) & TASKBAR) != 0;
+ animation.setFloat(alphaTarget, AnimatedFloat.VALUE, isTaskbarVisible ? 1f : 0f, LINEAR);
+ animation.setFloat(scaleTarget, AnimatedFloat.VALUE, toState.getTaskbarScale(mLauncher),
+ LINEAR);
+ }
+}
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..21a2d51
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -0,0 +1,537 @@
+/*
+ * 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.AnimatorSet;
+import android.animation.LayoutTransition;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+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;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * 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 ColorDrawable mBackgroundDrawable;
+ private final int mDividerWidth;
+ private final int mDividerHeight;
+ 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];
+
+ // Initialized in TaskbarController constructor.
+ private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
+ // Scale on elements that aren't icons.
+ private float mNonIconScale;
+ private int mItemMarginLeftRight;
+
+ // Initialized in init().
+ private LayoutTransition mLayoutTransition;
+ private int mHotseatStartIndex;
+ private int mHotseatEndIndex;
+ private View mHotseatRecentsDivider;
+ private int mRecentsStartIndex;
+ private int mRecentsEndIndex;
+
+ // Delegate touches to the closest view if within mIconTouchSize.
+ private boolean mDelegateTargeted;
+ private View mDelegateView;
+
+ private boolean mIsDraggingItem;
+ // Only non-null when the corresponding Folder is open.
+ private @Nullable FolderIcon mLeaveBehindFolderIcon;
+
+ 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);
+
+ Resources resources = getResources();
+ mBackgroundDrawable = (ColorDrawable) getBackground();
+ mDividerWidth = resources.getDimensionPixelSize(R.dimen.taskbar_divider_thickness);
+ mDividerHeight = resources.getDimensionPixelSize(R.dimen.taskbar_divider_height);
+ mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
+ mIsRtl = Utilities.isRtl(resources);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ }
+
+ protected void construct(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
+ mControllerCallbacks = taskbarViewCallbacks;
+ mNonIconScale = mControllerCallbacks.getNonIconScale(this);
+ mItemMarginLeftRight = getResources().getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
+ mItemMarginLeftRight = Math.round(mItemMarginLeftRight * mNonIconScale);
+ }
+
+ protected void init(int numHotseatIcons, int numRecentIcons) {
+ mHotseatStartIndex = 0;
+ mHotseatEndIndex = mHotseatStartIndex + numHotseatIcons - 1;
+ updateHotseatItems(new ItemInfo[numHotseatIcons]);
+
+ int dividerIndex = mHotseatEndIndex + 1;
+ mHotseatRecentsDivider = addDivider(dividerIndex);
+
+ mRecentsStartIndex = dividerIndex + 1;
+ mRecentsEndIndex = mRecentsStartIndex + numRecentIcons - 1;
+ updateRecentTasks(new Task[numRecentIcons]);
+
+ mLayoutTransition = new LayoutTransition();
+ addUpdateListenerForAllLayoutTransitions(() -> {
+ if (getLayoutTransition() == mLayoutTransition) {
+ mControllerCallbacks.onItemPositionsChanged(this);
+ }
+ });
+ setLayoutTransition(mLayoutTransition);
+ }
+
+ private void addUpdateListenerForAllLayoutTransitions(Runnable onUpdate) {
+ addUpdateListenerForLayoutTransition(LayoutTransition.CHANGE_APPEARING, onUpdate);
+ addUpdateListenerForLayoutTransition(LayoutTransition.CHANGE_DISAPPEARING, onUpdate);
+ addUpdateListenerForLayoutTransition(LayoutTransition.CHANGING, onUpdate);
+ addUpdateListenerForLayoutTransition(LayoutTransition.APPEARING, onUpdate);
+ addUpdateListenerForLayoutTransition(LayoutTransition.DISAPPEARING, onUpdate);
+ }
+
+ private void addUpdateListenerForLayoutTransition(int transitionType, Runnable onUpdate) {
+ Animator anim = mLayoutTransition.getAnimator(transitionType);
+ if (anim instanceof ValueAnimator) {
+ ((ValueAnimator) anim).addUpdateListener(valueAnimator -> onUpdate.run());
+ } else {
+ AnimatorSet animSet = new AnimatorSet();
+ ValueAnimator updateAnim = ValueAnimator.ofFloat(0, 1);
+ updateAnim.addUpdateListener(valueAnimator -> onUpdate.run());
+ animSet.playTogether(anim, updateAnim);
+ mLayoutTransition.setAnimator(transitionType, animSet);
+ }
+ }
+
+ protected void cleanup() {
+ endAllLayoutTransitionAnimators();
+ setLayoutTransition(null);
+ removeAllViews();
+ mHotseatRecentsDivider = null;
+ }
+
+ private void endAllLayoutTransitionAnimators() {
+ mLayoutTransition.getAnimator(LayoutTransition.CHANGE_APPEARING).end();
+ mLayoutTransition.getAnimator(LayoutTransition.CHANGE_DISAPPEARING).end();
+ mLayoutTransition.getAnimator(LayoutTransition.CHANGING).end();
+ mLayoutTransition.getAnimator(LayoutTransition.APPEARING).end();
+ mLayoutTransition.getAnimator(LayoutTransition.DISAPPEARING).end();
+ }
+
+ /**
+ * Sets the alpha of the background color behind all the Taskbar contents.
+ * @param alpha 0 is fully transparent, 1 is fully opaque.
+ */
+ public void setBackgroundAlpha(float alpha) {
+ mBackgroundDrawable.setAlpha((int) (alpha * 255));
+ }
+
+ /**
+ * 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];
+ int hotseatIndex = mHotseatStartIndex + i;
+ View hotseatView = getChildAt(hotseatIndex);
+
+ // 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) {
+ removeView(hotseatView);
+ ActivityContext activityContext = ActivityContext.lookupContext(getContext());
+ if (isFolder) {
+ FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
+ FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
+ ActivityContext.lookupContext(getContext()), this, folderInfo);
+ folderIcon.setTextVisible(false);
+ hotseatView = folderIcon;
+ } else {
+ hotseatView = inflate(expectedLayoutResId);
+ }
+ int iconSize = activityContext.getDeviceProfile().iconSizePx;
+ LayoutParams lp = new LayoutParams(iconSize, iconSize);
+ lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+ addView(hotseatView, hotseatIndex, 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(mControllerCallbacks.getItemOnClickListener());
+ hotseatView.setOnLongClickListener(
+ mControllerCallbacks.getItemOnLongClickListener());
+ } else if (isFolder) {
+ hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+ hotseatView.setOnLongClickListener(
+ mControllerCallbacks.getItemOnLongClickListener());
+ } else {
+ hotseatView.setOnClickListener(null);
+ hotseatView.setOnLongClickListener(null);
+ hotseatView.setTag(null);
+ }
+ updateHotseatItemVisibility(hotseatView);
+ }
+
+ updateHotseatRecentsDividerVisibility();
+ }
+
+ protected void updateHotseatItemsVisibility() {
+ for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+ updateHotseatItemVisibility(getChildAt(i));
+ }
+ }
+
+ private void updateHotseatItemVisibility(View hotseatView) {
+ if (hotseatView.getTag() != null) {
+ hotseatView.setVisibility(VISIBLE);
+ } else {
+ int oldVisibility = hotseatView.getVisibility();
+ int newVisibility = mControllerCallbacks.getEmptyHotseatViewVisibility(this);
+ hotseatView.setVisibility(newVisibility);
+ if (oldVisibility == GONE && newVisibility != GONE) {
+ // By default, the layout transition only runs when going to VISIBLE,
+ // but we want it to run when going to GONE to INVISIBLE as well.
+ getLayoutTransition().showChild(this, hotseatView, oldVisibility);
+ }
+ }
+ }
+
+ private View addDivider(int dividerIndex) {
+ View divider = inflate(R.layout.taskbar_divider);
+ LayoutParams lp = new LayoutParams(mDividerWidth, mDividerHeight);
+ lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+ divider.setScaleX(mNonIconScale);
+ divider.setScaleY(mNonIconScale);
+ addView(divider, dividerIndex, lp);
+ return divider;
+ }
+
+ /**
+ * Inflates/binds the Recents items to show in the Taskbar given their Tasks.
+ */
+ protected void updateRecentTasks(Task[] tasks) {
+ for (int i = 0; i < tasks.length; i++) {
+ Task task = tasks[i];
+ int recentsIndex = mRecentsStartIndex + i;
+ View recentsView = getChildAt(recentsIndex);
+
+ // Inflate empty icon Views.
+ if (recentsView == null) {
+ BubbleTextView btv = (BubbleTextView) inflate(R.layout.taskbar_app_icon);
+ LayoutParams lp = new LayoutParams(btv.getIconSize(), btv.getIconSize());
+ lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+ recentsView = btv;
+ addView(recentsView, recentsIndex, lp);
+ }
+
+ // Apply the Task, or hide the view if there is none for a given index.
+ if (recentsView instanceof BubbleTextView && task != null) {
+ applyTaskToBubbleTextView((BubbleTextView) recentsView, task);
+ recentsView.setVisibility(VISIBLE);
+ recentsView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+ recentsView.setOnLongClickListener(
+ mControllerCallbacks.getItemOnLongClickListener());
+ } else {
+ recentsView.setVisibility(GONE);
+ recentsView.setOnClickListener(null);
+ recentsView.setOnLongClickListener(null);
+ }
+ }
+
+ updateHotseatRecentsDividerVisibility();
+ }
+
+ private void applyTaskToBubbleTextView(BubbleTextView btv, Task task) {
+ if (task.icon != null) {
+ Drawable icon = task.icon.getConstantState().newDrawable().mutate();
+ btv.applyIconAndLabel(icon, task.titleDescription);
+ }
+ btv.setTag(task);
+ }
+
+ protected void updateRecentTaskAtIndex(int taskIndex, Task task) {
+ View taskView = getChildAt(mRecentsStartIndex + taskIndex);
+ if (taskView instanceof BubbleTextView) {
+ applyTaskToBubbleTextView((BubbleTextView) taskView, task);
+ }
+ }
+
+ /**
+ * Make the divider VISIBLE between the Hotseat and Recents if there is at least one icon in
+ * each, otherwise make it GONE.
+ */
+ private void updateHotseatRecentsDividerVisibility() {
+ if (mHotseatRecentsDivider == null) {
+ return;
+ }
+
+ boolean hasAtLeastOneHotseatItem = false;
+ for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+ if (getChildAt(i).getVisibility() != GONE) {
+ hasAtLeastOneHotseatItem = true;
+ break;
+ }
+ }
+
+ boolean hasAtLeastOneRecentItem = false;
+ for (int i = mRecentsStartIndex; i <= mRecentsEndIndex; i++) {
+ if (getChildAt(i).getVisibility() != GONE) {
+ hasAtLeastOneRecentItem = true;
+ break;
+ }
+ }
+
+ mHotseatRecentsDivider.setVisibility(hasAtLeastOneHotseatItem && hasAtLeastOneRecentItem
+ ? VISIBLE : GONE);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean handled = delegateTouchIfNecessary(event);
+ return super.onTouchEvent(event) || handled;
+ }
+
+ /**
+ * 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;
+ }
+
+ @Override
+ public boolean onDragEvent(DragEvent event) {
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ mIsDraggingItem = true;
+ AbstractFloatingView.closeAllOpenViews(ActivityContext.lookupContext(getContext()));
+ 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() {
+ View firstHotseatView = null, lastHotseatView = null;
+ for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ if (firstHotseatView == null) {
+ firstHotseatView = child;
+ }
+ lastHotseatView = child;
+ }
+ }
+ if (firstHotseatView == null || lastHotseatView == null) {
+ return new RectF();
+ }
+ View leftmostHotseatView = !mIsRtl ? firstHotseatView : lastHotseatView;
+ View rightmostHotseatView = !mIsRtl ? lastHotseatView : firstHotseatView;
+ return new RectF(
+ leftmostHotseatView.getLeft() - mItemMarginLeftRight,
+ leftmostHotseatView.getTop(),
+ rightmostHotseatView.getRight() + mItemMarginLeftRight,
+ rightmostHotseatView.getBottom());
+ }
+
+ // 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 ActivityContext.lookupContext(getContext()).getLayoutInflater()
+ .inflate(layoutResId, this, false);
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ // Ignore, we just implement Insettable to draw behind system insets.
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index ec3a490..01616d4 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -16,19 +16,19 @@
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.RECENTS_GRID_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_TRANSLATION;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import android.util.FloatProperty;
@@ -37,9 +37,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 +61,19 @@
@Override
public void setState(@NonNull LauncherState state) {
float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
- SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+ RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
ADJACENT_PAGE_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 +90,27 @@
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],
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
+ PagedOrientationHandler orientationHandler =
+ ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
+ FloatProperty taskViewsFloat = orientationHandler.getSplitSelectTaskOffset(
+ TASK_PRIMARY_TRANSLATION, TASK_SECONDARY_TRANSLATION, mLauncher.getDeviceProfile());
+ setter.setFloat(mRecentsView, taskViewsFloat,
+ toState.getOverviewSecondaryTranslation(mLauncher),
+ config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, 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..bb1f6fc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -60,6 +60,14 @@
mListeners.add(r);
}
+ @Override
+ public void removeChangeListener(Runnable r) {
+ if (mListeners == null) {
+ return;
+ }
+ mListeners.remove(r);
+ }
+
private void registerDeviceConfigChangedListener(Context context) {
DeviceConfig.addOnPropertiesChangedListener(
NAMESPACE_LAUNCHER,
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
similarity index 81%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
rename to quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 597c17b..b4aa596 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -15,7 +15,6 @@
*/
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;
@@ -29,20 +28,17 @@
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.LauncherSettings;
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;
@@ -53,8 +49,7 @@
/**
* A BubbleTextView with a ring around it's drawable
*/
-public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
- LauncherAccessibilityDelegate.AccessibilityActionHandler {
+public class PredictedAppIcon extends DoubleShadowBubbleTextView {
private static final int RING_SHADOW_COLOR = 0x99000000;
private static final float RING_EFFECT_RATIO = 0.095f;
@@ -90,8 +85,13 @@
public void onDraw(Canvas canvas) {
int count = canvas.save();
if (!mIsPinned) {
- boolean isBadged = getTag() instanceof WorkspaceItemInfo
- && !Process.myUserHandle().equals(((ItemInfo) getTag()).user);
+ 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;
+ }
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);
@@ -148,29 +148,6 @@
}
@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) {
@@ -179,12 +156,19 @@
}
}
+ public boolean isPinned() {
+ return mIsPinned;
+ }
+
private int getOutlineOffsetX() {
return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
}
private int getOutlineOffsetY() {
- return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+ if (mDisplay != DISPLAY_TASKBAR) {
+ return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+ }
+ return (getMeasuredHeight() / 2) - mNormalizedIconRadius;
}
private void drawEffect(Canvas canvas, boolean isBadged) {
@@ -249,17 +233,13 @@
*/
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);
+ 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;
- mOffsetX = icon.getOutlineOffsetX();
- mOffsetY = icon.getOutlineOffsetY();
- mIconRadius = icon.mNormalizedIconRadius;
+ mIcon = icon;
mOutlinePaint.setStyle(Paint.Style.FILL);
mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
}
@@ -269,7 +249,8 @@
*/
@Override
public void drawUnderItem(Canvas canvas) {
- getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
+ getShape().drawShape(canvas, mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY(),
+ mIcon.mNormalizedIconRadius, mOutlinePaint);
}
/**
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/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
similarity index 62%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
rename to quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 9a03d92..0ceb8c7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -17,63 +17,65 @@
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.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.HINT_STATE_TWO_BUTTON_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+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.os.Bundle;
-import android.util.Log;
+import android.view.HapticFeedbackConstants;
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.LauncherSettings.Favorites;
import com.android.launcher3.LauncherState;
+import com.android.launcher3.QuickstepAccessibilityDelegate;
import com.android.launcher3.Workspace;
-import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
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.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.data.AppInfo;
+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.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.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.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;
@@ -81,7 +83,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.OptionalInt;
+import java.util.Objects;
import java.util.stream.Stream;
public class QuickstepLauncher extends BaseQuickstepLauncher {
@@ -93,33 +95,63 @@
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();
- }
- }
+ private FixedContainerItems mAllAppsPredictions;
+ private HotseatPredictionController mHotseatPredictionController;
@Override
protected void setupViews() {
super.setupViews();
- if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
- mHotseatPredictionController = new HotseatPredictionController(this);
- }
+ 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);
- OptionalInt allAppsRank = PredictionUiStateManager.INSTANCE.get(this).getAllAppsRank(info);
- allAppsRank.ifPresent(logger::withRank);
+
+ 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);
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
- }
+ 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
@@ -129,12 +161,10 @@
}
@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);
+ public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
+ // Only pause is taskbar controller is not present
+ mHotseatPredictionController.setPauseUIUpdate(getTaskbarController() == null);
+ return super.startActivitySafely(v, intent, item);
}
@Override
@@ -145,36 +175,22 @@
onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
}
- if (mHotseatPredictionController != null && ((changeBits & ACTIVITY_STATE_STARTED) != 0
+ if (((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());
- }
+ protected void showAllAppsFromIntent(boolean alreadyOnHome) {
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY);
+ super.showAllAppsFromIntent(alreadyOnHome);
}
@Override
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
- if (mHotseatPredictionController != null) {
- return Stream.concat(super.getSupportedShortcuts(),
- Stream.of(mHotseatPredictionController));
- } else {
- return super.getSupportedShortcuts();
- }
+ return Stream.concat(
+ super.getSupportedShortcuts(), Stream.of(mHotseatPredictionController));
}
/**
@@ -196,20 +212,31 @@
}
@Override
- public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) {
- super.bindPredictedItems(appInfos, ranks);
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.showCachedItems(appInfos, ranks);
+ 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 (getTaskbarController() != null && updated.stream()
+ .filter(w -> w.container == CONTAINER_HOTSEAT).findFirst().isPresent()) {
+ getTaskbarController().onHotseatUpdated();
}
}
@Override
public void onDestroy() {
super.onDestroy();
- if (mHotseatPredictionController != null) {
- mHotseatPredictionController.destroy();
- mHotseatPredictionController = null;
- }
+ mHotseatPredictionController.destroy();
}
@Override
@@ -219,18 +246,18 @@
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) {
+ 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 recentsView = getOverviewPanel();
- DiscoveryBounce.showForOverviewIfNeeded(this,
- recentsView.getPagedOrientationHandler());
RecentsView rv = getOverviewPanel();
sendCustomAccessibilityEvent(
rv.getPageAt(rv.getCurrentPage()), TYPE_VIEW_FOCUSED, null);
@@ -240,14 +267,13 @@
RecentsView rv = getOverviewPanel();
TaskView tasktolaunch = rv.getTaskViewAt(0);
if (tasktolaunch != null) {
- tasktolaunch.launchTask(false, success -> {
+ tasktolaunch.launchTask(success -> {
if (!success) {
getStateManager().goToState(OVERVIEW);
- tasktolaunch.notifyTaskLaunchFailed(TAG);
} else {
getStateManager().moveToRestState();
}
- }, MAIN_EXECUTOR.getHandler());
+ });
} else {
getStateManager().goToState(NORMAL);
}
@@ -259,41 +285,26 @@
@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");
- }
+ switch (mode) {
+ case NO_BUTTON:
+ list.add(new NoButtonQuickSwitchTouchController(this));
+ list.add(new NavBarToHomeTouchController(this));
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));
- }
- }
+ 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) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
similarity index 60%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
rename to quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 085b9b3..9097c8b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,11 +15,17 @@
*/
package com.android.launcher3.uioverrides;
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
+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;
@@ -57,7 +63,7 @@
mRecentsView.updateEmptyMessage();
mRecentsView.resetTaskVisuals();
}
- setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state);
+ setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
}
@@ -68,24 +74,48 @@
if (toState.overviewUi) {
// While animating into recents, update the visible task data as needed
- builder.addOnFrameCallback(mRecentsView::loadVisibleTaskData);
+ builder.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
mRecentsView.updateEmptyMessage();
} else {
builder.addListener(
AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
}
- setAlphas(builder, toState);
+ // 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);
}
- private void setAlphas(PropertySetter propertySetter, LauncherState state) {
- float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
+ /**
+ * @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,
- buttonAlpha, LINEAR);
+ clearAllButtonAlpha, LINEAR);
+ float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS)
+ && mRecentsView.shouldShowOverviewActionsForState(state) ? 1 : 0;
propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
- MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
+ MultiValueAlpha.VALUE, overviewButtonAlpha, config.getInterpolator(
+ ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
+
+ float splitPlaceholderAlpha = state.areElementsVisible(mLauncher, SPLIT_PLACHOLDER_VIEW) ?
+ 0.7f : 0;
+ propertySetter.setFloat(mRecentsView.getSplitPlaceholder(), ALPHA_FLOAT,
+ splitPlaceholderAlpha, LINEAR);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
index 36c0e34..1417995 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java
@@ -102,9 +102,11 @@
private void notifyChange() {
// Create a new array to avoid concurrent modification when the activity destroys itself.
mTempListeners = mListeners.toArray(mTempListeners);
- for (OnChangeListener listener : mTempListeners) {
+ for (int i = mTempListeners.length - 1; i >= 0; --i) {
+ final OnChangeListener listener = mTempListeners[i];
if (listener != null) {
listener.onExtractedColorsChanged(this);
+ mTempListeners[i] = null;
}
}
}
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..4e03971 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -16,16 +16,13 @@
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.allapps.AllAppsContainerView;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.quickstep.SysUINavigationMode;
/**
* Definition for AllApps state
@@ -42,7 +39,7 @@
};
public AllAppsState(int id) {
- super(id, ContainerType.ALLAPPS, STATE_FLAGS);
+ super(id, LAUNCHER_STATE_ALLAPPS, STATE_FLAGS);
}
@Override
@@ -65,13 +62,7 @@
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;
}
@@ -87,16 +78,21 @@
@Override
public int getVisibleElements(Launcher launcher) {
- return ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT;
+ return ALL_APPS_CONTENT;
}
@Override
public float[] getOverviewScaleAndOffset(Launcher launcher) {
- return new float[] {0.9f, 0};
+ return new float[] {0.9f, 1};
}
@Override
public LauncherState getHistoryForState(LauncherState previousState) {
return previousState == OVERVIEW ? OVERVIEW : NORMAL;
}
+
+ @Override
+ public float getWorkspaceScrimAlpha(Launcher launcher) {
+ return 1;
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
similarity index 76%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
rename to quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 8ff05f2..aa770d2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -15,12 +15,14 @@
*/
package com.android.launcher3.uioverrides.states;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
+
import android.content.Context;
import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
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;
@@ -33,7 +35,7 @@
| FLAG_WORKSPACE_INACCESSIBLE | FLAG_NON_INTERACTIVE | FLAG_CLOSE_POPUPS;
public BackgroundAppState(int id) {
- this(id, LauncherLogProto.ContainerType.TASKSWITCHER);
+ this(id, LAUNCHER_STATE_BACKGROUND);
}
protected BackgroundAppState(int id, int logContainer) {
@@ -68,21 +70,15 @@
@Override
public int getVisibleElements(Launcher launcher) {
return super.getVisibleElements(launcher)
- & ~OVERVIEW_BUTTONS & ~VERTICAL_SWIPE_INDICATOR;
+ & ~OVERVIEW_ACTIONS
+ & ~CLEAR_ALL_BUTTON
+ & ~VERTICAL_SWIPE_INDICATOR
+ | TASKBAR;
}
@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);
+ public boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) {
+ return false;
}
@Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
similarity index 81%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
rename to quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index fc0dcd5..6f084a1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -15,13 +15,15 @@
*/
package com.android.launcher3.uioverrides.states;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
+
import android.content.Context;
+import android.graphics.Point;
import android.graphics.Rect;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.quickstep.views.RecentsView;
/**
@@ -34,7 +36,7 @@
FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_WORKSPACE_INACCESSIBLE;
public OverviewModalTaskState(int id) {
- super(id, ContainerType.OVERVIEW, STATE_FLAGS);
+ super(id, LAUNCHER_STATE_OVERVIEW, STATE_FLAGS);
}
@Override
@@ -44,7 +46,7 @@
@Override
public int getVisibleElements(Launcher launcher) {
- return OVERVIEW_BUTTONS;
+ return OVERVIEW_ACTIONS | CLEAR_ALL_BUTTON;
}
@Override
@@ -69,13 +71,12 @@
}
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();
+ Point taskSize = activity.<RecentsView>getOverviewPanel().getSelectedTaskSize();
+ Rect modalTaskSize = new Rect();
+ activity.<RecentsView>getOverviewPanel().getModalTaskSize(modalTaskSize);
- float scale = (float) newHeight / taskHeight;
+ 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..6cdeb0f
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -0,0 +1,161 @@
+/*
+ * 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 static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+
+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.config.FeatureFlags;
+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 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 ? 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 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 float getWorkspaceScrimAlpha(Launcher launcher) {
+ return 1f;
+ }
+
+ @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) {
+ return 1f;
+ }
+
+ @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/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
similarity index 90%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
rename to quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
index 2c7373e..965f474 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -15,8 +15,9 @@
*/
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.userevent.nano.LauncherLogProto;
/**
* State to indicate we are about to launch a recent task. Note that this state is only used when
@@ -26,7 +27,7 @@
public class QuickSwitchState extends BackgroundAppState {
public QuickSwitchState(int id) {
- super(id, LauncherLogProto.ContainerType.APP);
+ super(id, LAUNCHER_STATE_BACKGROUND);
}
@Override
@@ -39,6 +40,6 @@
@Override
public int getVisibleElements(Launcher launcher) {
- return NONE;
+ 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..ba61923
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.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.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.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+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_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_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.views.RecentsView.RECENTS_SCALE_PROPERTY;
+
+import android.animation.ValueAnimator;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.allapps.AllAppsContainerView;
+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;
+
+ // 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) {
+ 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_DEACCEL);
+
+ if (SysUINavigationMode.getMode(mActivity) == NO_BUTTON) {
+ config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME);
+ } else {
+ 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);
+ 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 ((fromState == NORMAL || fromState == HINT_STATE
+ || fromState == HINT_STATE_TWO_BUTTON) && 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);
+ config.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
+ } 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
+ RecentsView overview = mActivity.getOverviewPanel();
+ 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_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..722d74a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
@@ -0,0 +1,52 @@
+/*
+ * 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 getOverviewSecondaryTranslation(Launcher launcher) {
+ RecentsView recentsView = launcher.getOverviewPanel();
+ PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+ int splitPosition = recentsView.getSplitPlaceholder().getSplitController()
+ .getActiveSplitPositionOption().mStagePosition;
+ 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/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
similarity index 72%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index c1a585e..b8caf81 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -17,19 +17,18 @@
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.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.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
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;
@@ -45,16 +44,14 @@
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.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;
-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.
@@ -63,6 +60,8 @@
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;
@@ -101,37 +100,19 @@
}
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;
@@ -156,12 +137,14 @@
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 */));
+ AnimatorControllerWithResistance.createRecentsResistanceFromOverviewAnim(mLauncher,
+ builder);
+
+ if (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,
@@ -179,8 +162,8 @@
if (topView != null) {
topView.addHintCloseAnim(mPullbackDistance, PULLBACK_INTERPOLATOR, builder);
}
- mCurrentAnimation = builder.createPlaybackController()
- .setOnCancelRunnable(this::clearState);
+ mCurrentAnimation = builder.createPlaybackController();
+ mCurrentAnimation.getTarget().addListener(newCancelListener(this::clearState));
}
private void clearState() {
@@ -201,29 +184,32 @@
@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()) {
+ if (LIVE_TILE.get()) {
RecentsView recentsView = mLauncher.getOverviewPanel();
recentsView.switchToScreenshot(null,
() -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
}
- mLauncher.getStateManager().goToState(mEndState, true,
- () -> onSwipeInteractionCompleted(mEndState));
+ if (mStartState.overviewUi) {
+ new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState))
+ .animateWithVelocity(velocity);
+ } else {
+ mLauncher.getStateManager().goToState(mEndState, true,
+ () -> onSwipeInteractionCompleted(mEndState));
+ }
if (mStartState != mEndState) {
- logStateChange(mStartState.containerType, logAction);
+ logHomeGesture();
}
AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher);
if (topOpenView != null) {
AbstractFloatingView.closeAllOpenViews(mLauncher);
- logStateChange(topOpenView.getLogContainerType(), logAction);
+ // TODO: add to WW log
}
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ 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();
@@ -240,17 +226,10 @@
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());
+ private void logHomeGesture() {
mLauncher.getStatsLogManager().logger()
- .withSrcState(StatsLogManager.containerTypeToAtomState(mStartState.containerType))
- .withDstState(StatsLogManager.containerTypeToAtomState(mEndState.containerType))
+ .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..74a253e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -0,0 +1,307 @@
+/*
+ * 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_ALPHA;
+import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.anim.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_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.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.anim.Interpolators;
+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 {
+
+
+ // 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.ofFloat(
+ mLauncher.getScrimView(),
+ VIEW_ALPHA,
+ mFromState.getWorkspaceScrimAlpha(mLauncher),
+ mToState.getWorkspaceScrimAlpha(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, () -> {
+ 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());
+ }
+
+ @Override
+ protected StateAnimationConfig getConfigForStates(
+ LauncherState fromState, LauncherState toState) {
+ if (fromState == NORMAL && toState == ALL_APPS) {
+ StateAnimationConfig builder = new StateAnimationConfig();
+ builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(
+ ACCEL,
+ 0,
+ ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD));
+ builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(
+ ACCEL,
+ ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD,
+ ALL_APPS_CONTENT_FADE_MAX_CLAMPING_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();
+
+ builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(
+ DEACCEL,
+ 1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD,
+ 1 - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD));
+ builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(
+ DEACCEL,
+ 1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD,
+ 1));
+ return builder;
+ }
+ return super.getConfigForStates(fromState, toState);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
similarity index 74%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 1b439d1..697516d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -15,19 +15,17 @@
*/
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.LauncherAnimUtils.VIEW_ALPHA;
+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_BUTTONS;
+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.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;
@@ -36,20 +34,20 @@
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_ALL_ANIMATIONS;
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.DisplayController.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.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;
@@ -60,25 +58,18 @@
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.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.ShelfPeekAnim;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
import com.android.quickstep.util.StaggeredWorkspaceAnim;
import com.android.quickstep.views.LauncherRecentsView;
@@ -87,22 +78,25 @@
* 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 {
+ BothAxesSwipeDetector.Listener {
/** The minimum progress of the scale/translationY animation until drag end. */
- private static final float Y_ANIM_MIN_PROGRESS = 0.15f;
+ private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
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 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 ShelfPeekAnim mShelfPeekAnim;
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;
@@ -113,16 +107,16 @@
// and the other two to set overview properties based on x and y progress.
private AnimatorPlaybackController mNonOverviewAnim;
private AnimatorPlaybackController mXOverviewAnim;
- private AnimatorPlaybackController mYOverviewAnim;
+ private AnimatedFloat 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());
+ mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
mMotionPauseDetector = new MotionPauseDetector(mLauncher);
mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
R.dimen.motion_pause_detector_min_displacement_from_app);
@@ -174,7 +168,7 @@
if (start) {
mStartState = mLauncher.getStateManager().getState();
- mMotionPauseDetector.setOnMotionPauseListener(this);
+ mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
// We have detected horizontal drag start, now allow swipe up as well.
mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP,
@@ -184,28 +178,8 @@
}
}
- @Override
- public void onMotionPauseChanged(boolean isPaused) {
+ private void onMotionPauseDetected() {
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() {
@@ -235,8 +209,8 @@
config.duration = (long) (Math.max(mXRange, mYRange) * 2);
config.animFlags = config.animFlags | SKIP_OVERVIEW;
mNonOverviewAnim = mLauncher.getStateManager()
- .createAnimationToNewWorkspace(toState, config)
- .setOnCancelRunnable(this::clearState);
+ .createAnimationToNewWorkspace(toState, config);
+ mNonOverviewAnim.getTarget().addListener(mClearStateOnCancelListener);
}
private void setupOverviewAnimators() {
@@ -244,12 +218,12 @@
final LauncherState toState = OVERVIEW;
// Set RecentView's initial properties.
- SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+ RECENTS_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);
+ (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
// As we drag right, animate the following properties:
@@ -257,8 +231,8 @@
// - 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);
+ xAnim.setFloat(mLauncher.getScrimView(), VIEW_ALPHA,
+ toState.getWorkspaceScrimAlpha(mLauncher), LINEAR);
mXOverviewAnim = xAnim.createPlaybackController();
mXOverviewAnim.dispatchOnStart();
@@ -266,11 +240,22 @@
// - RecentsView scale
// - RecentsView fullscreenProgress
PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
- yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
+ yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
+ SCALE_DOWN_INTERPOLATOR);
yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
- mYOverviewAnim = yAnim.createPlaybackController();
- mYOverviewAnim.dispatchOnStart();
+ 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
@@ -286,27 +271,14 @@
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.setDisallowPause(-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);
+ mYOverviewAnim.updateValue(yProgress);
}
return true;
}
@@ -316,16 +288,17 @@
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);
+ 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, logAction);
+ onAnimationToStateCompleted(OVERVIEW);
}
});
overviewAnim.start();
@@ -353,9 +326,11 @@
} 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.
+ // If user isn't flinging, just snap to the closest state.
boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
- targetState = passedHorizontalThreshold ? QUICK_SWITCH : NORMAL;
+ boolean passedVerticalThreshold = mYOverviewAnim.value > 1f;
+ targetState = passedHorizontalThreshold && !passedVerticalThreshold
+ ? QUICK_SWITCH : NORMAL;
}
// Animate the various components to the target state.
@@ -374,9 +349,9 @@
boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
- float yProgress = mYOverviewAnim.getProgressFraction();
+ float yProgress = mYOverviewAnim.value;
float startYProgress = Utilities.boundToRange(yProgress
- - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, 1f);
+ - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, mMaxYProgress);
final float endYProgress;
if (flingUpToNormal) {
endYProgress = 1;
@@ -386,12 +361,11 @@
} else {
endYProgress = 0;
}
- long yDuration = BaseSwipeDetector.calculateDuration(velocity.y,
- Math.abs(endYProgress - startYProgress));
- ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer();
- yOverviewAnim.setFloatValues(startYProgress, endYProgress);
+ 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.dispatchOnStart();
+ mYOverviewAnim.updateValue(startYProgress);
ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
if (flingUpToNormal && !mIsHomeScreenVisible) {
@@ -399,7 +373,7 @@
// animation as from an app.
StateAnimationConfig config = new StateAnimationConfig();
// Update mNonOverviewAnim to do nothing so it doesn't interfere.
- config.animFlags = 0;
+ config.animFlags = SKIP_ALL_ANIMATIONS;
updateNonOverviewAnim(targetState, config);
nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
@@ -410,7 +384,8 @@
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();
+ mNonOverviewAnim.getTarget().removeListener(mClearStateOnCancelListener);
+ mNonOverviewAnim.dispatchOnCancel();
}
float startProgress = mNonOverviewAnim.getProgressFraction();
float endProgress = canceled ? 0 : 1;
@@ -419,7 +394,7 @@
}
nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
- mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState, logAction));
+ mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState));
cancelAnimations();
xOverviewAnim.start();
@@ -427,27 +402,17 @@
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());
+ private void onAnimationToStateCompleted(LauncherState targetState) {
mLauncher.getStatsLogManager().logger()
.withSrcState(LAUNCHER_STATE_HOME)
- .withDstState(StatsLogManager.containerTypeToAtomState(targetState.containerType))
- .log(getLauncherAtomEvent(mStartState.containerType, targetState.containerType,
+ .withDstState(targetState.statsLogOrdinal)
+ .log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal,
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();
@@ -456,9 +421,8 @@
mXOverviewAnim.getAnimationPlayer().cancel();
}
if (mYOverviewAnim != null) {
- mYOverviewAnim.getAnimationPlayer().cancel();
+ mYOverviewAnim.cancelAnimation();
}
- mShelfPeekAnim.setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
mMotionPauseDetector.clear();
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
similarity index 97%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
index 845699a..4df0f63 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -76,7 +76,7 @@
* @return the animation
*/
PendingAnimation createSwipeDownToTaskAppAnimation(long duration, Interpolator interpolator) {
- mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
+ mRecentsView.setCurrentPage(mRecentsView.getDestinationPage());
TaskView taskView = mRecentsView.getCurrentPageTaskView();
if (taskView == null) {
throw new IllegalStateException("There is no task view to animate to.");
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 20ee61d..b8f9452 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -23,20 +23,9 @@
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 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 +33,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,9 +49,15 @@
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;
+
+ /**
+ * Minimum clamping progress for fading in all apps content
+ */
+ protected static final float ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD = 0.4f;
+
/**
* The progress at which recents will begin fading out when swiping up from overview.
@@ -75,37 +66,24 @@
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 +96,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 +109,27 @@
@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));
+ 0, ALL_APPS_CONTENT_FADE_MAX_CLAMPING_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));
return builder;
}
@@ -217,24 +137,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 +159,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 +192,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 +204,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 +217,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/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
similarity index 85%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index c643858..94a7442 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -21,6 +21,7 @@
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;
@@ -41,14 +42,12 @@
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.TaskUtils;
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.
@@ -92,24 +91,23 @@
@Override
public void onDragStart(boolean start, float startDisplacement) {
super.onDragStart(start, startDisplacement);
- mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ mStartContainerType = LAUNCHER_STATE_BACKGROUND;
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
}
@Override
- protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
- super.onSwipeInteractionCompleted(targetState, logAction);
+ protected void onSwipeInteractionCompleted(LauncherState targetState) {
+ super.onSwipeInteractionCompleted(targetState);
}
@Override
- protected float initCurrentAnimation(int animComponents) {
+ protected float initCurrentAnimation() {
StateAnimationConfig config = new StateAnimationConfig();
setupInterpolators(config);
config.duration = (long) (getShiftRange() * 2);
mCurrentAnimation = mLauncher.getStateManager()
- .createAnimationToNewWorkspace(mToState, config)
- .setOnCancelRunnable(this::clearState);
+ .createAnimationToNewWorkspace(mToState, config);
+ mCurrentAnimation.getTarget().addListener(mClearStateOnCancelListener);
mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator ->
updateFullscreenProgress((Float) valueAnimator.getAnimatedValue()));
return 1 / getShiftRange();
@@ -139,28 +137,21 @@
private void updateFullscreenProgress(float progress) {
mOverviewPanel.setFullscreenProgress(progress);
- int sysuiFlags = 0;
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_OVERVIEW, sysuiFlags);
+ } else {
+ mLauncher.getSystemUiController().updateUiState(
+ UI_STATE_OVERVIEW, mOverviewPanel.hasLightBackground());
}
- 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/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/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
similarity index 78%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 0ee5d04..c2e5cda 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -16,15 +16,14 @@
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.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
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 android.view.animation.Interpolator;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
@@ -33,10 +32,10 @@
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.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.util.FlingBlockCheck;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
@@ -51,18 +50,16 @@
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 mAllowGoingUp;
+ private boolean mAllowGoingDown;
private boolean mNoIntercept;
@@ -78,11 +75,19 @@
mRecentsView = activity.getOverviewPanel();
mIsRtl = Utilities.isRtl(activity.getResources());
SingleAxisSwipeDetector.Direction dir =
- mRecentsView.getPagedOrientationHandler().getOppositeSwipeDirection();
+ mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection();
mDetector = new SingleAxisSwipeDetector(activity, this, dir);
}
- private boolean canInterceptTouch() {
+ 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();
}
@@ -118,7 +123,7 @@
clearState();
}
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mNoIntercept = !canInterceptTouch();
+ mNoIntercept = !canInterceptTouch(ev);
if (mNoIntercept) {
return false;
}
@@ -144,15 +149,26 @@
break;
}
mTaskBeingDragged = view;
- if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
+ 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.
- directionsToDetectScroll = DIRECTION_POSITIVE;
+ // 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.
- directionsToDetectScroll = i == mRecentsView.getCurrentPage()
- ? DIRECTION_BOTH : DIRECTION_POSITIVE;
+ mAllowGoingUp = true;
+ if (i == mRecentsView.getCurrentPage()) {
+ mAllowGoingDown = true;
+ directionsToDetectScroll = DIRECTION_BOTH;
+ } else {
+ directionsToDetectScroll = upDirection;
+ }
}
break;
}
@@ -185,18 +201,14 @@
// No need to init
return;
}
- int scrollDirections = mDetector.getScrollDirections();
- if (goingUp && ((scrollDirections & DIRECTION_POSITIVE) == 0)
- || !goingUp && ((scrollDirections & DIRECTION_NEGATIVE) == 0)) {
+ if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) {
// 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;
+ mCurrentAnimation.getTarget().removeListener(this);
+ mCurrentAnimation.dispatchOnCancel();
}
PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
@@ -206,14 +218,20 @@
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) {
- mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
+ currentInterpolator = Interpolators.LINEAR;
+ pa = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
true /* animateTaskView */, true /* removeTask */, maxDuration);
mEndDisplacement = -secondaryTaskDimension;
} else {
- mPendingAnimation = mRecentsView.createTaskLaunchAnimation(
- mTaskBeingDragged, maxDuration, Interpolators.ZOOM_IN);
+ 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();
@@ -222,12 +240,11 @@
mEndDisplacement = secondaryLayerDimension - mTempCords[1];
}
mEndDisplacement *= verticalFactor;
+ mCurrentAnimation = pa.createPlaybackController();
- if (mCurrentAnimation != null) {
- mCurrentAnimation.setOnCancelRunnable(null);
- }
- mCurrentAnimation = mPendingAnimation.createPlaybackController()
- .setOnCancelRunnable(this::clearState);
+ // 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();
@@ -262,11 +279,6 @@
mCurrentAnimation.setPlayFraction(Utilities.boundToRange(
totalDisplacement * mProgressMultiplier, 0, 1));
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
- if (mRecentsView.getCurrentPage() != 0 || isGoingUp) {
- mRecentsView.redrawLiveTile(true);
- }
- }
return true;
}
@@ -274,7 +286,6 @@
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;
@@ -283,11 +294,9 @@
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(
@@ -296,24 +305,10 @@
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.setEndAction(this::clearState);
mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
- velocity, mEndDisplacement, animationDuration);
- }
-
- private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) {
- if (mPendingAnimation != null) {
- mPendingAnimation.finish(wasSuccess, logAction);
- mPendingAnimation = null;
- }
- clearState();
+ velocity * orientationHandler.getSecondaryTranslationDirectionFactor(),
+ mEndDisplacement, animationDuration);
}
private void clearState() {
@@ -321,9 +316,5 @@
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/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
similarity index 91%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
index 0ed5291..8f9c014 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
@@ -32,8 +32,8 @@
}
@Override
- protected float initCurrentAnimation(int animComponents) {
- float multiplier = super.initCurrentAnimation(animComponents);
+ protected float initCurrentAnimation() {
+ float multiplier = super.initCurrentAnimation();
return mLauncher.getDeviceProfile().isSeascape() ? multiplier : -multiplier;
}
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..3f7190f
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
@@ -0,0 +1,162 @@
+/*
+ * 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.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+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
+ 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 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..2a903eb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -0,0 +1,1708 @@
+/*
+ * 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.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_OVERVIEW;
+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_SCROLLING_FINISHED;
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+import static com.android.quickstep.util.SwipePipToHomeAnimator.FRACTION_END;
+import static com.android.quickstep.util.SwipePipToHomeAnimator.FRACTION_START;
+import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
+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.ObjectAnimator;
+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.os.Build;
+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.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.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.TaskInfoCompat;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+
+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<>();
+ protected 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 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 static final long SWIPE_PIP_TO_HOME_DURATION = 425;
+ 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 (!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) {
+ T createdActivity = mActivityInterface.getCreatedActivity();
+ if (createdActivity != null) {
+ initTransitionEndpoints(createdActivity.getDeviceProfile());
+ }
+ 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);
+
+ mStateCallback.setState(STATE_LAUNCHER_PRESENT);
+ if (alreadyOnHome) {
+ onLauncherStart();
+ } else {
+ activity.runOnceOnStart(this::onLauncherStart);
+ }
+
+ 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;
+ }
+ 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);
+
+ mRecentsView.onGestureEndTargetCalculated(mGestureState.getEndTarget());
+ });
+
+ notifyGestureStartedAsync();
+ }
+
+ private void onDeferredActivityLaunch() {
+ if (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();
+ } 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).
+ * @return Whether we can create the launcher controller or update its progress.
+ */
+ private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
+ return mGestureState.getEndTarget() != HOME;
+ }
+
+ @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()) {
+ 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);
+
+ if (swipeUpThresholdPassed) {
+ mActivity.getSystemUiController().updateUiState(
+ UI_STATE_OVERVIEW, mRecentsView.hasLightBackground());
+ } else {
+ mActivity.getSystemUiController().updateUiState(
+ UI_STATE_OVERVIEW, 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) {
+ InteractionJankMonitorWrapper.begin(mRecentsView,
+ InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH, 2000 /* ms timeout */);
+ InteractionJankMonitorWrapper.begin(mRecentsView,
+ InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
+ }
+ 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.
+ */
+ @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) {
+ if (cancel) {
+ mParallelRunningAnim.cancel();
+ } else {
+ 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 (mDeviceState.isButtonNavMode()) {
+ // 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;
+ } else if (endTarget == RECENTS) {
+ if (mRecentsView != null) {
+ int nearestPage = mRecentsView.getDestinationPage();
+ boolean isScrolling = false;
+ 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 (!mDeviceState.isButtonNavMode() || 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(long duration);
+
+ private final 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) {
+ 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);
+
+ 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;
+ HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration);
+ mIsSwipingPipToHome = homeAnimFactory.supportSwipePipToHome()
+ && runningTaskTarget != null
+ && runningTaskTarget.taskInfo.pictureInPictureParams != null
+ && TaskInfoCompat.isAutoEnterPipEnabled(
+ runningTaskTarget.taskInfo.pictureInPictureParams);
+ if (mIsSwipingPipToHome) {
+ mSwipePipToHomeAnimator = getSwipePipToHomeAnimator(
+ homeAnimFactory, runningTaskTarget, start);
+ mSwipePipToHomeAnimator.setDuration(SWIPE_PIP_TO_HOME_DURATION);
+ mSwipePipToHomeAnimator.setInterpolator(interpolator);
+ mSwipePipToHomeAnimator.setFloatValues(FRACTION_START, FRACTION_END);
+ mSwipePipToHomeAnimator.start();
+ mRunningWindowAnim = RunningWindowAnim.wrap(mSwipePipToHomeAnimator);
+ } else {
+ mSwipePipToHomeAnimator = null;
+ 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);
+ }
+ });
+ windowAnim.start(mContext, velocityPxPerMs);
+ mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+ }
+ homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
+ mLauncherTransitionController = null;
+ } 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);
+ S state = mActivityInterface.stateFromGestureEndTarget(mGestureState.getEndTarget());
+ if (mRecentsView != null && state.displayOverviewTasksAsGrid(mDp)) {
+ animatorSet.play(ObjectAnimator.ofFloat(mRecentsView, RECENTS_GRID_PROGRESS, 1));
+ }
+ animatorSet.setDuration(duration).setInterpolator(interpolator);
+ animatorSet.start();
+ mRunningWindowAnim = RunningWindowAnim.wrap(animatorSet);
+ }
+ }
+
+ private SwipePipToHomeAnimator getSwipePipToHomeAnimator(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 Rect destinationBounds = SystemUiProxy.INSTANCE.get(mContext)
+ .startSwipePipToHome(taskInfo.topActivity,
+ TaskInfoCompat.getTopActivityInfo(taskInfo),
+ runningTaskTarget.taskInfo.pictureInPictureParams,
+ homeRotation,
+ mDp.hotseatBarSizePx);
+ final Rect startBounds = new Rect();
+ updateProgressForStartRect(new Matrix(), startProgress).round(startBounds);
+ final SwipePipToHomeAnimator swipePipToHomeAnimator = new SwipePipToHomeAnimator(
+ runningTaskTarget.taskId,
+ taskInfo.topActivity,
+ runningTaskTarget.leash.getSurfaceControl(),
+ TaskInfoCompat.getPipSourceRectHint(
+ runningTaskTarget.taskInfo.pictureInPictureParams),
+ TaskInfoCompat.getWindowConfigurationBounds(taskInfo),
+ startBounds,
+ destinationBounds,
+ 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)) {
+ swipePipToHomeAnimator.setFromRotation(mTaskViewSimulator, windowRotation);
+ }
+ swipePipToHomeAnimator.addListener(new AnimatorListenerAdapter() {
+ private boolean mHasAnimationEnded;
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mHasAnimationEnded) return;
+ // Ensure Launcher ends in NORMAL state, we intentionally skip other callbacks
+ // since they are not relevant in this swipe-pip-to-home case.
+ homeAnimFactory.createActivityAnimationToHome().dispatchOnStart();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mHasAnimationEnded) return;
+ mHasAnimationEnded = true;
+ 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);
+ }
+ });
+ 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);
+ anim.addOnUpdateListener((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);
+ }
+ return 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() {
+ 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
+ * handle (in case of quick switch).
+ */
+ private void cancelCurrentAnimation() {
+ mCanceled = true;
+ mCurrentShift.cancelAnimation();
+ }
+
+ private void invalidateHandler() {
+ if (!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();
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mActivityRestartListener);
+ mTaskSnapshot = null;
+ }
+
+ private void invalidateHandlerWithLauncher() {
+ endLauncherTransitionController();
+
+ mRecentsView.onGestureAnimationEnd();
+ resetLauncherListeners();
+ }
+
+ private void endLauncherTransitionController() {
+ 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;
+ }
+ }
+
+ private void resetLauncherListeners() {
+ // Reset the callback for deferred activity launches
+ if (!LIVE_TILE.get()) {
+ mActivityInterface.setOnDeferredActivityLaunchCallback(null);
+ }
+ mActivity.getRootView().setOnApplyWindowInsetsListener(null);
+ }
+
+ 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 (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) {
+ UI_HELPER_EXECUTOR.execute(() -> {
+ if (mRecentsAnimationController == null) return;
+ final ThumbnailData taskSnapshot =
+ mRecentsAnimationController.screenshotTask(runningTaskId);
+ MAIN_EXECUTOR.execute(() -> {
+ mTaskSnapshot = taskSnapshot;
+ if (!updateThumbnail(runningTaskId)) {
+ setScreenshotCapturedState();
+ }
+ });
+ });
+ return;
+ }
+ finishTransitionPosted = updateThumbnail(runningTaskId);
+ }
+ if (!finishTransitionPosted) {
+ setScreenshotCapturedState();
+ }
+ }
+ }
+
+ // Returns whether finish transition was posted.
+ private boolean updateThumbnail(int runningTaskId) {
+ boolean finishTransitionPosted = false;
+ 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.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 (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 {
+ 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());
+ mRecentsAnimationController.setFinishTaskBounds(
+ mSwipePipToHomeAnimator.getTaskId(),
+ mSwipePipToHomeAnimator.getDestinationBounds(),
+ mSwipePipToHomeAnimator.getFinishTransaction());
+ mIsSwipingPipToHome = false;
+ }
+ }
+
+ protected abstract void finishRecentsControllerToHome(Runnable callback);
+
+ private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
+ endLauncherTransitionController();
+ mActivityInterface.onSwipeUpToRecentsComplete();
+ mRecentsView.onSwipeUpAnimationSuccess();
+ if (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.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 (!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();
+ mRecentsAnimationController.finish(true /* toRecents */, null);
+ }
+ }, true /* freezeTaskList */);
+ } else {
+ mActivityInterface.onLaunchTaskFailed();
+ Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
+ 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.
+ * @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) {
+ 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) {
+ 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..147297a 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,28 +15,27 @@
*/
package com.android.quickstep;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
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.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.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.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
+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 +44,19 @@
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.taskbar.TaskbarController;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.WindowBounds;
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.RecentsView;
+import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -106,7 +108,7 @@
public abstract void onAssistantVisibilityChanged(float visibility);
public abstract AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
- boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
+ boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback);
public abstract ActivityInitListener createActivityInitListener(
Predicate<Boolean> onInitListener);
@@ -124,6 +126,11 @@
return null;
}
+ @Nullable
+ public TaskbarController getTaskbarController() {
+ return null;
+ }
+
public final boolean isResumed() {
ACTIVITY_TYPE activity = getCreatedActivity();
return activity != null && activity.hasBeenResumed();
@@ -150,20 +157,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,97 +197,141 @@
}
/**
- * 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 (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) + taskMargin,
+ 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 PointF getTaskDimension(Context context, DeviceProfile dp) {
+ PointF dimension = new PointF();
if (dp.isMultiWindowMode) {
WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
- taskWidth = bounds.availableSize.x;
- taskHeight = bounds.availableSize.y;
+ dimension.x = bounds.availableSize.x;
+ dimension.y = bounds.availableSize.y;
+ } else if (TaskView.CLIP_STATUS_AND_NAV_BARS) {
+ dimension.x = dp.availableWidthPx;
+ dimension.y = dp.availableHeightPx;
} else {
- taskWidth = dp.availableWidthPx;
- taskHeight = dp.availableHeightPx;
+ dimension.x = dp.widthPx;
+ dimension.y = dp.heightPx;
}
+ return dimension;
+ }
- // 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.overviewTaskMarginPx,
+ 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. */
+ public final int getOverviewActionsHeight(Context context) {
Resources res = context.getResources();
- float actionsBottomMargin = 0;
+ int actionsBottomMargin = 0;
if (getMode(context) == Mode.THREE_BUTTONS) {
actionsBottomMargin = res.getDimensionPixelSize(
R.dimen.overview_actions_bottom_margin_three_button);
@@ -293,37 +339,56 @@
actionsBottomMargin = res.getDimensionPixelSize(
R.dimen.overview_actions_bottom_margin_gesture);
}
- float overviewActionsHeight = actionsBottomMargin
+ return actionsBottomMargin
+ res.getDimensionPixelSize(R.dimen.overview_actions_height);
- return overviewActionsHeight;
}
+ /**
+ * Called when the gesture ends and the animation starts towards the given target. No-op by
+ * default, but subclasses can override to add an additional animation with the same duration.
+ */
+ public @Nullable Animator getParallelAnimationToLauncher(
+ GestureState.GestureEndTarget endTarget, long duration) {
+ return null;
+ }
+
+ /**
+ * See {@link com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags}
+ * @param systemUiStateFlags The latest SystemUiStateFlags
+ */
+ public void onSystemUiFlagsChanged(int systemUiStateFlags) {
+ }
+
+ /**
+ * 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 +416,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 +434,6 @@
}
@Override
- public void onTransitionCancelled() {
- mActivity.getStateManager().goToState(mStartState, false /* animate */);
- }
-
- @Override
public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
if (mIsAttachedToWindow == attached && animate) {
return;
@@ -397,16 +464,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/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
similarity index 80%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
rename to quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 33b9cde..e13d1a4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -18,6 +18,7 @@
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;
@@ -26,12 +27,10 @@
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.util.AnimatorControllerWithResistance;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -59,9 +58,7 @@
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;
+ return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
} else {
return dp.heightPx - outRect.bottom;
}
@@ -84,7 +81,8 @@
/** 6 */
@Override
public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
- boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
+ boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
+ notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
factory.initUI();
return factory;
@@ -107,7 +105,7 @@
@Override
public RecentsView getVisibleRecentsView() {
RecentsActivity activity = getCreatedActivity();
- if (activity != null && activity.hasWindowFocus()) {
+ if (activity != null && activity.hasBeenResumed()) {
return activity.getOverviewPanel();
}
return null;
@@ -140,20 +138,11 @@
}
@Override
- public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+ public void onExitOverview(RotationTouchHelper 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;
}
@@ -169,10 +158,23 @@
}
@Override
- protected float getExtraSpace(Context context, DeviceProfile dp,
- PagedOrientationHandler orientationHandler) {
- return showOverviewActions(context)
- ? context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height)
- : 0;
+ 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());
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
similarity index 61%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
rename to quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index fc7a119..7e4a352 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -15,14 +15,37 @@
*/
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.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;
@@ -32,18 +55,33 @@
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.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.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
- BaseSwipeUpHandlerV2<RecentsActivity, FallbackRecentsView> {
+ 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;
@@ -89,7 +127,13 @@
protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
- mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle());
+ 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;
}
@@ -130,17 +174,20 @@
}
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) {
- super(null);
mDuration = duration;
if (mRunningOverHome) {
@@ -162,6 +209,15 @@
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);
@@ -218,5 +274,91 @@
.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(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..ebdc1e6 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";
@@ -133,6 +143,9 @@
private Set<Integer> mPreviouslyAppearedTaskIds = new HashSet<>();
private int mLastStartedTaskId = -1;
+ /** The time when the swipe up gesture is triggered. */
+ private long mSwipeUpStartTimeMs;
+
public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
mHomeIntent = componentObserver.getHomeIntent();
mOverviewIntent = componentObserver.getOverviewIntent();
@@ -201,7 +214,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;
}
@@ -334,6 +348,14 @@
mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
}
+ void setSwipeUpStartTimeMs(long uptimeMs) {
+ mSwipeUpStartTimeMs = uptimeMs;
+ }
+
+ long getSwipeUpStartTimeMs() {
+ return mSwipeUpStartTimeMs;
+ }
+
public void dump(PrintWriter pw) {
pw.println("GestureState:");
pw.println(" gestureID=" + mGestureId);
@@ -343,4 +365,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/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java b/quickstep/src/com/android/quickstep/ImageActionsApi.java
similarity index 73%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
rename to quickstep/src/com/android/quickstep/ImageActionsApi.java
index ba8ba33..8cb64c2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
+++ b/quickstep/src/com/android/quickstep/ImageActionsApi.java
@@ -22,11 +22,14 @@
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;
@@ -61,6 +64,20 @@
*/
@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;
@@ -68,20 +85,22 @@
UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(mContext,
mBitmapSupplier.get(), crop, intent, (uri, intentForUri) -> {
- intentForUri
- .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
- .putExtra(EXTRA_STREAM, uri);
+ 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() {
- ImageActionUtils.startShareActivity(mContext, mBitmapSupplier, null, null, TAG);
+ public void startShareActivity(Rect crop) {
+ ImageActionUtils.startShareActivity(mContext, mBitmapSupplier, crop, null, TAG);
}
/**
@@ -96,4 +115,12 @@
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/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
similarity index 62%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
rename to quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index edefbe1..878f5c9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -16,18 +16,16 @@
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.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 static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
+import android.animation.Animator;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Rect;
-import android.util.Log;
-import android.view.animation.Interpolator;
+import android.view.MotionEvent;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -37,21 +35,17 @@
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.taskbar.TaskbarController;
import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
+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.util.ShelfPeekAnim.ShelfAnimState;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.plugins.shared.LauncherOverlayManager;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -76,26 +70,13 @@
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;
+ return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
} 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) {
@@ -105,7 +86,7 @@
// recents, we assume the first task is invisible, making translation off by one task.
launcher.getStateManager().reapplyState();
launcher.getRootView().setForceHideBackArrow(false);
- notifyRecentsOfOrientation(deviceState);
+ notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
}
@Override
@@ -119,30 +100,14 @@
@Override
public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
- boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
- notifyRecentsOfOrientation(deviceState);
+ boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
+ notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
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);
@@ -193,6 +158,16 @@
@Nullable
@Override
+ public TaskbarController getTaskbarController() {
+ BaseQuickstepLauncher launcher = getCreatedActivity();
+ if (launcher == null) {
+ return null;
+ }
+ return launcher.getTaskbarController();
+ }
+
+ @Nullable
+ @Override
public RecentsView getVisibleRecentsView() {
Launcher launcher = getVisibleLauncher();
return launcher != null && launcher.getStateManager().getState().overviewUi
@@ -203,24 +178,18 @@
@UiThread
private Launcher getVisibleLauncher() {
Launcher launcher = getCreatedActivity();
- return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus()
+ return (launcher != null) && launcher.isStarted() && launcher.hasBeenResumed()
? 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);
+ closeOverlay();
launcher.getStateManager().goToState(OVERVIEW,
launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
return true;
@@ -228,7 +197,7 @@
@Override
- public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+ public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
final StateManager<LauncherState> stateManager = getCreatedActivity().getStateManager();
stateManager.addStateListener(
new StateManager.StateListener<LauncherState>() {
@@ -244,11 +213,11 @@
});
}
- private void notifyRecentsOfOrientation(RecentsAnimationDeviceState deviceState) {
+ private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
// reset layout on swipe to home
RecentsView recentsView = getCreatedActivity().getOverviewPanel();
- recentsView.setLayoutRotation(deviceState.getCurrentActiveRotation(),
- deviceState.getDisplayRotation());
+ recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+ rotationTouchHelper.getDisplayRotation());
}
@Override
@@ -262,23 +231,6 @@
}
@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 &&
@@ -309,37 +261,62 @@
}
@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);
- }
- }
+ void onOverviewServiceBound() {
+ final BaseQuickstepLauncher activity = getCreatedActivity();
+ if (activity == null) return;
+ activity.getAppTransitionManager().registerRemoteTransitions();
}
-}
\ No newline at end of file
+ @Override
+ public @Nullable Animator getParallelAnimationToLauncher(GestureEndTarget endTarget,
+ long duration) {
+ TaskbarController taskbarController = getTaskbarController();
+ if (taskbarController == null) {
+ return null;
+ }
+ LauncherState toState = stateFromGestureEndTarget(endTarget);
+ return taskbarController.createAnimToLauncher(toState, duration);
+ }
+
+ @Override
+ public void onSystemUiFlagsChanged(int systemUiStateFlags) {
+ TaskbarController taskbarController = getTaskbarController();
+ if (taskbarController == null) {
+ return;
+ }
+ boolean isImeVisible = (systemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
+ taskbarController.setIsImeVisible(isImeVisible);
+ }
+
+ @Override
+ public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+ TaskbarController taskbarController = getTaskbarController();
+ if (taskbarController == null) {
+ return super.deferStartingActivity(deviceState, ev);
+ }
+ return taskbarController.isEventOverAnyTaskbarItem(ev);
+ }
+
+ @Override
+ public boolean shouldCancelCurrentGesture() {
+ TaskbarController taskbarController = getTaskbarController();
+ if (taskbarController == null) {
+ return super.shouldCancelCurrentGesture();
+ }
+ return taskbarController.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..311ac83
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.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.quickstep;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+
+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.LauncherState;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.util.RectFSpringAnim;
+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
+ 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(long duration) {
+ HomeAnimationFactory homeAnimFactory;
+ if (mActivity != null) {
+ final TaskView runningTaskView = mRecentsView.getRunningTaskView();
+ final View workspaceView;
+ if (runningTaskView != null
+ && !mIsSwipingPipToHome
+ && 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();
+
+ if (canUseWorkspaceView) {
+ // 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;
+ homeAnimFactory = new LauncherHomeAnimationFactory() {
+ @Override
+ public RectF getWindowTargetRect() {
+ return iconLocation;
+ }
+
+ @Override
+ public void setAnimation(RectFSpringAnim anim) {
+ anim.addAnimatorListener(floatingIconView);
+ floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged);
+ floatingIconView.setFastFinishRunnable(anim::end);
+ }
+
+ @Override
+ public void update(RectF currentRect, float progress, float radius) {
+ floatingIconView.update(currentRect, 1f, progress, windowAlphaThreshold,
+ radius, false);
+ }
+
+ @Override
+ public void onCancel() {
+ floatingIconView.fastFinish();
+ }
+ };
+ } else {
+ homeAnimFactory = new LauncherHomeAnimationFactory();
+ }
+ } else {
+ homeAnimFactory = new HomeAnimationFactory() {
+ @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 */);
+ }
+
+ private class LauncherHomeAnimationFactory extends HomeAnimationFactory {
+ @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) {
+ new StaggeredWorkspaceAnim(mActivity, velocity,
+ true /* animateOverviewScrim */).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..a749f9a 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -30,16 +30,18 @@
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,6 +53,37 @@
*/
class OrientationTouchTransformer {
+ 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 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;
@@ -60,10 +93,14 @@
private final Matrix mTmpMatrix = new Matrix();
private final float[] mTmpPoint = new float[2];
- private SparseArray<OrientationRectF> mSwipeTouchRegions = new SparseArray<>(MAX_ORIENTATIONS);
+ private 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 int mNavBarLargerGesturalHeight;
private boolean mEnableMultipleRegions;
private Resources mResources;
private OrientationRectF mLastRectTouched;
@@ -83,7 +120,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 +140,36 @@
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 (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 +178,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.realSize, 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 +209,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 +230,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,41 +241,41 @@
* 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.realSize, 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);
}
Point size = display.realSize;
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 {
@@ -235,10 +291,12 @@
+ getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
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,6 +322,10 @@
}
+ boolean touchInOneHandedModeRegion(MotionEvent ev) {
+ return mOneHandedModeRegion.contains(ev.getX(), ev.getY());
+ }
+
private int getNavbarSize(String resName) {
return ResourceUtils.getNavbarSize(resName, mResources);
}
@@ -313,7 +375,9 @@
}
for (int i = 0; i < MAX_ORIENTATIONS; i++) {
- OrientationRectF rect = mSwipeTouchRegions.get(i);
+ CurrentDisplay display = new CurrentDisplay(mCurrentDisplay.size, i);
+ OrientationRectF rect = mSwipeTouchRegions.get(display);
+
if (TestProtocol.sDebugTracing) {
Log.d(TestProtocol.NO_SWIPE_TO_HOME, "transform:DOWN, rect=" + rect);
}
@@ -327,7 +391,7 @@
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,12 +453,12 @@
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
+ + "mCurrentRotation: " + mCurrentDisplay.rotation
+ "mRotation: " + mRotation);
}
event.transform(mTmpMatrix);
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..923d4f1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -0,0 +1,265 @@
+/*
+ * 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));
+ }
+
+ 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);
+ AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory()
+ .newHandler(gestureState, cmd.createTime);
+ interactionHandler.setGestureEndCallback(
+ () -> onTransitionComplete(cmd, interactionHandler));
+
+ Intent intent = new Intent(interactionHandler.getLaunchIntent());
+ interactionHandler.initWhenReady(intent);
+
+ 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);
+ }
+ };
+
+ 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.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..fb8f9fe 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 =
@@ -117,10 +117,6 @@
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 +258,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..192738f 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -15,21 +15,34 @@
*/
package com.android.quickstep;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+
+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) {
@@ -48,8 +61,14 @@
super.init(context);
+ LIVE_TILE.initialize(context);
+
// 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/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
similarity index 88%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
rename to quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 6f4d34c..39af0db 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -53,9 +53,15 @@
Bundle::putInt, PortraitStatesTouchController::getHotseatTop);
}
- case TestProtocol.REQUEST_OVERVIEW_ACTIONS_ENABLED: {
+ case TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED: {
response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
- FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
+ 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;
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 70b4f20..2f1538b 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -22,10 +22,12 @@
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;
@@ -51,6 +53,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;
@@ -64,6 +69,11 @@
mActivityManagerWrapper.registerTaskStackListener(this);
}
+ @VisibleForTesting
+ public boolean isLoadingTasksInBackground() {
+ return mLoadingTasksInBackground;
+ }
+
/**
* Fetches the task keys skipping any local cache.
*/
@@ -83,6 +93,10 @@
* @return The change id of the current task list
*/
public synchronized int getTasks(boolean loadKeysOnly, Consumer<ArrayList<Task>> callback) {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: keysOnly=" + loadKeysOnly
+ + " callback=" + callback);
+ }
final int requestLoadId = mChangeId;
if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) {
// The list is up to date, send the callback on the next frame,
@@ -90,22 +104,38 @@
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(() -> {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: no new tasks");
+ }
+ callback.accept(result);
+ });
}
return requestLoadId;
}
// Kick off task loading in the background
+ mLoadingTasksInBackground = true;
UI_HELPER_EXECUTOR.execute(() -> {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: loading in bg start");
+ }
if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
}
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: loading in bg end");
+ }
TaskLoadResult loadResult = mResultsBg;
mMainThreadExecutor.execute(() -> {
+ mLoadingTasksInBackground = false;
mResultsUi = loadResult;
if (callback != null) {
ArrayList<Task> result = copyOf(mResultsUi);
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: callback w/ bg results");
+ }
callback.accept(result);
}
});
@@ -191,6 +221,7 @@
} else {
task = new Task(taskKey);
}
+ task.setLastSnapshotData(rawTask);
allTasks.add(task);
}
@@ -200,9 +231,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/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
similarity index 78%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
rename to quickstep/src/com/android/quickstep/RecentsActivity.java
index 6f2f9fb..1340abb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -18,9 +18,10 @@
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.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.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
@@ -29,7 +30,6 @@
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;
@@ -41,7 +41,10 @@
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.R;
+import com.android.launcher3.WrappedAnimationRunnerImpl;
+import com.android.launcher3.WrappedLauncherAnimationRunner;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -49,24 +52,27 @@
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.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.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.RemoteAnimationRunnerCompat;
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.
@@ -87,6 +93,9 @@
private StateManager<RecentsState> mStateManager;
+ // Strong refs to runners which are cleared when the activity is destroyed
+ private WrappedAnimationRunnerImpl mActivityLaunchAnimationRunner;
+
/**
* Init drag layer and overview panel views.
*/
@@ -97,8 +106,14 @@
mFallbackRecentsView = findViewById(R.id.overview_panel);
mActionsView = findViewById(R.id.overview_actions_view);
+ SplitPlaceholderView splitPlaceholderView = findViewById(R.id.split_placeholder);
+ splitPlaceholderView.init(
+ new SplitSelectStateController(
+ SystemUiProxy.INSTANCE.get(this))
+ );
+
mDragLayer.recreateControllers();
- mFallbackRecentsView.init(mActionsView);
+ mFallbackRecentsView.init(mActionsView, splitPlaceholderView);
}
@Override
@@ -118,7 +133,6 @@
* etc.)
*/
protected void onHandleConfigChanged() {
- mUserEventDispatcher = null;
initDeviceProfile();
AbstractFloatingView.closeOpenViews(this, true,
@@ -164,34 +178,40 @@
}
@Override
- public ActivityOptions getActivityLaunchOptions(final View v) {
+ public ActivityOptionsWrapper getActivityLaunchOptions(final View v) {
if (!(v instanceof TaskView)) {
- return null;
+ return super.getActivityLaunchOptions(v);
}
final TaskView taskView = (TaskView) v;
- RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler,
- true /* startAtFrontOfQueue */) {
+ RunnableList onEndCallback = new RunnableList();
- @Override
- public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
- AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
- wallpaperTargets);
- anim.addListener(resetStateListener());
- result.setAnimation(anim, RecentsActivity.this);
- }
+ mActivityLaunchAnimationRunner = (int transit,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets,
+ AnimationResult result) -> {
+ AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
+ wallpaperTargets);
+ anim.addListener(resetStateListener());
+ result.setAnimation(anim, RecentsActivity.this, onEndCallback::executeAllAndDestroy);
};
- return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
- runner, RECENTS_LAUNCH_DURATION,
+
+ final LauncherAnimationRunner wrapper = new WrappedLauncherAnimationRunner<>(
+ mUiHandler, mActivityLaunchAnimationRunner, true /* startAtFrontOfQueue */);
+ RemoteAnimationAdapterCompat adapterCompat = new RemoteAnimationAdapterCompat(
+ wrapper, RECENTS_LAUNCH_DURATION,
RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
- - STATUS_BAR_TRANSITION_PRE_DELAY));
+ - STATUS_BAR_TRANSITION_PRE_DELAY);
+ return new ActivityOptionsWrapper(
+ ActivityOptionsCompat.makeRemoteAnimation(adapterCompat),
+ onEndCallback);
}
/**
* Composes the animations for a launch from the recents list if possible.
*/
- private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
+ private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
RemoteAnimationTargetCompat[] appTargets,
RemoteAnimationTargetCompat[] wallpaperTargets) {
AnimatorSet target = new AnimatorSet();
@@ -246,7 +266,7 @@
setupViews();
getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
- Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
+ mFallbackRecentsView.hasLightBackground());
ACTIVITY_TRACKER.handleCreate(this);
}
@@ -288,6 +308,7 @@
protected void onDestroy() {
super.onDestroy();
ACTIVITY_TRACKER.onActivityDestroyed(this);
+ mActivityLaunchAnimationRunner = null;
}
@Override
@@ -297,14 +318,12 @@
}
public void startHome() {
- startActivity(new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ startActivity(createHomeIntent());
}
@Override
- protected StateHandler<RecentsState>[] createStateHandlers() {
- return new StateHandler[] { new FallbackRecentsStateController(this) };
+ protected void collectStateHandlers(List<StateHandler> out) {
+ out.add(new FallbackRecentsStateController(this));
}
@Override
@@ -321,7 +340,7 @@
@Override
public AtomicAnimationFactory<RecentsState> createAtomicAnimationFactory() {
- return new RecentsAtomicAnimationFactory<>(this, 0);
+ return new RecentsAtomicAnimationFactory<>(this);
}
private AnimatorListenerAdapter resetStateListener() {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 4e9aa61..82e8a93 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -15,49 +15,35 @@
*/
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.THREAD_POOL_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.graphics.Rect;
+import android.window.PictureInPictureSurfaceTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import com.android.launcher3.util.Preconditions;
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;
public RecentsAnimationController(RecentsAnimationControllerCompat controller,
boolean allowMinimizeSplitScreen,
@@ -108,40 +94,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) {
+ THREAD_POOL_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,18 +128,6 @@
@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);
}
@@ -179,8 +135,9 @@
public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
mOnFinishedListener.accept(this);
UI_HELPER_EXECUTOR.execute(() -> {
- mController.setInputConsumerEnabled(false);
mController.finish(toRecents, sendUserLeaveHint);
+ InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+ InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
if (callback != null) {
MAIN_EXECUTOR.execute(callback);
}
@@ -188,6 +145,21 @@
}
/**
+ * Sets the final bounds 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 destinationBounds bounds of the final PiP window
+ * @param finishTransaction leash operations for the final transform.
+ */
+ public void setFinishTaskBounds(int taskId, Rect destinationBounds,
+ PictureInPictureSurfaceTransaction finishTransaction) {
+ UI_HELPER_EXECUTOR.execute(
+ () -> mController.setFinishTaskBounds(taskId, destinationBounds,
+ finishTransaction));
+ }
+
+ /**
* Enables the input consumer to start intercepting touches in the app window.
*/
public void enableInputConsumer() {
@@ -197,75 +169,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..23e35f6 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -16,21 +16,24 @@
package com.android.quickstep;
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.DisplayHolder.CHANGE_ALL;
+import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_FRAME_DELAY;
+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_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;
@@ -44,28 +47,32 @@
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.Region;
+import android.net.Uri;
import android.os.Process;
+import android.os.SystemProperties;
import android.os.UserManager;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.DisplayMetrics;
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.DisplayHolder;
+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 java.io.PrintWriter;
import java.util.ArrayList;
@@ -77,13 +84,16 @@
*/
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 DisplayHolder mDisplayHolder;
private final int mDisplayId;
- private int mDisplayRotation;
+ private final RotationTouchHelper mRotationTouchHelper;
private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
@@ -94,6 +104,9 @@
private final Region mDeferredGestureRegion = new Region();
private boolean mAssistantAvailable;
private float mAssistantVisibility;
+ private boolean mIsOneHandedModeEnabled;
+ private boolean mIsSwipeToNotificationEnabled;
+ private final boolean mIsOneHandedModeSupported;
private boolean mIsUserUnlocked;
private final ArrayList<Runnable> mUserUnlockedActions = new ArrayList<>();
@@ -107,85 +120,26 @@
}
};
- 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, DisplayController.getDefaultDisplay(context));
+ }
+
+ public RecentsAnimationDeviceState(Context context, DisplayHolder displayHolder) {
mContext = context;
+ mDisplayHolder = displayHolder;
mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
- mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
- mDisplayId = mDefaultDisplay.getInfo().id;
- runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
+ mDisplayId = mDisplayHolder.getInfo().id;
+ mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
+ runOnDestroy(() -> mDisplayHolder.removeChangeListener(this));
+ mRotationTouchHelper = new RotationTouchHelper(context, mDisplayHolder);
+ runOnDestroy(mRotationTouchHelper::destroy);
// Register for user unlocked if necessary
mIsUserUnlocked = context.getSystemService(UserManager.class)
@@ -207,10 +161,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 +181,34 @@
}
}
- 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) {
- @Override
- public void onOrientationChanged(int degrees) {
- int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
- mSensorRotation);
- if (newRotation == mSensorRotation) {
- return;
- }
- mSensorRotation = newRotation;
- mPrioritizeDeviceRotation = true;
+ 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));
- if (newRotation == mCurrentAppRotation) {
- // When user rotates device to the orientation of the foreground app after
- // quickstepping
- toggleSecondaryNavBarsForRotation();
- }
- }
- };
- }
-
- private void setupOrientationSwipeHandler() {
- ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
- mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
- .unregisterTaskStackListener(mFrozenTaskListener);
- runOnDestroy(mOnDestroyFrozenTaskRunnable);
- }
-
- private void destroyOrientationSwipeHandlerCallback() {
- ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
- mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
+ 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));
+ }
}
private void runOnDestroy(Runnable action) {
@@ -297,11 +233,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);
+ mDisplayHolder.removeChangeListener(this);
+ mDisplayHolder.addChangeListener(this);
+ onDisplayInfoChanged(mDisplayHolder.getInfo(), CHANGE_ALL);
if (newMode == NO_BUTTON) {
mExclusionListener.register();
@@ -309,47 +254,26 @@
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, mDisplayHolder.getInfo());
mMode = newMode;
}
@Override
- public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
+ public void onDisplayInfoChanged(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;
}
- 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 +301,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 +386,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,7 +395,8 @@
*/
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
@@ -488,6 +420,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 +476,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 +539,42 @@
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 || mIsSwipeToNotificationEnabled) {
+ final Info displayInfo = mDisplayHolder.getInfo();
+ return (mRotationTouchHelper.touchInOneHandedModeRegion(ev)
+ && displayInfo.rotation != Surface.ROTATION_90
+ && displayInfo.rotation != Surface.ROTATION_270
+ && displayInfo.metrics.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));
- }
-
- /**
- * 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 +586,10 @@
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);
+ mRotationTouchHelper.dump(pw);
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 517501a..ba24e6a 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,11 +25,13 @@
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.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;
@@ -40,6 +41,8 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
import java.util.function.Consumer;
/**
@@ -52,6 +55,9 @@
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,12 +67,10 @@
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);
+ mIconCache = new TaskIconCache(context, RECENTS_MODEL_EXECUTOR);
+ mThumbnailCache = new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR);
ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
IconProvider.registerIconChangeListener(context,
@@ -93,15 +97,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,6 +104,14 @@
}
/**
+ * @return Whether the task list is currently updating in the background
+ */
+ @VisibleForTesting
+ public boolean isLoadingTasksInBackground() {
+ return mTaskList.isLoadingTasksInBackground();
+ }
+
+ /**
* 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
@@ -140,7 +143,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 +172,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) {
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
new file mode 100644
index 0000000..2cf3212
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -0,0 +1,377 @@
+/*
+ * 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.DisplayHolder.CHANGE_ALL;
+import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_FRAME_DELAY;
+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.view.MotionEvent;
+import android.view.OrientationEventListener;
+
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DisplayController.DisplayHolder;
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
+import com.android.launcher3.util.DisplayController.Info;
+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 java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class RotationTouchHelper implements
+ SysUINavigationMode.NavigationModeChangeListener,
+ DisplayInfoChangeListener {
+
+ private final OrientationTouchTransformer mOrientationTouchTransformer;
+ private final DisplayHolder mDisplayHolder;
+ private final SysUINavigationMode mSysUiNavMode;
+ private final 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;
+
+ public RotationTouchHelper(Context context, DisplayHolder displayHolder) {
+ mContext = context;
+ mDisplayHolder = displayHolder;
+ Resources resources = mContext.getResources();
+ mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
+ mDisplayId = mDisplayHolder.getInfo().id;
+
+ mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
+ () -> QuickStepContract.getWindowCornerRadius(resources));
+
+ // Register for navigation mode changes
+ onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
+ runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
+
+ mOrientationListener = new OrientationEventListener(context) {
+ @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();
+ }
+ }
+ };
+ }
+
+ private void setupOrientationSwipeHandler() {
+ ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
+ mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
+ .unregisterTaskStackListener(mFrozenTaskListener);
+ runOnDestroy(mOnDestroyFrozenTaskRunnable);
+ }
+
+ private void destroyOrientationSwipeHandlerCallback() {
+ ActivityManagerWrapper.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();
+ }
+ }
+
+ 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(mDisplayHolder.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) {
+ mDisplayHolder.removeChangeListener(this);
+ mDisplayHolder.addChangeListener(this);
+ onDisplayInfoChanged(mDisplayHolder.getInfo(), CHANGE_ALL);
+
+ mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayHolder.getInfo(),
+ mContext.getResources());
+ if (!mMode.hasGestures && newMode.hasGestures) {
+ setupOrientationSwipeHandler();
+ } else if (mMode.hasGestures && !newMode.hasGestures){
+ destroyOrientationSwipeHandlerCallback();
+ }
+
+ mMode = newMode;
+ }
+
+ public int getDisplayRotation() {
+ return mDisplayRotation;
+ }
+
+ @Override
+ public void onDisplayInfoChanged(Info info, int flags) {
+ if (info.id != mDisplayId|| flags == CHANGE_FRAME_DELAY) {
+ // ignore displays that aren't running launcher and frame refresh rate changes
+ 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, mDisplayHolder.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, mDisplayHolder.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)
+ .onQuickSwitchToNewTask(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(mDisplayHolder.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);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
similarity index 75%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
rename to quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index dc8f1c5..a8c09dc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -16,8 +16,7 @@
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 static com.android.launcher3.anim.Interpolators.LINEAR;
import android.animation.Animator;
import android.content.Context;
@@ -25,10 +24,8 @@
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;
@@ -37,7 +34,7 @@
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.AnimatorControllerWithResistance;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.TaskViewSimulator;
import com.android.quickstep.util.TransformParams;
@@ -48,7 +45,6 @@
public abstract class SwipeUpAnimationLogic {
protected static final Rect TEMP_RECT = new Rect();
- private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
protected DeviceProfile mDp;
@@ -69,12 +65,8 @@
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;
+ protected AnimatorControllerWithResistance mWindowTransitionController;
public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
GestureState gestureState, TransformParams transformParams) {
@@ -84,8 +76,9 @@
mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
mTransformParams = transformParams;
- mTaskViewSimulator.setLayoutRotation(
- mDeviceState.getCurrentActiveRotation(), mDeviceState.getDisplayRotation());
+ mTaskViewSimulator.getOrientationState().update(
+ mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
+ mDeviceState.getRotationTouchHelper().getDisplayRotation());
}
protected void initTransitionEndpoints(DeviceProfile dp) {
@@ -95,23 +88,15 @@
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;
- }
+ mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
- mTaskViewSimulator.addAppToOverviewAnim(pa, t -> t * mDragLengthFactor);
- mWindowTransitionController = pa.createPlaybackController();
+ 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
@@ -124,13 +109,6 @@
} 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);
@@ -148,12 +126,6 @@
protected abstract class HomeAnimationFactory {
- public FloatingIconView mIconView;
-
- public HomeAnimationFactory(@Nullable FloatingIconView iconView) {
- mIconView = iconView;
- }
-
public @NonNull RectF getWindowTargetRect() {
PagedOrientationHandler orientationHandler = getOrientationHandler();
DeviceProfile dp = mDp;
@@ -174,6 +146,38 @@
public void playAtomicAnimation(float velocity) {
// No-op
}
+
+ public void setAnimation(RectFSpringAnim anim) { }
+
+ public void update(RectF currentRect, float progress, float radius) { }
+
+ public void onCancel() { }
+
+ /**
+ * @return {@code true} if this factory supports animating an Activity to PiP window on
+ * swiping up to home.
+ */
+ public boolean supportSwipePipToHome() {
+ return false;
+ }
+ }
+
+ /**
+ * 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;
}
/**
@@ -184,30 +188,19 @@
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));
+ Matrix homeToWindowPositionMap = new Matrix();
+ final RectF startRect = updateProgressForStartRect(
+ homeToWindowPositionMap, 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);
- }
+ homeAnimationFactory.setAnimation(anim);
SpringAnimationRunner runner = new SpringAnimationRunner(
homeAnimationFactory, cropRectF, homeToWindowPositionMap);
@@ -242,32 +235,27 @@
final RectF mWindowCurrentRect = new RectF();
final Matrix mHomeToWindowPositionMap;
+ final HomeAnimationFactory mAnimationFactory;
- 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) {
+ mAnimationFactory = factory;
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
@@ -282,10 +270,7 @@
.setCornerRadius(cornerRadius);
mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
- if (mFIV != null) {
- mFIV.update(currentRect, 1f, progress,
- mWindowAlphaThreshold, mMatrix.mapRadius(cornerRadius), false);
- }
+ mAnimationFactory.update(currentRect, progress, mMatrix.mapRadius(cornerRadius));
}
@Override
@@ -298,9 +283,7 @@
@Override
public void onCancel() {
- if (mFIV != null) {
- mFIV.fastFinish();
- }
+ mAnimationFactory.onCancel();
}
@Override
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index 05ce2a2..efec037 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -16,15 +16,19 @@
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;
@@ -37,16 +41,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 +64,20 @@
new MainThreadInitializedObject<>(SysUINavigationMode::new);
private static final String TAG = "SysUINavigationMode";
-
private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
private static final String NAV_BAR_INTERACTION_MODE_RES_NAME =
"config_navBarInteractionMode";
+ private static final String TARGET_OVERLAY_PACKAGE = "android";
private final Context mContext;
private Mode mMode;
+ private int mNavBarGesturalHeight;
+ private int mNavBarLargerGesturalHeight;
+
private final List<NavigationModeChangeListener> mChangeListeners = new ArrayList<>();
+ private final List<OneHandedModeChangeListener> mOneHandedOverlayChangeListeners =
+ new ArrayList<>();
public SysUINavigationMode(Context context) {
mContext = context;
@@ -76,8 +87,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 +101,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 +156,12 @@
}
}
+ private void dispatchOneHandedOverlayChange() {
+ for (OneHandedModeChangeListener listener : mOneHandedOverlayChangeListeners) {
+ listener.onOneHandedModeChanged(mNavBarLargerGesturalHeight);
+ }
+ }
+
public Mode addModeChangeListener(NavigationModeChangeListener listener) {
mChangeListeners.add(listener);
return mMode;
@@ -113,41 +171,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..1f332c4 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,72 @@
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 com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
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.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 ISplitScreen mSplitScreen;
+ private IOneHanded mOneHanded;
+ private IShellTransitions mShellTransitions;
+ private IStartingWindow mStartingWindow;
private final DeathRecipient mSystemUiProxyDeathRecipient = () -> {
- MAIN_EXECUTOR.execute(() -> setProxy(null));
+ MAIN_EXECUTOR.execute(() -> clearProxy());
};
// 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
@@ -66,12 +104,23 @@
return null;
}
- public void setProxy(ISystemUiProxy proxy) {
+ public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen,
+ IOneHanded oneHanded, IShellTransitions shellTransitions,
+ IStartingWindow startingWindow) {
unlinkToDeath();
mSystemUiProxy = proxy;
+ mPip = pip;
+ mSplitScreen = splitScreen;
+ mOneHanded = oneHanded;
+ mShellTransitions = shellTransitions;
+ mStartingWindow = startingWindow;
linkToDeath();
}
+ public void clearProxy() {
+ setProxy(null, null, null, null, null, null);
+ }
+
// TODO(141886704): Find a way to remove this
public void setLastSystemUiStateFlags(int stateFlags) {
mLastSystemUiStateFlags = stateFlags;
@@ -114,17 +163,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 +189,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) {
@@ -271,21 +300,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 {
@@ -321,20 +335,6 @@
}
}
- /**
- * 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);
- }
- }
- }
-
@Override
public void onQuickSwitchToNewTask(int rotation) {
if (mSystemUiProxy != null) {
@@ -352,10 +352,241 @@
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);
+ }
+ }
+ }
+
+ 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) {
+ if (mPip != null) {
+ try {
+ mPip.stopSwipePipToHome(componentName, destinationBounds);
+ } 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");
+ }
+ }
+ }
+
+ public void unregisterSplitScreenListener(ISplitScreenListener listener) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.unregisterSplitScreenListener(listener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call unregisterSplitScreenListener");
+ }
+ }
+ }
+
+ 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");
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index cad51f4..b6dad2d 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -19,19 +19,30 @@
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+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;
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 +50,34 @@
// 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) {
+ BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface();
+ if (LIVE_TILE.get() && activityInterface.isInLiveTileMode()
+ && activityInterface.getCreatedActivity() != null) {
+ RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel();
+ recentsView.launchSideTaskInLiveTileModeForRestartedApp(task.taskId);
+ ActivityManagerWrapper.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 +119,26 @@
@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 (LIVE_TILE.get() && activityInterface.isInLiveTileMode()
+ && activityInterface.getCreatedActivity() != null) {
+ RecentsView recentsView =
+ activityInterface.getCreatedActivity().getOverviewPanel();
+ RemoteAnimationTargetCompat[] apps = new RemoteAnimationTargetCompat[1];
+ apps[0] = appearedTaskTarget;
+ recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId, apps);
+ return;
+ }
if (mController != null) {
if (mLastAppearedTaskTarget == null
|| appearedTaskTarget.taskId != mLastAppearedTaskTarget.taskId) {
@@ -116,10 +151,19 @@
}
}
});
+ 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).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 +181,14 @@
return mCallbacks;
}
+ public void setLiveTileCleanUpHandler(Runnable cleanUpHandler) {
+ mLiveTileCleanUpHandler = cleanUpHandler;
+ }
+
+ public void enableLiveTileRestartListener() {
+ ActivityManagerWrapper.getInstance().registerTaskStackListener(mLiveTileRestartListener);
+ }
+
/**
* Finishes the running recents animation.
*/
@@ -146,7 +198,7 @@
Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
? mController::finishAnimationToHome
: mController::finishAnimationToApp);
- cleanUpRecentsAnimation(null /* canceledThumbnail */);
+ cleanUpRecentsAnimation();
}
}
@@ -173,11 +225,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;
}
+ ActivityManagerWrapper.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..ba1c413 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,41 +15,38 @@
*/
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 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.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.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;
/**
@@ -57,7 +54,7 @@
*/
public class TaskIconCache {
- private final Handler mBackgroundHandler;
+ private final Executor mBgExecutor;
private final AccessibilityManager mAccessibilityManager;
private final Context mContext;
@@ -65,9 +62,9 @@
private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
private final IconProvider mIconProvider;
- public TaskIconCache(Context context, Looper backgroundLooper) {
+ public TaskIconCache(Context context, Executor bgExecutor) {
mContext = context;
- mBackgroundHandler = new Handler(backgroundLooper);
+ mBgExecutor = bgExecutor;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
Resources res = context.getResources();
@@ -83,31 +80,27 @@
* @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;
}
@@ -120,9 +113,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 +135,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 +164,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,6 +173,21 @@
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) {
@@ -191,7 +198,7 @@
}
mDefaultIcons.put(userId, info);
}
- return new FastBitmapDrawable(info);
+ return info.newIcon(mContext);
}
}
@@ -208,12 +215,6 @@
}
}
- public static abstract class IconLoadRequest extends HandlerRunnable {
- IconLoadRequest(Handler handler) {
- super(handler, null);
- }
- }
-
private static class TaskCacheEntry {
public Drawable icon;
public String contentDescription = "";
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
similarity index 68%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
rename to quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 1eaacd3..1ad5f2c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -18,7 +18,8 @@
import static android.view.Surface.ROTATION_0;
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.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;
@@ -30,21 +31,26 @@
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.util.MainThreadInitializedObject;
+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.plugins.OverscrollPlugin;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -56,11 +62,18 @@
*/
public class TaskOverlayFactory implements ResourceBasedOverride {
- public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView) {
+ 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);
}
@@ -90,20 +103,34 @@
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 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,
@@ -119,12 +146,11 @@
*/
public static class TaskOverlay<T extends OverviewActionsView> {
- private final Context mApplicationContext;
+ protected final Context mApplicationContext;
protected final TaskThumbnailView mThumbnailView;
private T mActionsView;
- private ImageActionsApi mImageApi;
- private boolean mIsAllowedByPolicy;
+ protected ImageActionsApi mImageApi;
protected TaskOverlay(TaskThumbnailView taskThumbnailView) {
mApplicationContext = taskThumbnailView.getContext().getApplicationContext();
@@ -146,33 +172,35 @@
*/
public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
boolean rotated) {
- final boolean isAllowedByPolicy = mThumbnailView.isRealSnapshot();
+ getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
- getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+ if (thumbnail != null) {
+ getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+ boolean isAllowedByPolicy = mThumbnailView.isRealSnapshot();
+ getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
+ }
+ }
- getActionsView().setCallbacks(new OverlayUICallbacks() {
- @Override
- public void onShare() {
- if (isAllowedByPolicy) {
- mImageApi.startShareActivity();
- } else {
- showBlockedByPolicyMessage();
- }
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onScreenshot() {
- saveScreenshot(task);
- }
- });
+ /**
+ * End rendering live tile in Overview.
+ *
+ * @param callback callback to run, after switching to screenshot
+ */
+ public void endLiveTileMode(@NonNull Runnable callback) {
+ if (LIVE_TILE.get()) {
+ RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView();
+ recentsView.switchToScreenshot(
+ () -> recentsView.finishRecentsAnimation(true /* toRecents */, callback));
+ } else {
+ callback.run();
+ }
}
/**
* Called to save screenshot of the task thumbnail.
*/
@SuppressLint("NewApi")
- private void saveScreenshot(Task task) {
+ protected void saveScreenshot(Task task) {
if (mThumbnailView.isRealSnapshot()) {
mImageApi.saveScreenshot(mThumbnailView.getThumbnail(),
getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
@@ -201,6 +229,12 @@
}
/**
+ * 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,
@@ -230,7 +264,7 @@
return mThumbnailView.getScaledInsets();
}
- private void showBlockedByPolicyMessage() {
+ protected void showBlockedByPolicyMessage() {
Toast.makeText(
mThumbnailView.getContext(),
R.string.blocked_by_policy,
@@ -252,6 +286,29 @@
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));
+ }
+ }
}
/**
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
similarity index 88%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
rename to quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index ff051b6..c06e9a9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -18,11 +18,9 @@
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;
@@ -36,13 +34,13 @@
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.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.Executors;
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;
@@ -57,13 +55,11 @@
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());
@@ -98,6 +94,23 @@
}
}
+ 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;
@@ -216,6 +229,16 @@
}
@Override
+ public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
+ SystemShortcut shortcut = super.getShortcut(activity, taskView);
+ if (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(
@@ -229,9 +252,6 @@
@Override
protected boolean onActivityStarted(BaseDraggingActivity activity) {
- SystemUiProxy.INSTANCE.get(activity).onSplitScreenInvoked();
- activity.getUserEventDispatcher().logActionOnControl(TAP,
- LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
return true;
}
};
@@ -287,15 +307,9 @@
@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());
+ 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);
@@ -310,16 +324,11 @@
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 SCREENSHOT = (activity, tv) -> tv.getThumbnail().getTaskOverlay()
+ .getScreenshotShortcut(activity, tv.getItemInfo());
TaskShortcutFactory MODAL = (activity, tv) -> {
- if (ENABLE_OVERVIEW_ACTIONS.get() && ENABLE_OVERVIEW_SELECTIONS.get()) {
+ 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..558230b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -0,0 +1,450 @@
+/*
+ * 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.LauncherState.NORMAL;
+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.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+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.View;
+
+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.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;
+
+/**
+ * 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, DepthController depthController,
+ PendingAnimation out) {
+ boolean isRunningTask = v.isRunningTask();
+ TransformParams params = null;
+ TaskViewSimulator tsv = null;
+ if (LIVE_TILE.get() && isRunningTask) {
+ params = v.getRecentsView().getLiveTileParams();
+ tsv = v.getRecentsView().getLiveTileTaskViewSimulator();
+ }
+ createRecentsWindowAnimator(v, skipViewChanges, appTargets, wallpaperTargets,
+ 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, DepthController depthController,
+ PendingAnimation out, @Nullable TransformParams params,
+ @Nullable TaskViewSimulator tsv) {
+ boolean isQuickSwitch = v.isEndQuickswitchCuj();
+ v.setEndQuickswitchCuj(false);
+
+ boolean inLiveTileMode = LIVE_TILE.get() && v.getRecentsView().getRunningTaskIndex() != -1;
+ final RemoteAnimationTargets targets =
+ new RemoteAnimationTargets(appTargets, wallpaperTargets,
+ inLiveTileMode ? MODE_CLOSING : MODE_OPENING);
+
+ 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 parallaxCenterAndAdjacentTask =
+ taskIndex != recentsView.getCurrentPage() && !(dp.isTablet
+ && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
+ 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.getDefaultDisplay(context).getInfo().rotation;
+ tsv.getOrientationState().update(displayRotation, displayRotation);
+
+ tsv.setPreview(targets.apps[targets.apps.length - 1]);
+ tsv.fullScreenProgress.value = 0;
+ tsv.recentsViewScale.value = 1;
+ 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);
+ 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));
+ 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 AnimatorSet anim,
+ @NonNull TaskView v, @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, 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,
+ 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.getDefaultDisplay(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, 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,
+ 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.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) {
+ stateManager.goToState(NORMAL, false);
+ }
+ };
+ }
+ pa.add(launcherAnim);
+ if (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/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
similarity index 73%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
rename to quickstep/src/com/android/quickstep/TouchInteractionService.java
index 6e0b517..e9d2eb4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -20,11 +20,16 @@
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.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
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.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+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_SYSUI_PROXY;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
@@ -38,17 +43,23 @@
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;
@@ -58,16 +69,15 @@
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.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.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;
@@ -75,6 +85,7 @@
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;
@@ -91,40 +102,28 @@
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.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;
-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> {
+ ProtoTraceable<LauncherTraceProto.Builder> {
private static final String TAG = "TouchInteractionService";
@@ -139,6 +138,9 @@
*/
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;
@@ -149,25 +151,50 @@
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));
MAIN_EXECUTOR.execute(() -> {
- SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy);
+ SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
+ splitscreen, onehanded, shellTransitions, startingWindow);
TouchInteractionService.this.initInputMonitor();
preloadOverview(true /* fromInit */);
+ mDeviceState.runOnUserUnlocked(() -> {
+ final BaseActivityInterface ai =
+ mOverviewComponentObserver.getActivityInterface();
+ if (ai == null) return;
+ ai.onOverviewServiceBound();
+ });
});
sIsInitialized = true;
}
@BinderThread
- @Override
public void onOverviewToggle() {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
- mOverviewCommandHelper.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) {
- mOverviewCommandHelper.onOverviewShown(triggeredFromAltTab);
+ if (triggeredFromAltTab) {
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW_NEXT_FOCUS);
+ } else {
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_SHOW);
+ }
}
@BinderThread
@@ -175,14 +202,14 @@
public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
if (triggeredFromAltTab && !triggeredFromHomeKey) {
// onOverviewShownFromAltTab hides the overview and ends at the target app
- mOverviewCommandHelper.onOverviewHidden();
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HIDE);
}
}
@BinderThread
@Override
public void onTip(int actionType, int viewType) {
- mOverviewCommandHelper.onTip(actionType, viewType);
+ // Please delete this method from the interface
}
@BinderThread
@@ -206,25 +233,15 @@
@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);
- }
+ // 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();
+ TouchInteractionService.this.onSystemUiFlagsChanged(lastFlags);
});
}
@@ -237,27 +254,11 @@
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;
+ private RotationTouchHelper mRotationTouchHelper;
public static boolean isConnected() {
return sConnected;
@@ -267,9 +268,9 @@
return sIsInitialized;
}
- private final BaseSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
+ private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
this::createLauncherSwipeHandler;
- private final BaseSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
+ private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
this::createFallbackSwipeHandler;
private ActivityManagerWrapper mAM;
@@ -288,6 +289,8 @@
private InputMonitorCompat mInputMonitorCompat;
private InputEventReceiver mInputEventReceiver;
+ private DisplayManager mDisplayManager;
+
@Override
public void onCreate() {
super.onCreate();
@@ -296,9 +299,12 @@
mMainChoreographer = Choreographer.getInstance();
mAM = ActivityManagerWrapper.getInstance();
mDeviceState = new RecentsAnimationDeviceState(this);
+ mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
+ mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged);
mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
ProtoTracer.INSTANCE.get(this).add(this);
+ mDisplayManager = getSystemService(DisplayManager.class);
sConnected = true;
}
@@ -316,17 +322,21 @@
private void initInputMonitor() {
disposeEventHandlers();
- if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
+
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.TIS_NO_EVENTS, "initInputMonitor: isButtonMode? "
+ + mDeviceState.isButtonNavMode());
+ }
+
+ if (mDeviceState.isButtonNavMode()) {
return;
}
- Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
- mDeviceState.getDisplayId());
- mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
+ mInputMonitorCompat = new InputMonitorCompat("swipe-up", mDeviceState.getDisplayId());
mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
mMainChoreographer, this::onInputEvent);
- mDeviceState.updateGestureTouchRegions();
+ mRotationTouchHelper.updateGestureTouchRegions();
}
/**
@@ -337,16 +347,23 @@
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();
+ mTaskAnimationManager = new TaskAnimationManager(this);
mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
- mOverviewCommandHelper = new OverviewCommandHelper(this, mDeviceState,
- mOverviewComponentObserver);
+ mOverviewCommandHelper = new OverviewCommandHelper(this,
+ mOverviewComponentObserver, mTaskAnimationManager);
mResetGestureInputConsumer = new ResetGestureInputConsumer(mTaskAnimationManager);
mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
mInputConsumer.registerInputConsumer();
- onSystemUiFlagsChanged();
+ onSystemUiFlagsChanged(mDeviceState.getSystemUiStateFlags());
onAssistantVisibilityChanged();
// Temporarily disable model preload
@@ -390,7 +407,7 @@
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_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
am.registerSystemAction(allAppsAction, SYSTEM_ACTION_ID_ALL_APPS);
} else {
am.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
@@ -398,17 +415,25 @@
}
@UiThread
- private void onSystemUiFlagsChanged() {
+ private void onSystemUiFlagsChanged(int lastSysUIFlags) {
if (mDeviceState.isUserUnlocked()) {
- SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(
- mDeviceState.getSystemUiStateFlags());
+ int systemUiStateFlags = mDeviceState.getSystemUiStateFlags();
+ SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags);
mOverviewComponentObserver.onSystemUiStateChanged();
+ mOverviewComponentObserver.getActivityInterface().onSystemUiFlagsChanged(
+ systemUiStateFlags);
- // 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();
+ 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();
+ }
}
}
}
@@ -431,8 +456,8 @@
}
disposeEventHandlers();
mDeviceState.destroy();
- SystemUiProxy.INSTANCE.get(this).setProxy(null);
- ProtoTracer.INSTANCE.get(TouchInteractionService.this).stop();
+ SystemUiProxy.INSTANCE.get(this).clearProxy();
+ ProtoTracer.INSTANCE.get(this).stop();
ProtoTracer.INSTANCE.get(this).remove(this);
getSystemService(AccessibilityManager.class)
@@ -454,6 +479,15 @@
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);
@@ -470,9 +504,9 @@
if (TestProtocol.sDebugTracing) {
Log.d(TestProtocol.NO_SWIPE_TO_HOME, "TouchInteractionService.onInputEvent:DOWN");
}
- mDeviceState.setOrientationTransformIfNeeded(event);
+ mRotationTouchHelper.setOrientationTransformIfNeeded(event);
- if (mDeviceState.isInSwipeUpTouchRegion(event)) {
+ if (mRotationTouchHelper.isInSwipeUpTouchRegion(event)) {
if (TestProtocol.sDebugTracing) {
Log.d(TestProtocol.NO_SWIPE_TO_HOME,
"TouchInteractionService.onInputEvent:isInSwipeUpTouchRegion");
@@ -481,6 +515,7 @@
// 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);
@@ -498,10 +533,21 @@
this,
mGestureState,
InputConsumer.NO_OP, mInputMonitorCompat,
- mOverviewComponentObserver.assistantGestureIsConstrained());
+ mDeviceState,
+ event);
+ } else if (mDeviceState.canTriggerOneHandedAction(event)
+ && !mDeviceState.isOneHandedModeActive()) {
+ // 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)
+ && !mDeviceState.isOneHandedModeActive()) {
+ // Consume gesture event for triggering one handed feature.
+ mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
+ InputConsumer.NO_OP, mInputMonitorCompat);
} else {
mUncheckedConsumer = InputConsumer.NO_OP;
}
@@ -509,7 +555,7 @@
// Other events
if (mUncheckedConsumer != InputConsumer.NO_OP) {
// Only transform the event if we are handling it in a proper consumer
- mDeviceState.setOrientationTransformIfNeeded(event);
+ mRotationTouchHelper.setOrientationTransformIfNeeded(event);
}
}
@@ -527,18 +573,24 @@
}
}
- boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
+ 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();
}
- private GestureState createGestureState(GestureState previousGestureState) {
+ public GestureState createGestureState(GestureState previousGestureState) {
GestureState gestureState = new GestureState(mOverviewComponentObserver,
ActiveGestureLog.INSTANCE.generateAndSetLogId());
if (mTaskAnimationManager.isRecentsAnimationRunning()) {
@@ -547,7 +599,7 @@
gestureState.updatePreviouslyAppearedTaskIds(
previousGestureState.getPreviouslyAppearedTaskIds());
} else {
- gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
+ gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.0",
() -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
}
return gestureState;
@@ -584,20 +636,15 @@
}
if (mDeviceState.isFullyGesturalNavMode()) {
if (mDeviceState.canTriggerAssistantAction(event, newGestureState.getRunningTask())) {
- base = new AssistantInputConsumer(
- this,
- newGestureState,
- base,
- mInputMonitorCompat,
- mOverviewComponentObserver.assistantGestureIsConstrained());
+ 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()) {
- TaskOverlayFactory factory =
- TaskOverlayFactory.INSTANCE.get(getApplicationContext());
- plugin = factory.getLocalOverscrollPlugin(); // may be null
+ plugin = OverscrollPluginFactory.INSTANCE.get(
+ getApplicationContext()).getLocalOverscrollPlugin();
}
// If not local plugin was forced, use the actual overscroll plugin if available.
@@ -626,6 +673,11 @@
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);
@@ -634,15 +686,16 @@
if (mDeviceState.isScreenPinningActive()) {
base = mResetGestureInputConsumer;
}
+
+ if (mDeviceState.canTriggerOneHandedAction(event)) {
+ base = new OneHandedModeInputConsumer(this, mDeviceState, base,
+ mInputMonitorCompat);
+ }
}
return base;
}
private void handleOrientationSetup(InputConsumer baseInputConsumer) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "handleOrientationSetup.1");
- }
-
baseInputConsumer.notifyOrientationSetup();
}
@@ -660,7 +713,7 @@
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",
+ gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.assistant",
() -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
ComponentName runningComponent =
@@ -669,17 +722,16 @@
runningComponent != null && runningComponent.equals(homeComponent);
}
- if (gestureState.getRunningTask() == null) {
+ if (LIVE_TILE.get() && gestureState.getActivityInterface().isInLiveTileMode()) {
+ return createOverviewInputConsumer(
+ previousGestureState, gestureState, event, forceOverviewInputConsumer);
+ } else 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 {
@@ -687,22 +739,21 @@
}
}
+ public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
+ return !mOverviewComponentObserver.isHomeAndOverviewSame()
+ ? mFallbackSwipeHandlerFactory : mLauncherSwipeHandlerFactory;
+ }
+
private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
MotionEvent event) {
- final BaseSwipeUpHandler.Factory factory;
- if (!mOverviewComponentObserver.isHomeAndOverviewSame()) {
- factory = mFallbackSwipeHandlerFactory;
- } else {
- factory = mLauncherSwipeHandlerFactory;
- }
-
+ 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, disableHorizontalSwipe, factory);
+ mInputMonitorCompat, mInputEventReceiver, disableHorizontalSwipe, factory);
}
private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState) {
@@ -724,8 +775,9 @@
if (activity.getRootView().hasWindowFocus()
|| previousGestureState.isRunningAnimationToLauncher()
- || (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
- && forceOverviewInputConsumer)) {
+ || (ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+ && forceOverviewInputConsumer)
+ || (LIVE_TILE.get()) && gestureState.getActivityInterface().isInLiveTileMode()) {
return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
false /* startingInActivityBounds */);
} else {
@@ -749,6 +801,11 @@
private void reset() {
mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
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);
+ }
}
private void preloadOverview(boolean fromInit) {
@@ -771,13 +828,7 @@
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) {
+ 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.
@@ -801,6 +852,13 @@
}
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;
}
@@ -810,10 +868,10 @@
@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()) {
+ LinkedList<String> args = new LinkedList(Arrays.asList(rawArgs));
+ switch (args.pollFirst()) {
case "cmd":
- if (args.peekArg() == null) {
+ if (args.peekFirst() == null) {
printAvailableCommands(pw);
} else {
onCommand(pw, args);
@@ -833,6 +891,9 @@
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
@@ -844,8 +905,7 @@
pw.println(" mConsumer=" + mConsumer.getName());
ActiveGestureLog.INSTANCE.dump("", pw);
pw.println("ProtoTrace:");
- pw.println(" file="
- + ProtoTracer.INSTANCE.get(TouchInteractionService.this).getTraceFile());
+ pw.println(" file=" + ProtoTracer.INSTANCE.get(this).getTraceFile());
}
}
@@ -854,24 +914,26 @@
pw.println(" clear-touch-log: Clears the touch interaction log");
}
- private void onCommand(PrintWriter pw, ArgList args) {
- switch (args.nextArg()) {
+ private void onCommand(PrintWriter pw, LinkedList<String> args) {
+ switch (args.pollFirst()) {
case "clear-touch-log":
ActiveGestureLog.INSTANCE.clear();
break;
}
}
- private BaseSwipeUpHandler createLauncherSwipeHandler(
- GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
+ private AbsSwipeUpHandler createLauncherSwipeHandler(
+ GestureState gestureState, long touchTimeMs) {
return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
- gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
+ gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
+ mInputConsumer);
}
- private BaseSwipeUpHandler createFallbackSwipeHandler(
- GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
+ private AbsSwipeUpHandler createFallbackSwipeHandler(
+ GestureState gestureState, long touchTimeMs) {
return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
- gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
+ gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
+ mInputConsumer);
}
protected boolean shouldNotifyBackGesture() {
@@ -901,11 +963,16 @@
}
@Override
- public void writeToProto(LauncherTraceProto proto) {
- if (proto.touchInteractionService == null) {
- proto.touchInteractionService = new TouchInteractionServiceProto();
+ public void writeToProto(LauncherTraceProto.Builder proto) {
+ TouchInteractionServiceProto.Builder serviceProto =
+ TouchInteractionServiceProto.newBuilder();
+ serviceProto.setServiceConnected(true);
+
+ if (mOverviewComponentObserver != null) {
+ mOverviewComponentObserver.writeToProto(serviceProto);
}
- proto.touchInteractionService.serviceConnected = true;
- proto.touchInteractionService.serviceConnected = true;
+ 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/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
similarity index 95%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
rename to quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
index be3fdde..a4a7bd3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
@@ -21,7 +21,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.Utilities;
-import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.TouchController;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.SysUINavigationMode;
@@ -43,7 +43,7 @@
SysUINavigationMode.Mode sysUINavigationMode = SysUINavigationMode.getMode(mActivity);
if (sysUINavigationMode == SysUINavigationMode.Mode.NO_BUTTON) {
NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode,
- DefaultDisplay.INSTANCE.get(mActivity).getInfo());
+ DisplayController.getDefaultDisplay(mActivity).getInfo());
mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
true /* disableHorizontalSwipe */, navBarPosition,
null /* onInterceptTouch */, this);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
similarity index 75%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
rename to quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 3f1e7ba..8962ec9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -15,17 +15,19 @@
*/
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.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.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;
@@ -59,15 +61,11 @@
@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);
+ setter.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
mRecentsView.updateEmptyMessage();
setProperties(toState, config, setter);
@@ -75,20 +73,27 @@
private void setProperties(RecentsState state, StateAnimationConfig config,
PropertySetter setter) {
- float buttonAlpha = state.hasButtons() ? 1 : 0;
+ float clearAllButtonAlpha = state.hasClearAllButton() ? 1 : 0;
setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
- buttonAlpha, LINEAR);
+ clearAllButtonAlpha, LINEAR);
+ float overviewButtonAlpha =
+ state.hasOverviewActions() && mRecentsView.shouldShowOverviewActionsForState(state)
+ ? 1 : 0;
setter.setFloat(mActivity.getActionsView().getVisibilityAlpha(),
- MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
+ MultiValueAlpha.VALUE, overviewButtonAlpha, LINEAR);
float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
- 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],
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);
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
similarity index 75%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
rename to quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d20bbe9..854a140 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -15,6 +15,7 @@
*/
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.MODAL_TASK;
@@ -23,13 +24,17 @@
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
+import android.util.Log;
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;
@@ -37,7 +42,7 @@
import java.util.ArrayList;
@TargetApi(Build.VERSION_CODES.R)
-public class FallbackRecentsView extends RecentsView<RecentsActivity>
+public class FallbackRecentsView extends RecentsView<RecentsActivity, RecentsState>
implements StateListener<RecentsState> {
private RunningTaskInfo mHomeTaskInfo;
@@ -52,8 +57,8 @@
}
@Override
- public void init(OverviewActionsView actionsView) {
- super.init(actionsView);
+ public void init(OverviewActionsView actionsView, SplitPlaceholderView splitPlaceholderView) {
+ super.init(actionsView, splitPlaceholderView);
setOverviewStateEnabled(true);
setOverlayEnabled(true);
}
@@ -61,12 +66,7 @@
@Override
public void startHome() {
mActivity.startHome();
- }
-
- @Override
- public boolean shouldUseMultiWindowTaskSizeStrategy() {
- // Just use the activity task size for multi-window as well.
- return false;
+ mActivity.getStateManager().goToState(RecentsState.HOME);
}
/**
@@ -76,18 +76,18 @@
*/
public void onGestureAnimationStartOnHome(RunningTaskInfo homeTaskInfo) {
mHomeTaskInfo = homeTaskInfo;
- onGestureAnimationStart(homeTaskInfo == null ? -1 : homeTaskInfo.taskId);
+ onGestureAnimationStart(homeTaskInfo);
}
/**
- * When the gesture ends and recents view become interactive, we also remove the temporary
+ * 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 onGestureAnimationEnd() {
- super.onGestureAnimationEnd();
- if (mHomeTaskInfo != null) {
+ public void onGestureEndTargetCalculated(GestureState.GestureEndTarget endTarget) {
+ super.onGestureEndTargetCalculated(endTarget);
+ if (mHomeTaskInfo != null && endTarget == RECENTS) {
TaskView tv = getTaskView(mHomeTaskInfo.taskId);
if (tv != null) {
PendingAnimation pa = createTaskDismissAnimation(tv, true, false, 150);
@@ -107,22 +107,27 @@
}
@Override
- protected boolean shouldAddDummyTaskView(int runningTaskId) {
- if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == runningTaskId
+ protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
+ if (mHomeTaskInfo != null && runningTaskInfo != null &&
+ mHomeTaskInfo.taskId == runningTaskInfo.taskId
&& 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.
+ // 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.shouldAddDummyTaskView(runningTaskId);
+ return super.shouldAddStubTaskView(runningTaskInfo);
}
@Override
protected void applyLoadPlan(ArrayList<Task> tasks) {
- // When quick-switching on 3p-launcher, we add a "dummy" tile corresponding to Launcher
+ // 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()) {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED,
+ "FallbackRecentsView.applyLoadPlan: running task is home");
+ }
// Check if the task list has running task
boolean found = false;
for (Task t : tasks) {
@@ -151,6 +156,11 @@
}
@Override
+ protected boolean isHomeTask(TaskView taskView) {
+ return mHomeTaskInfo != null && taskView.hasTaskId(mHomeTaskInfo.taskId);
+ }
+
+ @Override
public void setModalStateEnabled(boolean isModalState) {
super.setModalStateEnabled(isModalState);
if (isModalState) {
@@ -165,6 +175,8 @@
@Override
public void onStateTransitionStart(RecentsState toState) {
setOverviewStateEnabled(true);
+ setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
+ setOverviewFullscreenEnabled(toState.isFullScreen());
setFreezeViewVisibility(true);
}
@@ -179,7 +191,7 @@
super.setOverviewStateEnabled(enabled);
if (enabled) {
RecentsState state = mActivity.getStateManager().getState();
- setDisallowScrollToClearAll(!state.hasButtons());
+ setDisallowScrollToClearAll(!state.hasClearAllButton());
}
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsDragLayer.java
rename to quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
similarity index 71%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
rename to quickstep/src/com/android/quickstep/fallback/RecentsState.java
index 211a30c..bbe279e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -20,6 +20,8 @@
import android.content.Context;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.BaseState;
import com.android.quickstep.RecentsActivity;
@@ -29,14 +31,19 @@
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_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);
- public static final RecentsState DEFAULT = new RecentsState(0, FLAG_HAS_BUTTONS);
+ public static final RecentsState DEFAULT = new RecentsState(0,
+ FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID);
public static final RecentsState MODAL_TASK = new ModalState(1,
- FLAG_DISABLE_RESTORE | FLAG_HAS_BUTTONS | FLAG_MODAL);
+ FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_MODAL
+ | FLAG_SHOW_AS_GRID);
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 final int ordinal;
private final int mFlags;
@@ -82,14 +89,34 @@
return hasFlag(FLAG_FULL_SCREEN);
}
- public boolean hasButtons() {
- return hasFlag(FLAG_HAS_BUTTONS);
+ /**
+ * 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);
}
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 {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsTaskController.java
rename to quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
similarity index 97%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
index 5ad48eb..0c2c92c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -99,7 +99,8 @@
case ACTION_POINTER_DOWN: {
if (mState == STATE_INACTIVE) {
int pointerIndex = ev.getActionIndex();
- if (mDeviceState.isInSwipeUpTouchRegion(ev, pointerIndex)
+ if (mDeviceState.getRotationTouchHelper()
+ .isInSwipeUpTouchRegion(ev, pointerIndex)
&& mDelegate.allowInterceptByParent()) {
setActive(ev);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
similarity index 82%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
index 89e6931..a3cd7df 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
@@ -25,12 +25,6 @@
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;
@@ -47,13 +41,15 @@
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.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
*/
@@ -80,7 +76,6 @@
private float mTimeFraction;
private long mDragTime;
private float mLastProgress;
- private int mDirection;
private BaseActivityInterface mActivityInterface;
private final float mDragDistThreshold;
@@ -89,19 +84,18 @@
private final int mAngleThreshold;
private final float mSquaredSlop;
private final Context mContext;
- private final GestureDetector mGestureDetector;
- private final boolean mIsAssistGestureConstrained;
+ private final Consumer<MotionEvent> mGestureDetector;
public AssistantInputConsumer(
Context context,
GestureState gestureState,
InputConsumer delegate,
InputMonitorCompat inputMonitor,
- boolean isAssistGestureConstrained) {
+ RecentsAnimationDeviceState deviceState,
+ MotionEvent startEvent) {
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);
@@ -112,7 +106,11 @@
mSquaredSlop = slop * slop;
mActivityInterface = gestureState.getActivityInterface();
- mGestureDetector = new GestureDetector(context, new AssistantGestureListener());
+ boolean flingDisabled = deviceState.isAssistantGestureIsConstrained()
+ || deviceState.isInDeferredGestureRegion(startEvent);
+ mGestureDetector = flingDisabled
+ ? ev -> { }
+ : new GestureDetector(context, new AssistantGestureListener())::onTouchEvent;
}
@Override
@@ -197,8 +195,6 @@
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);
@@ -211,7 +207,7 @@
break;
}
- mGestureDetector.onTouchEvent(ev);
+ mGestureDetector.accept(ev);
if (mState != STATE_ACTIVE) {
mDelegate.onMotionEvent(ev);
@@ -223,29 +219,26 @@
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;
+ startAssistantInternal();
} else {
SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(mLastProgress);
}
}
}
- private void startAssistantInternal(int gestureType) {
- UserEventDispatcher.newInstance(mContext)
- .logActionOnContainer(gestureType, mDirection, NAVBAR);
-
+ 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;
}
/**
@@ -253,7 +246,6 @@
*/
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
@@ -264,20 +256,14 @@
private class AssistantGestureListener extends SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- if (!mIsAssistGestureConstrained
- && isValidAssistantGestureAngle(velocityX, -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(FLING);
-
- Bundle args = new Bundle();
- args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE);
- SystemUiProxy.INSTANCE.get(mContext).startAssistant(args);
- mLaunchedAssistant = true;
+ startAssistantInternal();
}
return true;
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
similarity index 88%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 67a15a7..8da2fd3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -4,6 +4,7 @@
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;
@@ -53,4 +54,9 @@
mDelegate.onMotionEvent(event);
event.recycle();
}
+
+ @Override
+ public void writeToProtoInternal(InputConsumerProto.Builder inputConsumerProto) {
+ mDelegate.writeToProtoInternal(inputConsumerProto);
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
similarity index 92%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 3a97216..85ecab1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -19,9 +19,11 @@
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.quickstep.BaseSwipeUpHandlerV2.MIN_PROGRESS_FOR_OVERVIEW;
+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;
@@ -35,13 +37,12 @@
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.launcher3.util.DisplayController;
import com.android.quickstep.AnimatedFloat;
import com.android.quickstep.GestureState;
import com.android.quickstep.InputConsumer;
@@ -60,7 +61,7 @@
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.
+ * A placeholder input consumer used when the device is still locked, e.g. from secure camera.
*/
public class DeviceLockedInputConsumer implements InputConsumer,
RecentsAnimationCallbacks.RecentsAnimationListener, BuilderProxy {
@@ -115,7 +116,7 @@
R.dimen.device_locked_y_offset);
// Do not use DeviceProfile as the user data might be locked
- mDisplaySize = DefaultDisplay.INSTANCE.get(context).getInfo().realSize;
+ mDisplaySize = DisplayController.getDefaultDisplay(context).getInfo().realSize;
// Init states
mStateCallback = new MultiStateCallback(STATE_NAMES);
@@ -147,7 +148,7 @@
if (!mThresholdCrossed) {
// Cancel interaction in case of multi-touch interaction
int ptrIdx = ev.getActionIndex();
- if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
+ if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx)) {
int action = ev.getAction();
ev.setAction(ACTION_CANCEL);
finishTouchTracking(ev);
@@ -180,12 +181,11 @@
*/
private void finishTouchTracking(MotionEvent ev) {
if (mThresholdCrossed && ev.getAction() == ACTION_UP) {
- mVelocityTracker.computeCurrentVelocity(1000,
- ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
+ mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
float velocityY = mVelocityTracker.getYVelocity();
float flingThreshold = mContext.getResources()
- .getDimension(R.dimen.quickstep_fling_threshold_velocity);
+ .getDimension(R.dimen.quickstep_fling_threshold_speed);
boolean dismissTask;
if (Math.abs(velocityY) > flingThreshold) {
@@ -204,9 +204,7 @@
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));
+ mContext.startActivity(createHomeIntent());
mHomeLaunched = true;
}
mStateCallback.setState(STATE_HANDLER_INVALIDATED);
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..cd69cf1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -0,0 +1,184 @@
+/*
+ * 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 String TAG = "OneHandedModeInputConsumer";
+ private static final int ANGLE_MAX = 150;
+ private static final int ANGLE_MIN = 30;
+
+ private final Context mContext;
+ private final DisplayController.DisplayHolder mDisplayHolder;
+ 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;
+ mDisplayHolder = DisplayController.getDefaultDisplay(mContext);
+ mDeviceState = deviceState;
+ mDragDistThreshold = context.getResources().getDimensionPixelSize(
+ R.dimen.gestures_onehanded_drag_threshold);
+ mSquaredSlop = Utilities.squaredTouchSlop(context);
+ mDisplaySize = mDisplayHolder.getInfo().realSize;
+ 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.isOneHandedModeEnabled()) {
+ if (!mDeviceState.isOneHandedModeActive()) {
+ SystemUiProxy.INSTANCE.get(mContext).startOneHandedMode();
+ }
+ } else if (mDeviceState.isSwipeToNotificationEnabled()) {
+ SystemUiProxy.INSTANCE.get(mContext).expandNotificationPanel();
+ }
+ }
+
+ 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/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
similarity index 88%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 26df9c7..9878d45 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -28,19 +28,21 @@
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 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.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;
@@ -50,21 +52,25 @@
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.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.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;
@@ -86,12 +92,14 @@
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 BaseSwipeUpHandler.Factory mHandlerFactory;
+ private final AbsSwipeUpHandler.Factory mHandlerFactory;
private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
private final MotionPauseDetector mMotionPauseDetector;
@@ -99,13 +107,15 @@
private VelocityTracker mVelocityTracker;
- private BaseSwipeUpHandler mInteractionHandler;
+ 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;
@@ -123,6 +133,8 @@
// 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(
@@ -132,8 +144,8 @@
public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
TaskAnimationManager taskAnimationManager, GestureState gestureState,
boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
- InputMonitorCompat inputMonitorCompat, boolean disableHorizontalSwipe,
- Factory handlerFactory) {
+ InputMonitorCompat inputMonitorCompat, InputEventReceiver inputEventReceiver,
+ boolean disableHorizontalSwipe, Factory handlerFactory) {
super(base);
mDeviceState = deviceState;
mNavBarPosition = mDeviceState.getNavBarPosition();
@@ -151,6 +163,7 @@
mOnCompleteCallback = onCompleteCallback;
mVelocityTracker = VelocityTracker.obtain();
mInputMonitorCompat = inputMonitorCompat;
+ mInputEventReceiver = inputEventReceiver;
boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
@@ -163,6 +176,8 @@
mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
+ mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+ mDisplayManager = getSystemService(DisplayManager.class);
}
@Override
@@ -188,6 +203,17 @@
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()) {
@@ -211,6 +237,9 @@
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);
@@ -230,7 +259,7 @@
if (!mPassedPilferInputSlop) {
// Cancel interaction in case of multi-touch interaction
int ptrIdx = ev.getActionIndex();
- if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
+ if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) {
forceCancelGesture(ev);
}
}
@@ -347,10 +376,8 @@
}
TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
mInputMonitorCompat.pilferPointers();
-
- mActivityInterface.closeOverlay();
- ActivityManagerWrapper.getInstance().closeSystemWindows(
- CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ // 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);
@@ -359,10 +386,9 @@
private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
- mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs,
- mTaskAnimationManager.isRecentsAnimationRunning());
+ mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs);
mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
- mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
+ mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener());
Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
mInteractionHandler.initWhenReady(intent);
@@ -390,8 +416,7 @@
if (ev.getActionMasked() == ACTION_CANCEL) {
mInteractionHandler.onGestureCancelled();
} else {
- mVelocityTracker.computeCurrentVelocity(1000,
- ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
+ mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
float velocity = mNavBarPosition.isRightEdge()
@@ -424,7 +449,7 @@
@Override
public void notifyOrientationSetup() {
- mDeviceState.onStartGesture();
+ mRotationTouchHelper.onStartGesture();
}
@Override
@@ -467,4 +492,11 @@
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/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
similarity index 86%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index 9bfe84f..fa9e0ec 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -15,16 +15,16 @@
*/
package com.android.quickstep.inputconsumers;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.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.BaseState;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.TestProtocol;
@@ -32,14 +32,14 @@
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.ActivityManagerWrapper;
import com.android.systemui.shared.system.InputMonitorCompat;
/**
* Input consumer for handling touch on the recents/Launcher activity.
*/
-public class OverviewInputConsumer<T extends StatefulActivity<?>>
+public class OverviewInputConsumer<S extends BaseState<S>, T extends StatefulActivity<S>>
implements InputConsumer {
private final T mActivity;
@@ -80,9 +80,6 @@
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);
@@ -91,8 +88,7 @@
mTargetHandledTouch = true;
if (!mStartingInActivityBounds) {
mActivityInterface.closeOverlay();
- ActivityManagerWrapper.getInstance()
- .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
ActiveGestureLog.INSTANCE.addLog("startQuickstep");
}
if (mInputMonitor != null) {
@@ -104,7 +100,7 @@
@Override
public void onKeyEvent(KeyEvent ev) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (LIVE_TILE.get()) {
mActivity.dispatchKeyEvent(ev);
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
similarity index 79%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 1c77a05..864e08d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -15,9 +15,12 @@
*/
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;
@@ -27,9 +30,6 @@
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;
@@ -79,20 +79,19 @@
@Override
public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
- mContext.startActivity(mGestureState.getHomeIntent());
+ try {
+ mContext.startActivity(mGestureState.getHomeIntent());
+ } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
+ mContext.startActivity(createHomeIntent());
+ }
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)
+ int state = (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);
+ : LAUNCHER_STATE_HOME;
activity.getStatsLogManager().logger()
- .withSrcState(LAUNCHER_STATE_HOME)
- .withDstState(LAUNCHER_STATE_HOME)
+ .withSrcState(LAUNCHER_STATE_BACKGROUND)
+ .withDstState(state)
.withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
.setWorkspace(
LauncherAtom.WorkspaceContainer.newBuilder()
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/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
similarity index 79%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
index b9827ff..a8bf333 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java
@@ -42,18 +42,16 @@
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();
+ 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();
});
}
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/AssistantGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
index 6862f07..5c81e5f 100644
--- a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
@@ -104,7 +104,13 @@
hideFeedback();
hideHandCoachingAnimation();
showRippleEffect(
- () -> mTutorialFragment.changeController(ASSISTANT_COMPLETE));
+ () -> {
+ if (mTutorialFragment.isTutorialComplete()) {
+ mTutorialFragment.changeController(ASSISTANT_COMPLETE);
+ } else {
+ mTutorialFragment.continueTutorial();
+ }
+ });
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..16886ec 100644
--- a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
@@ -24,7 +24,7 @@
/** Shows the Home gesture interactive tutorial. */
public class AssistantGestureTutorialFragment extends TutorialFragment {
@Override
- int getHandAnimationResId() {
+ Integer getHandAnimationResId() {
return R.drawable.assistant_gesture;
}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 921e568..a1b7e01 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -36,9 +36,8 @@
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;
+ return R.string.back_gesture_intro_title;
case BACK_NAVIGATION_COMPLETE:
return R.string.gesture_tutorial_confirm_title;
}
@@ -49,9 +48,8 @@
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;
+ return R.string.back_gesture_intro_subtitle;
case BACK_NAVIGATION_COMPLETE:
return R.string.back_gesture_tutorial_confirm_subtitle;
}
@@ -130,7 +128,13 @@
hideFeedback();
hideHandCoachingAnimation();
showRippleEffect(
- () -> mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE));
+ () -> {
+ if (mTutorialFragment.isTutorialComplete()) {
+ mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE);
+ } else {
+ mTutorialFragment.continueTutorial();
+ }
+ });
break;
case BACK_CANCELLED_FROM_LEFT:
showFeedback(R.string.back_gesture_feedback_cancelled_left_edge);
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index bef50ea..41db684 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -24,7 +24,7 @@
/** Shows the Back gesture interactive tutorial. */
public class BackGestureTutorialFragment extends TutorialFragment {
@Override
- int getHandAnimationResId() {
+ Integer getHandAnimationResId() {
return R.drawable.back_gesture;
}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
index e4b348e..9489bac 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -143,6 +143,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..8b6777b 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -15,8 +15,6 @@
*/
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;
@@ -25,11 +23,14 @@
import android.view.View;
import android.view.Window;
+import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import com.android.launcher3.R;
import com.android.quickstep.interaction.TutorialController.TutorialType;
+import java.util.ArrayDeque;
+import java.util.Deque;
import java.util.List;
/** Shows the gesture interactive sandbox in full screen mode. */
@@ -37,6 +38,9 @@
private static final String LOG_TAG = "GestureSandboxActivity";
+ private static final String KEY_TUTORIAL_STEPS = "tutorial_steps";
+
+ private Deque<TutorialType> mTutorialSteps;
private TutorialFragment mFragment;
@Override
@@ -45,7 +49,9 @@
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);
+ mFragment = TutorialFragment.newInstance(mTutorialSteps.pop());
getSupportFragmentManager().beginTransaction()
.add(R.id.gesture_tutorial_fragment_container, mFragment)
.commit();
@@ -72,17 +78,65 @@
}
}
- 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());
+ 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 mTutorialSteps.isEmpty();
+ }
+
+ /**
+ * 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;
+ mFragment = TutorialFragment.newInstance(mTutorialSteps.pop());
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.gesture_tutorial_fragment_container, mFragment)
+ .runOnCommit(() -> mFragment.onAttachedToWindow())
+ .commit();
+ }
+
+ private String[] getTutorialStepNames() {
+ String[] tutorialStepNames = new String[mTutorialSteps.size()];
+
+ int i = 0;
+ for (TutorialType tutorialStep : mTutorialSteps) {
+ tutorialStepNames[i++] = tutorialStep.name();
}
+
+ return tutorialStepNames;
+ }
+
+ private Deque<TutorialType> getTutorialSteps(Bundle extras) {
+ Deque<TutorialType> defaultSteps = new ArrayDeque<>();
+ defaultSteps.push(TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
+
+ if (extras == null || !extras.containsKey(KEY_TUTORIAL_STEPS)) {
+ return defaultSteps;
+ }
+
+ String[] tutorialStepNames = extras.getStringArray(KEY_TUTORIAL_STEPS);
+
+ if (tutorialStepNames == null) {
+ return defaultSteps;
+ }
+
+ Deque<TutorialType> tutorialSteps = new ArrayDeque<>();
+ for (String tutorialStepName : tutorialStepNames) {
+ tutorialSteps.addLast(TutorialType.valueOf(tutorialStepName));
+ }
+
+ return tutorialSteps;
}
private void hideSystemUI() {
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
index 0edabd4..fbf3a0a 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -38,7 +38,7 @@
Integer getTitleStringId() {
switch (mTutorialType) {
case HOME_NAVIGATION:
- return R.string.home_gesture_tutorial_playground_title;
+ return R.string.home_gesture_intro_title;
case HOME_NAVIGATION_COMPLETE:
return R.string.gesture_tutorial_confirm_title;
}
@@ -48,7 +48,7 @@
@Override
Integer getSubtitleStringId() {
if (mTutorialType == TutorialType.HOME_NAVIGATION) {
- return R.string.home_gesture_tutorial_playground_subtitle;
+ return R.string.home_gesture_intro_subtitle;
}
return null;
}
@@ -94,8 +94,13 @@
case HOME_NAVIGATION:
switch (result) {
case HOME_GESTURE_COMPLETED: {
- animateFakeTaskViewHome(finalVelocity, () ->
- mTutorialFragment.changeController(HOME_NAVIGATION_COMPLETE));
+ animateFakeTaskViewHome(finalVelocity, () -> {
+ if (mTutorialFragment.isTutorialComplete()) {
+ mTutorialFragment.changeController(HOME_NAVIGATION_COMPLETE);
+ } else {
+ mTutorialFragment.continueTutorial();
+ }
+ });
break;
}
case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
index e2a9d12..63595a5 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
@@ -21,7 +21,7 @@
/** Shows the Home gesture interactive tutorial. */
public class HomeGestureTutorialFragment extends TutorialFragment {
@Override
- int getHandAnimationResId() {
+ Integer getHandAnimationResId() {
return R.drawable.home_gesture;
}
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..31f26d1 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
@@ -39,7 +39,7 @@
Integer getTitleStringId() {
switch (mTutorialType) {
case OVERVIEW_NAVIGATION:
- return R.string.overview_gesture_tutorial_playground_title;
+ return R.string.overview_gesture_intro_title;
case OVERVIEW_NAVIGATION_COMPLETE:
return R.string.gesture_tutorial_confirm_title;
}
@@ -49,7 +49,7 @@
@Override
Integer getSubtitleStringId() {
if (mTutorialType == TutorialType.OVERVIEW_NAVIGATION) {
- return R.string.overview_gesture_tutorial_playground_subtitle;
+ return R.string.overview_gesture_intro_subtitle;
}
return null;
}
@@ -104,8 +104,13 @@
showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge);
break;
case OVERVIEW_GESTURE_COMPLETED:
- fadeOutFakeTaskView(true, () ->
- mTutorialFragment.changeController(OVERVIEW_NAVIGATION_COMPLETE));
+ fadeOutFakeTaskView(true, () -> {
+ if (mTutorialFragment.isTutorialComplete()) {
+ mTutorialFragment.changeController(OVERVIEW_NAVIGATION_COMPLETE);
+ } else {
+ mTutorialFragment.continueTutorial();
+ }
+ });
break;
case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
case HOME_OR_OVERVIEW_CANCELLED:
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
index 3357b70..93200bb 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
@@ -21,7 +21,7 @@
/** Shows the Overview gesture interactive tutorial. */
public class OverviewGestureTutorialFragment extends TutorialFragment {
@Override
- int getHandAnimationResId() {
+ Integer getHandAnimationResId() {
return R.drawable.overview_gesture;
}
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/SandboxLauncherRenderer.java b/quickstep/src/com/android/quickstep/interaction/SandboxLauncherRenderer.java
new file mode 100644
index 0000000..80ffe66
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/SandboxLauncherRenderer.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.view.View;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
+
+/** Renders a fake Launcher for use in the Sandbox. */
+class SandboxLauncherRenderer extends LauncherPreviewRenderer {
+ SandboxLauncherRenderer(Context context, InvariantDeviceProfile idp, boolean migrated) {
+ super(context, idp, migrated);
+ }
+
+ @Override
+ public boolean shouldShowRealLauncherPreview() {
+ return false;
+ }
+
+ @Override
+ public boolean shouldShowQsb() {
+ return false;
+ }
+
+ @Override
+ public View.OnLongClickListener getWorkspaceChildOnLongClickListener() {
+ return null;
+ }
+}
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..4c1a595
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
@@ -0,0 +1,102 @@
+/*
+ * 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 android.view.View;
+
+import androidx.annotation.Nullable;
+
+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);
+ }
+
+ @Nullable
+ @Override
+ Integer getTitleStringId() {
+ return R.string.sandbox_mode_title;
+ }
+
+ @Nullable
+ @Override
+ Integer getSubtitleStringId() {
+ return R.string.sandbox_mode_subtitle;
+ }
+
+ @Nullable
+ @Override
+ Integer getActionButtonStringId() {
+ return null;
+ }
+
+ @Override
+ void onActionButtonClicked(View button) {
+ mTutorialFragment.closeTutorial();
+ }
+
+ @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_RIGHT:
+ showFeedback(R.string.back_gesture_feedback_cancelled_right_edge);
+ break;
+ case BACK_CANCELLED_FROM_LEFT:
+ showFeedback(R.string.back_gesture_feedback_cancelled_left_edge);
+ 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, () -> {
+ 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..68c63bf 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;
@@ -60,16 +61,21 @@
@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);
+
+ private final ViewSwipeUpAnimation mTaskViewSwipeUpAnimation;
private float mFakeTaskViewRadius;
private Rect mFakeTaskViewRect = new Rect();
private RunningWindowAnim mRunningWindowAnim;
+ private boolean mShowTasks = false;
+ private boolean mShowPreviousTasks = false;
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();
@@ -82,16 +88,22 @@
.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() {
+
+ 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() {
@@ -110,18 +122,25 @@
AnimatorListenerAdapter resetTaskView = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
+ mFakeIconView.setVisibility(View.INVISIBLE);
mFakeTaskView.setVisibility(View.INVISIBLE);
mFakeTaskView.setAlpha(1);
+ mFakePreviousTaskView.setVisibility(View.INVISIBLE);
+ mFakePreviousTaskView.setAlpha(1);
+ mShowTasks = false;
+ mShowPreviousTasks = false;
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.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
fadeAnim.addListener(resetTaskView);
AnimatorSet animset = fadeAnim.buildAnim();
animset.setStartDelay(100);
@@ -131,6 +150,8 @@
});
} else {
anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+ anim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
+ anim.setViewAlpha(mFakeIconView, 0, ACCEL);
anim.addListener(resetTaskView);
}
if (onEndRunnable != null) {
@@ -145,8 +166,10 @@
hideFeedback();
hideHandCoachingAnimation();
cancelRunningAnimation();
+ mFakePreviousTaskView.setVisibility(View.INVISIBLE);
+ 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)));
@@ -158,11 +181,31 @@
if (displacement == null || 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) {
+ mTaskViewSwipeUpAnimation.updateDisplacement(displacement);
+ }
+ }
+ }
+
+ @Override
+ public void onMotionPaused(boolean unused) {
+ 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 +224,7 @@
@Override
public void updateFinalShift() {
- float progress = mCurrentShift.value / mDragLengthFactor;
- mWindowTransitionController.setPlayFraction(progress);
+ mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
mTaskViewSimulator.apply(mTransformParams);
}
@@ -202,7 +244,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);
@@ -218,6 +260,25 @@
fakeHomeIconLeft + fakeHomeIconSizePx,
fakeHomeIconTop + fakeHomeIconSizePx);
}
+
+ @Override
+ public void update(RectF rect, float progress, float radius) {
+ mFakeIconView.setVisibility(View.VISIBLE);
+ mFakeIconView.update(rect, progress,
+ 1f - SHAPE_PROGRESS_DURATION /* shapeProgressStart */,
+ radius,
+ 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 +299,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..12d2efc 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -25,9 +25,12 @@
import android.widget.TextView;
import androidx.annotation.CallSuper;
+import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
+import com.android.launcher3.views.ClipIconView;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
@@ -46,10 +49,13 @@
final TextView mTitleTextView;
final TextView mSubtitleTextView;
final TextView mFeedbackView;
+ final View mLauncherView;
+ final ClipIconView mFakeIconView;
final View mFakeTaskView;
+ final View mFakePreviousTaskView;
final View mRippleView;
final RippleDrawable mRippleDrawable;
- final TutorialHandAnimation mHandCoachingAnimation;
+ @Nullable final TutorialHandAnimation mHandCoachingAnimation;
final ImageView mHandCoachingView;
final Button mActionTextButton;
final Button mActionButton;
@@ -60,13 +66,17 @@
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);
mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
+ mLauncherView = getMockLauncherView();
+ 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();
@@ -79,6 +89,17 @@
mHideFeedbackRunnable =
() -> mFeedbackView.animate().alpha(0).setDuration(FEEDBACK_ANIMATION_MS)
.withEndAction(this::showHandCoachingAnimation).start();
+
+ if (mLauncherView != null) {
+ rootView.addView(mLauncherView, 0);
+ }
+ if (mContext != null) {
+ rootView.setBackground(mContext.getDrawable(getMockWallpaperResId()));
+ mFakeTaskView.setBackground(mContext.getDrawable(getMockAppTaskThumbnailResId()));
+ mFakePreviousTaskView.setBackground(
+ mContext.getDrawable(getMockPreviousAppTaskThumbnailResId()));
+ mFakeIconView.setBackground(mContext.getDrawable(getMockAppIconResId()));
+ }
}
void setTutorialType(TutorialType tutorialType) {
@@ -105,6 +126,33 @@
return null;
}
+ @DrawableRes
+ protected int getMockAppTaskThumbnailResId() {
+ return R.drawable.default_sandbox_app_task_thumbnail;
+ }
+
+ @DrawableRes
+ protected int getMockPreviousAppTaskThumbnailResId() {
+ return R.drawable.default_sandbox_app_previous_task_thumbnail;
+ }
+
+ @Nullable
+ public View getMockLauncherView() {
+ InvariantDeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext);
+
+ return new SandboxLauncherRenderer(mContext, dp, true).getRenderedView();
+ }
+
+ @DrawableRes
+ public int getMockAppIconResId() {
+ return R.drawable.default_sandbox_app_icon;
+ }
+
+ @DrawableRes
+ public int getMockWallpaperResId() {
+ return R.drawable.default_sandbox_wallpaper;
+ }
+
void showFeedback(int resId) {
hideHandCoachingAnimation();
mFeedbackView.setText(resId);
@@ -140,13 +188,16 @@
void onActionTextButtonClicked(View button) {}
void showHandCoachingAnimation() {
- if (isComplete()) {
+ if (isComplete() || mHandCoachingAnimation == null) {
return;
}
mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
}
void hideHandCoachingAnimation() {
+ if (mHandCoachingAnimation == null) {
+ return;
+ }
mHandCoachingAnimation.stop();
mHandCoachingView.setVisibility(View.INVISIBLE);
}
@@ -162,6 +213,9 @@
} else {
showHandCoachingAnimation();
}
+ if (mLauncherView != null) {
+ mLauncherView.setVisibility(isComplete() ? View.INVISIBLE : View.VISIBLE);
+ }
}
private void updateTitles() {
@@ -216,6 +270,7 @@
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..413387e 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -41,8 +41,8 @@
TutorialType mTutorialType;
@Nullable TutorialController mTutorialController = null;
- View mRootView;
- TutorialHandAnimation mHandCoachingAnimation;
+ RootSandboxLayout mRootView;
+ @Nullable TutorialHandAnimation mHandCoachingAnimation = null;
EdgeBackGestureHandler mEdgeBackGestureHandler;
NavBarGestureHandler mNavBarGestureHandler;
@@ -52,6 +52,7 @@
fragment = new BackGestureTutorialFragment();
tutorialType = TutorialType.RIGHT_EDGE_BACK_NAVIGATION;
}
+
Bundle args = new Bundle();
args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
fragment.setArguments(args);
@@ -74,13 +75,17 @@
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 getHandAnimationResId() {
+ return null;
+ }
abstract TutorialController createController(TutorialType type);
@@ -107,15 +112,19 @@
@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());
+ Integer handAnimationResId = getHandAnimationResId();
+ if (handAnimationResId != null) {
+ mHandCoachingAnimation =
+ new TutorialHandAnimation(getContext(), mRootView, handAnimationResId);
+ }
return mRootView;
}
@@ -128,18 +137,27 @@
@Override
public void onPause() {
super.onPause();
- mHandCoachingAnimation.stop();
+
+ if (mHandCoachingAnimation != null) {
+ mHandCoachingAnimation.stop();
+ }
}
@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() {
@@ -164,14 +182,28 @@
super.onSaveInstanceState(savedInstanceState);
}
- View getRootView() {
+ RootSandboxLayout getRootView() {
return mRootView;
}
- TutorialHandAnimation getHandAnimation() {
+ @Nullable TutorialHandAnimation getHandAnimation() {
return mHandCoachingAnimation;
}
+ void continueTutorial() {
+ if (!(getContext() instanceof GestureSandboxActivity)) {
+ closeTutorial();
+ return;
+ }
+ GestureSandboxActivity gestureSandboxActivity = (GestureSandboxActivity) getContext();
+
+ if (gestureSandboxActivity == null) {
+ closeTutorial();
+ return;
+ }
+ gestureSandboxActivity.continueTutorial();
+ }
+
void closeTutorial() {
FragmentActivity activity = getActivity();
if (activity != null) {
@@ -182,4 +214,13 @@
void startSystemNavigationSetting() {
startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS"));
}
+
+ boolean isTutorialComplete() {
+ if (!(getContext() instanceof GestureSandboxActivity)) {
+ return true;
+ }
+ GestureSandboxActivity gestureSandboxActivity = (GestureSandboxActivity) getContext();
+
+ return gestureSandboxActivity == null || gestureSandboxActivity.isTutorialComplete();
+ }
}
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..b336d2f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -0,0 +1,181 @@
+/*
+ * 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.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
+
+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.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;
+ 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);
+ }
+ 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..66c24c8 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,79 @@
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;
+
+ 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));
}
@@ -175,6 +137,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 +148,11 @@
private Optional<FromState> mFromState = Optional.empty();
private Optional<ToState> mToState = Optional.empty();
private Optional<String> mEditText = Optional.empty();
+ private SliceItem mSliceItem;
+
+ StatsCompatLogger(Context context) {
+ mContext = context;
+ }
@Override
public StatsLogger withItemInfo(ItemInfo itemInfo) {
@@ -221,10 +190,8 @@
@Override
public StatsLogger withContainerInfo(ContainerInfo containerInfo) {
- if (mItemInfo != DEFAULT_ITEM_INFO) {
- throw new IllegalArgumentException(
+ checkState(mItemInfo == DEFAULT_ITEM_INFO,
"ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
- }
this.mContainerInfo = Optional.of(containerInfo);
return this;
}
@@ -248,41 +215,81 @@
}
@Override
+ public StatsLogger withSliceItem(@NonNull SliceItem sliceItem) {
+ this.mSliceItem = checkNotNull(sliceItem, "expected valid sliceItem but received null");
+ checkState(mItemInfo == DEFAULT_ITEM_INFO,
+ "ItemInfo and SliceItem are mutual exclusive; cannot log both.");
+ 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 (mSliceItem != null) {
+ Executors.MODEL_EXECUTOR.execute(
+ () -> {
+ LauncherAtom.ItemInfo.Builder itemInfoBuilder =
+ LauncherAtom.ItemInfo.newBuilder().setSlice(
+ LauncherAtom.Slice.newBuilder().setUri(
+ mSliceItem.getSlice().getUri().toString()));
+ 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 +300,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 +317,10 @@
atomInfo));
}
+ for (StatsLogConsumer consumer : LOGS_CONSUMER) {
+ consumer.consume(event, atomInfo);
+ }
+
SysUiStatsLog.write(
SysUiStatsLog.LAUNCHER_EVENT,
SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
@@ -337,11 +351,19 @@
}
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 +379,8 @@
return info.getWidget().getPackageName();
case TASK:
return info.getTask().getPackageName();
+ case SEARCH_ACTION_ITEM:
+ return info.getSearchActionItem().getPackageName();
default:
return null;
}
@@ -372,6 +396,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 +430,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 +448,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 +468,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 +488,16 @@
return "ALLAPPS";
default:
return "INVALID";
-
}
}
+
+
+ /**
+ * 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/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/AssistContentRequester.java b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
new file mode 100644
index 0000000..3730284
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
@@ -0,0 +1,103 @@
+/*
+ * 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.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;
+
+ public AssistContentRequester(Context context) {
+ mActivityTaskManager = ActivityTaskManager.getService();
+ mPackageName = context.getApplicationContext().getPackageName();
+ mCallbackExecutor = Executors.MAIN_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(int taskId, Callback callback) {
+ try {
+ mActivityTaskManager.requestAssistDataForTask(
+ new AssistDataReceiver(callback, mCallbackExecutor), taskId, mPackageName);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Requesting assist content failed for task: " + taskId, e);
+ }
+ }
+
+ private static final class AssistDataReceiver extends IAssistDataReceiver.Stub {
+
+ private final Executor mExecutor;
+ private final Callback mCallback;
+
+ AssistDataReceiver(Callback callback, Executor callbackExecutor) {
+ mCallback = callback;
+ mExecutor = callbackExecutor;
+ }
+
+ @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;
+ }
+
+ mExecutor.execute(() -> mCallback.onAssistContentAvailable(content));
+ }
+
+ @Override
+ public void onHandleAssistScreenshot(Bitmap screenshot) {}
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java b/quickstep/src/com/android/quickstep/util/AssistantUtilities.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
rename to quickstep/src/com/android/quickstep/util/AssistantUtilities.java
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..2285d74 100644
--- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
+++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
@@ -19,25 +19,34 @@
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.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;
@@ -71,6 +80,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 +131,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
@@ -103,6 +163,30 @@
}
/**
+ * 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..8151d41 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, 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/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/NavigationModeFeatureFlag.java b/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java
new file mode 100644
index 0000000..351adf4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java
@@ -0,0 +1,65 @@
+/*
+ * 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.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+
+import android.content.Context;
+
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SysUINavigationMode;
+
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** A feature flag that listens to navigation mode changes. */
+public class NavigationModeFeatureFlag implements
+ SysUINavigationMode.NavigationModeChangeListener {
+
+ public static final NavigationModeFeatureFlag LIVE_TILE = new NavigationModeFeatureFlag(
+ ENABLE_QUICKSTEP_LIVE_TILE::get, mode -> mode.hasGestures);
+
+ private final Supplier<Boolean> mBasePredicate;
+ private final Predicate<SysUINavigationMode.Mode> mModePredicate;
+ private boolean mSupported;
+ private OverviewComponentObserver mObserver;
+
+ private NavigationModeFeatureFlag(Supplier<Boolean> basePredicate,
+ Predicate<SysUINavigationMode.Mode> modePredicate) {
+ mBasePredicate = basePredicate;
+ mModePredicate = modePredicate;
+ }
+
+ public boolean get() {
+ return mBasePredicate.get() && mSupported && mObserver.isHomeAndOverviewSame();
+ }
+
+ public void initialize(Context context) {
+ onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(context).getMode());
+ SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this);
+
+ // Temporary solution to disable live tile for the fallback launcher
+ RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(context);
+ mObserver = new OverviewComponentObserver(context, rads);
+ }
+
+ @Override
+ public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+ mSupported = mModePredicate.test(newMode);
+ }
+}
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..1128dac
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
@@ -0,0 +1,149 @@
+/*
+ * 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 static com.android.launcher3.anim.Interpolators.DEACCEL;
+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.clampToProgress;
+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 android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.util.Log;
+import android.view.animation.Interpolator;
+
+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;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Runs an animation from overview to home. Currently, this animation is just a wrapper around the
+ * normal state transition, in order to keep RecentsView at the same scale and translationY that
+ * it started out at as it translates offscreen. It also scrolls RecentsView to page 0 and may play
+ * a {@link StaggeredWorkspaceAnim} if we're starting from an upward fling.
+ */
+public class OverviewToHomeAnim {
+
+ private static final String TAG = "OverviewToHomeAnim";
+
+ // 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;
+
+ 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 StaggeredWorkspaceAnim}.
+ */
+ 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 playStaggeredWorkspaceAnim = velocity < 0;
+ if (playStaggeredWorkspaceAnim) {
+ StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
+ mLauncher, velocity, false /* animateOverviewScrim */);
+ staggeredWorkspaceAnim.addAnimatorListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ mIsHomeStaggeredAnimFinished = true;
+ maybeOverviewToHomeAnimComplete();
+ }
+ });
+ anim.play(staggeredWorkspaceAnim.getAnimators());
+ } else {
+ mIsHomeStaggeredAnimFinished = true;
+ }
+
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ int numPagesToScroll = recentsView.getNextPage() - DEFAULT_PAGE;
+ int scrollDuration = Math.min(MAX_PAGE_SCROLL_DURATION,
+ numPagesToScroll * PER_PAGE_SCROLL_DURATION);
+ int duration = Math.max(scrollDuration, startState.getTransitionDuration(mLauncher));
+
+ StateAnimationConfig config = new UseFirstInterpolatorStateAnimConfig();
+ config.duration = duration;
+ boolean isLayoutNaturalToLauncher = recentsView.getPagedOrientationHandler()
+ .isLayoutNaturalToLauncher();
+ config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, isLayoutNaturalToLauncher
+ ? clampToProgress(FAST_OUT_SLOW_IN, 0, 0.75f) : FINAL_FRAME);
+ config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, FINAL_FRAME);
+ config.setInterpolator(ANIM_OVERVIEW_SCALE, FINAL_FRAME);
+ config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, INSTANT);
+ if (!isLayoutNaturalToLauncher) {
+ config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL);
+ }
+ 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();
+ recentsView.snapToPage(DEFAULT_PAGE, duration);
+ }
+
+ private void maybeOverviewToHomeAnimComplete() {
+ if (mIsHomeStaggeredAnimFinished && mIsOverviewHidden) {
+ mOnReachedHome.run();
+ }
+ }
+
+ /**
+ * Wrapper around StateAnimationConfig that doesn't allow interpolators to be set if they are
+ * already set. This ensures they aren't overridden before being used.
+ */
+ private static class UseFirstInterpolatorStateAnimConfig extends StateAnimationConfig {
+ @Override
+ public void setInterpolator(int animId, Interpolator interpolator) {
+ if (mInterpolators[animId] == null || interpolator == null) {
+ super.setInterpolator(animId, interpolator);
+ }
+ }
+ }
+}
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/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
similarity index 85%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
rename to quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
index 5b0d503..ba70bf7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java
@@ -32,16 +32,11 @@
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);
+ public RecentsAtomicAnimationFactory(ACTIVITY_TYPE activity) {
+ super(MY_ANIM_COUNT);
mActivity = activity;
}
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index d822b6c..188efad 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -23,24 +23,18 @@
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 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.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.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.WindowBounds;
import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.systemui.shared.system.ConfigurationCompat;
+import com.android.quickstep.views.TaskView;
import java.lang.annotation.Retention;
import java.util.function.IntConsumer;
@@ -74,14 +68,8 @@
public final 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 +79,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 +113,19 @@
| 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;
- @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 +135,6 @@
public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy,
IntConsumer rotationChangeListener) {
mContext = context;
- mContentResolver = context.getContentResolver();
mSharedPrefs = Utilities.getPrefs(context);
mOrientationListener = new OrientationEventListener(context) {
@Override
@@ -162,20 +153,22 @@
Resources res = context.getResources();
int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
* res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE;
- if (originalSmallestWidth < 600) {
+ if (originalSmallestWidth < 600 && !mContext.getResources().getBoolean(
+ R.bool.allow_rotation)) {
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 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 +182,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 +195,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 +216,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 +257,7 @@
}
});
}
+ return updateHandler();
}
@Override
@@ -271,8 +268,8 @@
}
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() {
@@ -281,10 +278,7 @@
}
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();
@@ -298,9 +292,7 @@
public void initListeners() {
if (isMultipleOrientationSupportedByDevice()) {
mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
- mContentResolver.registerContentObserver(
- Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
- false, mSystemAutoRotateObserver);
+ mSettingsCache.register(ROTATION_SETTING_URI, mRotationChangeListener);
}
initFlags();
}
@@ -311,7 +303,7 @@
public void destroyListeners() {
if (isMultipleOrientationSupportedByDevice()) {
mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
- mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
+ mSettingsCache.unregister(ROTATION_SETTING_URI, mRotationChangeListener);
}
setRotationWatcherEnabled(false);
}
@@ -335,6 +327,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,8 +368,12 @@
*/
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;
+ 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;
+ }
if (dp.isMultiWindowMode) {
WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(mContext);
@@ -512,19 +515,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
+ "]";
}
@@ -540,4 +567,8 @@
? idp.landscapeProfile
: idp.portraitProfile;
}
+
+ private static String nameAndAddress(Object obj) {
+ return obj.getClass().getSimpleName() + "@" + obj.hashCode();
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
rename to quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index 04308c8..5c6da16 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -21,6 +21,7 @@
import android.os.Handler;
import com.android.launcher3.LauncherAnimationRunner;
+import com.android.launcher3.WrappedAnimationRunnerImpl;
import com.android.launcher3.WrappedLauncherAnimationRunner;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -28,24 +29,16 @@
public abstract class RemoteAnimationProvider {
- LauncherAnimationRunner mAnimationRunner;
+ WrappedAnimationRunnerImpl 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 */);
-
+ handler, mAnimationRunner, false /* startAtFrontOfQueue */);
return ActivityOptionsCompat.makeRemoteAnimation(
new RemoteAnimationAdapterCompat(wrapper, duration, 0));
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
index a770e8e..176478f 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
@@ -32,7 +32,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;
@@ -77,7 +77,7 @@
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.getDefaultDisplay(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..d9154ed
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -0,0 +1,151 @@
+/*
+ * 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 android.animation.AnimatorSet;
+import android.app.ActivityOptions;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherAnimationRunner;
+import com.android.launcher3.WrappedAnimationRunnerImpl;
+import com.android.launcher3.WrappedLauncherAnimationRunner;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.quickstep.SystemUiProxy;
+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;
+
+/**
+ * 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;
+
+ public SplitSelectStateController(SystemUiProxy systemUiProxy) {
+ mSystemUiProxy = systemUiProxy;
+ }
+
+ /**
+ * To be called after first task selected
+ */
+ public void setInitialTaskSelect(TaskView taskView, SplitPositionOption positionOption) {
+ mInitialTaskView = taskView;
+ mInitialPosition = positionOption;
+ }
+
+ /**
+ * To be called after second task selected
+ */
+ public void setSecondTaskId(TaskView taskView) {
+ // Assume initial mInitialTaskId is for top/left part of screen
+ WrappedAnimationRunnerImpl initialSplitRunnerWrapped = new SplitLaunchAnimationRunner(
+ mInitialTaskView, 0);
+ WrappedAnimationRunnerImpl secondarySplitRunnerWrapped = new SplitLaunchAnimationRunner(
+ taskView, 1);
+ RemoteAnimationRunnerCompat initialSplitRunner = new WrappedLauncherAnimationRunner(
+ new Handler(Looper.getMainLooper()), initialSplitRunnerWrapped,
+ true /* startAtFrontOfQueue */);
+ RemoteAnimationRunnerCompat secondarySplitRunner = new WrappedLauncherAnimationRunner(
+ 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();
+ }
+
+ @Nullable
+ public SplitPositionOption getActiveSplitPositionOption() {
+ return mInitialPosition;
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * Remote animation runner for animation to launch an app.
+ */
+ private class SplitLaunchAnimationRunner implements WrappedAnimationRunnerImpl {
+
+ 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.composeRecentsSplitLaunchAnimator(anim, mV,
+ appTargets, wallpaperTargets, 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;
+ }
+
+ public boolean isSplitSelectActive() {
+ return mInitialTaskView != null;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
similarity index 74%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
rename to quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 3cafd42..c12bd9b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -18,10 +18,8 @@
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;
@@ -36,15 +34,14 @@
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.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;
@@ -59,7 +56,10 @@
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;
@@ -81,84 +81,107 @@
DeviceProfile grid = launcher.getDeviceProfile();
Workspace workspace = launcher.getWorkspace();
- CellLayout cellLayout = (CellLayout) workspace.getChildAt(workspace.getCurrentPage());
- ShortcutAndWidgetContainer currentPage = cellLayout.getShortcutsAndWidgets();
- ViewGroup hotseat = launcher.getHotseat();
+ 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.getVisiblePages()
+ .forEach(page -> addAnimationForPage((CellLayout) page, totalRows));
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);
+ ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets();
if (grid.isVerticalBarLayout()) {
- for (int i = hotseatChild.getChildCount() - 1; i >= 0; i--) {
- View child = hotseatChild.getChildAt(i);
+ 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 {
- for (int i = hotseatChild.getChildCount() - 1; i >= 0; i--) {
- View child = hotseatChild.getChildAt(i);
- addStaggeredAnimationForView(child, grid.inv.numRows + 1, totalRows);
+ final int hotseatRow, qsbRow, taskbarRow;
+ if (grid.isTaskbarPresent) {
+ qsbRow = grid.inv.numRows + 1;
+ hotseatRow = grid.inv.numRows + 2;
+ } else {
+ hotseatRow = grid.inv.numRows + 1;
+ qsbRow = grid.inv.numRows + 2;
+ }
+ // Taskbar and hotseat overlap.
+ taskbarRow = hotseatRow;
+
+ for (int i = hotseatIcons.getChildCount() - 1; i >= 0; i--) {
+ View child = hotseatIcons.getChildAt(i);
+ addStaggeredAnimationForView(child, hotseatRow, totalRows);
}
- if (launcher.getAppsView().getSearchUiManager()
- .isQsbVisible(NORMAL.getVisibleElements(launcher))) {
- addStaggeredAnimationForView(launcher.getAppsView().getSearchView(),
- grid.inv.numRows + 2, totalRows);
- }
+ addStaggeredAnimationForView(hotseat.getQsb(), qsbRow, totalRows);
+ addStaggeredAnimationForView(hotseat.getTaskbarView(), taskbarRow, totalRows);
}
if (animateOverviewScrim) {
- PendingAnimation pendingAnimation = new PendingAnimation(ALPHA_DURATION_MS);
- addScrimAnimationForState(launcher, NORMAL, pendingAnimation);
+ PendingAnimation pendingAnimation = new PendingAnimation(DURATION_MS);
+ launcher.getWorkspace().getStateTransitionAnimation()
+ .setScrim(pendingAnimation, NORMAL, new StateAnimationConfig());
mAnimators.play(pendingAnimation.buildAnim());
}
- addDepthAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+ addDepthAnimationForState(launcher, NORMAL, DURATION_MS);
- mAnimators.play(launcher.getDragLayer().getScrim().createSysuiMultiplierAnim(0f, 1f)
- .setDuration(ALPHA_DURATION_MS));
+ mAnimators.play(launcher.getDragLayer().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f)
+ .setDuration(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);
}
});
}
+ 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 = ANIM_ALL_COMPONENTS | SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER;
+ config.animFlags = SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER;
config.duration = 0;
// setRecentsAttachedToAppWindow() will animate recents out.
launcher.getStateManager().createAtomicAnimation(BACKGROUND_APP, NORMAL, config).start();
@@ -167,7 +190,8 @@
launcher.<RecentsView>getOverviewPanel().getScroller().forceFinished(true);
if (animateOverviewScrim) {
- addScrimAnimationForState(launcher, BACKGROUND_APP, NO_ANIM_PROPERTY_SETTER);
+ launcher.getWorkspace().getStateTransitionAnimation()
+ .setScrim(NO_ANIM_PROPERTY_SETTER, BACKGROUND_APP, config);
}
}
@@ -205,13 +229,15 @@
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(0)
- .setStartVelocity(mVelocity)
+ .setEndValue(endTransY)
+ .setStartVelocity(springVelocity)
.build(v, VIEW_TRANSLATE_Y);
springTransY.setStartDelay(startDelay);
springTransY.addListener(new AnimatorListenerAdapter() {
@@ -236,16 +262,6 @@
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;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java b/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/SurfaceTransactionApplier.java
rename to quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java
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..a1240e0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -0,0 +1,272 @@
+/*
+ * 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.animation.ValueAnimator;
+import android.content.ComponentName;
+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.View;
+import android.window.PictureInPictureSurfaceTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+
+/**
+ * An {@link Animator} that animates an Activity to PiP (picture-in-picture) window when
+ * swiping up (in gesture navigation mode). Note that this class is derived from
+ * {@link com.android.wm.shell.pip.PipAnimationController.PipTransitionAnimator}.
+ *
+ * TODO: consider sharing this class including the animator and leash operations between
+ * Launcher and SysUI. Also, there should be one source of truth for the corner radius of the
+ * PiP window, which would ideally be on SysUI side as well.
+ */
+public class SwipePipToHomeAnimator extends ValueAnimator {
+ private static final String TAG = SwipePipToHomeAnimator.class.getSimpleName();
+
+ public static final float FRACTION_START = 0f;
+ public static final float FRACTION_END = 1f;
+
+ private final int mTaskId;
+ private final ComponentName mComponentName;
+ private final SurfaceControl mLeash;
+ private final Rect mAppBounds = new Rect();
+ private final Rect mStartBounds = new Rect();
+ private final Rect mDestinationBounds = new Rect();
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+
+ /** for calculating the transform in {@link #onAnimationUpdate(ValueAnimator)} */
+ private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect());
+ private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
+ private final Rect mSourceHintRectInsets;
+ private final Rect mSourceInsets = new Rect();
+
+ /** for rotation via {@link #setFromRotation(TaskViewSimulator, int)} */
+ private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0;
+ private final Rect mDestinationBoundsTransformed = new Rect();
+ private final Rect mDestinationBoundsAnimation = 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;
+
+ /**
+ * @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 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
+ */
+ public SwipePipToHomeAnimator(int taskId,
+ @NonNull ComponentName componentName,
+ @NonNull SurfaceControl leash,
+ @Nullable Rect sourceRectHint,
+ @NonNull Rect appBounds,
+ @NonNull Rect startBounds,
+ @NonNull Rect destinationBounds,
+ @NonNull View view) {
+ mTaskId = taskId;
+ mComponentName = componentName;
+ mLeash = leash;
+ mAppBounds.set(appBounds);
+ mStartBounds.set(startBounds);
+ mDestinationBounds.set(destinationBounds);
+ mDestinationBoundsTransformed.set(mDestinationBounds);
+ mDestinationBoundsAnimation.set(mDestinationBounds);
+ mSurfaceTransactionHelper = new PipSurfaceTransactionHelper();
+
+ if (sourceRectHint == null) {
+ mSourceHintRectInsets = null;
+ } else {
+ mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left,
+ sourceRectHint.top - appBounds.top,
+ appBounds.right - sourceRectHint.right,
+ appBounds.bottom - sourceRectHint.bottom);
+ }
+
+ addListener(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;
+ }
+ });
+ addUpdateListener(this::onAnimationUpdate);
+ }
+
+ /** sets the from rotation if it's different from the target rotation. */
+ public void setFromRotation(TaskViewSimulator taskViewSimulator,
+ @RecentsOrientedState.SurfaceRotation int fromRotation) {
+ if (fromRotation != Surface.ROTATION_90 && fromRotation != Surface.ROTATION_270) {
+ Log.wtf(TAG, "Not a supported rotation, rotation=" + fromRotation);
+ return;
+ }
+ mFromRotation = fromRotation;
+ 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);
+
+ // set the animation destination bounds for RectEvaluator calculation.
+ // bounds and insets are calculated as if the transition is from mAppBounds to
+ // mDestinationBoundsAnimation, separated from rotate / scale / position.
+ mDestinationBoundsAnimation.set(mAppBounds.left, mAppBounds.top,
+ mAppBounds.left + mDestinationBounds.width(),
+ mAppBounds.top + mDestinationBounds.height());
+ }
+
+ private void onAnimationUpdate(ValueAnimator animator) {
+ if (mHasAnimationEnded) return;
+ final SurfaceControl.Transaction tx =
+ PipSurfaceTransactionHelper.newSurfaceControlTransaction();
+ onAnimationUpdate(tx, animator.getAnimatedFraction());
+ tx.apply();
+ }
+
+ private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx,
+ float fraction) {
+ final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds,
+ mDestinationBoundsAnimation);
+ final PictureInPictureSurfaceTransaction op;
+ if (mSourceHintRectInsets == null) {
+ // no source rect hint been set, directly scale the window down
+ op = onAnimationScale(fraction, tx, bounds);
+ } else {
+ // scale and crop according to the source rect hint
+ op = onAnimationScaleAndCrop(fraction, tx, bounds);
+ }
+ return op;
+ }
+
+ /** scale the window directly with no source rect hint being set */
+ private PictureInPictureSurfaceTransaction onAnimationScale(
+ float fraction, SurfaceControl.Transaction tx, Rect bounds) {
+ if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+ final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
+ 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 fraction, SurfaceControl.Transaction tx,
+ Rect bounds) {
+ final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
+ mSourceHintRectInsets);
+ if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+ final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
+ 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;
+ }
+
+ /** @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, FRACTION_END);
+ }
+
+ private RotatedPosition getRotatedPosition(float fraction) {
+ final float degree, positionX, positionY;
+ if (mFromRotation == Surface.ROTATION_90) {
+ degree = -90 * fraction;
+ positionX = fraction * (mDestinationBoundsTransformed.left - mAppBounds.left)
+ + mAppBounds.left;
+ positionY = fraction * (mDestinationBoundsTransformed.bottom - mAppBounds.top)
+ + mAppBounds.top;
+ } else {
+ degree = 90 * fraction;
+ positionX = fraction * (mDestinationBoundsTransformed.right - mAppBounds.left)
+ + mAppBounds.left;
+ positionY = fraction * (mDestinationBoundsTransformed.top - mAppBounds.top)
+ + mAppBounds.top;
+ }
+ return new RotatedPosition(degree, positionX, positionY);
+ }
+
+ 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/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java b/quickstep/src/com/android/quickstep/util/TaskCornerRadius.java
similarity index 100%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskCornerRadius.java
rename to quickstep/src/com/android/quickstep/util/TaskCornerRadius.java
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
similarity index 70%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
rename to quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index c9ed498..e63f8bb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -17,29 +17,28 @@
import static com.android.launcher3.states.RotationHelper.deltaRotation;
import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
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.Configuration;
+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 android.util.IntProperty;
+
+import androidx.annotation.NonNull;
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;
@@ -50,32 +49,24 @@
*/
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;
+ @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
@@ -86,17 +77,19 @@
// TaskView properties
private final FullscreenDrawParams mCurrentFullscreenParams;
- private float mCurveScale = 1;
+ 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();
- private final ScrollState mScrollState = new ScrollState();
- private final int mPageSpacing;
+ 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 boolean mScrollValid = false;
+ private int mOrientationStateId;
public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
mContext = context;
@@ -104,9 +97,10 @@
mOrientationState = new RecentsOrientedState(context, sizeStrategy, i -> { });
mOrientationState.setGestureActive(true);
-
mCurrentFullscreenParams = new FullscreenDrawParams(context);
- mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+ mOrientationStateId = mOrientationState.getStateId();
+ Resources resources = context.getResources();
+ mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
}
/**
@@ -114,23 +108,14 @@
*/
public void setDp(DeviceProfile dp) {
mDp = dp;
- mOrientationState.setMultiWindowMode(mDp.isMultiWindowMode);
mLayoutValid = false;
}
/**
- * @see com.android.quickstep.views.RecentsView#setLayoutRotation(int, int)
+ * Sets the orientation state used for this animation
*/
- 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);
+ public void setOrientationState(@NonNull RecentsOrientedState orientationState) {
+ mOrientationState = orientationState;
mLayoutValid = false;
}
@@ -170,15 +155,16 @@
/**
* Updates the scroll for RecentsView
*/
- public void setScroll(int scroll) {
- if (mScrollState.scroll != scroll) {
- mScrollState.scroll = scroll;
- mScrollValid = false;
- }
+ 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
+ * 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);
@@ -186,6 +172,14 @@
}
/**
+ * 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() {
@@ -197,6 +191,18 @@
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;
}
@@ -228,37 +234,26 @@
if (mDp == null || mThumbnailPosition.isEmpty()) {
return;
}
- if (!mLayoutValid) {
+ 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());
+ mDp, mOrientationState.getRecentsActivityRotation(), isRtlEnabled);
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);
+ float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
mCurrentFullscreenParams.setProgress(
- progress, recentsViewScale.value, mTaskRect.width(), mDp, mPositionHelper);
+ fullScreenProgress, recentsViewScale.value, mTaskRect.width(), mDp,
+ mPositionHelper);
// Apply thumbnail matrix
RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
@@ -270,14 +265,21 @@
mMatrix.postTranslate(insets.left, insets.top);
mMatrix.postScale(scale, scale);
- // Apply TaskView matrix: translate, scale, scroll
+ // Apply TaskView matrix: translate, scroll
mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
- mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
+ 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, mScrollState.scroll);
+ mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value);
- // Apply recensView matrix
+ // 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
@@ -295,6 +297,12 @@
builder.withMatrix(mMatrix)
.withWindowCrop(mTmpCropRect)
.withCornerRadius(getCurrentCornerRadius());
+
+ if (LIVE_TILE.get() && params.getRecentsSurface() != null) {
+ // When relativeLayer = 0, it reverts the surfaces back to the original order.
+ builder.withRelativeLayerTo(params.getRecentsSurface(),
+ mDrawsBelowRecents ? Integer.MIN_VALUE : 0);
+ }
}
/**
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
similarity index 90%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
rename to quickstep/src/com/android/quickstep/util/TransformParams.java
index 0135f74..756331d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -16,6 +16,7 @@
package com.android.quickstep.util;
import android.util.FloatProperty;
+import android.view.SurfaceControl;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
@@ -58,6 +59,7 @@
private float mCornerRadius;
private RemoteAnimationTargets mTargetSet;
private SurfaceTransactionApplier mSyncTransactionApplier;
+ private SurfaceControl mRecentsSurface;
private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
@@ -138,6 +140,8 @@
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);
@@ -165,6 +169,20 @@
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() {
@@ -179,6 +197,10 @@
return mCornerRadius;
}
+ public SurfaceControl getRecentsSurface() {
+ return mRecentsSurface;
+ }
+
public RemoteAnimationTargets getTargetSet() {
return mTargetSet;
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java b/quickstep/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
similarity index 95%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
rename to quickstep/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
index 29b9558..7bbde30 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
+++ b/quickstep/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
@@ -21,13 +21,14 @@
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 android.view.ViewConfiguration;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
/**
@@ -50,7 +51,8 @@
NavBarPosition navBarPosition, Runnable onInterceptTouch,
OnSwipeUpListener onSwipeUp) {
mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
- mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+ mMinFlingVelocity = context.getResources().getDimension(
+ R.dimen.quickstep_fling_threshold_speed);
mNavBarPosition = navBarPosition;
mDisableHorizontalSwipe = disableHorizontalSwipe;
mOnInterceptTouch = onInterceptTouch;
@@ -130,7 +132,7 @@
}
private void onGestureEnd(MotionEvent ev) {
- mVelocityTracker.computeCurrentVelocity(1000);
+ mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
float velocityX = mVelocityTracker.getXVelocity();
float velocityY = mVelocityTracker.getYVelocity();
float velocity = mNavBarPosition.isRightEdge()
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
similarity index 90%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
rename to quickstep/src/com/android/quickstep/views/AllAppsEduView.java
index 0979c07..97a239c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
+++ b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
@@ -16,6 +16,7 @@
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.anim.Interpolators.ACCEL;
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -42,8 +43,10 @@
import com.android.launcher3.Launcher;
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.dragndrop.DragLayer;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.Themes;
@@ -51,6 +54,7 @@
/**
* View used to educate the user on how to access All Apps when in No Nav Button navigation mode.
+ * Consumes all touches until after the animation is completed and the view is removed.
*/
public class AllAppsEduView extends AbstractFloatingView {
@@ -101,18 +105,23 @@
}
@Override
- public void logActionCommand(int command) {
- // TODO
- }
-
- @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;
+ }
+
+ @Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- return mAnimation != null && mAnimation.isRunning();
+ return true;
}
private void playAnimation() {
@@ -139,7 +148,12 @@
config.userControlled = false;
AnimatorPlaybackController stateAnimationController =
mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, config);
- float maxAllAppsProgress = 0.15f;
+ float maxAllAppsProgress = mLauncher.getDeviceProfile().isLandscape ? 0.35f : 0.15f;
+
+ AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
+ PendingAnimation allAppsAlpha = new PendingAnimation(config.duration);
+ allAppsController.setAlphas(ALL_APPS, config, allAppsAlpha);
+ mAnimation.play(allAppsAlpha.buildAnim());
ValueAnimator intro = ValueAnimator.ofFloat(0, 1f);
intro.setInterpolator(LINEAR);
@@ -191,7 +205,8 @@
@Override
public void onAnimationEnd(Animator animation) {
mAnimation = null;
- stateAnimationController.dispatchOnCancel();
+ // Handles cancelling the animation used to hint towards All Apps.
+ mLauncher.getStateManager().goToState(NORMAL, false);
handleClose(false);
}
});
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..4300329
--- /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 scrollFromEdge, boolean gridEnabled) {
+ RecentsView recentsView = getRecentsView();
+ if (recentsView == null) {
+ return;
+ }
+
+ PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+ float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
+ if (orientationSize == 0) {
+ return;
+ }
+
+ int leftEdgeScroll = recentsView.getLeftMostChildScroll();
+ int adjustedScrollFromEdge = scrollFromEdge - leftEdgeScroll;
+ 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/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
similarity index 74%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
rename to quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index b06d4bc..7c8041c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -17,6 +17,8 @@
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;
@@ -27,6 +29,8 @@
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;
@@ -35,6 +39,9 @@
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;
@@ -42,7 +49,7 @@
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.launcher3.Utilities;
import com.android.systemui.shared.recents.model.Task;
import java.time.Duration;
@@ -62,6 +69,10 @@
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;
@@ -69,18 +80,10 @@
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);
+ replaceBanner(null);
mAppRemainingTimeMs = 0;
}
@@ -91,14 +94,10 @@
mActivity, mTaskView);
toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
toast.setOnClickListener(this::openAppUsageSettings);
- setTaskFooter(toast);
+ replaceBanner(toast);
mTaskView.setContentDescription(
getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
- RecentsView rv = mTaskView.getRecentsView();
- if (rv != null) {
- rv.onDigitalWellbeingToastShown();
- }
}
public String getText() {
@@ -217,8 +216,8 @@
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);
+
+ // 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);
@@ -234,4 +233,70 @@
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/recents_ui_overrides/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
similarity index 70%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
rename to quickstep/src/com/android/quickstep/views/IconView.java
index 7cc00b7..5b0ade0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -21,11 +21,7 @@
import android.util.AttributeSet;
import android.view.View;
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.FastBitmapDrawable;
-
-import java.util.ArrayList;
+import com.android.launcher3.Utilities;
/**
* A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
@@ -33,14 +29,8 @@
*/
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);
}
@@ -94,16 +84,6 @@
}
@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);
@@ -115,22 +95,6 @@
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);
@@ -140,4 +104,16 @@
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..f5a8ff8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -0,0 +1,270 @@
+/*
+ * 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 static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+
+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.quickstep.util.OverviewToHomeAnim;
+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() {
+ Runnable onReachedHome = () -> mActivity.getStateManager().goToState(NORMAL, false);
+ OverviewToHomeAnim overviewToHomeAnim = new OverviewToHomeAnim(mActivity, onReachedHome);
+ if (LIVE_TILE.get()) {
+ switchToScreenshot(null,
+ () -> finishRecentsAnimation(true /* toRecents */,
+ () -> overviewToHomeAnim.animateWithVelocity(0)));
+ } else {
+ overviewToHomeAnim.animateWithVelocity(0);
+ }
+ }
+
+ @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
+ setTaskViewsResistanceTranslation(mTaskViewsSecondaryTranslation);
+ setTaskViewsPrimaryTranslation(mTaskViewsPrimaryTranslation);
+ }
+ }
+
+ @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/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
similarity index 74%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
rename to quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index a2da398..9adeea4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -16,9 +16,8 @@
package com.android.quickstep.views;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
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;
@@ -31,8 +30,10 @@
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;
@@ -52,35 +53,32 @@
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;
+ 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_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;
@@ -92,6 +90,9 @@
protected T mCallbacks;
+ private float mModalness;
+ private float mModalTransformY;
+
public OverviewActionsView(Context context) {
this(context, null);
}
@@ -102,7 +103,8 @@
public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr, 0);
- mMultiValueAlpha = new MultiValueAlpha(this, 4);
+ mMultiValueAlpha = new MultiValueAlpha(this, 5);
+ mMultiValueAlpha.setUpdateVisibility(true);
}
@Override
@@ -113,7 +115,7 @@
findViewById(R.id.action_screenshot).setOnClickListener(this);
if (ENABLE_OVERVIEW_SHARE.get()) {
share.setVisibility(VISIBLE);
- findViewById(R.id.share_space).setVisibility(VISIBLE);
+ findViewById(R.id.oav_three_button_space).setVisibility(VISIBLE);
}
}
@@ -140,13 +142,6 @@
}
@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()));
@@ -156,6 +151,7 @@
public void setInsets(Rect insets) {
mInsets.set(insets);
updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
+ updateHorizontalPadding();
}
public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean enable) {
@@ -166,7 +162,6 @@
}
boolean isHidden = mHiddenFlags != 0;
mMultiValueAlpha.getProperty(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
- setVisibility(isHidden ? INVISIBLE : VISIBLE);
}
/**
@@ -200,8 +195,31 @@
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) {
+ LayoutParams actionParams = (LayoutParams) findViewById(
+ R.id.action_buttons).getLayoutParams();
+ actionParams.setMargins(
+ actionParams.leftMargin, actionParams.topMargin, actionParams.rightMargin,
+ getBottomVerticalMargin(mode));
+ }
+
+ /**
+ * Set the device profile for this view to draw with.
+ */
+ public void setDp(DeviceProfile dp) {
+ requestLayout();
+ }
+
+ protected int getBottomVerticalMargin(Mode mode) {
int bottomMargin;
int orientation = getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
@@ -214,8 +232,29 @@
.getDimensionPixelSize(R.dimen.overview_actions_bottom_margin_gesture);
}
bottomMargin += mInsets.bottom;
- LayoutParams params = (LayoutParams) getLayoutParams();
- params.setMargins(
- params.leftMargin, params.topMargin, params.rightMargin, bottomMargin);
+ return bottomMargin;
+ }
+
+ /**
+ * 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);
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsExtraViewContainer.java
similarity index 93%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java
rename to quickstep/src/com/android/quickstep/views/RecentsExtraViewContainer.java
index 1ea6d4a..16bc3bc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsExtraViewContainer.java
@@ -23,7 +23,7 @@
/**
* Empty view to house recents overview extra card
*/
-public class RecentsExtraViewContainer extends FrameLayout implements RecentsView.PageCallbacks {
+public class RecentsExtraViewContainer extends FrameLayout {
private boolean mScrollable = false;
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..1a86680
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -0,0 +1,3290 @@
+/*
+ * 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.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+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_75;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+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.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.util.Executors.MAIN_EXECUTOR;
+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.util.NavigationModeFeatureFlag.LIVE_TILE;
+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.res.Configuration;
+import android.graphics.Canvas;
+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.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.Log;
+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.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+
+import androidx.annotation.Nullable;
+
+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.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.statehandlers.DepthController;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestProtocol;
+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.OverScroller;
+import com.android.launcher3.util.ResourceBasedOverride.Overrides;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.util.Themes;
+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.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.ActiveGestureLog;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.MultiValueUpdateListener;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SplitScreenBounds;
+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.wm.shell.pip.IPipAnimationListener;
+
+import java.util.ArrayList;
+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,
+ InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener,
+ SplitScreenBounds.OnChangeListener {
+
+ 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;
+ }
+ };
+
+ /**
+ * 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_TRANSLATION =
+ new FloatProperty<RecentsView>("taskPrimaryTranslation") {
+ @Override
+ public void setValue(RecentsView recentsView, float v) {
+ recentsView.setTaskViewsPrimaryTranslation(v);
+ }
+
+ @Override
+ public Float get(RecentsView recentsView) {
+ return recentsView.mTaskViewsPrimaryTranslation;
+ }
+ };
+
+ /** 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.mLastComputedTaskPushOutDistance = null;
+ view.mLiveTileTaskViewSimulator.recentsViewScale.value = scale;
+ view.updatePageOffsets();
+ view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation);
+ view.setTaskViewsPrimaryTranslation(view.mTaskViewsPrimaryTranslation);
+ }
+
+ @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;
+ }
+ };
+
+ 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 mLastComputedTaskPushOutDistance = null;
+ protected boolean mEnableDrawingLiveTile = false;
+ protected final Rect mTempRect = new Rect();
+ protected final RectF mTempRectF = new RectF();
+ private final PointF mTempPointF = new PointF();
+ private float mFullscreenScale;
+
+ 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 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 mAdjacentPageOffset = 0;
+ protected float mTaskViewsSecondaryTranslation = 0;
+ protected float mTaskViewsPrimaryTranslation = 0;
+ // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
+ private float mGridProgress = 0;
+
+ // The GestureEndTarget that is still in progress.
+ private GestureState.GestureEndTarget mCurrentGestureEndTarget;
+
+ IntSet mTopIdSet = new IntSet();
+
+ /**
+ * 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 mIPipAnimationListener =
+ 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;
+ protected int mFocusedTaskId = -1;
+ private float mFocusedTaskRatio;
+
+ private boolean mRunningTaskIconScaledDown = false;
+
+ private final boolean mHasLightBackground;
+ 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();
+ }
+ }
+ };
+
+ 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());
+ mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+ mLiveTileTaskViewSimulator.setOrientationState(mOrientationState);
+ mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
+
+ mHasLightBackground = Themes.getAttrBoolean(mActivity, android.R.attr.isLightTheme);
+ }
+
+ 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(TaskView.FLAG_UPDATE_ICON);
+ loadVisibleTaskData(TaskView.FLAG_UPDATE_ICON);
+ }
+
+ 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);
+ ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
+ mSyncTransactionApplier = new SurfaceTransactionApplier(this);
+ mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier);
+ RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
+ mIdp.addOnChangeListener(this);
+ mIPipAnimationListener.setActivity(mActivity);
+ 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);
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
+ mSyncTransactionApplier = null;
+ mLiveTileParams.setSyncTransactionApplier(null);
+ RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
+ mIdp.removeOnChangeListener(this);
+ SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
+ SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
+ mIPipAnimationListener.setActivity(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 launchSideTaskInLiveTileModeForRestartedApp(int taskId) {
+ if (mRunningTaskId != -1 && mRunningTaskId == taskId &&
+ getLiveTileParams().getTargetSet().findTask(taskId) != null) {
+ launchSideTaskInLiveTileMode(taskId, getLiveTileParams().getTargetSet().apps);
+ }
+ }
+
+ public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps) {
+ 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(new MultiValueUpdateListener() {
+ @Override
+ public void onUpdate(float percent) {
+ 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);
+ } else {
+ TaskViewUtils.composeRecentsLaunchAnimator(
+ anim, taskView, apps,
+ mLiveTileParams.getTargetSet().wallpapers, true /* launcherClosing */,
+ mActivity.getStateManager(), this,
+ getDepthController());
+ }
+ anim.addListener(new AnimatorListenerAdapter(){
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ cleanUp(false);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ cleanUp(true);
+ }
+
+ private void cleanUp(boolean canceled) {
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.finish(false /* toRecents */,
+ null /* onFinishComplete */);
+ if (canceled) {
+ mRecentsAnimationController = null;
+ } else {
+ mSizeStrategy.onLaunchTaskSuccess();
+ }
+ ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
+ }
+ }
+ });
+ 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(
+ mOverviewFullscreenEnabled, showAsGrid());
+ int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
+ mOverviewFullscreenEnabled));
+ 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);
+ }
+ }
+
+ if (enabled) {
+ mActivity.getSystemUiController().updateUiState(
+ UI_STATE_OVERVIEW, hasLightBackground());
+ } else {
+ mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
+ }
+ }
+
+ /**
+ * 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 boolean snapToPageInFreeScroll() {
+ return !showAsGrid();
+ }
+
+ @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 (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "applyLoadPlan: taskCount=" + tasks.size());
+ for (Task t : tasks) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "\t" + t);
+ }
+ }
+ if (mPendingAnimation != null) {
+ mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks));
+ return;
+ }
+
+ if (tasks == null || tasks.isEmpty()) {
+ removeTasksViewsAndClearAllButton();
+ onTaskStackUpdated();
+ return;
+ }
+
+ // 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();
+
+ 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();
+
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "applyLoadPlan: taskViewCount="
+ + getTaskViewCount());
+ }
+ }
+
+ 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() {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "getTaskViewCount:"
+ + " numChildren=" + getChildCount()
+ + " start=" + mTaskViewStartIndex
+ + " clearAll=" + indexOfChild(mClearAllButton));
+ }
+ 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 (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;
+ }
+ 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);
+ }
+
+ 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);
+ resetPaddingFromTaskSize();
+ DeviceProfile dp = mActivity.getDeviceProfile();
+ mLiveTileTaskViewSimulator.setDp(dp);
+ mActionsView.setDp(dp);
+ }
+
+ private void resetPaddingFromTaskSize() {
+ 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
+ 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;
+ float[] fullscreenTranslations = new float[taskCount];
+ int firstNonHomeTaskIndex = 0;
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ if (isHomeTask(taskView)) {
+ if (firstNonHomeTaskIndex == i) {
+ firstNonHomeTaskIndex++;
+ }
+ continue;
+ }
+
+ taskView.updateTaskSize();
+ fullscreenTranslations[i] += accumulatedTranslationX;
+ // Compensate space caused by TaskView scaling.
+ float widthDiff =
+ taskView.getLayoutParams().width * (1 - taskView.getFullscreenScale());
+ // Compensate page spacing widening caused by RecentsView scaling.
+ widthDiff += mPageSpacing * (1 - 1 / mFullscreenScale);
+ float fullscreenTranslationX = mIsRtl ? widthDiff : -widthDiff;
+ accumulatedTranslationX += fullscreenTranslationX;
+ }
+
+ // We need to maintain first non-home task's full screen translation at 0, now shift
+ // translation of all the TaskViews to achieve that.
+ for (int i = firstNonHomeTaskIndex; i < taskCount; i++) {
+ getTaskViewAt(i).setFullscreenTranslationX(
+ fullscreenTranslations[i] - fullscreenTranslations[firstNonHomeTaskIndex]);
+ }
+ mClearAllButton.setFullscreenTranslationPrimary(
+ accumulatedTranslationX - fullscreenTranslations[firstNonHomeTaskIndex]);
+
+ // Align ClearAllButton to the left (RTL) or right (non-RTL), which is different from other
+ // TaskViews.
+ int clearAllWidthDiff = mTaskWidth - mClearAllButton.getWidth();
+ mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff);
+
+ updateGridProperties(false);
+ }
+
+ 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 (mRunningTaskId != -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);
+
+ mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
+ if (LIVE_TILE.get() && mEnableDrawingLiveTile
+ && mLiveTileParams.getTargetSet() != null) {
+ redrawLiveTile();
+ }
+ 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);
+ int scrollFromEdge = mIsRtl ? scroll : (mMaxScroll - scroll);
+ mClearAllButton.onRecentsViewScroll(scrollFromEdge, mOverviewGridEnabled);
+ }
+
+ @Override
+ protected int getDestinationPage(int scaledScroll) {
+ if (!showAsGrid()) {
+ return super.getDestinationPage(scaledScroll);
+ }
+
+ final int childCount = getChildCount();
+ if (mPageScrolls == null || childCount != mPageScrolls.length) {
+ return -1;
+ }
+
+ // When in grid, 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;
+
+ mRecentsAnimationController = null;
+ mLiveTileParams.setTargetSet(null);
+
+ unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
+ setCurrentPage(0);
+ mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
+ LayoutUtils.setViewEnabled(mActionsView, true);
+ if (mOrientationState.setGestureActive(false)) {
+ updateOrientationHandler();
+ }
+ }
+
+ 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(0f);
+ }
+ 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();
+ updateChildTaskOrientations();
+ 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 onGestureEndTargetCalculated(GestureState.GestureEndTarget endTarget) {
+ mCurrentGestureEndTarget = endTarget;
+ if (showAsGrid()) {
+ mFocusedTaskId = mRunningTaskId;
+ mFocusedTaskRatio =
+ mLastComputedTaskSize.width() / (float) mLastComputedTaskSize.height();
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ setOnScrollChangeListener(null);
+ setEnableFreeScroll(true);
+ setEnableDrawingLiveTile(true);
+ if (!LIVE_TILE.get()) {
+ setRunningTaskViewShowScreenshot(true);
+ }
+ setRunningTaskHidden(false);
+ animateUpRunningTaskIconScale();
+
+ if (!showAsGrid() || getFocusedTaskView() != null) {
+ animateActionsViewIn();
+ }
+
+ mCurrentGestureEndTarget = 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;
+ setCurrentTask(runningTaskInfo == null ? -1 : runningTaskInfo.taskId);
+ 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.
+ * @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 (LIVE_TILE.get()) {
+ TaskView runningTaskView = getRunningTaskView();
+ if (runningTaskView != null) {
+ runningTaskView.setShowScreenshot(showScreenshot);
+ }
+ }
+ }
+
+ 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() {
+ animateUpRunningTaskIconScale(0);
+ }
+
+ public void animateUpRunningTaskIconScale(float startProgress) {
+ mRunningTaskIconScaledDown = false;
+ TaskView firstTask = getRunningTaskView();
+ if (firstTask != null) {
+ firstTask.animateIconScaleAndDimIntoView();
+ firstTask.setIconScaleAnimStartProgress(startProgress);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ 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;
+ float heightOffset = (boxLength + taskTopMargin) + mRowSpacing;
+ float taskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
+
+ int topRowWidth = 0;
+ int bottomRowWidth = 0;
+ float topAccumulatedTranslationX = 0;
+ float bottomAccumulatedTranslationX = 0;
+ IntSet topSet = new IntSet();
+ IntSet bottomSet = new IntSet();
+ float[] gridTranslations = new float[taskCount];
+ int firstNonHomeTaskIndex = 0;
+
+ if (!isTaskDismissal) {
+ mTopIdSet.clear();
+ }
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ if (isHomeTask(taskView)) {
+ if (firstNonHomeTaskIndex == i) {
+ firstNonHomeTaskIndex++;
+ }
+ continue;
+ }
+
+ // 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.isRunningTask()) {
+ topRowWidth += taskView.getLayoutParams().width + mPageSpacing;
+ bottomRowWidth += taskView.getLayoutParams().width + mPageSpacing;
+
+ // Center view vertically in case it's from different orientation.
+ taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
+ - taskView.getLayoutParams().height) / 2f);
+ } else if ((!isTaskDismissal && topRowWidth <= bottomRowWidth) || (isTaskDismissal
+ && mTopIdSet.contains(taskView.getTask().key.id))) {
+ gridTranslations[i] += topAccumulatedTranslationX;
+ topRowWidth += taskView.getLayoutParams().width + mPageSpacing;
+ topSet.add(i);
+ mTopIdSet.add(taskView.getTask().key.id);
+
+ taskView.setGridTranslationY(taskGridVerticalDiff);
+
+ // Move horizontally into empty space.
+ float widthOffset = 0;
+ for (int j = i - 1; bottomSet.contains(j); j--) {
+ widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+ }
+
+ float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
+ gridTranslations[i] += gridTranslationX;
+ topAccumulatedTranslationX += gridTranslationX;
+ } else {
+ gridTranslations[i] += bottomAccumulatedTranslationX;
+ bottomRowWidth += taskView.getLayoutParams().width + mPageSpacing;
+ bottomSet.add(i);
+
+ // Move into bottom row.
+ taskView.setGridTranslationY(heightOffset + taskGridVerticalDiff);
+
+ // Move horizontally into empty space.
+ float widthOffset = 0;
+ for (int j = i - 1; topSet.contains(j); j--) {
+ widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+ }
+
+ float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
+ gridTranslations[i] += gridTranslationX;
+ bottomAccumulatedTranslationX += gridTranslationX;
+ }
+ }
+
+ // We need to maintain first non-home task's grid translation at 0, now shift translation
+ // of all the TaskViews to achieve that.
+ for (int i = firstNonHomeTaskIndex; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ taskView.setGridTranslationX(
+ gridTranslations[i] - gridTranslations[firstNonHomeTaskIndex]);
+ }
+
+ // Use the accumulated translation of the longer row.
+ float clearAllAccumulatedTranslation = mIsRtl ? Math.max(topAccumulatedTranslationX,
+ bottomAccumulatedTranslationX) : Math.min(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.
+ float clearAllShortTotalCompensation = 0;
+ float longRowWidth = Math.max(topRowWidth, bottomRowWidth);
+ if (longRowWidth < mLastComputedGridSize.width()) {
+ float shortTotalCompensation = mLastComputedGridSize.width() - longRowWidth;
+ clearAllShortTotalCompensation =
+ mIsRtl ? -shortTotalCompensation : shortTotalCompensation;
+ }
+
+ float clearAllTotalTranslationX =
+ clearAllAccumulatedTranslation + clearAllShorterRowCompensation
+ + clearAllShortTotalCompensation;
+
+ mClearAllButton.setGridTranslationPrimary(
+ clearAllTotalTranslationX - gridTranslations[firstNonHomeTaskIndex]);
+ mClearAllButton.setGridScrollOffset(
+ mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left
+ : mLastComputedTaskSize.right - mLastComputedGridSize.right);
+
+ setGridProgress(mGridProgress);
+ }
+
+ protected boolean isHomeTask(TaskView taskView) {
+ return false;
+ }
+
+ /**
+ * 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, ACCEL_2);
+ FloatProperty<TaskView> secondaryViewTranslate =
+ taskView.getSecondaryDissmissTranslationProperty();
+ int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
+ int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
+
+ 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);
+ }
+
+ public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
+ boolean shouldRemoveTask, long duration) {
+ if (mPendingAnimation != null) {
+ mPendingAnimation.createPlaybackController().dispatchOnCancel();
+ }
+ 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 (!showAsGrid()) {
+ // For grid layout, don't animate other tasks when dismissing in grid for now.
+ // 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;
+ }
+ }
+
+ // Additional offset for fake landscape, if the pinning happens to the right or
+ // left, we need to scroll all the tasks away from the direction of the splaceholder
+ // view
+ if (isSplitSelectionActive()) {
+ int splitPosition = getSplitPlaceholder().getSplitController()
+ .getActiveSplitPositionOption().mStagePosition;
+ int direction = mOrientationHandler
+ .getSplitTranslationDirectionFactor(splitPosition);
+ int splitOffset = mOrientationHandler.getSplitAnimationTranslation(
+ mSplitPlaceholderView.getHeight(), mActivity.getDeviceProfile());
+ offset += direction * splitOffset;
+ }
+ int scrollDiff = newScroll[i] - oldScroll[i] + offset;
+ 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));
+ anim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
+ .setDuration(duration), ACCEL, sp);
+ needsCurveUpdates = true;
+ }
+ }
+ }
+
+ if (needsCurveUpdates) {
+ anim.addOnFrameCallback(this::updateCurveProperties);
+ }
+
+ if (LIVE_TILE.get() && getRunningTaskView() == taskView) {
+ anim.addOnFrameCallback(() -> {
+ mLiveTileTaskViewSimulator.taskSecondaryTranslation.value =
+ mOrientationHandler.getSecondaryValue(
+ taskView.getTranslationX(),
+ taskView.getTranslationY());
+ redrawLiveTile();
+ });
+ }
+
+ // 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 (LIVE_TILE.get() && taskView.isRunningTask() && success) {
+ finishRecentsAnimation(true /* toHome */, () -> onEnd(success));
+ } else {
+ onEnd(success);
+ }
+ }
+
+ @SuppressWarnings("WrongCall")
+ private void onEnd(boolean success) {
+ if (success) {
+ if (shouldRemoveTask) {
+ if (taskView.getTask() != null) {
+ UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
+ .removeTask(taskView.getTask().key.id));
+ mActivity.getStatsLogManager().logger()
+ .withItemInfo(taskView.getItemInfo())
+ .log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
+ }
+ }
+
+ int pageToSnapTo = mCurrentPage;
+ if (draggedIndex < pageToSnapTo ||
+ pageToSnapTo == (getTaskViewCount() - 1)) {
+ pageToSnapTo -= 1;
+ }
+ removeViewInLayout(taskView);
+
+ if (getTaskViewCount() == 0) {
+ removeViewInLayout(mClearAllButton);
+ startHome();
+ } else {
+ snapToPageImmediately(pageToSnapTo);
+ // Grid got messed up, reapply.
+ updateGridProperties(true);
+ if (showAsGrid() && getFocusedTaskView() == null) {
+ animateActionsViewOut();
+ }
+ }
+ // Update the layout synchronously so that the position of next view is
+ // immediately available.
+ onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
+ }
+ resetTaskVisuals();
+ onDismissAnimationEnds();
+ mPendingAnimation = null;
+ }
+ });
+ return anim;
+ }
+
+ 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
+ UI_HELPER_EXECUTOR.execute(
+ 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.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.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);
+ final int rotation = mActivity.getDisplay().getRotation();
+ if (mOrientationState.setRecentsRotation(rotation)) {
+ 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() != ROTATION_0
+ || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
+ mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
+ !mOrientationState.canRecentsActivityRotate() && isInLandscape);
+ updateChildTaskOrientations();
+ 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);
+ mFullscreenScale = getPagedViewOrientedState().getFullScreenScaleAndPivot(
+ mTempRect, mActivity.getDeviceProfile(), mTempPointF);
+ setPivotX(mTempPointF.x);
+ setPivotY(mTempPointF.y);
+ setTaskModalness(mTaskModalness);
+ mLastComputedTaskPushOutDistance = null;
+ updatePageOffsets();
+ setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+
+ private void updatePageOffsets() {
+ float offset = mAdjacentPageOffset;
+ float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness);
+ 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 modalMidpoint = getCurrentPage();
+
+ float midpointOffsetSize = 0;
+ float leftOffsetSize = midpoint - 1 >= 0
+ ? -getOffsetSize(midpoint - 1, midpoint, offset)
+ : 0;
+ float rightOffsetSize = midpoint + 1 < count
+ ? getOffsetSize(midpoint + 1, midpoint, offset)
+ : 0;
+
+ float modalMidpointOffsetSize = 0;
+ float modalLeftOffsetSize = modalMidpoint - 1 >= 0
+ ? -getOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset)
+ : 0;
+ float modalRightOffsetSize = modalMidpoint + 1 < count
+ ? getOffsetSize(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
+ : 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 * mOrientationHandler.getPrimaryTranslationDirectionFactor());
+ }
+ updateCurveProperties();
+ }
+
+ /**
+ * 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 getOffsetSize(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.
+ mTempRectF.set(mLastComputedTaskSize);
+ RectF taskPosition = mTempRectF;
+ float desiredLeft = getWidth();
+ // Used to calculate the scale of the task view based on its new offset.
+ 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.
+ View child = getChildAt(childIndex);
+ View midpointChild = getChildAt(midpointIndex);
+ int distanceFromMidpoint = Math.abs(mOrientationHandler.getChildStart(child)
+ - mOrientationHandler.getChildStart(midpointChild)
+ + getDisplacementFromScreenCenter(midpointIndex));
+ taskPosition.offset(distanceFromMidpoint, 0);
+ }
+ float distanceToOffscreen = desiredLeft - taskPosition.left;
+ // Finally, we 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.
+ if (mLastComputedTaskPushOutDistance == null) {
+ taskPosition.offsetTo(desiredLeft, 0);
+ getMatrix().mapRect(taskPosition);
+ mLastComputedTaskPushOutDistance = (taskPosition.left - desiredLeft) / getScaleX();
+ }
+ distanceToOffscreen -= mLastComputedTaskPushOutDistance;
+ 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 setTaskViewsPrimaryTranslation(float translation) {
+ mTaskViewsPrimaryTranslation = translation;
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView task = getTaskViewAt(i);
+ task.getPrimaryDismissTranslationProperty().set(task, translation / getScaleY());
+ }
+ mLiveTileTaskViewSimulator.recentsViewPrimaryTranslation.value = translation;
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * True if the background scrim of the recents view is light colored and the foreground elements
+ * should use dark colors.
+ */
+ public boolean hasLightBackground() {
+ return mHasLightBackground;
+ }
+
+ public void initiateSplitSelect(TaskView taskView, SplitPositionOption splitPositionOption) {
+ mSplitHiddenTaskView = taskView;
+ mSplitPlaceholderView.getSplitController().setInitialTaskSelect(taskView,
+ splitPositionOption);
+ mSplitHiddenTaskViewIndex = indexOfChild(taskView);
+ }
+
+ 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) {
+ mSplitPlaceholderView.getSplitController().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);
+
+ // x is correct, y is before tasks move up
+ int[] locationOnScreen = mSplitHiddenTaskView.getLocationOnScreen();
+ 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) {
+
+ int left = newScroll[i] + getPaddingStart();
+ int topMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
+ int top = -mSplitHiddenTaskView.getHeight() - locationOnScreen[1];
+ mSplitHiddenTaskView.layout(left, top,
+ left + mSplitHiddenTaskView.getWidth(),
+ top + mSplitHiddenTaskView.getHeight());
+ pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, TRANSLATION_Y,
+ -top + mSplitPlaceholderView.getHeight() - topMargin));
+ 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
+ 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) {
+ resetFromSplitSelectionState();
+ }
+ });
+
+ return pendingAnim;
+ }
+
+ private void resetFromSplitSelectionState() {
+ mSplitHiddenTaskView.setTranslationY(0);
+ 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 (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);
+ }
+
+ 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_OVERVIEW, targetSysUiFlags);
+ } else {
+ mActivity.getSystemUiController().updateUiState(
+ UI_STATE_OVERVIEW, hasLightBackground());
+ }
+
+ // 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 (LIVE_TILE.get()) {
+ mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
+ mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+ }
+ mPendingAnimation.addEndListener(isSuccess -> {
+ if (isSuccess) {
+ if (LIVE_TILE.get()) {
+ 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) {
+ mLiveTileTaskViewSimulator.setPreview(
+ recentsAnimationTargets.apps[recentsAnimationTargets.apps.length - 1]);
+ mLiveTileParams.setTargetSet(recentsAnimationTargets);
+ }
+ }
+
+ 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 (mIsRtl && mDisallowScrollToClearAll) {
+ // We aren't showing the clear all button,
+ // so use the leftmost task as the min scroll.
+ return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)));
+ }
+ return getLeftMostChildScroll();
+ }
+ return super.computeMinScroll();
+ }
+
+ /**
+ * Returns page scroll of the left most child.
+ */
+ public int getLeftMostChildScroll() {
+ return getScrollForPage(mIsRtl ? indexOfChild(mClearAllButton) : mTaskViewStartIndex);
+ }
+
+ @Override
+ protected int computeMaxScroll() {
+ if (getTaskViewCount() > 0) {
+ if (!mIsRtl && mDisallowScrollToClearAll) {
+ // We aren't showing the clear all button,
+ // so use the rightmost task as the min scroll.
+ return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)));
+ }
+ return getScrollForPage(mIsRtl ? mTaskViewStartIndex : indexOfChild(mClearAllButton));
+ }
+ return super.computeMaxScroll();
+ }
+
+ @Override
+ protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
+ ComputePageScrollsLogic scrollLogic) {
+ boolean pageScrollChanged = super.getPageScrolls(outPageScrolls, layoutChildren,
+ scrollLogic);
+
+ 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(mOverviewFullscreenEnabled,
+ showAsGrid());
+ } else if (child instanceof ClearAllButton) {
+ scrollDiff = ((ClearAllButton) child).getScrollAdjustment(
+ mOverviewFullscreenEnabled, showAsGrid());
+ }
+
+ if (scrollDiff != 0) {
+ outPageScrolls[i] += scrollDiff;
+ pageScrollChanged = true;
+ }
+ }
+ 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(mOverviewFullscreenEnabled,
+ 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(
+ mOverviewFullscreenEnabled));
+ }
+
+ 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());
+ }
+
+ /**
+ * 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;
+ }
+ // Unbound the scroll (due to overscroll) if the adjacent tasks are offset away from it.
+ // This allows the page to move freely, given there's no visual indication why it shouldn't.
+ int boundedScroll = mOrientationHandler.getPrimaryScroll(this);
+ int unboundedScroll = getUnboundedScroll();
+ float unboundedProgress = mAdjacentPageOffset;
+ int scroll = Math.round(unboundedScroll * unboundedProgress
+ + boundedScroll * (1 - unboundedProgress));
+ return getScrollForPage(pageIndex) - scroll;
+ }
+
+ /**
+ * 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) {
+ 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);
+ requestLayout();
+ }
+
+ /**
+ * 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) {
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ getTaskViewAt(i).showColorTint(show);
+ }
+ }
+
+ private boolean showAsGrid() {
+ return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
+ && mSizeStrategy.stateFromGestureEndTarget(
+ mCurrentGestureEndTarget).displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
+ }
+
+ 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);
+ }
+
+ private static class PinnedStackAnimationListener<T extends BaseActivity> extends
+ IPipAnimationListener.Stub {
+ private T mActivity;
+
+ public void setActivity(T activity) {
+ mActivity = activity;
+ }
+
+ @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);
+ }
+ });
+ }
+ }
+}
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..fb9be81
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/SplitPlaceholderView.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.quickstep.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.quickstep.util.SplitSelectStateController;
+
+public class SplitPlaceholderView extends View {
+
+ 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;
+
+ public SplitPlaceholderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void init(SplitSelectStateController controller) {
+ this.mSplitController = controller;
+ }
+
+ public SplitSelectStateController getSplitController() {
+ return mSplitController;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
similarity index 69%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
rename to quickstep/src/com/android/quickstep/views/TaskMenuView.java
index ef66b7a..658d71d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,80 +16,54 @@
package com.android.quickstep.views;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.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.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.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.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.config.FeatureFlags;
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;
+import com.android.quickstep.util.TaskCornerRadius;
/**
* Contains options for a recent task when long-pressing its icon.
*/
-public class TaskMenuView extends AbstractFloatingView {
+public class TaskMenuView extends AbstractFloatingView implements OnScrollChangedListener {
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);
@@ -99,14 +73,13 @@
super(context, attrs, defStyleAttr);
mActivity = BaseDraggingActivity.fromContext(context);
- mThumbnailTopMargin = getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+ setClipToOutline(true);
}
@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);
}
@@ -133,31 +106,35 @@
}
@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;
+ @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) {
+ PagedOrientationHandler pagedOrientationHandler = mTaskView.getPagedOrientationHandler();
+ int taskTopMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
+ float adjustedY = y + taskTopMargin;
// 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);
+ 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(x, mTaskView.getThumbnail()));
setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail()));
@@ -169,15 +146,17 @@
}
if (mIsOpen) {
mOptionLayout.removeAllViews();
- populateAndLayoutMenu();
+ if (!populateAndLayoutMenu()) {
+ close(false);
+ }
}
}
- public static TaskMenuView showForTask(TaskView taskView) {
+ 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) ? taskMenuView : null;
+ return taskMenuView.populateAndShowForTask(taskView);
}
private boolean populateAndShowForTask(TaskView taskView) {
@@ -186,35 +165,37 @@
}
mActivity.getDragLayer().addView(this);
mTaskView = taskView;
- populateAndLayoutMenu();
+ if (!populateAndLayoutMenu()) {
+ return false;
+ }
post(this::animateOpen);
+ mActivity.getRootView().getViewTreeObserver().addOnScrollChangedListener(this);
return true;
}
- private void populateAndLayoutMenu() {
+ @Override
+ public void onScrollChanged() {
+ RecentsView rv = mTaskView.getRecentsView();
+ setPosition(mTaskView.getX() - rv.getScrollX(), mTaskView.getY() - rv.getScrollY());
+ }
+
+ /** @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) {
- 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);
+ TaskOverlayFactory.getEnabledShortcuts(taskView, mActivity.getDeviceProfile())
+ .forEach(this::addMenuOption);
}
private void addMenuOption(SystemShortcut menuOption) {
@@ -224,7 +205,18 @@
menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
- menuOptionView.setOnClickListener(menuOption);
+ menuOptionView.setEnabled(menuOption.isEnabled());
+ menuOptionView.setAlpha(menuOption.isEnabled() ? 1 : 0.5f);
+ menuOptionView.setOnClickListener(view -> {
+ if (LIVE_TILE.get()) {
+ RecentsView recentsView = mTaskView.getRecentsView();
+ recentsView.switchToScreenshot(null,
+ () -> recentsView.finishRecentsAnimation(true /* toRecents */,
+ () -> menuOption.onClick(view)));
+ } else {
+ menuOption.onClick(view);
+ }
+ });
mOptionLayout.addView(menuOptionView);
}
@@ -240,10 +232,11 @@
setLayoutParams(params);
setScaleX(taskView.getScaleX());
setScaleY(taskView.getScaleY());
+ boolean canActivityRotate = taskView.getRecentsView()
+ .mOrientationState.isRecentsActivityRotationAllowed();
mOptionLayout.setOrientation(orientationHandler
- .getTaskMenuLayoutOrientation(mOptionLayout));
- setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top,
- taskView.getPagedOrientationHandler());
+ .getTaskMenuLayoutOrientation(canActivityRotate, mOptionLayout));
+ setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top);
}
private void animateOpen() {
@@ -264,9 +257,11 @@
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.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) {
@@ -280,7 +275,6 @@
}
}
});
- mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION);
mOpenCloseAnimator.start();
}
@@ -288,10 +282,11 @@
private void closeComplete() {
mIsOpen = false;
mActivity.getDragLayer().removeView(this);
+ mActivity.getRootView().getViewTreeObserver().removeOnScrollChangedListener(this);
}
private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
- float radius = Themes.getDialogCornerRadius(getContext());
+ 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);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
similarity index 71%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
rename to quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index dbea80a..685f725 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -16,7 +16,10 @@
package com.android.quickstep.views;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
import android.content.Context;
@@ -25,8 +28,6 @@
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;
@@ -43,32 +44,27 @@
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.R;
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.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);
@@ -86,12 +82,12 @@
};
private final BaseActivity mActivity;
- private final TaskOverlay mOverlay;
- private final boolean mIsDarkTextTheme;
+ 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();
@@ -102,9 +98,8 @@
private ThumbnailData mThumbnailData;
protected BitmapShader mBitmapShader;
- private float mDimAlpha = 1f;
- private float mDimAlphaMultiplier = 1f;
- private float mSaturation = 1f;
+ /** How much this thumbnail is dimmed, 0 not dimmed at all, 1 totally dimmed. */
+ private float mDimAlpha = 0f;
private boolean mOverlayEnabled;
private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
@@ -119,15 +114,15 @@
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
+ // Initialize with placeholder value. It is overridden later by TaskView
mFullscreenParams = TEMP_PARAMS.get(context);
+
+ mDimColor = Themes.getColorBackgroundFloating(context);
+ mDimmingPaintAfterClearing.setColor(mDimColor);
}
/**
@@ -135,7 +130,7 @@
* @param task
*/
public void bind(Task task) {
- mOverlay.reset();
+ getTaskOverlay().reset();
mTask = task;
int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
mPaint.setColor(color);
@@ -177,7 +172,7 @@
mBitmapShader = null;
mThumbnailData = null;
mPaint.setShader(null);
- mOverlay.reset();
+ getTaskOverlay().reset();
}
if (mOverviewScreenshotActionsPlugin != null) {
mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
@@ -185,15 +180,12 @@
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.
+ * 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;
@@ -201,6 +193,9 @@
}
public TaskOverlay getTaskOverlay() {
+ if (mOverlay == null) {
+ mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this);
+ }
return mOverlay;
}
@@ -208,13 +203,6 @@
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.
@@ -226,6 +214,10 @@
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());
@@ -250,10 +242,10 @@
public int getSysUiStatusNavFlags() {
if (mThumbnailData != null) {
int flags = 0;
- flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
+ flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_STATUS_BARS) != 0
? SystemUiController.FLAG_LIGHT_STATUS
: SystemUiController.FLAG_DARK_STATUS;
- flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
+ flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0
? SystemUiController.FLAG_LIGHT_NAV
: SystemUiController.FLAG_DARK_NAV;
return flags;
@@ -315,7 +307,7 @@
public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
float cornerRadius) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (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,
@@ -327,22 +319,14 @@
// 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) {
+ if (drawBackgroundOnly || 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);
- }
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
}
public TaskView getTaskView() {
@@ -357,39 +341,39 @@
}
private void updateOverlay() {
- if (mOverlayEnabled && mBitmapShader != null && mThumbnailData != null) {
- mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
+ if (mOverlayEnabled) {
+ getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
mPreviewPositionHelper.mIsOrientationChanged);
} else {
- mOverlay.reset();
+ getTaskOverlay().reset();
}
}
private void updateThumbnailPaintFilter() {
- int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255);
- ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation);
+ ColorFilter filter = getColorFilter(mDimAlpha);
mBackgroundPaint.setColorFilter(filter);
- mDimmingPaintAfterClearing.setAlpha(255 - mul);
+ int alpha = (int) (mDimAlpha * 255);
+ mDimmingPaintAfterClearing.setAlpha(alpha);
if (mBitmapShader != null) {
mPaint.setColorFilter(filter);
} else {
mPaint.setColorFilter(null);
- mPaint.setColor(Color.argb(255, mul, mul, mul));
+ mPaint.setColor(ColorUtils.blendARGB(Color.BLACK, mDimColor, alpha));
}
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());
+ int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState()
+ .getRecentsActivityRotation();
+ boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
- currentRotation);
+ currentRotation, isRtl);
mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
mPaint.setShader(mBitmapShader);
@@ -408,35 +392,8 @@
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);
+ private ColorFilter getColorFilter(float dimAmount) {
+ return Utilities.makeColorTintingColorFilter(mDimColor, dimAmount);
}
public Bitmap getThumbnail() {
@@ -465,7 +422,6 @@
// 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() {
@@ -475,40 +431,112 @@
/**
* Updates the matrix based on the provided parameters
*/
- public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
- int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) {
+ public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
+ int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation,
+ boolean isRtl) {
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);
+ RectF thumbnailClipHint = TaskView.CLIP_STATUS_AND_NAV_BARS
+ ? new RectF(thumbnailData.insets) : new RectF();
- // Landscape vs portrait change
+ 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;
+ && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN
+ && !(dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
isOrientationDifferent = isOrientationChange(deltaRotate)
&& windowingModeSupportsRotation;
- if (canvasWidth == 0) {
+ 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;
- // Scale the screenshot to always fit the width of the card.
- thumbnailScale = isOrientationDifferent
- ? canvasWidth / thumbnailHeight
- : canvasWidth / thumbnailWidth;
+
+ 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();
@@ -518,24 +546,24 @@
mClippedInsets.offsetTo(splitScreenInsets.left * scale,
splitScreenInsets.top * scale);
} else {
- mClippedInsets.offsetTo(thumbnailInsets.left * scale,
- thumbnailInsets.top * scale);
+ mClippedInsets.offsetTo(thumbnailClipHint.left * scale,
+ thumbnailClipHint.top * scale);
}
mMatrix.setTranslate(
- -thumbnailInsets.left * scale,
- -thumbnailInsets.top * scale);
+ -thumbnailClipHint.left * scale,
+ -thumbnailClipHint.top * scale);
} else {
- setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
+ setThumbnailRotation(deltaRotate, thumbnailClipHint, scale, thumbnailBounds);
}
final float widthWithInsets;
final float heightWithInsets;
if (isOrientationDifferent) {
- widthWithInsets = thumbnailPosition.height() * thumbnailScale;
- heightWithInsets = thumbnailPosition.width() * thumbnailScale;
+ widthWithInsets = thumbnailBounds.height() * thumbnailScale;
+ heightWithInsets = thumbnailBounds.width() * thumbnailScale;
} else {
- widthWithInsets = thumbnailPosition.width() * thumbnailScale;
- heightWithInsets = thumbnailPosition.height() * thumbnailScale;
+ widthWithInsets = thumbnailBounds.width() * thumbnailScale;
+ heightWithInsets = thumbnailBounds.height() * thumbnailScale;
}
mClippedInsets.left *= thumbnailScale;
mClippedInsets.top *= thumbnailScale;
@@ -551,22 +579,9 @@
}
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;
@@ -582,12 +597,12 @@
return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
}
- private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
+ private void setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale,
Rect thumbnailPosition) {
- int newLeftInset = 0;
- int newTopInset = 0;
- int translateX = 0;
- int translateY = 0;
+ float newLeftInset = 0;
+ float newTopInset = 0;
+ float translateX = 0;
+ float translateY = 0;
mMatrix.setRotate(90 * deltaRotate);
switch (deltaRotate) { /* Counter-clockwise */
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..b87d2bf
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -0,0 +1,1361 @@
+/*
+ * 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.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
+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.EXAGGERATED_EASE;
+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.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+
+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.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.AnimatorPlaybackController;
+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.Themes;
+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.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 {}
+
+ /** 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;
+ /**
+ * 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> 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> COLOR_TINT =
+ new FloatProperty<TaskView>("colorTint") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setColorTint(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.getColorTint();
+ }
+ };
+
+ 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 mBoxTranslationY;
+ // The following grid translations scales with mGridProgress.
+ private float mGridTranslationX;
+ private float mGridTranslationY;
+
+ 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];
+
+ // Colored tint for the task view and all its supplementary views (like the task icon and well
+ // being banner.
+ private final int mTintingColor;
+ private float mTintAmount;
+
+ 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((view) -> {
+ if (getTask() == null) {
+ return;
+ }
+ if (LIVE_TILE.get() && isRunningTask()) {
+ if (!mIsClickableAsLiveTile) {
+ return;
+ }
+
+ mIsClickableAsLiveTile = false;
+ RecentsView recentsView = getRecentsView();
+ RemoteAnimationTargets targets = recentsView.getLiveTileParams().getTargetSet();
+ recentsView.getLiveTileTaskViewSimulator().setDrawsBelowRecents(false);
+
+ AnimatorSet anim = new AnimatorSet();
+ TaskViewUtils.composeRecentsLaunchAnimator(
+ anim, this, targets.apps,
+ targets.wallpapers, true /* launcherClosing */,
+ mActivity.getStateManager(), recentsView,
+ recentsView.getDepthController());
+ anim.addListener(new AnimatorListenerAdapter() {
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ recentsView.getLiveTileTaskViewSimulator().setDrawsBelowRecents(true);
+ recentsView.finishRecentsAnimation(false, null);
+ mIsClickableAsLiveTile = true;
+ }
+ });
+ anim.start();
+ } else {
+ if (mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
+ // User tapped to select second split screen app
+ getRecentsView().confirmSplitSelect(this);
+ } else {
+ launchTaskAnimated();
+ }
+ }
+ mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
+ .log(LAUNCHER_TASK_LAUNCH_TAP);
+ });
+
+ mCurrentFullscreenParams = new FullscreenDrawParams(context);
+ mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
+
+ mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams,
+ mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx);
+ setOutlineProvider(mOutlineProvider);
+
+ mTintingColor = Themes.getColorBackgroundFloating(context);
+ }
+
+ /**
+ * 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;
+ }
+
+ public AnimatorPlaybackController createLaunchAnimationForRunningTask() {
+ return getRecentsView().createTaskLaunchAnimation(
+ this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR)
+ .createPlaybackController();
+ }
+
+ /**
+ * 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);
+ if (ActivityManagerWrapper.getInstance()
+ .startActivityFromRecents(mTask.key, opts.options)) {
+ 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);
+ }
+
+ 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.setScaleX(scale);
+ mIconView.setScaleY(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 = 0f;
+ mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY = 0f;
+ applyTranslationX();
+ applyTranslationY();
+ setTranslationZ(0);
+ setAlpha(mStableAlpha);
+ setIconScaleAndDim(1);
+ setColorTint(0);
+ }
+
+ public void setStableAlpha(float parentAlpha) {
+ mStableAlpha = parentAlpha;
+ setAlpha(mStableAlpha);
+ }
+
+ @Override
+ public void onRecycle() {
+ mFullscreenTranslationX = 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 = EXAGGERATED_EASE.getInterpolation(mFullscreenProgress);
+ scale *= Utilities.mapRange(fullScreenProgress, 1f, mFullscreenScale);
+ setScaleX(scale);
+ setScaleY(scale);
+ }
+
+ 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();
+ }
+
+ public void setFullscreenTranslationX(float fullscreenTranslationX) {
+ mFullscreenTranslationX = fullscreenTranslationX;
+ applyTranslationX();
+ }
+
+ 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 += mFullscreenTranslationX;
+ }
+ 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
+ + getFullscreenTrans(mFullscreenTranslationX)
+ + getGridTrans(mGridTranslationX));
+ }
+
+ private void applyTranslationY() {
+ setTranslationY(
+ mDismissTranslationY + mTaskOffsetTranslationY + mTaskResistanceTranslationY
+ + getGridTrans(mGridTranslationY) + mBoxTranslationY);
+ }
+
+ private float getGridTrans(float endTranslation) {
+ float progress = ACCEL_DEACCEL.getInterpolation(mGridProgress);
+ return Utilities.mapRange(progress, 0, endTranslation);
+ }
+
+ 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);
+ }
+
+ @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 || isRunningTask()) {
+ // Task will be focused and should use focused task size. Use runningTaskRatio
+ // that is associated with the original orientation of the focused task if possible.
+ boxWidth = taskWidth;
+ boxHeight = taskHeight;
+ thumbnailRatio = isFocusedTask ? getRecentsView().getFocusedTaskRatio() : 0;
+ } 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 = ACCEL_DEACCEL.getInterpolation(mFullscreenProgress);
+ 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);
+ }
+
+ private void setColorTint(float amount) {
+ mSnapshotView.setDimAlpha(amount);
+ mIconView.setIconColorTint(mTintingColor, amount);
+ mDigitalWellBeingToast.setBannerColorTint(mTintingColor, amount);
+ }
+
+ private float getColorTint() {
+ return mTintAmount;
+ }
+
+ /**
+ * Show the task view with a color tint (animates value).
+ */
+ public void showColorTint(boolean enable) {
+ ObjectAnimator tintAnimator = ObjectAnimator.ofFloat(
+ this, COLOR_TINT, enable ? MAX_PAGE_SCRIM_ALPHA : 0);
+ tintAnimator.setAutoCancel(true);
+ tintAnimator.start();
+ }
+
+ /**
+ * 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/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
index a412b39..4f27e21 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.quickstep.util.NavigationModeFeatureFlag.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 (!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..2a7da7e 100644
--- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -3,6 +3,8 @@
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_POSTSUBMIT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -11,12 +13,12 @@
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;
import com.android.launcher3.Launcher;
+import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.quickstep.views.DigitalWellBeingToast;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
@@ -34,9 +36,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..713fd07 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,8 +56,8 @@
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;
@@ -66,6 +68,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,8 +104,7 @@
}
mOrderSensitiveRules = RuleChain
- .outerRule(new FailureRewriterRule())
- .around(new NavigationModeSwitchRule(mLauncher))
+ .outerRule(new NavigationModeSwitchRule(mLauncher))
.around(new FailureWatcher(mDevice));
mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
@@ -112,11 +115,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()));
@@ -166,9 +174,15 @@
protected <T> T getFromRecents(Function<RecentsActivity, T> f) {
if (!TestHelpers.isInLauncherProcess()) return null;
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "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.GET_RECENTS_FAILED, "activity=" + activity);
+ }
if (activity == null) {
return false;
}
@@ -194,8 +208,13 @@
() -> 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 -> {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.GET_RECENTS_FAILED, "isLoading=" +
+ recents.<RecentsView>getOverviewPanel().isLoadingTasks());
+ }
+ assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3);
+ });
// Test flinging forward and backward.
overview.flingForward();
@@ -213,7 +232,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 +249,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 +260,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..5ffe315 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -31,13 +31,11 @@
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.ui.TaplTestsLauncher3;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import com.android.quickstep.views.RecentsView;
@@ -68,11 +66,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 +91,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",
@@ -132,6 +120,7 @@
getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward));
// Test opening a task.
+ startTestActivity(2);
OverviewTask task = mLauncher.pressHome().switchToOverview().getCurrentTask();
assertNotNull("overview.getCurrentTask() returned null (1)", task);
assertNotNull("OverviewTask.open returned null", task.open());
@@ -155,28 +144,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 +156,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 +185,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 +257,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..4ca1f59 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:
@@ -138,13 +138,13 @@
@Test
@NavigationModeSwitch(mode = ZERO_BUTTON)
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
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-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/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/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-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..04bde8f
--- /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="?android:attr/colorBackground" />
+ <corners
+ android:topLeftRadius="?android:attr/dialogCornerRadius"
+ android:topRightRadius="?android:attr/dialogCornerRadius" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/add_item_dialog_button_background.xml b/res/drawable/add_item_dialog_button_background.xml
new file mode 100644
index 0000000..1b4591f
--- /dev/null
+++ b/res/drawable/add_item_dialog_button_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<inset
+ android:insetLeft="@dimen/pin_widget_button_inset_horizontal"
+ android:insetRight="@dimen/pin_widget_button_inset_horizontal"
+ android:insetTop="@dimen/pin_widget_button_inset_vertical"
+ android:insetBottom="@dimen/pin_widget_button_inset_vertical"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:tint="?android:attr/colorAccent" android:shape="rectangle">
+ <corners android:radius="18dp" />
+ <solid android:color="#FFFFFF" />
+ <padding
+ android:left="@dimen/pin_widget_button_padding_horizontal"
+ android:top="@dimen/pin_widget_button_padding_vertical"
+ android:right="@dimen/pin_widget_button_padding_horizontal"
+ android:bottom="@dimen/pin_widget_button_padding_vertical" />
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/go/res/values/dimens.xml b/res/drawable/bg_widgets_searchbox.xml
similarity index 69%
copy from go/res/values/dimens.xml
copy to res/drawable/bg_widgets_searchbox.xml
index f1b1053..2a50a51 100644
--- a/go/res/values/dimens.xml
+++ b/res/drawable/bg_widgets_searchbox.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,7 @@
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
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="?android:attr/textColorPrimaryInverse" />
+ <corners android:radius="24dp" />
+</shape>
\ No newline at end of file
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/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..c0e20f1 100644
--- a/res/drawable/ic_allapps_search.xml
+++ b/res/drawable/ic_allapps_search.xml
@@ -19,6 +19,6 @@
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<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-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_deepshortcut_placeholder.xml b/res/drawable/ic_deepshortcut_placeholder.xml
index 85a9694..3fa8506 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/popupColorSecondary"/>
+ <foreground android:drawable="?attr/popupColorSecondary"/>
+</adaptive-icon>
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-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-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/go/res/values/dimens.xml b/res/drawable/middle_item_primary.xml
similarity index 68%
copy from go/res/values/dimens.xml
copy to res/drawable/middle_item_primary.xml
index f1b1053..0c04ea1 100644
--- a/go/res/values/dimens.xml
+++ b/res/drawable/middle_item_primary.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,8 @@
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
+<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/go/res/values/dimens.xml b/res/drawable/single_item_primary.xml
similarity index 68%
copy from go/res/values/dimens.xml
copy to res/drawable/single_item_primary.xml
index f1b1053..1c0889b 100644
--- a/go/res/values/dimens.xml
+++ b/res/drawable/single_item_primary.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,8 @@
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
+<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/go/res/values/dimens.xml b/res/drawable/single_item_secondary.xml
similarity index 68%
copy from go/res/values/dimens.xml
copy to res/drawable/single_item_secondary.xml
index f1b1053..4edc481 100644
--- a/go/res/values/dimens.xml
+++ b/res/drawable/single_item_secondary.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,8 @@
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
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/popupColorSecondary"/>
+ <corners android:radius="@dimen/popup_single_item_radius" />
+</shape>
\ No newline at end of file
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/go/res/values/dimens.xml b/res/drawable/widget_resize_frame.xml
similarity index 61%
copy from go/res/values/dimens.xml
copy to res/drawable/widget_resize_frame.xml
index f1b1053..d157f5d 100644
--- a/go/res/values/dimens.xml
+++ b/res/drawable/widget_resize_frame.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!--
+ 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.
@@ -13,8 +14,9 @@
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
+<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="?android:attr/colorAccent" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/widgets_list_bottom_ripple.xml b/res/drawable/widgets_list_bottom_ripple.xml
new file mode 100644
index 0000000..3a26091
--- /dev/null
+++ b/res/drawable/widgets_list_bottom_ripple.xml
@@ -0,0 +1,41 @@
+<?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="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:topLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:topRightRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_top_bottom_corner_radius" />
+ </shape>
+ </item>
+ <item android:id="@android:id/background">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground" />
+ <corners
+ android:topLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:topRightRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_top_bottom_corner_radius" />
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/widgets_list_middle_ripple.xml b/res/drawable/widgets_list_middle_ripple.xml
new file mode 100644
index 0000000..da025d7
--- /dev/null
+++ b/res/drawable/widgets_list_middle_ripple.xml
@@ -0,0 +1,42 @@
+<?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="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:topLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:topRightRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_content_corner_radius" />
+ </shape>
+ </item>
+
+ <item android:id="@android:id/background">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground" />
+ <corners
+ android:topLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:topRightRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_content_corner_radius" />
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/widgets_list_single_item_ripple.xml b/res/drawable/widgets_list_single_item_ripple.xml
new file mode 100644
index 0000000..b8b6f42
--- /dev/null
+++ b/res/drawable/widgets_list_single_item_ripple.xml
@@ -0,0 +1,41 @@
+<?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="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:topLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:topRightRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_top_bottom_corner_radius" />
+ </shape>
+ </item>
+ <item android:id="@android:id/background">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground" />
+ <corners
+ android:topLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:topRightRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_top_bottom_corner_radius" />
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/widgets_list_top_ripple.xml b/res/drawable/widgets_list_top_ripple.xml
new file mode 100644
index 0000000..6efc3e1
--- /dev/null
+++ b/res/drawable/widgets_list_top_ripple.xml
@@ -0,0 +1,42 @@
+<?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="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:topLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:topRightRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_content_corner_radius" />
+ </shape>
+ </item>
+
+ <item android:id="@android:id/background">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground" />
+ <corners
+ android:topLeftRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:topRightRadius="@dimen/widget_list_top_bottom_corner_radius"
+ android:bottomLeftRadius="@dimen/widget_list_content_corner_radius"
+ android:bottomRightRadius="@dimen/widget_list_content_corner_radius" />
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/go/res/values/dimens.xml b/res/drawable/widgets_tray_expand_button.xml
similarity index 64%
copy from go/res/values/dimens.xml
copy to res/drawable/widgets_tray_expand_button.xml
index f1b1053..8316e0f 100644
--- a/go/res/values/dimens.xml
+++ b/res/drawable/widgets_tray_expand_button.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,9 @@
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
+<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/layout/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml
index 830255b..d5e7333 100644
--- a/res/layout/add_item_confirmation_activity.xml
+++ b/res/layout/add_item_confirmation_activity.xml
@@ -17,70 +17,51 @@
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/add_item_confirmation"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:padding="24dp"
android:orientation="vertical">
- <ScrollView
+
+ <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" />
+
+ <include layout="@layout/widget_cell"
+ android:id="@+id/widget_cell"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
- android:clipToPadding="false">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <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" />
-
- <FrameLayout
- 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: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" >
-
- <include layout="@layout/widget_cell_content" />
-
- </com.android.launcher3.dragndrop.LivePreviewWidgetCell>
- </FrameLayout>
- </LinearLayout>
- </ScrollView>
+ android:layout_marginVertical="16dp" />
<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" >
+ android:padding="8dp"
+ android:orientation="horizontal">
<Button
- style="?android:attr/buttonBarButtonStyle"
+ style="@style/Widget.DeviceDefault.Button.Rounded.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onCancelClick"
android:text="@android:string/cancel" />
+
+ <Space
+ android:layout_width="4dp"
+ android:layout_height="wrap_content" />
+
<Button
- style="?android:attr/buttonBarButtonStyle"
+ style="@style/Widget.DeviceDefault.Button.Rounded.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onPlaceAutomaticallyClick"
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 67ec664..24d764e 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"
@@ -42,39 +40,10 @@
<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/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/app_widget_resize_frame.xml b/res/layout/app_widget_resize_frame.xml
index 12561b6..53db5ed 100644
--- a/res/layout/app_widget_resize_frame.xml
+++ b/res/layout/app_widget_resize_frame.xml
@@ -18,50 +18,72 @@
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="?android:attr/colorAccent" />
<!-- 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="?android:attr/colorAccent" />
<!-- 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="?android:attr/colorAccent" />
<!-- 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="?android:attr/colorAccent" />
+
+ <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" />
</FrameLayout>
</com.android.launcher3.AppWidgetResizeFrame>
\ No newline at end of file
diff --git a/res/layout/deep_shortcut.xml b/res/layout/deep_shortcut.xml
index 840a8b7..0d11b50 100644
--- a/res/layout/deep_shortcut.xml
+++ b/res/layout/deep_shortcut.xml
@@ -19,6 +19,7 @@
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:background="@drawable/middle_item_primary"
android:theme="@style/PopupItem" >
<com.android.launcher3.shortcuts.DeepShortcutTextView
@@ -31,6 +32,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 +48,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/scrim_view.xml b/res/layout/floating_surface_view.xml
similarity index 76%
copy from res/layout/scrim_view.xml
copy to res/layout/floating_surface_view.xml
index a604d56..434e84f 100644
--- a/res/layout/scrim_view.xml
+++ b/res/layout/floating_surface_view.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher3.views.ScrimView
+<com.android.launcher3.views.FloatingSurfaceView
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
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
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..f34e685 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,12 @@
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="?attr/allAppsScrimColor"
+ android:alpha="0" />
<include
android:id="@+id/apps_view"
@@ -71,6 +75,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..1691680 100644
--- a/res/layout/launcher_preview_layout.xml
+++ b/res/layout/launcher_preview_layout.xml
@@ -17,7 +17,8 @@
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" />
-
- <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>
+ layout="@layout/hotseat" />
</com.android.launcher3.InsettableFrameLayout>
\ No newline at end of file
diff --git a/res/layout/longpress_options_menu.xml b/res/layout/longpress_options_menu.xml
index 20bb5b8..3898365 100644
--- a/res/layout/longpress_options_menu.xml
+++ b/res/layout/longpress_options_menu.xml
@@ -18,7 +18,6 @@
android:id="@+id/deep_shortcuts_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"
diff --git a/res/layout/notification_content.xml b/res/layout/notification_content.xml
index d01be01..147aa30 100644
--- a/res/layout/notification_content.xml
+++ b/res/layout/notification_content.xml
@@ -96,14 +96,6 @@
</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"
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/personal_work_tabs.xml b/res/layout/personal_work_tabs.xml
new file mode 100644
index 0000000..5fb5bcb
--- /dev/null
+++ b/res/layout/personal_work_tabs.xml
@@ -0,0 +1,48 @@
+<?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_tab_height"
+ android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
+ android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
+ android:orientation="horizontal"
+ android:elevation="2dp"
+ 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.workprofile.PersonalWorkSlidingTabStrip>
\ No newline at end of file
diff --git a/res/layout/popup_container.xml b/res/layout/popup_container.xml
index c737407..04822fd 100644
--- a/res/layout/popup_container.xml
+++ b/res/layout/popup_container.xml
@@ -19,8 +19,15 @@
android:id="@+id/deep_shortcuts_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/notification_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:background="?attr/popupColorPrimary"
+ 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/search_container_hotseat.xml
similarity index 80%
rename from res/layout/scrim_view.xml
rename to res/layout/search_container_hotseat.xml
index a604d56..8f12ca0 100644
--- a/res/layout/scrim_view.xml
+++ b/res/layout/search_container_hotseat.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!--
+ 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.
@@ -13,8 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher3.views.ScrimView
+<View
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
+ 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..e3c60ec 100644
--- a/res/layout/secondary_launcher.xml
+++ b/res/layout/secondary_launcher.xml
@@ -67,7 +67,7 @@
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"
@@ -97,7 +97,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/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..9f45f30 100644
--- a/res/layout/system_shortcut.xml
+++ b/res/layout/system_shortcut.xml
@@ -19,6 +19,7 @@
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:background="@drawable/middle_item_primary"
android:theme="@style/PopupItem" >
<com.android.launcher3.BubbleTextView
@@ -30,7 +31,8 @@
android:paddingStart="@dimen/deep_shortcuts_text_padding_start"
android:paddingEnd="@dimen/popup_padding_end"
android:textSize="14sp"
- android:maxLines="2"
+ android:singleLine="true"
+ android:ellipsize="end"
android:textColor="?android:attr/textColorPrimary"
launcher:iconDisplay="shortcut_popup"
launcher:layoutHorizontal="true"
@@ -44,12 +46,4 @@
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" />
-
</com.android.launcher3.shortcuts.DeepShortcutView>
diff --git a/res/layout/system_shortcut_icons.xml b/res/layout/system_shortcut_icons.xml
index a340f4f..f992248 100644
--- a/res/layout/system_shortcut_icons.xml
+++ b/res/layout/system_shortcut_icons.xml
@@ -21,7 +21,7 @@
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_secondary"
android:clipToPadding="true">
<Space android:layout_width="0dp"
diff --git a/res/layout/scrim_view.xml b/res/layout/taskbar_view.xml
similarity index 76%
copy from res/layout/scrim_view.xml
copy to res/layout/taskbar_view.xml
index a604d56..96ae43d 100644
--- a/res/layout/scrim_view.xml
+++ b/res/layout/taskbar_view.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!--
+ 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.
@@ -13,8 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher3.views.ScrimView
+<Space
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
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:visibility="gone" />
\ No newline at end of file
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 923352e..8b18857 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -19,7 +19,6 @@
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 +26,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"
@@ -53,13 +49,10 @@
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..0f6fc6c 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -17,47 +17,65 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
- <LinearLayout
- 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">
+ <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"/>
- <!-- 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"
+ <ImageView
+ android:id="@+id/widget_badge"
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>
+ android:importantForAccessibility="no"
+ android:layout_gravity="end|bottom"
+ android:layout_margin="@dimen/profile_badge_margin"/>
+ </com.android.launcher3.widget.WidgetCellPreview>
- <!-- 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 name of the widget. -->
+ <TextView
+ android:id="@+id/widget_name"
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
+ android:layout_height="wrap_content"
+ 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 original dimensions of the widget -->
+ <TextView
+ android:id="@+id/widget_dims"
+ android:layout_width="match_parent"
+ 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/widgets_bottom_sheet.xml b/res/layout/widgets_bottom_sheet.xml
index 3fdfc96..8002d1d 100644
--- a/res/layout/widgets_bottom_sheet.xml
+++ b/res/layout/widgets_bottom_sheet.xml
@@ -19,36 +19,12 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingTop="28dp"
+ android:paddingTop="16dp"
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..a9d523a
--- /dev/null
+++ b/res/layout/widgets_bottom_sheet_content.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">
+ <View
+ android:layout_width="48dp"
+ android:layout_height="2dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="16dp"
+ 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>
+</merge>
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index f507a88..172284b 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,16 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?android:attr/colorPrimary"
+ android:background="?android:attr/colorBackgroundFloating"
android:elevation="4dp">
- <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"
+ tools:text="No widgets available" />
<!-- Fast scroller popup -->
<TextView
@@ -48,5 +51,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..ae877d4
--- /dev/null
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -0,0 +1,45 @@
+<?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: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/personal_work_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="16dp" />
+</merge>
\ No newline at end of file
diff --git a/res/layout/scrim_view.xml b/res/layout/widgets_full_sheet_recyclerview.xml
similarity index 79%
copy from res/layout/scrim_view.xml
copy to res/layout/widgets_full_sheet_recyclerview.xml
index a604d56..fbe559c 100644
--- a/res/layout/scrim_view.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.launcher3.views.ScrimView
+<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:id="@+id/scrim_view" />
\ No newline at end of file
+ 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..e5df175
--- /dev/null
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -0,0 +1,49 @@
+<?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/search_and_recommendations_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="16dp"
+ android:layout_marginBottom="16dp"
+ android:orientation="vertical">
+ <View
+ android:id="@+id/collapse_handle"
+ android:layout_width="48dp"
+ android:layout_height="2dp"
+ android:layout_marginTop="16dp"
+ android:elevation="2dp"
+ android:layout_gravity="center_horizontal"
+ android:background="?android:attr/textColorSecondary"/>
+ <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="16dp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:text="@string/widget_button_text"/>
+ <include layout="@layout/widgets_search_bar"/>
+
+ <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
+ android:id="@+id/recommended_widget_table"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:visibility="gone" />
+</LinearLayout>
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
new file mode 100644
index 0000000..598041c
--- /dev/null
+++ b/res/layout/widgets_list_row_header.xml
@@ -0,0 +1,78 @@
+<?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"
+ android:id="@+id/widgets_list_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="8dp"
+ android:background="@drawable/widgets_list_middle_ripple"
+ android:layout_marginBottom="@dimen/widget_list_entry_bottom_margin"
+ android:paddingVertical="@dimen/widget_list_header_view_vertical_padding"
+ android:orientation="horizontal">
+
+ <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: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="center_vertical"
+ 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: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_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..e3836df
--- /dev/null
+++ b/res/layout/widgets_search_bar.xml
@@ -0,0 +1,37 @@
+<?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="16dp"
+ android:background="@drawable/bg_widgets_searchbox"
+ android:elevation="2dp">
+
+ <com.android.launcher3.ExtendedEditText
+ android:id="@+id/widgets_search_bar_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="12dp"
+ 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:textColor="?android:attr/textColorSecondary"
+ android:textColorHint="?android:attr/textColorTertiary"/>
+
+ <ImageButton
+ android:id="@+id/widgets_search_cancel_button"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:padding="8dp"
+ android:src="@drawable/ic_gm_close_24"
+ android:background="?android:selectableItemBackground"
+ 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/quickstep/res/layout/scrim_view.xml b/res/layout/widgets_table_container.xml
similarity index 68%
copy from quickstep/res/layout/scrim_view.xml
copy to res/layout/widgets_table_container.xml
index 2cc37f9..0b5f0b9 100644
--- a/quickstep/res/layout/scrim_view.xml
+++ b/res/layout/widgets_table_container.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.quickstep.views.ShelfScrimView
+<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widgets_table"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:id="@+id/scrim_view" />
\ No newline at end of file
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="8dp"
+ android:background="@drawable/widgets_list_middle_ripple"
+ android:layout_marginBottom="@dimen/widget_list_entry_bottom_margin"/>
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/values-night-v26/styles.xml b/res/values-night/styles.xml
similarity index 85%
rename from res/values-night-v26/styles.xml
rename to res/values-night/styles.xml
index 510e1f4..07a5096 100644
--- a/res/values-night-v26/styles.xml
+++ b/res/values-night/styles.xml
@@ -21,6 +21,8 @@
<style name="AppItemActivityTheme" parent="@android:style/Theme.DeviceDefault.Dialog.Alert">
<item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
+ <item name="android:windowBackground">@drawable/add_item_dialog_background</item>
+ <item name="android:windowNoTitle">true</item>
</style>
</resources>
\ No newline at end of file
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
index 09bdaaf..d50115b 100644
--- a/res/values-sw600dp/config.xml
+++ b/res/values-sw600dp/config.xml
@@ -1,3 +1,6 @@
<resources>
<bool name="allow_rotation">true</bool>
+
+ <!-- Hotseat -->
+ <bool name="hotseat_transpose_layout_with_orientation">false</bool>
</resources>
diff --git a/res/values-sw720dp/config.xml b/res/values-sw720dp/config.xml
index 1f401c4..33fc553 100644
--- a/res/values-sw720dp/config.xml
+++ b/res/values-sw720dp/config.xml
@@ -1,10 +1,6 @@
<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/styles.xml b/res/values-sw720dp/styles.xml
index f322e9f..c1e6eca 100644
--- a/res/values-sw720dp/styles.xml
+++ b/res/values-sw720dp/styles.xml
@@ -18,16 +18,6 @@
-->
<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>
@@ -36,5 +26,4 @@
<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-v31/colors.xml b/res/values-v31/colors.xml
new file mode 100644
index 0000000..24aac10
--- /dev/null
+++ b/res/values-v31/colors.xml
@@ -0,0 +1,33 @@
+<?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>
+ <color name="popup_color_primary_light">@android:color/system_neutral1_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_primary_dark">@android:color/system_neutral1_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="workspace_text_color_light">@android:color/system_neutral1_50</color>
+ <color name="workspace_text_color_dark">@android:color/system_neutral1_900</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>
+</resources>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 26e6cba..99f6c75 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");
@@ -30,13 +29,12 @@
<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="folderDotColor" format="color" />
<attr name="folderFillColor" format="color" />
@@ -45,7 +43,6 @@
<attr name="folderTextColor" format="color" />
<attr name="folderHintColor" format="color" />
<attr name="workProfileOverlayTextColor" format="color" />
- <attr name="disabledIconAlpha" format="float" />
<!-- BubbleTextView specific attributes. -->
<declare-styleable name="BubbleTextView">
@@ -57,6 +54,8 @@
<enum name="folder" value="2" />
<enum name="widget_section" value="3" />
<enum name="shortcut_popup" value="4" />
+ <enum name="hero_app" value="5" />
+ <enum name="taskbar" value="6" />
</attr>
<attr name="centerVertically" format="boolean" />
</declare-styleable>
@@ -68,6 +67,12 @@
<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" />
@@ -122,10 +127,24 @@
<!-- 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" />
<attr name="defaultLayoutId" format="reference" />
<attr name="demoModeLayoutId" format="reference" />
+ <attr name="isScalable" format="boolean" />
+ <attr name="devicePaddingId" format="reference" />
+
+ <!-- whether the grid option is shown to the user -->
+ <attr name="visible" format="boolean" />
+
+ </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,6 +152,12 @@
<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" />
@@ -145,6 +170,9 @@
<attr name="allAppsIconSize" format="float" />
<!-- allAppsIconTextSize defaults to iconTextSize, if not specified -->
<attr name="allAppsIconTextSize" format="float" />
+
+ <!-- numAllAppsColumns defaults to GridDisplayOption.numColumns, if not specified -->
+ <attr name="numAllAppsColumns" format="integer" />
</declare-styleable>
<declare-styleable name="CellLayout">
@@ -159,15 +187,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..0b30253 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -35,11 +35,24 @@
<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_fake_previous_task_view_color">#9CCC65</color> <!-- Light Green -->
<color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
<color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
+
+ <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_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="workspace_text_color_light">#FFF</color>
+ <color name="workspace_text_color_dark">#FF212121</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>
+
</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index ca25325..57f626c 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -24,7 +24,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>
@@ -59,17 +59,14 @@
<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" />
@@ -90,8 +87,7 @@
<!-- 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 +109,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"/>
@@ -142,7 +139,7 @@
<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>
@@ -187,4 +184,10 @@
<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..51dddab 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,19 +39,14 @@
<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>
+
<!-- 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="drop_target_vertical_gap">20dp</dimen>
@@ -55,6 +54,13 @@
<!-- 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>
<!-- Fast scroll -->
<dimen name="fastscroll_track_min_width">6dp</dimen>
@@ -78,6 +84,7 @@
<dimen name="fastscroll_end_margin">-26dp</dimen>
<!-- All Apps -->
+ <dimen name="all_apps_open_vertical_translate">96dp</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>
@@ -96,7 +103,6 @@
<!-- The size of corner radius of the arrow in the arrow toast. -->
<dimen name="arrow_toast_corner_radius">2dp</dimen>
-
<!-- Search bar in All Apps -->
<dimen name="all_apps_header_max_elevation">3dp</dimen>
<dimen name="all_apps_header_scroll_to_elevation">16dp</dimen>
@@ -108,8 +114,16 @@
<dimen name="work_profile_footer_text_size">16sp</dimen>
<!-- Widget tray -->
- <dimen name="widget_preview_label_vertical_padding">8dp</dimen>
- <dimen name="widget_preview_label_horizontal_padding">16dp</dimen>
+ <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_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_bottom_margin">2dp</dimen>
<dimen name="widget_preview_shadow_blur">0.5dp</dimen>
<dimen name="widget_preview_key_shadow_distance">1dp</dimen>
@@ -131,6 +145,12 @@
<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>
@@ -147,16 +167,19 @@
<dimen name="drag_flingToDeleteMinVelocity">-1500dp</dimen>
<dimen name="spring_loaded_panel_border">1dp</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,35 +197,33 @@
<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">0dp</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>
@@ -247,10 +268,34 @@
<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>
<!-- Onboarding bottomsheet related -->
<dimen name="bottom_sheet_edu_padding">24dp</dimen>
+ <!-- Search related -->
+ <dimen name="search_hero_title_size">16sp</dimen>
+ <dimen name="search_hero_subtitle_size">14sp</dimen>
+ <dimen name="search_hero_inline_button_size">12sp</dimen>
+ <dimen name="search_settings_icon_size">36dp</dimen>
+ <dimen name="search_settings_icon_vertical_offset">16dp</dimen>
+ <dimen name="search_line_spacing">4dp</dimen>
+ <dimen name="search_decoration_corner_radius">28dp</dimen>
+ <dimen name="search_decoration_padding">1dp</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>
+
</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..39c49bd
--- /dev/null
+++ b/res/values/id.xml
@@ -0,0 +1,21 @@
+<?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="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..1371e91 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -36,15 +36,20 @@
<!-- 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>
@@ -54,6 +59,37 @@
<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>
+ <!-- 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 is no widgets shown in the popup view showing all available widgets
+ installed on the device. [CHAR_LIMIT=none] -->
+ <string name="no_widgets_available">No widgets available</string>
+ <!-- Text shown when there are no matching widget search results for user's search query.
+ [CHAR_LIMIT=none] -->
+ <string name="no_search_results">No search results</string>
<!-- All Apps -->
<!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
@@ -67,15 +103,21 @@
<!-- Label for an icon representing any generic app. [CHAR_LIMIT=50] -->
<string name="label_application">App</string>
+ <!--All apps Search-->
+ <!-- Section title for apps [CHAR_LIMIT=50] -->
+ <string name="search_corpus_apps">Apps</string>
+ <!-- try instant app action for play search result [CHAR_LIMIT=50 -->
+ <string name="search_action_try_now">Try Now</string>
+
<!-- Popup items -->
<!-- Text to display as the header above notifications. [CHAR_LIMIT=30] -->
<string name="notifications_header">Notifications</string>
<!-- 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 -->
@@ -88,10 +130,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>
@@ -178,13 +216,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 -->
+ <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
<string name="styles_wallpaper_button_text">Styles & wallpapers</string>
- <!-- Text for settings button [CHAR LIMIT=20]-->
+ <!-- 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 +264,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>
@@ -344,11 +380,21 @@
<!--- heading shown when user opens work apps tab while work apps are paused -->
<string name="work_apps_paused_title">Work profile is 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">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 profile is paused. 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">Pause work apps</string>
+ <!-- button string shown enable work profile -->
+ <string name="work_apps_enable_btn_text">Turn on</string>
+ <!-- A hint shown in launcher settings develop options filter box -->
+ <string name="developer_options_filter_hint">Filter</string>
<!-- A tip shown pointing at work toggle -->
<string name="work_switch_tip">Pause work apps and notifications</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 25f21f3..a27cdac 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -31,25 +31,25 @@
<style name="LauncherTheme" parent="@style/BaseLauncherTheme">
<item name="android:textColorSecondary">#DE000000</item>
- <item name="allAppsScrimColor">#FFFFFFFF</item>
+ <item name="allAppsScrimColor">?android:attr/colorBackgroundFloating</item>
<item name="allAppsInterimScrimAlpha">46</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="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="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="folderFillColor">?android:attr/colorBackground</item>
<item name="folderIconBorderColor">?android:attr/colorPrimary</item>
- <item name="folderTextColor">#FF212121</item>
+ <item name="folderTextColor">?android:attr/textColorPrimary</item>
<item name="folderHintColor">#89616161</item>
<item name="loadingIconColor">#CCFFFFFF</item>
<item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
@@ -74,7 +74,7 @@
</style>
<style name="LauncherTheme.DarkText" parent="@style/LauncherTheme">
- <item name="workspaceTextColor">#FF212121</item>
+ <item name="workspaceTextColor">@color/workspace_text_color_dark</item>
<item name="allAppsInterimScrimAlpha">128</item>
<item name="workspaceShadowColor">@android:color/transparent</item>
<item name="workspaceAmbientShadowColor">@android:color/transparent</item>
@@ -88,24 +88,24 @@
</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:colorPrimary">#FF212121</item>
- <item name="allAppsScrimColor">#FF000000</item>
+ <item name="allAppsScrimColor">?android:attr/colorBackgroundFloating</item>
<item name="allAppsInterimScrimAlpha">102</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="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="folderDotColor">?android:attr/colorPrimary</item>
+ <item name="folderFillColor">?android:attr/colorBackground</item>
+ <item name="folderIconBorderColor">?android:attr/colorPrimary</item>
+ <item name="folderTextColor">?android:attr/textColorPrimary</item>
<item name="folderHintColor">#89CCCCCC</item>
<item name="isMainColorDark">true</item>
<item name="loadingIconColor">#99FFFFFF</item>
@@ -125,7 +125,7 @@
<item name="allAppsInterimScrimAlpha">25</item>
<item name="folderFillColor">#CDFFFFFF</item>
<item name="folderTextColor">?attr/workspaceTextColor</item>
- <item name="workspaceTextColor">#FF212121</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>
@@ -143,12 +143,23 @@
<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">
+ <style name="AppItemActivityTheme" parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
<item name="widgetsTheme">@style/WidgetContainerTheme</item>
+ <item name="android:windowBackground">@drawable/add_item_dialog_background</item>
+ <item name="android:windowNoTitle">true</item>
</style>
<style name="HomeSettingsTheme" parent="@android:style/Theme.DeviceDefault.Settings">
<item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="preferenceTheme">@style/HomeSettingsPreferenceTheme</item>
+ </style>
+
+ <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>
@@ -212,6 +225,16 @@
<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" >
<item name="android:shadowRadius">2.0</item>
@@ -260,4 +283,8 @@
<item name="android:colorControlHighlight">#DFE1E5</item>
<item name="android:colorForeground">@color/all_apps_bg_hand_fill_dark</item>
</style>
+
+ <style name="Widget.DeviceDefault.Button.Rounded.Colored" parent="@android:style/Widget.DeviceDefault.Button.Colored">
+ <item name="android:background">@drawable/add_item_dialog_button_background</item>
+ </style>
</resources>
diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml
index 3455cb8..e4bea50 100644
--- a/res/xml/launcher_preferences.xml
+++ b/res/xml/launcher_preferences.xml
@@ -15,7 +15,8 @@
-->
<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"
@@ -30,25 +31,31 @@
</intent>
</com.android.launcher3.settings.NotificationDotsPreference>
+ <!--
+ 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..f375549
--- /dev/null
+++ b/res/xml/size_limits_80x104.xml
@@ -0,0 +1,75 @@
+<?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="97dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="16dp"/>
+ <workspaceBottomPadding
+ launcher:a="0.56"
+ launcher:b="0"
+ launcher:c="16dp"/>
+ <hotseatBottomPadding
+ launcher:a="0.44"
+ launcher:b="0"
+ launcher:c="16dp"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="107dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="16dp"/>
+ <workspaceBottomPadding
+ launcher:a="0"
+ launcher:b="36dp"/>
+ <hotseatBottomPadding
+ launcher:a="1"
+ launcher:b="0"
+ launcher:c="52dp"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="9999dp">
+ <workspaceTopPadding
+ launcher:a="0.38"
+ launcher:c="36dp"/>
+ <workspaceBottomPadding
+ launcher:a="0.62"
+ 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..bf32362
--- /dev/null
+++ b/robolectric_tests/Android.bp
@@ -0,0 +1,58 @@
+// 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.runner",
+ "androidx.test.rules",
+ "mockito-robolectric-prebuilt",
+ ],
+ 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/resources/robolectric.properties
similarity index 69%
rename from robolectric_tests/config/robolectric.properties
rename to robolectric_tests/resources/robolectric.properties
index a8e0cb3..abb6968 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/resources/robolectric.properties
@@ -1,19 +1,15 @@
-sdk=29
+sdk=30
+
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
+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..8baf5a3 100644
--- a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
@@ -41,7 +41,6 @@
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 +50,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.
@@ -68,7 +68,7 @@
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;
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/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 3a252dc..fb08c56 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))
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index 87fe3c0..aab6c25 100644
--- a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -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;
@@ -74,7 +75,8 @@
// 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);
+ 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/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/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..dbf4b3e
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.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.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 null;
+ }
+
+ @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..4d151f1 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);
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 0388087..849f98b 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,7 +349,7 @@
}
/**
- * 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 {
@@ -360,7 +362,7 @@
"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/LauncherAppWidgetProviderInfoTest.java b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
new file mode 100644
index 0000000..92f77f2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Point;
+
+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;
+
+@RunWith(RobolectricTestRunner.class)
+public final class LauncherAppWidgetProviderInfoTest {
+
+ private static final int CELL_SIZE = 50;
+
+ 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_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.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.minResizeWidth = 80;
+ info.minResizeHeight = 80;
+ InvariantDeviceProfile idp = createIDP();
+
+ info.initSpans(mContext, idp);
+
+ assertThat(info.minSpanX).isEqualTo(2);
+ assertThat(info.minSpanY).isEqualTo(2);
+ }
+
+ private InvariantDeviceProfile createIDP() {
+ DeviceProfile profile = Mockito.mock(DeviceProfile.class);
+ Mockito.when(profile.getCellSize()).thenReturn(new Point(CELL_SIZE, CELL_SIZE));
+
+ InvariantDeviceProfile idp = new InvariantDeviceProfile();
+ idp.landscapeProfile = idp.portraitProfile = profile;
+ 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..cc36f63
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.google.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].
+ List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderB, mContentE);
+ // GIVEN the user has interacted with B.
+ mHeaderB.setIsWidgetListShown(true);
+
+ // 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);
+ }
+
+
+ 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..6b5678c
--- /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.WidgetPreviewLoader;
+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.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 WidgetPreviewLoader 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, 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..12a092d
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.WidgetPreviewLoader;
+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.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 WidgetPreviewLoader 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,
+ /* searchBarUIHelper= */ null);
+ mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
+ LayoutInflater.from(mTestActivity), mOnHeaderClickListener, 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..e090341
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.WidgetPreviewLoader;
+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.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 WidgetPreviewLoader 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,
+ /* searchBarUIHelper= */ null);
+ mViewHolderBinder = new WidgetsListSearchHeaderViewHolderBinder(
+ LayoutInflater.from(mTestActivity), mOnHeaderClickListener, 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..0935d1c
--- /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.WidgetPreviewLoader;
+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.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 WidgetPreviewLoader 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,
+ /* searchBarUIHelper= */ null);
+ mViewHolderBinder = new WidgetsListTableViewHolderBinder(
+ mContext,
+ LayoutInflater.from(mTestActivity),
+ mOnIconClickListener,
+ mOnLongClickListener,
+ mWidgetPreviewLoader,
+ 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..2d22c45
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.google.test";
+ private final PackageItemInfo mPackageItemInfo = new PackageItemInfo(PACKAGE_NAME);
+ 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(mPackageItemInfo,
+ /* 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(mPackageItemInfo);
+ }
+
+ @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(mPackageItemInfo,
+ /* 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(mPackageItemInfo);
+ }
+
+ @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(mPackageItemInfo,
+ /* 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(mPackageItemInfo);
+ }
+
+ 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..4e6f17c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.content.Context;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.search.SearchAlgorithm;
+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 java.util.ArrayList;
+
+@RunWith(RobolectricTestRunner.class)
+public class WidgetsSearchBarControllerTest {
+
+ private WidgetsSearchBarController mController;
+ private Context mContext;
+ private ExtendedEditText mEditText;
+ private ImageButton mCancelButton;
+ @Mock
+ private SearchModeListener mSearchModeListener;
+ @Mock
+ private SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mEditText = new ExtendedEditText(mContext);
+ mCancelButton = new ImageButton(mContext);
+ mController = new WidgetsSearchBarController(
+ mSearchAlgorithm, mEditText, mCancelButton, mSearchModeListener);
+ }
+
+ @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..e68edd3
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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, 2x3
+ // Row 1: 2x4
+ // Row 2: 4x4
+ assertThat(widgetItemInTable).hasSize(3);
+ assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2, mWidget2x3);
+ assertThat(widgetItemInTable.get(1)).containsExactly(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, 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_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, 2x4
+ // Row 2: 4x4
+ // Row 3: shortcut3, shortcut1, shortcut2
+ assertThat(widgetItemInTable).hasSize(4);
+ assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
+ assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4);
+ assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
+ assertThat(widgetItemInTable.get(3)).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..95cdbdd 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,10 @@
TYPE_SNACKBAR,
TYPE_LISTENER,
TYPE_ALL_APPS_EDU,
-
+ TYPE_DRAG_DROP_POPUP,
TYPE_TASK_MENU,
- TYPE_OPTIONS_POPUP
+ TYPE_OPTIONS_POPUP,
+ TYPE_ICON_SURFACE
})
@Retention(RetentionPolicy.SOURCE)
public @interface FloatingViewType {}
@@ -76,20 +76,23 @@
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_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 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;
// 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 +104,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 +126,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 +143,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 +151,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..5d41bb5 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -2,6 +2,7 @@
import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT;
import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH;
+import static com.android.launcher3.Utilities.ATLEAST_S;
import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X;
import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y;
@@ -13,17 +14,24 @@
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Bundle;
import android.util.AttributeSet;
+import android.util.SizeF;
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 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.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import java.util.ArrayList;
import java.util.List;
@@ -36,13 +44,21 @@
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 =
+ public static final MainThreadInitializedObject<Point[]> CELL_SIZE =
new MainThreadInitializedObject<>(c -> {
InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
return new Point[] {inv.landscapeProfile.getCellSize(),
inv.portraitProfile.getCellSize()};
});
+ // Represents the border spacing size on the grid in the two orientations.
+ public static final MainThreadInitializedObject<int[]> BORDER_SPACING_SIZE =
+ new MainThreadInitializedObject<>(c -> {
+ InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
+ return new int[] {inv.landscapeProfile.cellLayoutBorderSpacingPx,
+ inv.portraitProfile.cellLayoutBorderSpacingPx};
+ });
+
private static final int HANDLE_COUNT = 4;
private static final int INDEX_LEFT = 0;
private static final int INDEX_TOP = 1;
@@ -59,6 +75,7 @@
private LauncherAppWidgetHostView mWidgetView;
private CellLayout mCellLayout;
private DragLayer mDragLayer;
+ private ImageButton mReconfigureButton;
private Rect mWidgetPadding;
@@ -88,6 +105,8 @@
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 +145,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 +171,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;
@@ -171,6 +199,8 @@
mMinHSpan = info.minSpanX;
mMinVSpan = info.minSpanY;
+ mMaxHSpan = info.maxSpanX;
+ mMaxVSpan = info.maxSpanY;
mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(getContext(),
widgetView.getAppWidgetInfo().provider, null);
@@ -183,6 +213,17 @@
mDragHandles[INDEX_RIGHT].setVisibility(GONE);
}
+ mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button);
+ if (info.isReconfigurable()) {
+ mReconfigureButton.setVisibility(VISIBLE);
+ mReconfigureButton.setOnClickListener(view -> mLauncher
+ .getAppWidgetHost()
+ .startConfigActivity(
+ mLauncher,
+ mWidgetView.getAppWidgetId(),
+ Launcher.REQUEST_RECONFIGURE_APPWIDGET));
+ }
+
// 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.
@@ -279,8 +320,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 +343,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 +352,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) {
@@ -351,28 +393,80 @@
}
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);
+ int spanX, int spanY) {
+ List<SizeF> sizes = getWidgetSizes(launcher, spanX, spanY);
+ if (ATLEAST_S) {
+ widgetView.updateAppWidgetSize(new Bundle(), sizes);
+ } else {
+ Rect bounds = getMinMaxSizes(sizes, null /* outRect */);
+ widgetView.updateAppWidgetSize(new Bundle(), bounds.left, bounds.top, bounds.right,
+ bounds.bottom);
+ }
}
- public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
- if (rect == null) {
- rect = new Rect();
- }
+ private static SizeF getWidgetSize(Context context, Point cellSize, int spanX, int spanY,
+ int borderSpacing) {
final float density = context.getResources().getDisplayMetrics().density;
+ final float hBorderSpacing = (spanX - 1) * borderSpacing;
+ final float vBorderSpacing = (spanY - 1) * borderSpacing;
+
+ return new SizeF(((spanX * cellSize.x) + hBorderSpacing) / density,
+ ((spanY * cellSize.y) + vBorderSpacing) / density);
+ }
+
+ /** Returns the list of sizes for a widget of given span, in dp. */
+ public static ArrayList<SizeF> getWidgetSizes(Context context, int spanX, int spanY) {
final Point[] cellSize = CELL_SIZE.get(context);
+ final int[] borderSpacing = BORDER_SPACING_SIZE.get(context);
- // Compute landscape size
- int landWidth = (int) ((spanX * cellSize[0].x) / density);
- int landHeight = (int) ((spanY * cellSize[0].y) / density);
+ SizeF landSize = getWidgetSize(context, cellSize[0], spanX, spanY, borderSpacing[0]);
+ SizeF portSize = getWidgetSize(context, cellSize[1], spanX, spanY, borderSpacing[1]);
- // 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;
+ ArrayList<SizeF> sizes = new ArrayList<>(2);
+ sizes.add(landSize);
+ sizes.add(portSize);
+ return sizes;
+ }
+
+ /**
+ * 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.
+ * @param outRect Rectangle in which the result can be stored, to avoid extra allocations. If
+ * null, a new rectangle will be allocated.
+ * @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.
+ */
+ public static Rect getMinMaxSizes(List<SizeF> sizes, @Nullable Rect outRect) {
+ if (outRect == null) {
+ outRect = new Rect();
+ }
+ if (sizes.isEmpty()) {
+ outRect.set(0, 0, 0, 0);
+ } else {
+ SizeF first = sizes.get(0);
+ outRect.set((int) first.getWidth(), (int) first.getHeight(), (int) first.getWidth(),
+ (int) first.getHeight());
+ for (int i = 1; i < sizes.size(); i++) {
+ outRect.union((int) sizes.get(i).getWidth(), (int) sizes.get(i).getHeight());
+ }
+ }
+ return outRect;
+ }
+
+ /**
+ * Returns the range of sizes a widget may be displayed, given its span.
+ *
+ * @param context Context in which the View is rendered.
+ * @param spanX Width of the widget, in cells.
+ * @param spanY Height of the widget, in cells.
+ * @param outRect Rectangle in which the result can be stored, to avoid extra allocations. If
+ * null, a new rectangle will be allocated.
+ */
+ public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY,
+ @Nullable Rect outRect) {
+ return getMinMaxSizes(getWidgetSizes(context, spanX, spanY), outRect);
}
@Override
@@ -384,8 +478,9 @@
}
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 +571,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 +595,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 +629,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;
}
@@ -537,11 +644,6 @@
}
@Override
- public void logActionCommand(int command) {
- // TODO: Log this case.
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_WIDGET_RESIZE_FRAME) != 0;
}
@@ -581,12 +683,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 +706,24 @@
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);
+ }
}
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..f61bc05 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -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..f77f7e8 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -31,14 +31,11 @@
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;
@@ -82,7 +79,6 @@
new ArrayList<>();
protected DeviceProfile mDeviceProfile;
- protected UserEventDispatcher mUserEventDispatcher;
protected StatsLogManager mStatsLogManager;
protected SystemUiController mSystemUiController;
@@ -144,22 +140,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());
@@ -289,7 +279,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 +332,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..5ba03ed 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -17,7 +17,7 @@
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.DisplayHolder.CHANGE_ROTATION;
import android.app.ActivityOptions;
import android.content.ActivityNotFoundException;
@@ -27,6 +27,8 @@
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Process;
import android.os.StrictMode;
@@ -40,20 +42,25 @@
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;
@@ -74,6 +81,7 @@
protected boolean mIsSafeModeEnabled;
private Runnable mOnStartCallback;
+ private RunnableList mOnResumeCallbacks = new RunnableList();
private int mThemeRes = R.style.AppTheme;
@@ -82,9 +90,9 @@
super.onCreate(savedInstanceState);
- mIsSafeModeEnabled = TraceHelper.whitelistIpcs("isSafeMode",
+ mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> getPackageManager().isSafeMode());
- DefaultDisplay.INSTANCE.get(this).addChangeListener(this);
+ DisplayController.getDefaultDisplay(this).addChangeListener(this);
// Update theme
WallpaperColorInfo.INSTANCE.get(this).addOnChangeListener(this);
@@ -96,6 +104,16 @@
}
@Override
+ protected void onResume() {
+ super.onResume();
+ mOnResumeCallbacks.executeAllAndClear();
+ }
+
+ public void addOnResumeCallback(Runnable callback) {
+ mOnResumeCallbacks.add(callback);
+ }
+
+ @Override
public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
updateTheme();
}
@@ -147,21 +165,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) {
+ 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).toBundle() : null;
UserHandle user = item == null ? null : item.user;
// Prepare intent
@@ -176,25 +208,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 +233,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 +247,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);
@@ -255,7 +279,7 @@
protected void onDestroy() {
super.onDestroy();
WallpaperColorInfo.INSTANCE.get(this).removeOnChangeListener(this);
- DefaultDisplay.INSTANCE.get(this).removeChangeListener(this);
+ DisplayController.getDefaultDisplay(this).removeChangeListener(this);
}
public void runOnceOnStart(Runnable action) {
@@ -300,4 +324,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..d333b49 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,7 +16,7 @@
package com.android.launcher3;
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
+import static com.android.launcher3.graphics.IconShape.getShape;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -26,13 +26,16 @@
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
+import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
+import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.os.Process;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.Property;
@@ -43,8 +46,11 @@
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.config.FeatureFlags;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.FolderIcon;
@@ -52,13 +58,16 @@
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.LauncherIcons;
+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,20 +80,28 @@
* 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;
+ private static final int DISPLAY_HERO_APP = 5;
+ protected static final int DISPLAY_TASKBAR = 6;
- 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);
private float mScaleForReorderBounce = 1f;
+ protected final Paint mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Path mHighlightPath = new Path();
+ protected int mHighlightColor = Color.TRANSPARENT;
+ private final BlurMaskFilter mHighlightShadowFilter;
+
private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
= new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
@Override
@@ -116,7 +133,7 @@
private Drawable mIcon;
private boolean mCenterVertically;
- private final int mDisplay;
+ protected final int mDisplay;
private final CheckLongPressHelper mLongPressHelper;
@@ -145,7 +162,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 +189,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 +198,10 @@
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx);
setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx);
defaultIconSize = grid.folderChildIconSizePx;
+ } else if (mDisplay == DISPLAY_HERO_APP) {
+ defaultIconSize = grid.allAppsIconSizePx;
+ } else if (mDisplay == DISPLAY_TASKBAR) {
+ defaultIconSize = grid.iconSizePx;
} else {
// widget_selection or shortcut_popup
defaultIconSize = grid.iconSizePx;
@@ -196,6 +220,10 @@
setEllipsize(TruncateAt.END);
setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
setTextAlpha(1f);
+
+ int shadowSize = context.getResources().getDimensionPixelSize(
+ R.dimen.blur_size_click_shadow);
+ mHighlightShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.INNER);
}
@Override
@@ -235,6 +263,7 @@
mDotScaleAnim.start();
}
+ @UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
applyFromWorkspaceItem(info, false);
}
@@ -251,16 +280,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 +299,48 @@
// 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);
+ /**
+ * Apply label and tag using a {@link SearchActionItemInfo}
+ */
+ @UiThread
+ public void applyFromSearchActionItemInfo(SearchActionItemInfo searchActionItemInfo) {
+ applyIconAndLabel(searchActionItemInfo);
+ setTag(searchActionItemInfo);
+ }
+
+ @UiThread
+ protected void applyIconAndLabel(ItemInfoWithIcon info) {
+ FastBitmapDrawable iconDrawable = info.newIcon(getContext());
mDotParams.color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
setIcon(iconDrawable);
+ applyLabel(info);
+ }
+
+ @UiThread
+ private void applyLabel(ItemInfoWithIcon info) {
setText(info.title);
if (info.contentDescription != null) {
setContentDescription(info.isDisabled()
@@ -300,6 +350,16 @@
}
/**
+ * Directly set the icon and label.
+ */
+ @UiThread
+ public void applyIconAndLabel(Drawable icon, CharSequence label) {
+ setIcon(icon);
+ setText(label);
+ setContentDescription(label);
+ }
+
+ /**
* Overrides the default long press timeout.
*/
public void setLongPressTimeoutFactor(float longPressTimeoutFactor) {
@@ -331,10 +391,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 +404,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 +427,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);
@@ -391,18 +451,55 @@
@Override
public void onDraw(Canvas canvas) {
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && mHighlightColor != Color.TRANSPARENT) {
+ int count = canvas.save();
+ drawFocusHighlight(canvas);
+ canvas.restoreToCount(count);
+ }
super.onDraw(canvas);
drawDotIfNecessary(canvas);
}
+ protected void drawFocusHighlight(Canvas canvas) {
+ boolean isBadged = getTag() instanceof ItemInfo && !Process.myUserHandle().equals(
+ ((ItemInfo) getTag()).user);
+ float insetScale = (HIGHLIGHT_SCALE - 1) / 2;
+ canvas.translate(-getIconSize() * insetScale, -insetScale * getIconSize());
+ float outlineSize = getIconSize() * HIGHLIGHT_SCALE;
+ mHighlightPath.reset();
+ mHighlightPaint.reset();
+ getIconBounds(mDotParams.iconBounds);
+ getShape().addToPath(mHighlightPath, mDotParams.iconBounds.left, mDotParams.iconBounds.top,
+ outlineSize / 2);
+ if (isBadged) {
+ float borderSize = outlineSize - getIconSize();
+ float badgeSize = LauncherIcons.getBadgeSizeForIconSize(getIconSize()) + borderSize;
+ float badgeInset = outlineSize - badgeSize;
+ getShape().addToPath(mHighlightPath, mDotParams.iconBounds.left + badgeInset,
+ mDotParams.iconBounds.top + badgeInset, badgeSize / 2);
+ }
+ mHighlightPaint.setMaskFilter(mHighlightShadowFilter);
+ mHighlightPaint.setColor(mDotParams.color);
+ canvas.drawPath(mHighlightPath, mHighlightPaint);
+ mHighlightPaint.setMaskFilter(null);
+ mHighlightPaint.setColor(mHighlightColor);
+ canvas.drawPath(mHighlightPath, mHighlightPaint);
+ }
+
/**
* 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 (mActivity instanceof Launcher && ((Launcher) mActivity).isViewInTaskbar(this)) {
+ // TODO: support notification dots in Taskbar
+ return;
+ }
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 +538,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 +602,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 +616,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 +732,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(Drawable icon) {
if (mIsIconVisible) {
applyCompoundDrawables(icon);
}
@@ -607,21 +766,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 +811,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 +822,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 +921,24 @@
@Override
public SafeCloseable prepareDrawDragView() {
- if (getIcon() instanceof FastBitmapDrawable) {
- FastBitmapDrawable icon = (FastBitmapDrawable) getIcon();
- icon.setScale(1f);
- }
+ int highlightColor = mHighlightColor;
+ mHighlightColor = Color.TRANSPARENT;
+ resetIconScale();
setForceHideDot(true);
- return () -> { };
+ return () -> mHighlightColor = highlightColor;
+ }
+
+ 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..4740079 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -21,14 +21,9 @@
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;
@@ -45,11 +40,7 @@
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.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.
@@ -73,6 +64,7 @@
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;
@@ -90,9 +82,6 @@
/** 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;
-
protected CharSequence mText;
protected ColorStateList mOriginalTextColor;
protected Drawable mDrawable;
@@ -102,7 +91,6 @@
private int mToolTipLocation;
private AnimatorSet mCurrentColorAnim;
- @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter;
public ButtonDropTarget(Context context, AttributeSet attrs) {
this(context, attrs, 0);
@@ -178,8 +166,7 @@
mToolTip.showAsDropDown(this, x, y);
}
- d.dragView.setColor(mHoverColor);
- animateTextColor(mHoverColor);
+ d.dragView.setAlpha(DRAG_VIEW_HOVER_OVER_OPACITY);
if (d.stateAnnouncer != null) {
d.stateAnnouncer.cancel();
}
@@ -191,57 +178,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);
} 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);
+ mActive = !options.isKeyboardDrag && supportsDrop(dragObject.dragInfo);
mDrawable.setColorFilter(null);
if (mCurrentColorAnim != null) {
mCurrentColorAnim.cancel();
@@ -395,6 +346,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..c16c44b 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -16,6 +16,8 @@
package com.android.launcher3;
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
import android.animation.Animator;
@@ -50,12 +52,14 @@
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
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.AppWidgetHostViewDrawable;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.PreviewBackground;
import com.android.launcher3.graphics.DragPreviewProvider;
@@ -86,6 +90,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;
@@ -174,6 +180,7 @@
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;
@@ -203,14 +210,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);
@@ -227,7 +237,7 @@
mBackground.setCallback(this);
mBackground.setAlpha(0);
- mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * grid.iconSizePx);
+ mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * deviceProfile.iconSizePx);
// Initialize the data structures used for the drag visualization.
mEaseOutInterpolator = Interpolators.DEACCEL_2_5; // Quint ease out
@@ -286,7 +296,8 @@
}
mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType);
- mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+ mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+ mBorderSpacing);
addView(mShortcutsAndWidgets);
}
@@ -303,6 +314,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 +323,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 +341,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 +356,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 +366,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();
}
@@ -469,8 +488,8 @@
for (int j = 0; j < mCountY; j++) {
canvas.save();
- int transX = i * mCellWidth;
- int transY = j * mCellHeight;
+ int transX = i * mCellWidth + (i * mBorderSpacing);
+ int transY = j * mCellHeight + (j * mBorderSpacing);
canvas.translate(getPaddingLeft() + transX, getPaddingTop() + transY);
@@ -585,6 +604,9 @@
if (child instanceof BubbleTextView) {
BubbleTextView bubbleChild = (BubbleTextView) child;
bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
+ if (mActivity.getDeviceProfile().isScalableGrid) {
+ bubbleChild.setCenterVertically(mContainerType != HOTSEAT);
+ }
}
child.setScaleX(mChildScale);
@@ -700,11 +722,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 +748,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 +781,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,10 +839,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);
+ return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth)
+ - ((mCountX - 1) * mBorderSpacing);
}
public Drawable getScrimBackground() {
@@ -851,8 +859,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,
@@ -956,15 +964,18 @@
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) {
mDragCell[0] = cellX;
mDragCell[1] = cellY;
+ applyColorExtraction(dragObject, mDragCell, spanX, spanY);
+
+ if (outlineProvider == null || outlineProvider.generatedDragOutline == null) {
+ return;
+ }
+
+ Bitmap dragOutline = outlineProvider.generatedDragOutline;
+
final int oldIndex = mDragOutlineCurrent;
mDragOutlineAnims[oldIndex].animateOut();
mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
@@ -983,11 +994,11 @@
}
// Center horizontaly
- left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
+ left += (r.width() - dragOutline.getWidth()) / 2;
if (v != null && v.getViewType() == DraggableView.DRAGGABLE_WIDGET) {
// Center vertically
- top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
+ top += (r.height() - 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));
@@ -1006,6 +1017,22 @@
}
}
+ /** Applies the local color extraction to a dragging widget object. */
+ private void applyColorExtraction(DropTarget.DragObject dragObject, int[] targetCell, int spanX,
+ int spanY) {
+ // Apply local extracted color if the DragView is an AppWidgetHostViewDrawable.
+ Drawable drawable = dragObject.dragView.getDrawable();
+ if (drawable instanceof AppWidgetHostViewDrawable) {
+ Workspace workspace =
+ Launcher.getLauncher(dragObject.dragView.getContext()).getWorkspace();
+ int screenId = workspace.getIdForScreen(this);
+ int pageId = workspace.getPageIndexForScreenId(screenId);
+ AppWidgetHostViewDrawable hostViewDrawable = ((AppWidgetHostViewDrawable) drawable);
+ cellToRect(targetCell[0], targetCell[1], spanX, spanY, mTempRect);
+ hostViewDrawable.getAppWidgetHostView().handleDrag(mTempRect, pageId);
+ }
+ }
+
@SuppressLint("StringFormatMatches")
public String getItemMoveDescription(int cellX, int cellY) {
if (mContainerType == HOTSEAT) {
@@ -2009,7 +2036,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 +2174,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 +2184,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;
@@ -2515,10 +2542,11 @@
final int hStartPadding = getPaddingLeft();
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 +2564,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 +2685,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 +2711,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..e46aad2 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,9 +53,6 @@
@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);
}
@@ -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..aea38a0 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -16,6 +16,9 @@
package com.android.launcher3;
+import static com.android.launcher3.ResourceUtils.pxFromDp;
+import static com.android.launcher3.Utilities.dpiFromPx;
+
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -23,14 +26,21 @@
import android.graphics.PointF;
import android.graphics.Rect;
import android.view.Surface;
+import android.view.WindowInsets;
+import android.view.WindowManager;
import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.DevicePaddings.DevicePadding;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.icons.DotRenderer;
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;
+
public class DeviceProfile {
private static final float TABLET_MIN_DPS = 600;
@@ -38,7 +48,7 @@
public final InvariantDeviceProfile inv;
- private final DefaultDisplay.Info mInfo;
+ private final Info mInfo;
// Device properties
public final boolean isTablet;
@@ -59,6 +69,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
@@ -72,18 +84,27 @@
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;
@@ -93,10 +114,19 @@
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;
@@ -117,12 +147,18 @@
public final int hotseatBarSidePaddingEndPx;
// All apps
+ public int allAppsOpenVerticalTranslate;
public int allAppsCellHeightPx;
public int allAppsCellWidthPx;
public int allAppsIconSizePx;
public int allAppsIconDrawablePaddingPx;
public float allAppsIconTextSizePx;
+ // Overview
+ public int overviewTaskMarginPx;
+ public int overviewTaskIconSizePx;
+ public int overviewTaskThumbnailTopMarginPx;
+
// Widgets
public final PointF appWidgetScale = new PointF(1.0f, 1.0f);
@@ -140,7 +176,13 @@
public DotRenderer mDotRendererWorkSpace;
public DotRenderer mDotRendererAllApps;
- DeviceProfile(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info,
+ // 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;
+
+ DeviceProfile(Context context, InvariantDeviceProfile inv, Info info,
Point minSize, Point maxSize, int width, int height, boolean isLandscape,
boolean isMultiWindowMode, boolean transposeLayoutWithOrientation,
Point windowPosition) {
@@ -148,46 +190,89 @@
this.inv = inv;
this.isLandscape = isLandscape;
this.isMultiWindowMode = isMultiWindowMode;
+ this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
windowX = windowPosition.x;
windowY = windowPosition.y;
+ isScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
+
// Determine sizes.
widthPx = width;
heightPx = height;
+ int nonFinalAvailableHeightPx;
if (isLandscape) {
availableWidthPx = maxSize.x;
- availableHeightPx = minSize.y;
+ nonFinalAvailableHeightPx = minSize.y;
} else {
availableWidthPx = minSize.x;
- availableHeightPx = maxSize.y;
+ nonFinalAvailableHeightPx = maxSize.y;
}
mInfo = info;
// 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;
+ float swDPs = dpiFromPx(Math.min(info.smallestSize.x, info.smallestSize.y), info.metrics);
+ boolean allowRotation = context.getResources().getBoolean(R.bool.allow_rotation);
+ // Tablet UI is built with assumption that simulated landscape is disabled.
+ isTablet = allowRotation && swDPs >= TABLET_MIN_DPS;
+ isLargeTablet = isTablet && 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);
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 = DisplayController.INSTANCE.get(context).getHolder(mInfo.id)
+ .getDisplayContext().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, mInfo.metrics, 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 (FeatureFlags.ENABLE_TWO_PANEL_HOME.get() && isTablet) {
+ cellLayoutPaddingLeftRightPx =
+ res.getDimensionPixelSize(R.dimen.two_panel_home_side_padding);
+ cellLayoutBottomPaddingPx = 0;
+ } else if (isLandscape) {
cellLayoutPaddingLeftRightPx = 0;
cellLayoutBottomPaddingPx = cellLayoutPadding;
} else {
@@ -217,17 +302,40 @@
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)
+ int hotseatExtraVerticalSize =
+ res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size);
+ hotseatBarSizePx = pxFromDp(inv.iconSize, mInfo.metrics, 1f)
+ (isVerticalBarLayout()
? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
- : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
- + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
+ : (hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx
+ + (isScalableGrid ? 0 : hotseatExtraVerticalSize)));
+
+ 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;
// 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 / iconScale);
+ 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 * iconScale);
+ workspaceBottomPadding = Math.round(paddingWorkspaceBottom * iconScale);
+ extraHotseatBottomPadding = Math.round(paddingHotseatBottom * iconScale);
+
+ hotseatBarSizePx += extraHotseatBottomPadding;
+ hotseatBarBottomPaddingPx += extraHotseatBottomPadding;
+ } 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.
@@ -250,6 +358,24 @@
IconShape.DEFAULT_PATH_SIZE);
}
+ 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.
+ return cellLayoutBorderSpacingPx > widgetPadding.left
+ && cellLayoutBorderSpacingPx > widgetPadding.top
+ && cellLayoutBorderSpacingPx > widgetPadding.right
+ && cellLayoutBorderSpacingPx > widgetPadding.bottom;
+ }
+
public Builder toBuilder(Context context) {
Point size = new Point(availableWidthPx, availableHeightPx);
return new Builder(context, inv, mInfo)
@@ -279,12 +405,7 @@
.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.
@@ -305,37 +426,74 @@
}
/**
- * 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 = iconSizePx;
+ 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 +501,46 @@
* 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) {
+ iconScale = scale;
+
// Workspace
final boolean isVerticalLayout = isVerticalBarLayout();
float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
- iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
- * scale));
+ iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
- 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, mInfo.metrics, scale);
+ cellHeightPx = pxFromDp(inv.minCellHeight, mInfo.metrics, scale);
+ int cellContentHeight = iconSizePx + iconDrawablePaddingPx
+ + Utilities.calculateTextHeight(iconTextSizePx);
+ cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+ desiredWorkspaceLeftRightMarginPx = (int) (desiredWorkspaceLeftRightOriginalPx * scale);
+ } else {
+ cellWidthPx = iconSizePx + iconDrawablePaddingPx;
+ cellHeightPx = iconSizePx + 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);
+ allAppsIconSizePx = pxFromDp(inv.allAppsIconSize, mInfo.metrics);
allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
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,9 +549,8 @@
}
allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
- if (isVerticalBarLayout()) {
- // Always hide the Workspace text with vertical bar layout.
- adjustToHideWorkspaceLabels();
+ if (isVerticalLayout) {
+ hideWorkspaceLabelsIfNotEnoughSpace();
}
// Hotseat
@@ -411,25 +578,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 +607,31 @@
}
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, mInfo.metrics, scale));
+ folderChildTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, 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) {
+ folderCellWidthPx = (int) (cellWidthPx * scale);
+ folderCellHeightPx = (int) (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);
}
@@ -476,9 +659,9 @@
// not matter.
Point padding = getTotalWorkspacePadding();
result.x = calculateCellWidth(availableWidthPx - padding.x
- - cellLayoutPaddingLeftRightPx * 2, numColumns);
+ - cellLayoutPaddingLeftRightPx * 2, cellLayoutBorderSpacingPx, numColumns);
result.y = calculateCellHeight(availableHeightPx - padding.y
- - cellLayoutBottomPaddingPx, numRows);
+ - cellLayoutBottomPaddingPx, cellLayoutBorderSpacingPx, numRows);
return result;
}
@@ -505,8 +688,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 +699,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 (FeatureFlags.ENABLE_TWO_PANEL_HOME.get()) {
+ 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);
}
@@ -570,19 +759,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;
}
/**
@@ -606,8 +796,14 @@
*/
public boolean updateIsSeascape(Context context) {
if (isVerticalBarLayout()) {
- boolean isSeascape = DefaultDisplay.INSTANCE.get(context).getInfo().rotation
- == Surface.ROTATION_270;
+ // Check an up-to-date info.
+ DisplayController.Info displayInfo = DisplayController.getDefaultDisplay(context)
+ .createInfoForContext(context);
+ if (displayInfo == null) {
+ return false;
+ }
+
+ boolean isSeascape = displayInfo.rotation == Surface.ROTATION_270;
if (mIsSeascape != isSeascape) {
mIsSeascape = isSeascape;
return true;
@@ -638,7 +834,103 @@
}
}
- 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, mInfo.metrics) + "dp)";
+ }
+
+ public void dump(String prefix, PrintWriter writer) {
+ writer.println(prefix + "DeviceProfile:");
+ writer.println(prefix + "\t1 dp = " + mInfo.metrics.density + " px");
+
+ writer.println(prefix + "\tisTablet:" + isTablet);
+ writer.println(prefix + "\tisLargeTablet:" + isLargeTablet);
+ writer.println(prefix + "\tisPhone:" + isPhone);
+ writer.println(prefix + "\ttransposeLayoutWithOrientation:"
+ + transposeLayoutWithOrientation);
+
+ writer.println(prefix + "\tisLandscape:" + isLandscape);
+ writer.println(prefix + "\tisMultiWindowMode:" + isMultiWindowMode);
+
+ 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 + 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 + 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 + "\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("scaleToFit", iconScale));
+ 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;
@@ -662,7 +954,7 @@
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;
@@ -672,7 +964,7 @@
private boolean mIsMultiWindowMode = false;
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;
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..c768493 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -131,7 +131,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 +145,6 @@
}
}
} else {
- int visibleCount = getVisibleButtonsCount();
int availableWidth = width / visibleCount;
boolean textVisible = true;
for (ButtonDropTarget buttons : mDropTargets) {
@@ -165,7 +167,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 +183,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..c79dabe 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -24,6 +24,7 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.UiThreadHelper;
@@ -131,6 +132,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..5429806 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -16,30 +16,41 @@
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 View mTaskbarView;
+ private final int mTaskbarViewHeight;
public Hotseat(Context context) {
this(context, null);
@@ -51,6 +62,15 @@
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);
+
+ mTaskbarView = LayoutInflater.from(context).inflate(R.layout.taskbar_view, this, false);
+ mTaskbarViewHeight = mTaskbarView.getLayoutParams().height;
+ // We want taskbar in the back so its background applies to Hotseat as well.
+ addView(mTaskbarView, 0);
}
/**
@@ -79,20 +99,12 @@
}
@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 +114,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 +165,76 @@
}
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));
+ mTaskbarView.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mTaskbarViewHeight, 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;
+
+ DeviceProfile dp = mActivity.getDeviceProfile();
+ int freeSpace = dp.isTaskbarPresent
+ ? dp.workspacePadding.bottom
+ : dp.hotseatBarSizePx - dp.hotseatCellHeightPx - mQsbHeight;
+ int bottom = b - t
+ - (int) (freeSpace * QSB_CENTER_FACTOR)
+ - (dp.isTaskbarPresent ? dp.taskbarSize : dp.getInsets().bottom);
+ int top = bottom - mQsbHeight;
+ mQsb.layout(left, top, right, bottom);
+
+ int taskbarWidth = mTaskbarView.getMeasuredWidth();
+ left = (r - l - taskbarWidth) / 2;
+ right = left + taskbarWidth;
+ bottom = b - t;
+ top = bottom - mTaskbarViewHeight;
+ mTaskbarView.layout(left, top, right, bottom);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Returns the Taskbar inside hotseat
+ */
+ public View getTaskbarView() {
+ return mTaskbarView;
+ }
}
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..754e988 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -18,7 +18,8 @@
import static com.android.launcher3.Utilities.getDevicePrefs;
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_FOUR_COLUMNS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
@@ -48,8 +49,8 @@
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.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;
@@ -111,6 +112,10 @@
public float allAppsIconSize;
public float allAppsIconTextSize;
+ public float minCellHeight;
+ public float minCellWidth;
+ public float borderSpacing;
+
private SparseArray<TypedValue> mExtraAttrs;
/**
@@ -123,6 +128,12 @@
*/
public int numAllAppsColumns;
+ /**
+ * Do not query directly. see {@link DeviceProfile#isScalableGrid}.
+ */
+ protected boolean isScalable;
+ public int devicePaddingId;
+
public String dbFile;
public int defaultLayoutId;
int demoModeLayoutId;
@@ -130,6 +141,8 @@
public DeviceProfile landscapeProfile;
public DeviceProfile portraitProfile;
+ @Nullable public DevicePaddings devicePaddings;
+
public Point defaultWallpaperSize;
public Rect defaultWidgetPadding;
@@ -152,6 +165,11 @@
iconTextSize = p.iconTextSize;
numHotseatIcons = p.numHotseatIcons;
numAllAppsColumns = p.numAllAppsColumns;
+ isScalable = p.isScalable;
+ devicePaddingId = p.devicePaddingId;
+ minCellHeight = p.minCellHeight;
+ minCellWidth = p.minCellWidth;
+ borderSpacing = p.borderSpacing;
dbFile = p.dbFile;
allAppsIconSize = p.allAppsIconSize;
allAppsIconTextSize = p.allAppsIconTextSize;
@@ -159,6 +177,7 @@
demoModeLayoutId = p.demoModeLayoutId;
mExtraAttrs = p.mExtraAttrs;
mOverlayMonitor = p.mOverlayMonitor;
+ devicePaddings = p.devicePaddings;
}
@TargetApi(23)
@@ -173,8 +192,7 @@
.putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(numColumns, numRows))
.apply();
- mConfigMonitor = new ConfigMonitor(context,
- APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
+ mConfigMonitor = new ConfigMonitor(context, this::onConfigChanged);
mOverlayMonitor = new OverlayMonitor(context);
}
@@ -198,7 +216,7 @@
// Get the display info based on default display and interpolate it to existing display
DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
- DefaultDisplay.INSTANCE.get(context).getInfo(),
+ DisplayController.getDefaultDisplay(context).getInfo(),
getPredefinedDeviceProfiles(context, gridName));
Info myInfo = new Info(context, display);
@@ -209,12 +227,27 @@
.add(myDisplayOption);
result.iconSize = defaultDisplayOption.iconSize;
result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
- result.allAppsIconSize = Math.min(
- defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
+ if (defaultDisplayOption.allAppsIconSize < myDisplayOption.allAppsIconSize) {
+ result.allAppsIconSize = defaultDisplayOption.allAppsIconSize;
+ result.numAllAppsColumns = defaultDisplayOption.numAllAppsColumns;
+ } else {
+ result.allAppsIconSize = myDisplayOption.allAppsIconSize;
+ result.numAllAppsColumns = myDisplayOption.numAllAppsColumns;
+ }
+ result.minCellHeight = defaultDisplayOption.minCellHeight;
+ result.minCellWidth = defaultDisplayOption.minCellWidth;
+ result.borderSpacing = defaultDisplayOption.borderSpacing;
+
initGrid(context, myInfo, result);
}
public static String getCurrentGridName(Context context) {
+ if (ENABLE_TWO_PANEL_HOME.get()) {
+ return ENABLE_TWO_PANEL_HOME.key;
+ }
+ if (ENABLE_FOUR_COLUMNS.get()) {
+ return ENABLE_FOUR_COLUMNS.key;
+ }
return Utilities.isGridOptionsEnabled(context)
? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null;
}
@@ -231,7 +264,7 @@
}
private String initGrid(Context context, String gridName) {
- DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(context).getInfo();
+ Info displayInfo = DisplayController.getDefaultDisplay(context).getInfo();
ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
@@ -240,7 +273,7 @@
}
private void initGrid(
- Context context, DefaultDisplay.Info displayInfo, DisplayOption displayOption) {
+ Context context, Info displayInfo, DisplayOption displayOption) {
GridOption closestProfile = displayOption.grid;
numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns;
@@ -250,7 +283,8 @@
demoModeLayoutId = closestProfile.demoModeLayoutId;
numFolderRows = closestProfile.numFolderRows;
numFolderColumns = closestProfile.numFolderColumns;
- numAllAppsColumns = closestProfile.numAllAppsColumns;
+ isScalable = closestProfile.isScalable;
+ devicePaddingId = closestProfile.devicePaddingId;
mExtraAttrs = closestProfile.extraAttrs;
@@ -261,6 +295,11 @@
iconTextSize = displayOption.iconTextSize;
fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
+ minCellHeight = displayOption.minCellHeight;
+ minCellWidth = displayOption.minCellWidth;
+ borderSpacing = displayOption.borderSpacing;
+ numAllAppsColumns = Math.round(displayOption.numAllAppsColumns);
+
if (Utilities.isGridOptionsEnabled(context)) {
allAppsIconSize = displayOption.allAppsIconSize;
allAppsIconTextSize = displayOption.allAppsIconTextSize;
@@ -269,6 +308,10 @@
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);
@@ -313,11 +356,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.
@@ -466,7 +504,7 @@
@VisibleForTesting
static DisplayOption invDistWeightedInterpolate(
- DefaultDisplay.Info displayInfo, ArrayList<DisplayOption> points) {
+ Info displayInfo, ArrayList<DisplayOption> points) {
Point smallestSize = new Point(displayInfo.smallestSize);
Point largestSize = new Point(displayInfo.largestSize);
@@ -581,11 +619,15 @@
private final int numHotseatIcons;
private final String dbFile;
- private final int numAllAppsColumns;
private final int defaultLayoutId;
private final int demoModeLayoutId;
+ private final boolean isScalable;
+ private final int devicePaddingId;
+
+ public final boolean visible;
+
private final SparseArray<TypedValue> extraAttrs;
public GridOption(Context context, AttributeSet attrs) {
@@ -606,8 +648,13 @@
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);
+
+ visible = a.getBoolean(R.styleable.GridDisplayOption_visible, true);
a.recycle();
@@ -623,6 +670,11 @@
private final float minHeightDps;
private final boolean canBeDefault;
+ private float numAllAppsColumns;
+ private float minCellHeight;
+ private float minCellWidth;
+ private float borderSpacing;
+
private float iconSize;
private float iconTextSize;
private float landscapeIconSize;
@@ -639,6 +691,12 @@
minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0);
canBeDefault = a.getBoolean(
R.styleable.ProfileDisplayOption_canBeDefault, false);
+ numAllAppsColumns = a.getInt(R.styleable.ProfileDisplayOption_numAllAppsColumns,
+ grid.numColumns);
+
+ 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,
@@ -661,23 +719,35 @@
minWidthDps = 0;
minHeightDps = 0;
canBeDefault = false;
+ numAllAppsColumns = 0;
+ minCellHeight = 0;
+ minCellWidth = 0;
+ borderSpacing = 0;
}
private DisplayOption multiply(float w) {
+ numAllAppsColumns *= w;
iconSize *= w;
landscapeIconSize *= w;
allAppsIconSize *= w;
iconTextSize *= w;
allAppsIconTextSize *= w;
+ minCellHeight *= w;
+ minCellWidth *= w;
+ borderSpacing *= w;
return this;
}
private DisplayOption add(DisplayOption p) {
+ numAllAppsColumns += p.numAllAppsColumns;
iconSize += p.iconSize;
landscapeIconSize += p.landscapeIconSize;
allAppsIconSize += p.allAppsIconSize;
iconTextSize += p.iconTextSize;
allAppsIconTextSize += p.allAppsIconTextSize;
+ minCellHeight += p.minCellHeight;
+ minCellWidth += p.minCellWidth;
+ borderSpacing += p.borderSpacing;
return this;
}
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0970dae..5091543 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,20 @@
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.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 +60,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 +77,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 +98,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 +111,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 +122,26 @@
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.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.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 +152,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 +159,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 +171,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 +179,21 @@
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 +271,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 +306,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 +341,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 +377,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,7 +427,7 @@
idp.addOnChangeListener(this);
mSharedPrefs = Utilities.getPrefs(this);
mIconCache = app.getIconCache();
- mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);
+ mAccessibilityDelegate = createAccessibilityDelegate();
mDragController = new DragController(this);
mAllAppsController = new AllAppsTransitionController(this);
@@ -389,11 +442,9 @@
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 +492,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,6 +546,7 @@
public void onEnterAnimationComplete() {
super.onEnterAnimationComplete();
mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
+ AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
}
@Override
@@ -529,7 +567,6 @@
}
private void onIdpChanged(InvariantDeviceProfile idp) {
- mUserEventDispatcher = null;
initDeviceProfile(idp);
dispatchDeviceProfileChanged();
@@ -543,15 +580,7 @@
}
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);
}
private void initDeviceProfile(InvariantDeviceProfile idp) {
@@ -814,7 +843,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,
@@ -884,8 +913,8 @@
mOverlayManager.onActivityStopped(this);
}
- logStopAndResume(Action.Command.STOP);
- mAppWidgetHost.setListenIfResumed(false);
+ logStopAndResume(false /* isResume */);
+ mAppWidgetHost.setActivityStarted(false);
NotificationListener.removeNotificationsChangedListener();
}
@@ -898,57 +927,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 +1028,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 +1036,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 +1066,19 @@
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 (mPrevLauncherState != state && !ALL_APPS.equals(state)
+ // Making sure mAllAppsSessionLogId is not null to avoid double logging.
+ && mAllAppsSessionLogId != null) {
+ getStatsLogManager().logger().log(LAUNCHER_ALLAPPS_EXIT);
+ mAllAppsSessionLogId = null;
+ }
}
@Override
@@ -1043,15 +1087,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 +1099,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 +1109,7 @@
if (!mDeferOverlayCallbacks) {
mOverlayManager.onActivityPaused(this);
}
+ mAppWidgetHost.setActivityResumed(false);
}
class LauncherOverlayCallbacksImpl implements LauncherOverlayCallbacks {
@@ -1098,7 +1134,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 +1191,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(mAppsView);
}
/**
@@ -1195,16 +1234,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");
@@ -1338,6 +1374,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;
}
@@ -1423,7 +1476,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
@@ -1437,24 +1490,36 @@
}
// 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 +1596,7 @@
LauncherAppState.getIDP(this).removeOnChangeListener(this);
mOverlayManager.onActivityDestroyed(this);
- mAppTransitionManager.unregisterRemoteAnimations();
mUserChangedCallbackCloseable.close();
- mAllAppsController.onActivityDestroyed();
}
public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1727,15 +1790,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 +1878,7 @@
mTouchInProgress = true;
break;
case MotionEvent.ACTION_UP:
- mLastTouchUpTime = System.currentTimeMillis();
+ mLastTouchUpTime = SystemClock.uptimeMillis();
// Follow through
case MotionEvent.ACTION_CANCEL:
mTouchInProgress = false;
@@ -1830,16 +1918,6 @@
@TargetApi(Build.VERSION_CODES.M)
@Override
- public ActivityOptions getActivityLaunchOptions(View v) {
- return mAppTransitionManager.getActivityLaunchOptions(this, v);
- }
-
- public LauncherAppTransitionManager getAppTransitionManager() {
- return mAppTransitionManager;
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- @Override
protected boolean onErrorStartingShortcut(Intent intent, ItemInfo info) {
// Due to legacy reasons, direct call shortcuts require Launchers to have the
// corresponding permission. Show the appropriate permission prompt if that
@@ -1860,13 +1938,19 @@
}
@Override
- public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
- @Nullable String sourceContainer) {
+ public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
+ if (isViewInTaskbar(v)) {
+ // Start the activity without the hacky workarounds below, which assume the View was
+ // clicked when Launcher was resumed and will be hidden until Launcher is re-resumed
+ // (this isn't the case for Taskbar).
+ return super.startActivitySafely(v, intent, 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 +1958,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 +1966,7 @@
// state when we return to launcher.
BubbleTextView btv = (BubbleTextView) v;
btv.setStayPressed(true);
- addOnResumeCallback(btv);
+ addOnResumeCallback(() -> btv.setStayPressed(false));
}
return success;
}
@@ -1921,15 +2005,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.
@@ -2174,9 +2254,6 @@
workspace.requestLayout();
}
- @Override
- public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
-
/**
* Add the views for a widget to the workspace.
*/
@@ -2405,8 +2482,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 +2503,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 +2528,7 @@
@Override
public void bindAllApplications(AppInfo[] apps, int flags) {
mAppsView.getAppsStore().setApps(apps, flags);
+ PopupContainerWithArrow.dismissInvalidPopup(this);
}
/**
@@ -2463,8 +2541,8 @@
}
@Override
- public void bindPromiseAppProgressUpdated(PromiseAppInfo app) {
- mAppsView.getAppsStore().updatePromiseAppProgress(app);
+ public void bindIncrementalDownloadProgressUpdated(AppInfo app) {
+ mAppsView.getAppsStore().updateProgressBar(app);
}
@Override
@@ -2479,9 +2557,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 +2585,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 +2644,7 @@
mDragLayer.dump(prefix, writer);
mStateManager.dump(prefix, writer);
mPopupDataProvider.dump(prefix, writer);
+ mDeviceProfile.dump(prefix, writer);
try {
FileLog.flushAll(writer);
@@ -2590,19 +2671,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 +2691,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 +2730,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 +2762,9 @@
return Stream.of(APP_INFO, WIDGETS, INSTALL);
}
+ protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
+ return new LauncherAccessibilityDelegate(this);
+ }
/**
* @see LauncherState#getOverviewScaleAndOffset(Launcher)
@@ -2708,6 +2773,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 +2791,63 @@
return (T) activityContext;
}
+ /**
+ * 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.
+ */
+ private void crossFadeWithPreviousAppearance() {
+ NonConfigInstance lastInstance = (NonConfigInstance) getLastNonConfigurationInstance();
+
+ 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();
+ }
/**
- * Callback for listening for onResume
+ * @return Whether the View is in the same window as the Taskbar window.
*/
- public interface OnResumeCallback {
+ public boolean isViewInTaskbar(View v) {
+ return false;
+ }
- void onLauncherResume();
+ 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..803f8d2 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -16,6 +16,9 @@
package com.android.launcher3;
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.util.IntProperty;
@@ -29,8 +32,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 +134,23 @@
return view.getAlpha();
}
};
+
+ /**
+ * 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..4754558 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,8 +17,11 @@
package com.android.launcher3;
import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
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;
@@ -33,7 +36,6 @@
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;
@@ -41,7 +43,7 @@
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.Preconditions;
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.widget.custom.CustomWidgetManager;
@@ -58,9 +60,9 @@
private final IconCache mIconCache;
private final WidgetPreviewLoader mWidgetCache;
private final InvariantDeviceProfile mInvariantDeviceProfile;
- private final PredictionModel mPredictionModel;
+ private SettingsCache.OnChangeListener mNotificationSettingsChangedListener;
- private SecureSettingsObserver mNotificationDotsObserver;
+ private SettingsCache mSettingsCache;
private InstallSessionTracker mInstallSessionTracker;
private SimpleBroadcastReceiver mModelChangeReceiver;
private SafeCloseable mCalendarChangeTracker;
@@ -107,17 +109,14 @@
new Handler().post( () -> mInvariantDeviceProfile.verifyConfigChangedInBackground(context));
mInstallSessionTracker = InstallSessionHelper.INSTANCE.get(context)
- .registerInstallTracker(mModel, MODEL_EXECUTOR);
+ .registerInstallTracker(mModel);
- 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();
- }
+ // Register an observer to rebind the notification listener when dots are re-enabled.
+ mSettingsCache = SettingsCache.INSTANCE.get(mContext);
+ mNotificationSettingsChangedListener = this::onNotificationSettingsChanged;
+ mSettingsCache.register(NOTIFICATION_BADGING_URI,
+ mNotificationSettingsChangedListener);
+ onNotificationSettingsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI));
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -126,10 +125,37 @@
mContext = context;
mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
+
+ // b/175329686 Temporary logic to gracefully migrate group of users to the new 4x5 grid.
+ String gridName = InvariantDeviceProfile.getCurrentGridName(context);
+ if (ENABLE_FOUR_COLUMNS.get()
+ || "reasonable".equals(gridName)
+ || ENABLE_FOUR_COLUMNS.key.equals(gridName)) {
+ // Reset flag and remove it from developer options to prevent it from being enabled
+ // again.
+ ENABLE_FOUR_COLUMNS.reset(context);
+ FeatureFlags.removeFlag(ENABLE_FOUR_COLUMNS);
+
+ // Force migration code to run
+ Utilities.getPrefs(context).edit()
+ .remove(KEY_MIGRATION_SRC_HOTSEAT_COUNT)
+ .remove(KEY_MIGRATION_SRC_WORKSPACE_SIZE)
+ .apply();
+
+ // We make an empty call here to ensure the database is created with the old IDP grid,
+ // so that when we set the new grid the migration can proceeds as expected.
+ LauncherSettings.Settings.call(context.getContentResolver(), "");
+
+ String newGridName = "practical";
+ Utilities.getPrefs(mContext).edit().putString("idp_grid_name", newGridName).commit();
+ mInvariantDeviceProfile.setCurrentGrid(context, "practical");
+ } else {
+ FeatureFlags.removeFlag(ENABLE_FOUR_COLUMNS);
+ }
+
mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
- mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
- mPredictionModel = PredictionModel.newInstance(mContext);
+ mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext));
}
protected void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
@@ -157,6 +183,7 @@
* Call from Application.onTerminate(), which is not guaranteed to ever be called.
*/
public void onTerminate() {
+ mModel.destroy();
if (mModelChangeReceiver != null) {
mContext.unregisterReceiver(mModelChangeReceiver);
}
@@ -172,8 +199,9 @@
}
CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
- if (mNotificationDotsObserver != null) {
- mNotificationDotsObserver.unregister();
+ if (mSettingsCache != null) {
+ mSettingsCache.unregister(NOTIFICATION_BADGING_URI,
+ mNotificationSettingsChangedListener);
}
}
@@ -185,10 +213,6 @@
return mModel;
}
- public PredictionModel getPredictionModel() {
- return mPredictionModel;
- }
-
public WidgetPreviewLoader getWidgetCache() {
return mWidgetCache;
}
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/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..699495c 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;
@@ -85,7 +86,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 +112,22 @@
*/
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);
}
/**
@@ -195,6 +197,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 +228,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 +346,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).
@@ -372,7 +398,8 @@
public void startLoaderForResults(LoaderResults results) {
synchronized (mLock) {
stopLoader();
- mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, mBgDataModel, results);
+ mLoaderTask = new LoaderTask(
+ mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, results);
// Always post the loader task, instead of running directly (even on same thread) so
// that we exit any nested synchronized blocks
@@ -410,7 +437,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 +445,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 +518,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 +547,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 +615,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..2ec5e90 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -71,6 +71,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;
@@ -100,10 +101,12 @@
public static final int SCHEMA_VERSION = 28;
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 +189,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 +373,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 +444,7 @@
getContext(), true /* forMigration */)));
return result;
}
+ return null;
}
case LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW: {
if (MULTI_DB_GRID_MIRATION_ALGO.get()) {
@@ -450,6 +458,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 +517,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 +531,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 +579,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;
}
@@ -694,11 +725,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) {
@@ -872,7 +917,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 +1100,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..83ddf64 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,23 @@
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mForceHideBackArrow;
+ private SysUiScrim mSysUiScrim;
+
public LauncherRootView(Context context, AttributeSet attrs) {
super(context, attrs);
mActivity = StatefulActivity.fromContext(context);
}
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);
@@ -89,6 +100,18 @@
}
}
+ public void setSysUiScrim(SysUiScrim scrim) {
+ mSysUiScrim = scrim;
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mSysUiScrim != null) {
+ 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);
@@ -119,4 +142,4 @@
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..22c257a 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,6 +193,7 @@
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;
@@ -164,6 +202,10 @@
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 String containerToString(int container) {
switch (container) {
@@ -186,6 +228,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 +275,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>
@@ -354,14 +372,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..0c509a1 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -16,12 +16,15 @@
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;
@@ -36,7 +39,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 +53,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 +91,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 +114,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 +141,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 +172,16 @@
/**
* 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 getOverviewFullscreenProgress() {
@@ -188,10 +189,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,11 +209,11 @@
return 1f;
}
- public float getWorkspaceScrimAlpha(Launcher launcher) {
+ public float getWorkspaceBackgroundAlpha(Launcher launcher) {
return 0;
}
- public float getOverviewScrimAlpha(Launcher launcher) {
+ public float getWorkspaceScrimAlpha(Launcher launcher) {
return 0;
}
@@ -220,6 +226,14 @@
}
/**
+ * For this state, how much additional vertical translation there should be for each of the
+ * child TaskViews.
+ */
+ public float getOverviewSecondaryTranslation(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 +246,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 +265,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..01f7c71 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -63,6 +63,7 @@
import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
+import java.util.List;
/**
* An abstraction of the original Workspace which supports browsing through a
@@ -195,9 +196,7 @@
mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density);
mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density);
- if (Utilities.ATLEAST_OREO) {
- setDefaultFocusHighlightEnabled(false);
- }
+ setDefaultFocusHighlightEnabled(false);
}
protected void setDefaultInterpolator(Interpolator interpolator) {
@@ -219,7 +218,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() {
@@ -284,7 +283,47 @@
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;
+ }
+
+ /**
+ * Returns the currently visible pages.
+ */
+ public Iterable<View> getVisiblePages() {
+ int panelCount = getPanelCount();
+ List<View> visiblePages = new ArrayList<>(panelCount);
+ for (int i = mCurrentPage; i < mCurrentPage + panelCount; i++) {
+ View page = getPageAt(i);
+ if (page != null) {
+ visiblePages.add(page);
+ }
+ }
+ return visiblePages;
+ }
+
+ /**
+ * Returns true if the view is on one of the current pages, false otherwise.
+ */
+ public boolean isVisible(View child) {
+ return getLeftmostVisiblePageForIndex(indexOfChild(child)) == mCurrentPage;
}
/**
@@ -550,6 +589,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 +623,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 +715,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 +726,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 +802,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 +851,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;
}
}
@@ -817,11 +879,13 @@
}
if (direction == View.FOCUS_LEFT) {
if (mCurrentPage > 0) {
- getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode);
+ int nextPage = validateNewPage(mCurrentPage - 1);
+ getPageAt(nextPage).addFocusables(views, direction, focusableMode);
}
- } else if (direction == View.FOCUS_RIGHT){
+ } else if (direction == View.FOCUS_RIGHT) {
if (mCurrentPage < getPageCount() - 1) {
- getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode);
+ int nextPage = validateNewPage(mCurrentPage + 1);
+ getPageAt(nextPage).addFocusables(views, direction, focusableMode);
}
}
}
@@ -1001,10 +1065,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();
- }
+ getVisiblePages().forEach(View::cancelLongPress);
}
protected float getScrollProgress(int screenCenter, View v, int page) {
@@ -1252,12 +1313,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();
@@ -1274,7 +1337,7 @@
if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) ||
((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) {
mScroller.springBack(initialScroll, minScroll, maxScroll);
- mNextPage = getPageNearestToCenterOfScreen();
+ mNextPage = getDestinationPage();
} else {
mScroller.setInterpolator(mDefaultInterpolator);
mScroller.fling(initialScroll, -velocity,
@@ -1282,11 +1345,12 @@
Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
int finalPos = mScroller.getFinalPos();
- mNextPage = getPageNearestToCenterOfScreen(finalPos);
+ mNextPage = getDestinationPage(finalPos);
int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
- if (finalPos > minScroll && finalPos < maxScroll) {
+ if (snapToPageInFreeScroll() && 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
@@ -1332,6 +1396,10 @@
return true;
}
+ protected boolean snapToPageInFreeScroll() {
+ return true;
+ }
+
protected boolean shouldFlingForVelocity(int velocity) {
float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
return Math.abs(velocity) > threshold;
@@ -1437,22 +1505,26 @@
}
}
+ 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,8 +1533,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());
+ snapToPage(getDestinationPage(), getPageSnapDuration());
}
protected boolean isInOverScroll() {
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..858b72e 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -33,23 +33,20 @@
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.widget.LauncherAppWidgetProviderInfo;
import java.net.URISyntaxException;
-import java.util.ArrayList;
/**
* Drop target which provides a secondary option for an item.
@@ -111,15 +108,12 @@
mCurrentAccessibilityAction = action;
if (action == UNINSTALL) {
- mHoverColor = getResources().getColor(R.color.uninstall_target_hover_tint);
setDrawable(R.drawable.ic_uninstall_shadow);
updateText(R.string.uninstall_drop_target_label);
} else if (action == DISMISS_PREDICTION) {
- mHoverColor = Themes.getColorAccent(getContext());
setDrawable(R.drawable.ic_block_shadow);
updateText(R.string.dismiss_prediction_label);
} else if (action == RECONFIGURE) {
- mHoverColor = Themes.getColorAccent(getContext());
setDrawable(R.drawable.ic_setup_shadow);
updateText(R.string.gadget_setup_text);
}
@@ -136,19 +130,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 +199,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 +223,7 @@
DeferredOnComplete deferred = (DeferredOnComplete) d.dragSource;
if (target != null) {
deferred.mPackageName = target.getPackageName();
- mLauncher.addOnResumeCallback(deferred);
+ mLauncher.addOnResumeCallback(deferred::onLauncherResume);
} else {
deferred.sendFailure();
}
@@ -283,8 +271,7 @@
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 +298,7 @@
@Override
public void onAccessibilityDrop(View view, ItemInfo item) {
+ doLog(new InstanceIdSequence().newInstanceId(), item);
performDropAction(view, item);
}
@@ -318,7 +306,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 +325,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..5036104 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.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);
- }
+ Log.i(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..2c24c3a 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -26,25 +26,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 +60,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 +88,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 +103,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);
}
}
@@ -116,11 +127,12 @@
final DeviceProfile profile = 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(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);
// Center the icon/folder
int cHeight = getCellContentHeight();
int cellPaddingY = (int) Math.max(0, ((lp.height - cHeight) / 2f));
@@ -220,4 +232,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..1776777 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;
@@ -34,8 +34,11 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
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 +47,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;
@@ -63,12 +66,13 @@
import android.view.ViewConfiguration;
import android.view.animation.Interpolator;
+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.FastBitmapDrawable;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.icons.ShortcutCachingLogic;
@@ -85,6 +89,7 @@
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;
@@ -106,18 +111,13 @@
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();
/**
* Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}.
@@ -141,6 +141,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();
@@ -405,6 +409,11 @@
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));
@@ -456,6 +465,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 +512,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 +524,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) {
@@ -606,7 +609,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 +665,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 +682,76 @@
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));
+ }
+
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
index 2c45c77..f43452c 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -37,6 +37,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.LauncherIcons;
@@ -50,6 +51,7 @@
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SQLiteCacheHelper;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.WidgetManagerHelper;
@@ -539,20 +541,17 @@
c.setBitmap(preview);
c.drawColor(0, PorterDuff.Mode.CLEAR);
}
- RectF boxRect = drawBoxWithShadow(c, size, size);
+
+ drawBoxWithShadow(c, size, size);
LauncherIcons li = LauncherIcons.obtain(mContext);
- Bitmap icon = li.createBadgedIconBitmap(
+ Drawable icon = li.createBadgedIconBitmap(
mutateOnMainThread(info.getFullResIcon(mIconCache)),
- Process.myUserHandle(), 0).icon;
+ Process.myUserHandle(), 0).newIcon(launcher);
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));
+ icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
+ icon.draw(c);
c.setBitmap(null);
return preview;
}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 1441e0b..981cf89 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.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,13 +63,13 @@
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;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dot.FolderDotInfo;
+import com.android.launcher3.dragndrop.AppWidgetHostViewDrawable;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.DragOptions;
@@ -81,11 +81,12 @@
import com.android.launcher3.folder.PreviewBackground;
import com.android.launcher3.graphics.DragPreviewProvider;
import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
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,12 +94,10 @@
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.Executors;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSparseArrayMap;
@@ -106,16 +105,22 @@
import com.android.launcher3.util.PackageUserKey;
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.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 +146,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 +189,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 +203,9 @@
private boolean mStripScreensOnPageStopMoving = false;
private DragPreviewProvider mOutlineProvider = null;
+
+ private WorkspaceDragScrim mWorkspaceDragScrim;
+
private boolean mWorkspaceFadeInAdjacentScreens;
final WallpaperOffsetInterpolator mWallpaperOffset;
@@ -301,23 +312,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 +391,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);
@@ -377,15 +421,6 @@
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.
@@ -406,7 +441,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 +459,15 @@
.log(LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
}
+ private boolean isTwoPanelEnabled() {
+ return mLauncher.mDeviceProfile.isTablet && FeatureFlags.ENABLE_TWO_PANEL_HOME.get();
+ }
+
+ @Override
+ protected int getPanelCount() {
+ return isTwoPanelEnabled() ? 2 : super.getPanelCount();
+ }
+
public void deferRemoveExtraEmptyScreen() {
mDeferRemoveExtraEmptyScreen = true;
}
@@ -434,11 +478,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 +564,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");
@@ -655,6 +711,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 +855,7 @@
private boolean shouldConsumeTouch(View v) {
return !workspaceIconsCanBeDragged()
- || (!workspaceInModalState() && indexOfChild(v) != mCurrentPage);
+ || (!workspaceInModalState() && !isVisible(v));
}
public boolean isSwitchingState() {
@@ -940,7 +997,10 @@
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();
@@ -1001,8 +1061,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 +1075,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 +1175,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)
@@ -1162,6 +1212,19 @@
}
}
+ public void setWorkspaceDragScrim(WorkspaceDragScrim workspaceDragScrim) {
+ mWorkspaceDragScrim = workspaceDragScrim;
+ }
+
+ @Override
+ public void invalidate() {
+ // The workspace scrim may need to be re-rendered based on the workspace scroll
+ if (mWorkspaceDragScrim != null) {
+ mWorkspaceDragScrim.invalidate();
+ }
+ super.invalidate();
+ }
+
@Override
public void computeScroll() {
super.computeScroll();
@@ -1173,13 +1236,6 @@
}
@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.
if (!mLauncher.isInState(ALL_APPS)) {
@@ -1400,7 +1456,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 +1529,10 @@
draggableView = (DraggableView) child;
}
- // The drag bitmap follows the touch point around on the screen
- final Bitmap b = previewProvider.createDragBitmap();
+ // The draggable drawable follows the touch point around on the screen
+ final Drawable drawable = previewProvider.createDrawable();
int halfPadding = previewProvider.previewPadding / 2;
- float scale = previewProvider.getScaleAndPosition(b, mTempXY);
+ float scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
int dragLayerX = mTempXY[0];
int dragLayerY = mTempXY[1];
@@ -1499,13 +1555,24 @@
.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);
+ if (drawable instanceof AppWidgetHostViewDrawable) {
+ mDragController.addDragListener(new AppWidgetHostViewDragListener(mLauncher));
+ }
+ DragView dv = mDragController.startDrag(
+ drawable,
+ draggableView,
+ dragLayerX,
+ dragLayerY,
+ source,
+ dragObject,
+ dragVisualizeOffset,
+ dragRect,
+ scale * iconScale,
+ scale,
+ dragOptions);
dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
return dv;
}
@@ -1699,7 +1766,6 @@
fi.addItem(destInfo);
fi.addItem(sourceInfo);
}
- mLauncher.folderCreatedFromItem(fi.getFolder(), destInfo);
return true;
}
return false;
@@ -1717,9 +1783,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);
@@ -1870,7 +1935,6 @@
};
}
}
-
mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
lp.cellX, lp.cellY, item.spanX, item.spanY);
} else {
@@ -1936,16 +2000,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
@@ -2026,7 +2100,7 @@
}
// Invalidating the scrim will also force this CellLayout
// to be invalidated so that it is highlighted if necessary.
- mLauncher.getDragLayer().getScrim().invalidate();
+ mLauncher.getDragLayer().getWorkspaceDragScrim().invalidate();
}
public CellLayout getCurrentDragOverlappingLayout() {
@@ -2085,40 +2159,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 ||
@@ -2230,19 +2304,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);
@@ -2400,9 +2482,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 +2532,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();
@@ -2556,7 +2638,11 @@
}
- public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) {
+ private Drawable createWidgetDrawable(ItemInfo widgetInfo, View layout) {
+ if (layout instanceof LauncherAppWidgetHostView) {
+ return new AppWidgetHostViewDrawable((LauncherAppWidgetHostView) layout);
+ }
+
int[] unScaledSize = estimateItemSize(widgetInfo);
int visibility = layout.getVisibility();
layout.setVisibility(VISIBLE);
@@ -2568,7 +2654,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,
@@ -2638,8 +2724,8 @@
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);
+ Drawable crossFadeDrawable = createWidgetDrawable(info, finalView);
+ dragView.setCrossFadeDrawable(crossFadeDrawable);
dragView.crossFade((int) (duration * 0.8f));
} else if (isWidget && external) {
scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]);
@@ -2892,8 +2978,10 @@
* @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);
+ List<CellLayout> cellLayouts = new ArrayList<>(getPanelCount() + 1);
+ cellLayouts.add(getHotseat());
+ getVisiblePages().forEach(page -> cellLayouts.add((CellLayout) page));
+
final Workspace.ItemOperator packageAndUser = (ItemInfo info, View view) -> info != null
&& info.getTargetComponent() != null
&& TextUtils.equals(info.getTargetComponent().getPackageName(), packageName)
@@ -2914,13 +3002,11 @@
// Order: App icons, app in folder. Items in hotseat get returned first.
if (ADAPTIVE_ICON_WINDOW_ANIM.get()) {
- return getFirstMatch(new CellLayout[] { getHotseat(), currentPage },
- packageAndUserAndApp, packageAndUserAndAppInFolder);
+ return getFirstMatch(cellLayouts, 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, packageAndUserAndApp);
}
}
@@ -2954,7 +3040,7 @@
* @param operators List of operators, in order starting from best matching operator.
* @return
*/
- private View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
+ View getFirstMatch(Iterable<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.
@@ -3002,38 +3088,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 */);
}
}
}
@@ -3085,7 +3160,7 @@
return false;
}
- 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 +3218,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 +3228,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)) {
@@ -3260,29 +3334,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..24de19f 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -28,15 +28,17 @@
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.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_WORKSPACE_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCRIM_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
import android.animation.ValueAnimator;
@@ -45,11 +47,11 @@
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.SysUiScrim;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.DynamicResource;
import com.android.systemui.plugins.ResourceProvider;
@@ -93,7 +95,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 +106,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;
+ setPivotToScaleWithWorkspace(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,10 +150,8 @@
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);
+ setScrim(propertySetter, state, config);
}
/**
@@ -182,12 +166,19 @@
- sibling.getLeft() - sibling.getTranslationX());
}
- 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,
+ public void setScrim(PropertySetter propertySetter, LauncherState state,
+ StateAnimationConfig config) {
+ WorkspaceDragScrim workspaceDragScrim = mLauncher.getDragLayer().getWorkspaceDragScrim();
+ propertySetter.setFloat(workspaceDragScrim, SCRIM_PROGRESS,
+ state.getWorkspaceBackgroundAlpha(mLauncher), LINEAR);
+
+ SysUiScrim sysUiScrim = mLauncher.getDragLayer().getSysUiScrim();
+ propertySetter.setFloat(sysUiScrim, SYSUI_PROGRESS,
state.hasFlag(FLAG_HAS_SYS_UI_SCRIM) ? 1 : 0, LINEAR);
+
+ propertySetter.setViewAlpha(mLauncher.getScrimView(),
+ state.getWorkspaceScrimAlpha(mLauncher),
+ config.getInterpolator(ANIM_WORKSPACE_SCRIM_FADE, LINEAR));
}
public void applyChildState(LauncherState state, CellLayout cl, int childIndex) {
@@ -202,17 +193,12 @@
int drawableAlpha = state.hasFlag(FLAG_WORKSPACE_HAS_BACKGROUNDS)
? Math.round(pageAlpha * 255) : 0;
- 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.setInt(cl.getScrimBackground(),
+ DRAWABLE_ALPHA, drawableAlpha, 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..a5852ba 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -3,17 +3,18 @@
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
import static com.android.launcher3.LauncherState.NORMAL;
+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;
@@ -25,7 +26,6 @@
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;
@@ -33,21 +33,26 @@
import com.android.launcher3.dragndrop.DragController.DragListener;
import com.android.launcher3.dragndrop.DragOptions;
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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener {
@@ -77,93 +82,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 +195,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,20 +213,9 @@
// 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);
@@ -219,9 +229,7 @@
Favorites.CONTAINER_DESKTOP,
screenId, coordinates[0], coordinates[1]);
- ArrayList<ItemInfo> itemList = new ArrayList<>();
- itemList.add(info);
- mLauncher.bindItems(itemList, true);
+ mLauncher.bindItems(Collections.singletonList(info), true);
announceConfirmation(R.string.item_added_to_workspace);
} else if (item instanceof PendingAddItemInfo) {
PendingAddItemInfo info = (PendingAddItemInfo) item;
@@ -242,47 +250,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,9 +283,8 @@
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;
@@ -303,28 +294,44 @@
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 +361,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);
+ AppWidgetResizeFrame.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 +412,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 +433,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 +494,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..1733e5d 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -18,9 +18,8 @@
import static com.android.launcher3.LauncherState.NORMAL;
+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 +30,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 +43,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;
@@ -73,9 +73,7 @@
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);
+ mLauncher.bindItems(Collections.singletonList(info), true);
AbstractFloatingView.closeAllOpenViews(mLauncher);
announceConfirmation(R.string.item_added_to_workspace);
}
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..406e785 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -15,7 +15,8 @@
*/
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;
@@ -32,6 +33,7 @@
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -42,6 +44,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
+import androidx.core.os.BuildCompat;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -55,35 +58,31 @@
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.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 {
- private static final float FLING_VELOCITY_MULTIPLIER = 135f;
+ private static final float FLING_VELOCITY_MULTIPLIER = 1000 * .8f;
// 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;
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 Paint mNavBarScrimPaint;
@@ -105,9 +104,9 @@
protected RecyclerViewFastScroller mTouchHandler;
protected final Point mFastScrollerOffset = new Point();
- private final MultiValueAlpha mMultiValueAlpha;
+ private Rect mInsets = new Rect();
- Rect mInsets = new Rect();
+ SearchAdapterProvider mSearchAdapterProvider;
public AllAppsContainerView(Context context) {
this(context, null);
@@ -123,6 +122,7 @@
mLauncher = BaseDraggingActivity.fromContext(context);
mLauncher.addOnDeviceProfileChangeListener(this);
+ mSearchAdapterProvider = mLauncher.createSearchAdapterProvider(this);
mSearchQueryBuilder = new SpannableStringBuilder();
Selection.setSelection(mSearchQueryBuilder, 0);
@@ -134,12 +134,6 @@
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);
-
- mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
}
/**
@@ -155,25 +149,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.inv.numAllAppsColumns);
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,9 +174,11 @@
break;
}
}
- rebindAdapters(hasWorkApps);
- if (hasWorkApps) {
- resetWorkProfile();
+ if (!mAH[AdapterHolder.MAIN].appsList.hasFilter()) {
+ rebindAdapters(hasWorkApps);
+ if (hasWorkApps) {
+ resetWorkProfile();
+ }
}
}
@@ -203,6 +188,21 @@
mAH[AdapterHolder.WORK].applyPadding();
}
+ private void hideInput() {
+ if (!BuildCompat.isAtLeastR() || !FeatureFlags.ENABLE_DEVICE_SEARCH.get()) return;
+
+ WindowInsets insets = getRootWindowInsets();
+ if (insets == null) return;
+
+ if (insets.isVisible(WindowInsets.Type.ime())) {
+ hideIme();
+ }
+ }
+
+ protected void hideIme() {
+ getWindowInsetsController().hide(WindowInsets.Type.ime());
+ }
+
/**
* Returns whether the view itself will handle the touch event or not.
*/
@@ -218,6 +218,7 @@
}
if (rv.getScrollbar().getThumbOffsetY() >= 0 &&
mLauncher.getDragLayer().isEventOverView(rv.getScrollbar(), ev)) {
+ hideInput();
return false;
}
return rv.shouldContainerScroll(ev, mLauncher.getDragLayer());
@@ -317,7 +318,7 @@
mSearchContainer = findViewById(R.id.search_container_all_apps);
mSearchUiManager = (SearchUiManager) mSearchContainer;
- mSearchUiManager.initialize(this);
+ mSearchUiManager.initializeSearch(this);
}
public SearchUiManager getSearchUiManager() {
@@ -335,13 +336,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();
@@ -372,7 +366,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 +384,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 */);
}
@@ -415,10 +404,20 @@
mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
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;
@@ -464,10 +463,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 +480,17 @@
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 == 0);
+ 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));
}
}
@@ -526,6 +530,18 @@
return mViewPager == null ? getActiveRecyclerView() : mViewPager;
}
+ /**
+ * 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();
@@ -594,11 +610,8 @@
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));
-
+ absorbSwipeUpVelocity(Math.abs(
+ Math.round(velocity * FLING_VELOCITY_MULTIPLIER)));
shouldSpring = false;
}
}
@@ -630,7 +643,8 @@
AdapterHolder(boolean isWork) {
mIsWork = isWork;
appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore, isWork);
- adapter = new AllAppsGridAdapter(mLauncher, getLayoutInflater(), appsList);
+ adapter = new AllAppsGridAdapter(mLauncher, getLayoutInflater(), appsList,
+ mSearchAdapterProvider);
appsList.setAdapter(adapter);
layoutManager = adapter.getLayoutManager();
}
@@ -652,6 +666,9 @@
applyVerticalFadingEdgeEnabled(verticalFadingEdge);
applyPadding();
setupOverlay();
+ if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+ recyclerView.addItemDecoration(mSearchAdapterProvider.getDecorator());
+ }
}
void setupOverlay() {
@@ -690,6 +707,7 @@
int bottomOffset = mWorkModeSwitch != null && mIsWork ? switchH : 0;
recyclerView.setPadding(padding.left, padding.top, padding.right,
padding.bottom + bottomOffset);
+ recyclerView.scrollToTop();
}
}
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 8ec4d27..5b4c4c5 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,8 +41,7 @@
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.allapps.search.SearchAdapterProvider;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.PackageManagerHelper;
@@ -50,7 +50,8 @@
/**
* 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,8 @@
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 SearchAdapterProvider mSearchAdapterProvider;
+
/**
* ViewHolder for each icon.
*/
@@ -82,6 +85,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 +238,15 @@
@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 if (mSearchAdapterProvider.isSearchView(viewType)) {
+ return totalSpans / mSearchAdapterProvider.getItemsPerRow(viewType, mAppsPerRow);
} else {
// Section breaks span the full width
- return mAppsPerRow;
+ return totalSpans;
}
}
}
@@ -189,7 +270,7 @@
private Intent mMarketSearchIntent;
public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater,
- AlphabeticalAppsList apps) {
+ AlphabeticalAppsList apps, SearchAdapterProvider searchAdapterProvider) {
Resources res = launcher.getResources();
mLauncher = launcher;
mApps = apps;
@@ -201,12 +282,19 @@
mOnIconClickListener = launcher.getItemOnClickListener();
+ mSearchAdapterProvider = searchAdapterProvider;
setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns);
}
public void setAppsPerRow(int appsPerRow) {
mAppsPerRow = appsPerRow;
- mGridLayoutMgr.setSpanCount(mAppsPerRow);
+ int totalSpans = mAppsPerRow;
+ for (int itemPerRow : mSearchAdapterProvider.getSupportedItemsPerRowArray()) {
+ if (totalSpans % itemPerRow != 0) {
+ totalSpans *= itemPerRow;
+ }
+ }
+ mGridLayoutMgr.setSpanCount(totalSpans);
}
/**
@@ -255,11 +343,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 +357,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:
+ if (mSearchAdapterProvider.isSearchView(viewType)) {
+ return mSearchAdapterProvider.onCreateViewHolder(mLayoutInflater, parent,
+ viewType);
+ }
throw new RuntimeException("Unexpected view type");
}
}
@@ -284,7 +375,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 +398,17 @@
case VIEW_TYPE_ALL_APPS_DIVIDER:
// nothing to do
break;
+ default:
+ mSearchAdapterProvider.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 +421,7 @@
@Override
public int getItemViewType(int position) {
- AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+ AdapterItem item = mApps.getAdapterItems().get(position);
return item.viewType;
}
-
}
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..66575eb 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -19,16 +19,19 @@
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 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;
+import android.view.WindowInsets;
import androidx.recyclerview.widget.RecyclerView;
@@ -37,11 +40,7 @@
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.logging.StatsLogManager;
import com.android.launcher3.views.RecyclerViewFastScroller;
import java.util.ArrayList;
@@ -50,7 +49,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 = true;
private AlphabeticalAppsList mApps;
private final int mNumAppsPerRow;
@@ -112,23 +113,6 @@
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 +120,9 @@
if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
mEmptySearchBackground.draw(c);
}
-
+ if (DEBUG) {
+ Log.d(TAG, "onDraw at = " + System.currentTimeMillis());
+ }
super.onDraw(c);
}
@@ -175,13 +161,6 @@
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();
@@ -202,6 +181,25 @@
}
@Override
+ public void onScrollStateChanged(int state) {
+ super.onScrollStateChanged(state);
+
+ StatsLogManager mgr = BaseDraggingActivity.fromContext(getContext()).getStatsLogManager();
+ switch (state) {
+ case SCROLL_STATE_DRAGGING:
+ mgr.logger().sendToInteractionJankMonitor(
+ LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this);
+ requestFocus();
+ getWindowInsetsController().hide(WindowInsets.Type.ime());
+ 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
@@ -277,7 +275,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 +349,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 +365,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 &&
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 3ae0a18..355ccad 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -21,10 +21,11 @@
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 +80,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;
@@ -145,10 +151,17 @@
});
}
- 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.
+ */
+ public void updateProgressBar(AppInfo app) {
updateAllIcons((child) -> {
if (child.getTag() == app) {
- child.applyProgressLevel(app.level);
+ child.applyProgressLevel();
}
});
}
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index a9b030e..0060b83 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,44 +1,48 @@
+/*
+ * 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_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.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.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.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
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 +54,24 @@
* 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 {
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;
@@ -86,10 +87,6 @@
private float mScrollRangeDelta = 0;
- // plugin related variables
- private AllAppsSearchPlugin mPlugin;
- private View mPluginContent;
-
public AllAppsTransitionController(Launcher l) {
mLauncher = l;
mShiftRange = mLauncher.getDeviceProfile().heightPx;
@@ -109,7 +106,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 +116,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,19 +148,12 @@
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;
@@ -193,43 +175,23 @@
*/
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, 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);
+ setter.setViewAlpha(mAppsView, hasAllAppsContent ? 1 : 0, allAppsFade);
}
public AnimatorListenerAdapter getProgressAnimatorListener() {
return AnimationSuccessListener.forRunnable(this::onProgressAnimationEnd);
}
- public void setupViews(AllAppsContainerView appsView, ScrimView scrimView) {
+ public void setupViews(AllAppsContainerView appsView) {
mAppsView = appsView;
- mScrimView = scrimView;
- PluginManagerWrapper.INSTANCE.get(mLauncher)
- .addPluginListener(this, AllAppsSearchPlugin.class, false);
+ 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);
+ }
}
/**
@@ -238,10 +200,6 @@
void setScrollRangeDelta(float delta) {
mScrollRangeDelta = delta;
mShiftRange = mLauncher.getDeviceProfile().heightPx - mScrollRangeDelta;
-
- if (mScrimView != null) {
- mScrimView.reInitUi();
- }
}
/**
@@ -249,50 +207,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..6957850 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;
@@ -60,61 +61,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,8 +68,8 @@
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)
@@ -132,7 +78,7 @@
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;
@@ -182,6 +128,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 +160,7 @@
* Returns the number of applications in this list.
*/
public int getNumFilteredApps() {
- return mFilteredApps.size();
+ return mAccessibilityResultsCount;
}
/**
@@ -206,22 +174,43 @@
* 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();
+ public boolean setSearchResults(ArrayList<AdapterItem> results) {
+ if (results == null || mSearchResults != results) {
+ boolean same = mSearchResults != null && mSearchResults.equals(results);
+ mSearchResults = results;
+ updateAdapterItems();
return !same;
}
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,11 +256,13 @@
}
// 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() {
@@ -292,41 +283,46 @@
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();
+ 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 +377,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/util/SafeCloseable.java b/src/com/android/launcher3/allapps/DecorationInfo.java
similarity index 69%
rename from src/com/android/launcher3/util/SafeCloseable.java
rename to src/com/android/launcher3/allapps/DecorationInfo.java
index ba8ee04..50b250c 100644
--- a/src/com/android/launcher3/util/SafeCloseable.java
+++ b/src/com/android/launcher3/allapps/DecorationInfo.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -13,14 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.launcher3.allapps;
-package com.android.launcher3.util;
-
-/**
- * Extension of closeable which does not throw an exception
- */
-public interface SafeCloseable extends AutoCloseable {
-
- @Override
- void close();
+public class DecorationInfo {
}
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index b4ff5ea..d8ef18e 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -17,9 +17,6 @@
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;
@@ -34,7 +31,6 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.OnboardingPrefs;
/**
@@ -116,19 +112,14 @@
}
@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) {
@@ -151,40 +142,7 @@
}
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, 0).show();
}
/**
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..733d867 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -15,8 +15,6 @@
*/
package com.android.launcher3.allapps;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Point;
@@ -26,7 +24,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 +34,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;
@@ -87,7 +84,6 @@
private int mSnappedScrolledY;
private int mTranslationY;
- private boolean mAllowTouchForwarding;
private boolean mForwardToRecyclerView;
protected boolean mTabsHidden;
@@ -194,6 +190,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);
@@ -333,10 +342,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 +370,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()) {
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index a6bc6cf..16ae250 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
+
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -70,8 +72,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 +86,20 @@
}
@Override
- public void onTabChanged(int pos) {
- super.onTabChanged(pos);
+ public void onActivePageChanged(int currentActivePage) {
+ super.onActivePageChanged(currentActivePage);
if (mUsingTabs) {
- if (pos == AdapterHolder.WORK) {
+ if (currentActivePage == AdapterHolder.WORK) {
WorkEduView.showWorkEduIfNeeded(mLauncher);
} else {
mWorkTabListener = WorkEduView.showEduFlowIfNeeded(mLauncher, mWorkTabListener);
}
}
}
+
+ @Override
+ protected void hideIme() {
+ super.hideIme();
+ mLauncher.getStatsLogManager().logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
+ }
}
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/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..a8185d6 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.inv.numHotseatIcons);
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..ef62da4
--- /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 isSearchView(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..cefb8cb
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.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.allapps.search;
+
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsGridAdapter;
+
+/**
+ * A UI expansion wrapper providing for search results
+ */
+public abstract class SearchAdapterProvider {
+
+ protected final BaseDraggingActivity mLauncher;
+
+ public SearchAdapterProvider(BaseDraggingActivity launcher, AllAppsContainerView appsView) {
+ mLauncher = launcher;
+ }
+
+ /**
+ * Called from RecyclerView.Adapter#onBindViewHolder
+ */
+ public abstract void onBindView(AllAppsGridAdapter.ViewHolder holder, int position);
+
+ /**
+ * Called from LiveSearchManager to notify slice status updates.
+ */
+ public void onSliceStatusUpdate(Uri sliceUri) {
+ }
+
+ /**
+ * Returns whether or not viewType can be handled by searchProvider
+ */
+ public abstract boolean isSearchView(int viewType);
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index ea0ff8b..f6d1651 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,33 +278,6 @@
}
}
- /** @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;
- return this;
- }
-
public void dispatchOnStart() {
callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
}
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 860cceb..7980138 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;
@@ -67,9 +64,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 +77,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.
@@ -175,75 +172,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/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index 4195933..4e90c9e 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -15,10 +15,12 @@
*/
package com.android.launcher3.anim;
+import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.anim.AnimatorPlaybackController.addAnimationHoldersRecur;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
@@ -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) {
@@ -149,7 +142,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 +156,38 @@
}
/**
- * 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 void addEndListener(Consumer<Boolean> listener) {
+ if (mProgressAnimator == null) {
+ mProgressAnimator = ValueAnimator.ofFloat(0, 1);
+ }
+ mProgressAnimator.addListener(new EndStateCallbackWrapper(listener));
}
- public static class EndState {
- public boolean isSuccess;
- public int logAction;
+ private static class EndStateCallbackWrapper extends AnimatorListenerAdapter {
- public EndState(boolean isSuccess, int logAction) {
- this.isSuccess = isSuccess;
- this.logAction = logAction;
+ private final Consumer<Boolean> mListener;
+ private boolean mCalled = false;
+
+ EndStateCallbackWrapper(Consumer<Boolean> listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (!mCalled) {
+ mCalled = true;
+ mListener.accept(false);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCalled) {
+ ValueAnimator anim = (ValueAnimator) animation;
+ mListener.accept(anim.getAnimatedFraction() > SUCCESS_TRANSITION_PROGRESS);
+ }
}
}
}
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..6331ef2 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -72,9 +72,6 @@
"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");
@@ -88,11 +85,19 @@
"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");
// 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");
+ "ENABLE_SUGGESTED_ACTIONS_OVERVIEW", true, "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_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,
@@ -123,9 +128,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");
@@ -138,28 +140,35 @@
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.");
-
// 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", false,
+ "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 = new DeviceFlag(
+ "ENABLE_SMARTSPACE_ENHANCED", false,
+ "Replace Smartspace with the enhanced version. "
+ + "Ignored if ENABLE_SMARTSPACE_UNIVERSAL is enabled.");
public static final BooleanFlag ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS =
getDebugFlag(
@@ -174,8 +183,47 @@
"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.");
+
+ public static final DeviceFlag ENABLE_FOUR_COLUMNS = new DeviceFlag(
+ "ENABLE_FOUR_COLUMNS", false, "Uses 4 columns in launcher grid."
+ + "Warning: This will permanently alter your home screen items and is not reversible.");
+
+ // 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_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 NOTIFY_CRASHES = getDebugFlag("NOTIFY_CRASHES", false,
+ "Sends a notification whenever launcher encounters an uncaught exception.");
public static void initialize(Context context) {
synchronized (sDebugFlags) {
@@ -186,6 +234,12 @@
}
}
+ public static void removeFlag(DebugFlag flag) {
+ synchronized (sDebugFlags) {
+ sDebugFlags.remove(flag);
+ }
+ }
+
static List<DebugFlag> getDebugFlags() {
synchronized (sDebugFlags) {
return new ArrayList<>(sDebugFlags);
@@ -235,6 +289,8 @@
}
public void addChangeListener(Context context, Runnable r) { }
+
+ public void removeChangeListener(Runnable r) {}
}
public static class DebugFlag extends BooleanFlag {
@@ -261,6 +317,15 @@
.getBoolean(key, defaultValue);
}
+ /**
+ * Resets value to default value.
+ */
+ public void reset(Context context) {
+ mCurrentValue = defaultValue;
+ context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+ .edit().putBoolean(key, defaultValue).apply();
+ }
+
@Override
protected StringBuilder appendProps(StringBuilder src) {
return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue);
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 0df6713..b7a7366 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -16,10 +16,11 @@
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.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;
@@ -36,28 +37,31 @@
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
+import android.view.Gravity;
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.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.views.BaseDragLayer;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.WidgetHostViewLoader;
import com.android.launcher3.widget.WidgetImageView;
import com.android.launcher3.widget.WidgetManagerHelper;
@@ -78,7 +82,7 @@
private LauncherAppState mApp;
private InvariantDeviceProfile mIdp;
- private LivePreviewWidgetCell mWidgetCell;
+ private WidgetCell mWidgetCell;
// Widget request specific options.
private LauncherAppWidgetHost mAppWidgetHost;
@@ -87,7 +91,6 @@
private Bundle mWidgetOptions;
private boolean mFinishOnPause = false;
- private InstantAppResolver mInstantAppResolver;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -101,7 +104,6 @@
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.
@@ -119,14 +121,18 @@
}
}
- mWidgetCell.setOnTouchListener(this);
- mWidgetCell.setOnLongClickListener(this);
+ WidgetImageView preview = mWidgetCell.findViewById(R.id.widget_preview);
+ preview.setOnTouchListener(this);
+ preview.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);
}
@Override
@@ -142,7 +148,7 @@
// 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) {
return false;
}
@@ -151,7 +157,7 @@
// Start home and pass the draw request params
PinItemDragListener listener = new PinItemDragListener(mRequest, bounds,
- img.getBitmap().getWidth(), img.getWidth());
+ img.getDrawable().getIntrinsicWidth(), img.getWidth());
// Start a system drag and drop. We use a transparent bitmap as preview for system drag
@@ -178,6 +184,7 @@
startActivity(homeIntent,
ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out)
.toBundle());
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED);
mFinishOnPause = true;
return false;
}
@@ -240,7 +247,7 @@
* Called when the cancel button is clicked.
*/
public void onCancelClick(View v) {
- logCommand(Action.Command.CANCEL);
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_CANCELLED);
finish();
}
@@ -249,8 +256,8 @@
*/
public void onPlaceAutomaticallyClick(View v) {
if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) {
- InstallShortcutReceiver.queueShortcut(mRequest.getShortcutInfo(), this);
- logCommand(Action.Command.CONFIRM);
+ ItemInstallQueue.INSTANCE.get(this).queueItem(mRequest.getShortcutInfo());
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY);
mRequest.accept();
finish();
return;
@@ -270,16 +277,17 @@
}
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);
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_PLACED_AUTOMATICALLY);
finish();
}
@Override
public void onBackPressed() {
- logCommand(Action.Command.BACK);
+ logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_BACK);
super.onBackPressed();
}
@@ -319,10 +327,20 @@
throw new UnsupportedOperationException();
}
- private void logCommand(int command) {
- getUserEventDispatcher().dispatchUserEvent(newLauncherEvent(
- newCommandAction(command),
- newItemTarget(mWidgetCell.getWidgetView(), mInstantAppResolver),
- newContainerTarget(ContainerType.PINITEM)), null);
+ private void logCommand(StatsLogManager.EventEnum command) {
+ getStatsLogManager().logger()
+ .withItemInfo((ItemInfo) mWidgetCell.getWidgetView().getTag())
+ .log(command);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ View view = getWindow().getDecorView();
+ WindowManager.LayoutParams layoutParams =
+ (WindowManager.LayoutParams) view.getLayoutParams();
+ layoutParams.gravity = Gravity.BOTTOM;
+ layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
+ getWindowManager().updateViewLayout(view, layoutParams);
}
}
diff --git a/src/com/android/launcher3/dragndrop/AppWidgetHostViewDrawable.java b/src/com/android/launcher3/dragndrop/AppWidgetHostViewDrawable.java
new file mode 100644
index 0000000..2135f5d
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/AppWidgetHostViewDrawable.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.dragndrop;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+
+/**
+ * A drawable which renders {@link LauncherAppWidgetHostView} to a canvas.
+ *
+ * TODO(b/183609936) Stop using that class and remove it.
+ */
+public final class AppWidgetHostViewDrawable extends Drawable {
+
+ private final LauncherAppWidgetHostView mAppWidgetHostView;
+ private Paint mPaint = new Paint();
+
+ public AppWidgetHostViewDrawable(LauncherAppWidgetHostView appWidgetHostView) {
+ mAppWidgetHostView = appWidgetHostView;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ int saveCount = canvas.saveLayer(0, 0, getIntrinsicWidth(), getIntrinsicHeight(), mPaint);
+ mAppWidgetHostView.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mAppWidgetHostView.getMeasuredWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mAppWidgetHostView.getMeasuredHeight();
+ }
+
+ @Override
+ public int getOpacity() {
+ // This is up to app widget provider. We don't know if the host view will cover anything
+ // behind the drawable.
+ return PixelFormat.UNKNOWN;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public int getAlpha() {
+ return mPaint.getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mPaint.getColorFilter();
+ }
+
+ /** Returns the {@link LauncherAppWidgetHostView}. */
+ public LauncherAppWidgetHostView getAppWidgetHostView() {
+ return mAppWidgetHostView;
+ }
+}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 03028d3..b7a70cb 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -25,9 +25,9 @@
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;
@@ -40,12 +40,14 @@
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 java.util.ArrayList;
+import java.util.Optional;
/**
* Class for initiating a drag within a view or across multiple views.
@@ -127,7 +129,7 @@
* 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
+ * @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
@@ -138,9 +140,18 @@
* @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(Bitmap b, DraggableView originalView, int dragLayerX, int dragLayerY,
- DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
- float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) {
+ 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) {
if (PROFILE_DRAWING_DURING_DRAG) {
android.os.Debug.startMethodTracing("Launcher");
}
@@ -171,8 +182,14 @@
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);
+ final DragView dragView = mDragObject.dragView = new DragView(
+ mLauncher,
+ drawable,
+ registrationX,
+ registrationY,
+ initialDragViewScale,
+ dragViewScaleOnDrop,
+ scaleDps);
dragView.setItemInfo(dragInfo);
mDragObject.dragComplete = false;
@@ -206,7 +223,6 @@
}
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
@@ -231,6 +247,11 @@
}
}
+ public Optional<InstanceId> getLogInstanceId() {
+ return Optional.ofNullable(mDragObject)
+ .map(dragObject -> dragObject.logInstanceId);
+ }
+
/**
* Call this from a drag source view like this:
*
@@ -463,10 +484,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);
}
@@ -544,7 +565,6 @@
}
}
final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
- mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView);
dispatchDropComplete(dropTargetAsView, accepted);
}
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index ddf44ca..c80fd90 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -41,12 +41,13 @@
import com.android.launcher3.CellLayout;
import com.android.launcher3.DropTargetBar;
import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherRootView;
import com.android.launcher3.R;
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.SysUiScrim;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.BaseDragLayer;
@@ -82,11 +83,9 @@
// 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 WorkspaceDragScrim mWorkspaceDragScrim;
+ private SysUiScrim mSysUiScrim;
+ private LauncherRootView mRootView;
/**
* Used to create a new DragLayer from XML.
@@ -102,15 +101,20 @@
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 WorkspaceDragScrim((this));
+ mWorkspaceDragScrim.setWorkspace(workspace);
+
+ // We delegate drawing of the workspace scrim to LauncherRootView (one level up), so as
+ // to avoid artifacts when translating the entire drag layer in the -1 transition.
+ mRootView = (LauncherRootView) getParent();
+ mSysUiScrim = new SysUiScrim(mRootView);
+ mRootView.setSysUiScrim(mSysUiScrim);
}
@Override
@@ -215,12 +219,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 +523,28 @@
@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);
- }
- }
-
- @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);
+ mSysUiScrim.setSize(w, h);
}
@Override
public void setInsets(Rect insets) {
super.setInsets(insets);
- mWorkspaceScrim.onInsetsChanged(insets, mAllowSysuiScrims);
- mOverviewScrim.onInsetsChanged(insets);
+ mSysUiScrim.onInsetsChanged(insets, mAllowSysuiScrims);
}
- public WorkspaceAndHotseatScrim getScrim() {
- return mWorkspaceScrim;
+ public WorkspaceDragScrim getWorkspaceDragScrim() {
+ return mWorkspaceDragScrim;
}
- public OverviewScrim getOverviewScrim() {
- return mOverviewScrim;
+ public SysUiScrim getSysUiScrim() {
+ return mSysUiScrim;
}
}
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..0f26ff4 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -44,7 +44,6 @@
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;
@@ -52,6 +51,7 @@
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;
@@ -67,9 +67,9 @@
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;
+ private boolean mShouldDraw = true;
+ private Drawable mDrawable;
+ private Drawable mCrossFadeDrawable;
@Thunk Paint mPaint;
private final int mBlurSizeOutline;
private final int mRegistrationX;
@@ -114,19 +114,21 @@
* 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 drawable 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.
*/
- public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY,
- final float initialScale, final float scaleOnDrop, final float finalScaleDps) {
+ public DragView(Launcher launcher, Drawable drawable, 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();
+ final float scale = (drawable.getIntrinsicWidth() + finalScaleDps)
+ / drawable.getIntrinsicWidth();
// Set the initial scale to avoid any jumps
setScaleX(initialScale);
@@ -144,8 +146,9 @@
}
});
- mBitmap = bitmap;
- setDragRegion(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()));
+ mDrawable = drawable;
+ setDragRegion(new Rect(0, 0, drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight()));
// The point in our scaled bitmap that the touch events are located
mRegistrationX = registrationX;
@@ -187,9 +190,6 @@
*/
@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) {
@@ -200,8 +200,8 @@
@Override
public void run() {
Object[] outObj = new Object[1];
- int w = mBitmap.getWidth();
- int h = mBitmap.getHeight();
+ int w = mDrawable.getIntrinsicWidth();
+ int h = mDrawable.getIntrinsicHeight();
Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
if (dr instanceof AdaptiveIconDrawable) {
@@ -217,11 +217,11 @@
mBadge.setBounds(badgeBounds);
// Do not draw the background in case of folder as its translucent
- mDrawBitmap = !(dr instanceof FolderAdaptiveIcon);
+ mShouldDraw = !(dr instanceof FolderAdaptiveIcon);
try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) {
Drawable nDr; // drawable to be normalized
- if (mDrawBitmap) {
+ if (mShouldDraw) {
nDr = dr;
} else {
// Since we just want the scale, avoid heavy drawing operations
@@ -311,7 +311,7 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight());
+ setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
}
/** Sets the scale of the view over the normal workspace icon size. */
@@ -355,29 +355,37 @@
return mDragRegion;
}
- public Bitmap getPreviewBitmap() {
- return mBitmap;
- }
-
@Override
protected void onDraw(Canvas canvas) {
mHasDrawn = true;
- if (mDrawBitmap) {
+ if (mShouldDraw) {
// Always draw the bitmap to mask anti aliasing due to clipPath
- boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null;
+ boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeDrawable != null;
if (crossFade) {
int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255;
mPaint.setAlpha(alpha);
}
- canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint);
+ mDrawable.setColorFilter(mPaint.getColorFilter());
+ mDrawable.setAlpha(mPaint.getAlpha());
+ mDrawable.setBounds(
+ new Rect(0, 0, mDrawable.getIntrinsicWidth(),
+ mDrawable.getIntrinsicHeight()));
+ mDrawable.draw(canvas);
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();
+ float sX = ((float) mDrawable.getIntrinsicWidth())
+ / mCrossFadeDrawable.getIntrinsicWidth();
+ float sY = ((float) mDrawable.getIntrinsicHeight())
+ / mCrossFadeDrawable.getIntrinsicHeight();
canvas.scale(sX, sY);
- canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint);
+ mCrossFadeDrawable.setColorFilter(mPaint.getColorFilter());
+ mCrossFadeDrawable.setAlpha(mPaint.getAlpha());
+ mDrawable.setBounds(
+ new Rect(0, 0, mDrawable.getIntrinsicWidth(),
+ mDrawable.getIntrinsicHeight()));
+ mCrossFadeDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
@@ -393,8 +401,8 @@
}
}
- public void setCrossFadeBitmap(Bitmap crossFadeBitmap) {
- mCrossFadeBitmap = crossFadeBitmap;
+ public void setCrossFadeDrawable(Drawable crossFadeDrawable) {
+ mCrossFadeDrawable = crossFadeDrawable;
}
public void crossFade(int duration) {
@@ -472,8 +480,8 @@
// Start the pick-up animation
DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);
- lp.width = mBitmap.getWidth();
- lp.height = mBitmap.getHeight();
+ lp.width = mDrawable.getIntrinsicWidth();
+ lp.height = mDrawable.getIntrinsicHeight();
lp.customPosition = true;
setLayoutParams(lp);
move(touchX, touchY);
@@ -553,6 +561,11 @@
return mInitialScale;
}
+ /** Returns the current {@link Drawable} that is rendered in this view. */
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+
private static class SpringFloatValue {
private static final FloatPropertyCompat<SpringFloatValue> VALUE =
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index 7788f93..0a1aba1 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -65,6 +65,9 @@
}
public Runnable getFlingAnimation(DropTarget.DragObject dragObject, DragOptions options) {
+ if (options == null) {
+ return null;
+ }
PointF vel = isFlingingToDelete();
options.isFlingToDelete = vel != null;
if (!options.isFlingToDelete) {
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..9f12e6e 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -16,7 +16,6 @@
package com.android.launcher3.dragndrop;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
import android.annotation.TargetApi;
import android.appwidget.AppWidgetManager;
@@ -31,17 +30,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.
@@ -101,18 +96,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..c67efef 100644
--- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
@@ -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;
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index d01e189..a1b7997 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -17,15 +17,16 @@
package com.android.launcher3.folder;
import static android.text.TextUtils.isEmpty;
+import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
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_CONVERTED_TO_ICON;
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 +35,34 @@
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.os.Build;
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.WindowInsetsAnimation;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Alarm;
import com.android.launcher3.BubbleTextView;
@@ -74,7 +85,6 @@
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 +96,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;
@@ -158,7 +169,11 @@
private AnimatorSet mCurrentAnimator;
private boolean mIsAnimatingClosed = false;
+ // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
+ // Anything specific to Launcher should use mLauncher, otherwise should use mActivityContext.
protected final Launcher mLauncher;
+ protected final ActivityContext mActivityContext;
+
protected DragController mDragController;
public FolderInfo mInfo;
private CharSequence mFromTitle;
@@ -208,6 +223,8 @@
private StatsLogManager mStatsLogManager;
+ @Nullable private FolderWindowInsetsAnimationCallback mFolderWindowInsetsAnimationCallback;
+
/**
* Used to inflate the Workspace from XML.
*
@@ -219,6 +236,7 @@
setAlwaysDrawnWithCacheEnabled(false);
mLauncher = Launcher.getLauncher(context);
+ mActivityContext = ActivityContext.lookupContext(context);
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
@@ -230,28 +248,35 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ final DeviceProfile dp = mLauncher.getDeviceProfile();
+ final int paddingLeftRight = dp.folderContentPaddingLeftRight;
+
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) {
+ mFolderWindowInsetsAnimationCallback =
+ new FolderWindowInsetsAnimationCallback(DISPATCH_MODE_STOP, this);
+
+ setWindowInsetsAnimationCallback(mFolderWindowInsetsAnimationCallback);
+ }
}
public boolean onLongClick(View v) {
@@ -373,6 +398,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;
}
@@ -421,9 +466,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,13 +522,14 @@
/**
* 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);
}
@@ -561,16 +607,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 +620,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) {
@@ -619,14 +656,6 @@
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();
}
});
@@ -667,6 +696,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,7 +737,7 @@
// 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);
}
@@ -713,6 +745,10 @@
if (mIsAnimatingClosed) {
return;
}
+
+ mContent.completePendingPageChanges();
+ mContent.snapToPageImmediately(mContent.getDestinationPage());
+
if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
mCurrentAnimator.cancel();
}
@@ -720,11 +756,17 @@
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 && mFolderWindowInsetsAnimationCallback != null) {
+ setWindowInsetsAnimationCallback(mFolderWindowInsetsAnimationCallback);
+ }
closeComplete(true);
announceAccessibilityChanges();
mIsAnimatingClosed = false;
@@ -741,12 +783,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);
}
@@ -985,7 +1028,7 @@
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();
@@ -1022,10 +1065,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 +1076,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);
@@ -1080,7 +1096,7 @@
}
protected int getContentAreaHeight() {
- DeviceProfile grid = mLauncher.getDeviceProfile();
+ DeviceProfile grid = mActivityContext.getDeviceProfile();
int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
- mFooterHeight;
int height = Math.min(maxContentAreaHeight,
@@ -1134,6 +1150,9 @@
* Rearranges the children based on their rank.
*/
public void rearrangeChildren() {
+ if (!mContent.areViewsBound()) {
+ return;
+ }
mContent.arrangeChildren(getIconsInReadingOrder());
mItemsInvalidated = true;
}
@@ -1179,7 +1198,10 @@
newIcon.requestFocus();
}
if (finalItem != null) {
- mLauncher.folderConvertedToItem(mFolderIcon.getFolder(), finalItem);
+ StatsLogger logger = mStatsLogManager.logger().withItemInfo(finalItem);
+ mDragController.getLogInstanceId().map(logger::withInstanceId)
+ .orElse(logger)
+ .log(LAUNCHER_FOLDER_CONVERTED_TO_ICON);
}
}
}
@@ -1352,7 +1374,7 @@
@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,
item.cellY);
@@ -1364,10 +1386,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 +1486,6 @@
}
statsLogger.log(LAUNCHER_FOLDER_LABEL_UPDATED);
- logFolderLabelState(mFromLabelState, toLabelState);
mFolderName.dispatchBackKey();
}
}
@@ -1477,27 +1498,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 +1581,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 +1601,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)) {
@@ -1627,8 +1616,7 @@
return true;
}
} else {
- mLauncher.getUserEventDispatcher().logActionTapOutside(
- newContainerTarget(LauncherLogProto.ContainerType.FOLDER));
+ // TODO: add ww log if need to gather tap outside to close folder
close(true);
return true;
}
@@ -1637,6 +1625,11 @@
return false;
}
+ @Override
+ public boolean canInterceptEventsInSystemGestureRegion() {
+ return true;
+ }
+
/**
* Alternative to using {@link #getClipToOutline()} as it only works with derivatives of
* rounded rect.
@@ -1663,14 +1656,63 @@
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;
+ }
+
+ /** Callback that animates a folder sliding up above the ime. */
+ @RequiresApi(api = Build.VERSION_CODES.R)
+ private static class FolderWindowInsetsAnimationCallback
+ extends WindowInsetsAnimation.Callback {
+
+ private final Folder mFolder;
+ float mFolderTranslationStart;
+ float mFolderTranslationEnd;
+
+ FolderWindowInsetsAnimationCallback(int dispatchMode, Folder folder) {
+ super(dispatchMode);
+
+ mFolder = folder;
+ }
+
+ @Override
+ public void onPrepare(@NonNull WindowInsetsAnimation animation) {
+ mFolderTranslationStart = mFolder.getTranslationY();
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsAnimation.Bounds onStart(
+ @NonNull WindowInsetsAnimation animation,
+ @NonNull WindowInsetsAnimation.Bounds bounds) {
+ mFolderTranslationEnd = mFolder.getTranslationY();
+
+ mFolder.setTranslationY(mFolderTranslationStart);
+
+ return super.onStart(animation, bounds);
+ }
+
+ @NonNull
+ @Override
+ public WindowInsets onProgress(@NonNull WindowInsets windowInsets,
+ @NonNull List<WindowInsetsAnimation> list) {
+ if (list.size() == 0) {
+ mFolder.setTranslationY(0);
+
+ return windowInsets;
+ }
+ float progress = list.get(0).getInterpolatedFraction();
+
+ mFolder.setTranslationY(
+ Utilities.mapRange(progress, mFolderTranslationStart, mFolderTranslationEnd));
+
+ return windowInsets;
+ }
+
}
}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 3d72b49..ee6ea99 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -39,14 +39,12 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
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;
@@ -69,7 +67,6 @@
private PreviewBackground mPreviewBackground;
private Context mContext;
- private Launcher mLauncher;
private final boolean mIsOpening;
@@ -92,8 +89,7 @@
mPreviewBackground = mFolderIcon.mBackground;
mContext = folder.getContext();
- mLauncher = folder.mLauncher;
- mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
+ mPreviewVerifier = new FolderGridOrganizer(folder.mActivityContext.getDeviceProfile().inv);
mIsOpening = isOpening;
@@ -114,14 +110,15 @@
* 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;
@@ -179,7 +176,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();
@@ -326,7 +323,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;
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 75275b2..6b02021 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -129,6 +129,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 +166,19 @@
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);
+ folder.setDragController(folder.mLauncher.getDragController());
- 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());
+ icon.setOnFocusChangeListener(folder.mLauncher.getFocusHandler());
+ icon.mBackground.paddingY = icon.isInHotseat()
+ ? 0 : activityContext.getDeviceProfile().cellYPaddingPx;
return icon;
}
@@ -197,7 +201,7 @@
icon.mFolderName.setText(folderInfo.title);
icon.mFolderName.setCompoundDrawablePadding(0);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
- lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
+ lp.topMargin = grid.cellYPaddingPx + grid.iconSizePx + grid.iconDrawablePaddingPx;
icon.setTag(folderInfo);
icon.setOnClickListener(ItemClickHandler.INSTANCE);
@@ -216,6 +220,7 @@
icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+ icon.mBackground.paddingY = icon.isInHotseat() ? 0 : grid.cellYPaddingPx;
icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
icon.mPreviewVerifier.setFolderInfo(folderInfo);
icon.updatePreviewItems(false);
@@ -476,7 +481,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);
}
@@ -578,6 +582,7 @@
public void setFolderBackground(PreviewBackground bg) {
mBackground = bg;
mBackground.setInvalidateDelegate(this);
+ mBackground.paddingY = isInHotseat() ? 0 : mActivity.getDeviceProfile().cellYPaddingPx;
}
@Override
@@ -694,9 +699,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 +716,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 +729,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 +749,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 +830,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/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 32531c0..c1f4643 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -36,7 +36,6 @@
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;
@@ -193,7 +192,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 +229,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 +305,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) {
@@ -484,7 +483,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 +499,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,7 +623,7 @@
@Override
protected boolean canScroll(float absVScroll, float absHScroll) {
- return AbstractFloatingView.getTopOpenViewWithType(mFolder.mLauncher,
+ return AbstractFloatingView.getTopOpenViewWithType(mFolder.mActivityContext,
TYPE_ALL & ~TYPE_FOLDER) == null;
}
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 27b906b..767fffe 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -74,6 +74,7 @@
int previewSize;
int basePreviewOffsetX;
int basePreviewOffsetY;
+ int paddingY;
private CellLayout mDrawingDelegate;
@@ -157,7 +158,7 @@
previewSize = grid.folderIconSizePx;
basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
- basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
+ basePreviewOffsetY = paddingY + topPadding + grid.folderIconOffsetYPx;
// Stroke width is 1dp
mStrokeWidth = context.getResources().getDisplayMetrics().density;
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 7f8a15c..8244f01 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;
@@ -33,12 +32,13 @@
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;
@@ -112,7 +112,7 @@
}
Drawable prepareCreateAnimation(final View destView) {
- Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1];
+ Drawable animateDrawable = ((BubbleTextView) destView).getIcon();
computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
destView.getMeasuredWidth());
mReferenceDrawable = animateDrawable;
@@ -394,12 +394,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);
}
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..0e61b98 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -33,8 +33,10 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.AppWidgetHostViewDrawable;
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 +90,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 new AppWidgetHostViewDrawable((LauncherAppWidgetHostView) mView);
+ }
+
int width = 0;
int height = 0;
// Assume scaleX == scaleY, which is always the case for workspace items.
@@ -105,8 +111,9 @@
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)));
}
public final void generateDragOutline(Bitmap preview) {
@@ -129,7 +136,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,8 +146,8 @@
}
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;
}
diff --git a/src/com/android/launcher3/graphics/GridOptionsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
similarity index 93%
rename from src/com/android/launcher3/graphics/GridOptionsProvider.java
rename to src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 08d7e4c..911f8c3 100644
--- a/src/com/android/launcher3/graphics/GridOptionsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -40,9 +40,9 @@
* /default_grid: Call update to set the current grid, with values
* name: name of the grid to apply
*/
-public class GridOptionsProvider extends ContentProvider {
+public class GridCustomizationsProvider extends ContentProvider {
- private static final String TAG = "GridOptionsProvider";
+ private static final String TAG = "GridCustomizationsProvider";
private static final String KEY_NAME = "name";
private static final String KEY_ROWS = "rows";
@@ -90,7 +90,10 @@
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)));
+ GridOption option = new GridOption(getContext(), Xml.asAttributeSet(parser));
+ if (option.visible) {
+ result.add(option);
+ }
}
}
} catch (IOException | XmlPullParserException e) {
diff --git a/src/com/android/launcher3/graphics/IconShape.java b/src/com/android/launcher3/graphics/IconShape.java
index 4369385..b208a40 100644
--- a/src/com/android/launcher3/graphics/IconShape.java
+++ b/src/com/android/launcher3/graphics/IconShape.java
@@ -43,8 +43,9 @@
import android.view.View;
import android.view.ViewOutlineProvider;
+import androidx.annotation.Nullable;
+
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;
@@ -59,8 +60,6 @@
import java.util.ArrayList;
import java.util.List;
-import androidx.annotation.Nullable;
-
/**
* Abstract representation of the shape of an icon shape
*/
@@ -381,9 +380,6 @@
* Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
*/
public static void init(Context context) {
- if (!Utilities.ATLEAST_OREO) {
- return;
- }
pickBestShape(context);
}
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 885fb66..5e9b179 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -19,6 +19,7 @@
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static android.view.View.VISIBLE;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
@@ -28,6 +29,7 @@
import android.annotation.TargetApi;
import android.app.Fragment;
import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
@@ -61,7 +63,6 @@
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
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;
@@ -70,11 +71,12 @@
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.LoaderResults;
import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDelegate;
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,14 +85,17 @@
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.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;
@@ -113,7 +118,8 @@
* 4) Measure and draw the view on a canvas
*/
@TargetApi(Build.VERSION_CODES.O)
-public class LauncherPreviewRenderer {
+public class LauncherPreviewRenderer extends ContextThemeWrapper
+ implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {
private static final String TAG = "LauncherPreviewRenderer";
@@ -123,7 +129,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));
@@ -157,7 +163,7 @@
*/
public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
MainThreadInitializedObject.ObjectProvider<T> provider) {
- if (!WHITELIST.contains(mainThreadInitializedObject)) {
+ if (!mAllowedObjects.contains(mainThreadInitializedObject)) {
throw new IllegalStateException("Leaking unknown objects");
}
if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
@@ -207,10 +213,14 @@
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;
public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, boolean migrated) {
+ super(context, R.style.AppTheme);
mUiHandler = new Handler(Looper.getMainLooper());
mContext = context;
mIdp = idp;
@@ -235,267 +245,277 @@
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);
}
/** Populate preview and render it. */
public View getRenderedView() {
- MainThreadRenderer renderer = new MainThreadRenderer(mContext);
- renderer.populate();
- return renderer.mRootView;
+ populate();
+ return mRootView;
}
- private class MainThreadRenderer extends ContextThemeWrapper
- implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {
+ public boolean shouldShowRealLauncherPreview() {
+ return ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER.get();
+ }
- private final LayoutInflater mHomeElementInflater;
- private final InsettableFrameLayout mRootView;
+ public boolean shouldShowQsb() {
+ return FeatureFlags.QSB_ON_FIRST_SCREEN;
+ }
- private final Hotseat mHotseat;
- private final CellLayout mWorkspace;
+ @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) {
- 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;
}
-
- @Override
- public View onCreateView(String name, Context context, AttributeSet attrs) {
- return onCreateView(null, name, context, attrs);
+ AppWidgetProviderInfo providerInfo = widgetProviderInfoMap.get(
+ new ComponentKey(info.providerName, info.user));
+ if (providerInfo == null) {
+ return;
}
+ inflateAndAddWidgets(info, LauncherAppWidgetProviderInfo.fromProviderInfo(
+ getApplicationContext(), providerInfo));
+ }
- @Override
- public BaseDragLayer getDragLayer() {
- throw new UnsupportedOperationException();
+ private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
+ WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
+ info.providerName);
+ if (widgetItem == null) {
+ return;
}
+ inflateAndAddWidgets(info, widgetItem.widgetInfo);
+ }
- @Override
- public DeviceProfile getDeviceProfile() {
- return mDp;
- }
+ private void inflateAndAddWidgets(
+ LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
+ AppWidgetHostView view = new AppWidgetHostView(mContext);
+ view.setAppWidget(-1, providerInfo);
+ view.updateAppWidget(null);
+ view.setTag(info);
+ addInScreenFromBind(view, info);
+ }
- @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);
+ private void populate() {
+ if (shouldShowRealLauncherPreview()) {
+ 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 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 Favorites.ITEM_TYPE_FOLDER:
+ inflateAndAddFolder((FolderInfo) itemInfo);
+ break;
+ default:
+ break;
}
- 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;
- }
- }
- for (ItemInfo itemInfo : currentAppWidgets) {
- switch (itemInfo.itemType) {
- case Favorites.ITEM_TYPE_APPWIDGET:
- case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
+ }
+ for (ItemInfo itemInfo : currentAppWidgets) {
+ switch (itemInfo.itemType) {
+ case Favorites.ITEM_TYPE_APPWIDGET:
+ case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
+ if (mMigrated) {
+ inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
+ workspaceResult.mWidgetProvidersMap);
+ } else {
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);
+ IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
+ mIdp.numHotseatIcons);
+ List<ItemInfo> predictions = workspaceResult.mHotseatPredictions == null
+ ? Collections.emptyList() : workspaceResult.mHotseatPredictions.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);
}
-
- // 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);
+ } 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);
+ }
}
+
+ // Add first page QSB
+ if (shouldShowQsb()) {
+ 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) {
@@ -541,8 +561,7 @@
return null;
}
- return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
- mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel);
+ return new WorkspaceResult(mBgDataModel, mBgDataModel.widgetsModel, null);
}
}
@@ -552,7 +571,7 @@
private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
- super(app, null, new BgDataModel(), null);
+ super(app, null, new BgDataModel(), new ModelDelegate(), null);
}
@Override
@@ -566,12 +585,13 @@
}
@Override
- public WorkspaceResult call() throws Exception {
+ public WorkspaceResult call() {
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);
+ loadWorkspace(allShortcuts, LauncherSettings.Favorites.PREVIEW_CONTENT_URI,
+ LauncherSettings.Favorites.SCREEN + " = 0 or "
+ + LauncherSettings.Favorites.CONTAINER + " = "
+ + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
+ return new WorkspaceResult(mBgDataModel, null, mWidgetProvidersMap);
}
}
@@ -591,16 +611,20 @@
private static class WorkspaceResult {
private final ArrayList<ItemInfo> mWorkspaceItems;
private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
- private final ArrayList<AppInfo> mCachedPredictedItems;
+ private final FixedContainerItems mHotseatPredictions;
private final WidgetsModel mWidgetsModel;
+ private final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap;
- private WorkspaceResult(ArrayList<ItemInfo> workspaceItems,
- ArrayList<LauncherAppWidgetInfo> appWidgets,
- ArrayList<AppInfo> cachedPredictedItems, WidgetsModel widgetsModel) {
- mWorkspaceItems = workspaceItems;
- mAppWidgets = appWidgets;
- mCachedPredictedItems = cachedPredictedItems;
- mWidgetsModel = widgetsModel;
+ private WorkspaceResult(BgDataModel dataModel,
+ WidgetsModel widgetsModel,
+ Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
+ synchronized (dataModel) {
+ mWorkspaceItems = dataModel.workspaceItems;
+ mAppWidgets = dataModel.appWidgets;
+ mHotseatPredictions = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
+ mWidgetsModel = widgetsModel;
+ mWidgetProvidersMap = widgetProviderInfoMap;
+ }
}
}
}
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..ac0ec5f 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -24,6 +24,7 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
+import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
@@ -34,10 +35,12 @@
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.anim.Interpolators;
+import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.util.Themes;
import java.lang.ref.WeakReference;
@@ -77,6 +80,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 +97,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,7 +113,23 @@
private ObjectAnimator mCurrentAnim;
+ private boolean mIsStartable;
+
public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
+ this(
+ info,
+ IconPalette.getPreloadProgressColor(context, info.bitmap.color),
+ getPreloadColors(context),
+ (context.getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_NIGHT_MASK
+ & Configuration.UI_MODE_NIGHT_YES) != 0) /* isDarkMode */;
+ }
+
+ public PreloadIconDrawable(
+ ItemInfoWithIcon info,
+ int indicatorColor,
+ int[] preloadColors,
+ boolean isDarkMode) {
super(info.bitmap);
mItem = info;
mShapePath = getShapePath();
@@ -114,9 +139,14 @@
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 +172,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 +181,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 +201,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 +240,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 +305,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 +328,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/Scrim.java b/src/com/android/launcher3/graphics/Scrim.java
index f90962d..a151cba 100644
--- a/src/com/android/launcher3/graphics/Scrim.java
+++ b/src/com/android/launcher3/graphics/Scrim.java
@@ -61,7 +61,11 @@
}
public void draw(Canvas canvas) {
- canvas.drawColor(setColorAlphaBound(mScrimColor, mScrimAlpha));
+ canvas.drawColor(setColorAlphaBound(mScrimColor, getScrimAlpha()));
+ }
+
+ protected int getScrimAlpha() {
+ return mScrimAlpha;
}
private void setScrimProgress(float progress) {
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
similarity index 83%
rename from src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
rename to src/com/android/launcher3/graphics/SysUiScrim.java
index 7b7ab5e..d9c648b 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -34,7 +34,6 @@
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;
@@ -44,41 +43,39 @@
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 class SysUiScrim extends Scrim {
- public static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_PROGRESS =
- new FloatProperty<WorkspaceAndHotseatScrim>("sysUiProgress") {
+ public static final FloatProperty<SysUiScrim> SYSUI_PROGRESS =
+ new FloatProperty<SysUiScrim>("sysUiProgress") {
@Override
- public Float get(WorkspaceAndHotseatScrim scrim) {
+ public Float get(SysUiScrim scrim) {
return scrim.mSysUiProgress;
}
@Override
- public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
+ public void setValue(SysUiScrim scrim, float value) {
scrim.setSysUiProgress(value);
}
};
- private static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_ANIM_MULTIPLIER =
- new FloatProperty<WorkspaceAndHotseatScrim>("sysUiAnimMultiplier") {
+ private static final FloatProperty<SysUiScrim> SYSUI_ANIM_MULTIPLIER =
+ new FloatProperty<SysUiScrim>("sysUiAnimMultiplier") {
@Override
- public Float get(WorkspaceAndHotseatScrim scrim) {
+ public Float get(SysUiScrim scrim) {
return scrim.mSysUiAnimMultiplier;
}
@Override
- public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
+ public void setValue(SysUiScrim scrim, float value) {
scrim.mSysUiAnimMultiplier = value;
scrim.reapplySysUiAlpha();
}
@@ -108,10 +105,6 @@
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();
@@ -127,9 +120,8 @@
private boolean mAnimateScrimOnNextDraw = false;
private float mSysUiAnimMultiplier = 1;
- public WorkspaceAndHotseatScrim(View view) {
+ public SysUiScrim(View view) {
super(view);
-
mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
view.getResources().getDisplayMetrics());
mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
@@ -139,28 +131,10 @@
onExtractedColorsChanged(mWallpaperColorInfo);
}
- public void setWorkspace(Workspace workspace) {
- mWorkspace = workspace;
- }
-
+ /**
+ * Draw the top and bottom scrims
+ */
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;
@@ -247,6 +221,11 @@
super.onExtractedColorsChanged(wallpaperColorInfo);
}
+ /**
+ * 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);
diff --git a/src/com/android/launcher3/graphics/WorkspaceDragScrim.java b/src/com/android/launcher3/graphics/WorkspaceDragScrim.java
new file mode 100644
index 0000000..d8dc563
--- /dev/null
+++ b/src/com/android/launcher3/graphics/WorkspaceDragScrim.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.graphics;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Workspace;
+
+/**
+ * Scrim drawn during SpringLoaded State (ie. Drag and Drop). Darkens the workspace except for
+ * the focused CellLayout.
+ */
+public class WorkspaceDragScrim extends Scrim {
+
+ private final Rect mHighlightRect = new Rect();
+
+ private Workspace mWorkspace;
+
+ public WorkspaceDragScrim(View view) {
+ super(view);
+ onExtractedColorsChanged(mWallpaperColorInfo);
+ }
+
+ /**
+ * Set the workspace that this scrim is acting on
+ * @param workspace
+ */
+ public void setWorkspace(Workspace workspace) {
+ mWorkspace = workspace;
+ mWorkspace.setWorkspaceDragScrim(this);
+ }
+
+ /**
+ * Cut out the focused paged of the Workspace and then draw the scrim
+ * @param canvas
+ */
+ 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();
+ }
+ }
+
+}
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..988794c 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;
@@ -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;
@@ -132,36 +131,35 @@
/**
* 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 +190,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 +258,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 +285,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);
}
@@ -326,7 +317,8 @@
}
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
@@ -344,12 +336,6 @@
return super.getEntryFromDB(cacheKey, entry, lowRes);
}
- public static abstract class IconLoadRequest extends HandlerRunnable {
- IconLoadRequest(Handler handler, Runnable endRunnable) {
- super(handler, endRunnable);
- }
- }
-
/**
* Interface for receiving itemInfo with high-res icon.
*/
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/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..c5ffff9 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -15,29 +15,32 @@
*/
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.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 +51,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 +100,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),
@@ -273,7 +262,159 @@
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)
+ ;
// ADD MORE
@@ -380,45 +521,56 @@
}
/**
+ * Sets logging fields from provided {@link SliceItem}.
+ */
+ default StatsLogger withSliceItem(SliceItem sliceItem) {
+ 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..fd51ba8 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -20,6 +20,7 @@
import android.content.pm.LauncherApps;
import android.content.pm.PackageInstaller.SessionInfo;
import android.os.UserHandle;
+import android.util.Log;
import android.util.LongSparseArray;
import android.util.Pair;
@@ -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;
/**
@@ -132,7 +136,9 @@
continue;
}
} else {
- workspaceInfo.setInstallProgress((int) sessionInfo.getProgress());
+ workspaceInfo.setProgressLevel(
+ (int) (sessionInfo.getProgress() * 100),
+ PackageInstallInfo.STATUS_INSTALLING);
}
if (hasActivity) {
@@ -164,6 +170,8 @@
// Save the WorkspaceItemInfo for binding in the workspace
addedItemsFinal.add(itemInfo);
+
+ Log.i(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..804e72e 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;
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index ebdfa8c..c6c0791 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;
@@ -406,7 +406,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..836d804
--- /dev/null
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -0,0 +1,383 @@
+/*
+ * 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.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()) {
+ 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) {
+ // 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) {
+ Log.i(LOG, "Adding PendingInstallShortcutInfo with no attached info to queue.");
+ } else {
+ Log.i(LOG, "Adding PendingInstallShortcutInfo to queue. Attached info: "
+ + itemInfo.first);
+ }
+
+ 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..19d9af9 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;
@@ -65,7 +67,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 +94,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 +137,8 @@
public boolean moveToNext() {
boolean result = super.moveToNext();
if (result) {
+ mActivityInfo = null;
+
// Load common properties.
itemType = getInt(itemTypeIndex);
container = getInt(containerIndex);
@@ -245,6 +252,10 @@
return info;
}
+ public LauncherActivityInfo getLauncherActivityInfo() {
+ return mActivityInfo;
+ }
+
/**
* Make an WorkspaceItemInfo object for a shortcut that is an application.
*/
@@ -264,25 +275,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
@@ -445,7 +456,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..f6c7c06 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -41,17 +41,15 @@
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
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.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
@@ -71,6 +69,7 @@
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;
@@ -85,10 +84,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 +96,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CancellationException;
/**
@@ -112,6 +112,7 @@
protected final LauncherAppState mApp;
private final AllAppsList mBgAllAppsList;
protected final BgDataModel mBgDataModel;
+ private final ModelDelegate mModelDelegate;
private FirstScreenBroadcast mFirstScreenBroadcast;
@@ -126,13 +127,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 +168,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,13 +190,23 @@
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
loadWorkspace(allShortcuts);
- loadCachedPredictions();
logger.addSplit("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();
+ logger.addSplit("sanitizeData");
+ }
+
verifyNotStopped();
mResults.bindWorkspace();
logger.addSplit("bindWorkspace");
+ mModelDelegate.workspaceLoadComplete();
// Notify the installer packages of packages with active installs on the first screen.
sendFirstScreenActiveInstallsBroadcast();
logger.addSplit("sendFirstScreenActiveInstallsBroadcast");
@@ -275,6 +288,7 @@
updateHandler.finish();
logger.addSplit("finish icon update");
+ mModelDelegate.modelLoadComplete();
transaction.commit();
} catch (CancellationException e) {
// Loader stopped, ignore
@@ -291,16 +305,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 +345,7 @@
synchronized (mBgDataModel) {
mBgDataModel.clear();
+ mPendingPackages.clear();
final HashMap<PackageUserKey, SessionInfo> installingPkgs =
mSessionHelper.getActiveSessions();
@@ -339,11 +356,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);
@@ -358,15 +375,12 @@
final int optionsIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.OPTIONS);
- 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 +404,7 @@
WorkspaceItemInfo info;
LauncherAppWidgetInfo appWidgetInfo;
+ LauncherAppWidgetProviderInfo widgetProviderInfo;
Intent intent;
String targetPkg;
@@ -417,16 +432,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 +501,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 +591,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 +667,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 &&
@@ -728,6 +747,19 @@
+ appWidgetInfo.spanX + "x" + appWidgetInfo.spanY);
continue;
}
+ widgetProviderInfo =
+ widgetHelper.getLauncherAppWidgetInfo(appWidgetId);
+ if (widgetProviderInfo != null
+ && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
+ || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
+ // This can happen when display size changes.
+ c.markDeleted("Widget removed, min sizes not met: "
+ + "span=" + appWidgetInfo.spanX + "x"
+ + appWidgetInfo.spanY + " minSpan="
+ + widgetProviderInfo.minSpanX + "x"
+ + widgetProviderInfo.minSpanY);
+ continue;
+ }
if (!c.isOnWorkspaceOrHotseat()) {
c.markDeleted("Widget found where container != " +
"CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!");
@@ -769,6 +801,9 @@
IOUtils.closeSilently(c);
}
+ // Load delegate items
+ mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts);
+
// Break early if we've stopped loading
if (mStopped) {
mBgDataModel.clear();
@@ -776,34 +811,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 +837,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 +861,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 +926,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());
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/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..312435d 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.
@@ -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..7bfa3ef 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -22,18 +22,16 @@
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.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 +40,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 +53,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 +92,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 +108,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,7 +120,8 @@
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);
}
}
@@ -158,19 +158,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 +183,128 @@
// 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 = 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 (!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 +332,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 +356,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..97071bb 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -4,11 +4,11 @@
import android.content.pm.PackageManager;
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 +43,20 @@
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;
+ }
}
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..e54f1e7 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;
@@ -180,6 +184,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 +245,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 +279,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 +301,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
@@ -344,7 +372,10 @@
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 +423,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..76b2ab0 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,83 @@
}
/**
+ * 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) {
+ FastBitmapDrawable drawable = 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..c04b7f0 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -26,7 +26,6 @@
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;
/**
@@ -195,13 +194,4 @@
public final boolean hasOptionFlag(int option) {
return (options & option) != 0;
}
-
- @Override
- public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
- builder.setWidget(LauncherAtom.Widget.newBuilder()
- .setSpanX(spanX)
- .setSpanY(spanY)
- .setComponentName(providerName.toString())
- .setPackageName(providerName.getPackageName()));
- }
}
diff --git a/src/com/android/launcher3/model/data/PackageItemInfo.java b/src/com/android/launcher3/model/data/PackageItemInfo.java
index b70d0d4..7617d7e 100644
--- a/src/com/android/launcher3/model/data/PackageItemInfo.java
+++ b/src/com/android/launcher3/model/data/PackageItemInfo.java
@@ -60,6 +60,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/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..e954480 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -21,10 +21,13 @@
import android.app.Notification;
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;
@@ -43,7 +46,8 @@
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;
@@ -53,7 +57,6 @@
private final View mIconView;
private final View mHeader;
- private final View mDivider;
private View mGutter;
@@ -61,8 +64,9 @@
private boolean mAnimatingNextIcon;
private int mNotificationHeaderTextColor = Notification.COLOR_DEFAULT;
- 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);
@@ -72,17 +76,29 @@
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);
+ }
+ });
+ }
+
+ public void updateBackgroundColor(int color) {
+ mMainView.updateBackgroundColor(color);
}
public void addGutter() {
if (mGutter == null) {
- mGutter = mContainer.inflateAndAdd(R.layout.notification_gutter, mContainer);
+ mGutter = mPopupContainer.inflateAndAdd(R.layout.notification_gutter, mRootView);
}
}
@@ -94,9 +110,8 @@
}
public void removeFooter() {
- if (mContainer.indexOfChild(mFooter) >= 0) {
- mContainer.removeView(mFooter);
- mContainer.removeView(mDivider);
+ if (mRootView.indexOfChild(mFooter) >= 0) {
+ mRootView.removeView(mFooter);
}
}
@@ -108,16 +123,15 @@
}
public void removeAllViews() {
- mContainer.removeView(mMainView);
- mContainer.removeView(mHeader);
+ mRootView.removeView(mMainView);
+ mRootView.removeView(mHeader);
- if (mContainer.indexOfChild(mFooter) >= 0) {
- mContainer.removeView(mFooter);
- mContainer.removeView(mDivider);
+ if (mRootView.indexOfChild(mFooter) >= 0) {
+ mRootView.removeView(mFooter);
}
if (mGutter != null) {
- mContainer.removeView(mGutter);
+ mRootView.removeView(mGutter);
}
}
@@ -136,11 +150,11 @@
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) {
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..c995666 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -17,6 +17,7 @@
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.ObjectAnimator;
@@ -41,7 +42,6 @@
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;
/**
@@ -97,15 +97,24 @@
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 updateBackgroundColor(int color) {
+ mBackgroundColor = color;
+ RippleDrawable rippleBackground = new RippleDrawable(ColorStateList.valueOf(
+ Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)),
+ new ColorDrawable(color), null);
+ mTextAndBackground.setBackground(rippleBackground);
+ if (mNotificationInfo != null) {
+ mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
+ mBackgroundColor));
+ }
}
public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) {
@@ -137,7 +146,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) {
@@ -168,10 +177,7 @@
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);
+ launcher.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED);
}
// SingleAxisSwipeDetector.Listener's
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
index 408796f..a7cd10d 100644
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
@@ -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..72f1c58 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;
@@ -28,20 +29,23 @@
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
+import android.util.Log;
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.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,35 @@
* - 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());
+ && !promiseIconAddedForId(sessionInfo.getSessionId())
+ && !new PackageManagerHelper(mAppContext).isAppInstalled(
+ sessionInfo.getAppPackageName(), getUserHandle(sessionInfo))) {
+ Log.i(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 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 +251,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..3736538 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -17,20 +17,22 @@
package com.android.launcher3.popup;
import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
+import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
-import android.graphics.CornerPathEffect;
import android.graphics.Outline;
-import android.graphics.Paint;
import android.graphics.Rect;
-import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.Gravity;
@@ -40,6 +42,8 @@
import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.InsettableFrameLayout;
@@ -49,7 +53,7 @@
import com.android.launcher3.anim.RevealOutlineAnimation;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.TriangleShape;
+import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
@@ -63,6 +67,9 @@
*/
public abstract class ArrowPopup<T extends BaseDraggingActivity> extends AbstractFloatingView {
+ // +1 for system shortcut view
+ private static final int MAX_NUM_CHILDREN = MAX_SHORTCUTS + 1;
+
private final Rect mTempRect = new Rect();
protected final LayoutInflater mInflater;
@@ -70,9 +77,15 @@
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;
@@ -82,6 +95,14 @@
private final Rect mStartRect = new Rect();
private final Rect mEndRect = new Rect();
+ private final GradientDrawable mRoundedTop;
+ private final GradientDrawable mRoundedBottom;
+
+ private Runnable mOnCloseCallback = () -> { };
+
+ private int mArrowColor;
+ private final int[] mColors;
+
public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mInflater = LayoutInflater.from(context);
@@ -99,11 +120,34 @@
// 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.setCornerRadii(new float[] { mOutlineRadius, mOutlineRadius, mOutlineRadius,
+ mOutlineRadius, smallerRadius, smallerRadius, smallerRadius, smallerRadius});
+
+ mRoundedBottom = new GradientDrawable();
+ mRoundedBottom.setCornerRadii(new float[] { smallerRadius, smallerRadius, smallerRadius,
+ smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius});
+
+ int primaryColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
+ int secondaryColor = Themes.getAttrColor(context, R.attr.popupColorSecondary);
+ ArgbEvaluator argb = new ArgbEvaluator();
+ mColors = new int[MAX_NUM_CHILDREN];
+ // Interpolate between the two colors, exclusive.
+ float step = 1f / (MAX_NUM_CHILDREN + 1);
+ for (int i = 0; i < mColors.length; ++i) {
+ mColors[i] = (int) argb.evaluate((i + 1) * step, primaryColor, secondaryColor);
+ }
}
public ArrowPopup(Context context, AttributeSet attrs) {
@@ -147,79 +191,164 @@
protected void onInflationComplete(boolean isReversed) { }
/**
+ * Set the margins and radius of backgrounds after views are properly ordered.
+ */
+ protected void assignMarginsAndBackgrounds() {
+ int count = getChildCount();
+ int totalVisibleShortcuts = 0;
+ for (int i = 0; i < count; i++) {
+ View view = getChildAt(i);
+ if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) {
+ totalVisibleShortcuts++;
+ }
+ }
+
+ int numVisibleShortcut = 0;
+ View lastView = null;
+ int numVisibleChild = 0;
+ for (int i = 0; i < count; i++) {
+ View view = getChildAt(i);
+ boolean isShortcut = view instanceof DeepShortcutView;
+ 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 (isShortcut) {
+ if (totalVisibleShortcuts == 1) {
+ view.setBackgroundResource(R.drawable.single_item_primary);
+ } else if (totalVisibleShortcuts > 1) {
+ if (numVisibleShortcut == 0) {
+ view.setBackground(mRoundedTop);
+ } else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) {
+ view.setBackground(mRoundedBottom);
+ } else {
+ view.setBackgroundResource(R.drawable.middle_item_primary);
+ }
+ numVisibleShortcut++;
+ }
+ }
+
+ int color = mColors[numVisibleChild % mColors.length];
+ setChildColor(view, color);
+
+ // Arrow color matches the first child or the last child.
+ if (!mIsAboveIcon && numVisibleChild == 0) {
+ mArrowColor = color;
+ } else if (mIsAboveIcon) {
+ mArrowColor = color;
+ }
+
+ numVisibleChild++;
+ }
+ }
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ }
+
+ /**
+ * Sets the background color of the child.
+ */
+ protected void setChildColor(View view, int color) {
+ Drawable bg = view.getBackground();
+ if (bg instanceof GradientDrawable) {
+ ((GradientDrawable) bg.mutate()).setColor(color);
+ } else if (bg instanceof ColorDrawable) {
+ ((ColorDrawable) bg.mutate()).setColor(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();
+ if (shouldAddArrow()) {
+ addArrow();
+ }
+ animateOpen();
+ }
+
+ /**
+ * Shows the popup at the desired location.
+ */
+ protected void show() {
+ setupForDisplay();
+ onInflationComplete(false);
+ assignMarginsAndBackgrounds();
+ 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.setBackground(new RoundedArrowDrawable(
+ mArrowWidth, mArrowHeight, mArrowPointRadius,
+ mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(),
+ mArrowOffsetHorizontal, -mArrowOffsetVertical,
+ !mIsAboveIcon, mIsLeftAligned,
+ mArrowColor));
mArrow.setElevation(getElevation());
}
- 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;
+ /**
+ * Returns whether or not we should add the arrow.
+ */
+ protected boolean shouldAddArrow() {
+ return true;
}
/**
@@ -252,10 +381,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();
getTargetObjectLocation(mTempRect);
InsettableFrameLayout dragLayer = getPopupContainer();
@@ -269,22 +407,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 +476,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;
}
}
@@ -394,13 +519,19 @@
return getChildCount() > 0 ? getChildAt(0) : this;
}
+ private int getArrowDuration() {
+ return shouldAddArrow()
+ ? getResources().getInteger(R.integer.config_popupArrowOpenCloseDuration)
+ : 0;
+ }
+
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 long arrowDuration = getArrowDuration();
final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
// Rectangular reveal.
@@ -462,7 +593,7 @@
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);
+ final long arrowDuration = getArrowDuration();
// Hide the arrow
Animator scaleArrow = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0)
@@ -507,22 +638,13 @@
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 arrowLeft = getArrowLeft();
int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0;
- mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
- arrowCenterY);
+ mStartRect.set(arrowLeft, arrowCenterY, arrowLeft + mArrowWidth, arrowCenterY);
- return new RoundedRectRevealOutlineProvider
- (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
+ return new RoundedRectRevealOutlineProvider(
+ mArrowPointRadius, mOutlineRadius, mStartRect, mEndRect);
}
/**
@@ -537,6 +659,14 @@
mDeferContainerRemoval = false;
getPopupContainer().removeView(this);
getPopupContainer().removeView(mArrow);
+ mOnCloseCallback.run();
+ }
+
+ /**
+ * 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..c282ae8 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;
@@ -52,7 +47,6 @@
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Launcher;
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;
@@ -66,6 +60,7 @@
import com.android.launcher3.notification.NotificationInfo;
import com.android.launcher3.notification.NotificationItemView;
import com.android.launcher3.notification.NotificationKeyData;
+import com.android.launcher3.notification.NotificationMainView;
import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
@@ -97,6 +92,7 @@
private BubbleTextView mOriginalIcon;
private NotificationItemView mNotificationItemView;
private int mNumNotifications;
+ private ViewGroup mNotificationContainer;
private ViewGroup mSystemShortcutContainer;
@@ -148,17 +144,6 @@
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 +160,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 +171,14 @@
return false;
}
+ @Override
+ protected void setChildColor(View v, int color) {
+ super.setChildColor(v, color);
+ if (v.getId() == R.id.notification_container && mNotificationItemView != null) {
+ mNotificationItemView.updateBackgroundColor(color);
+ }
+ }
+
/**
* Returns true if we can show the container.
*/
@@ -224,6 +216,7 @@
.filter(Objects::nonNull)
.collect(Collectors.toList()));
launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
+ container.requestFocus();
return container;
}
@@ -239,20 +232,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,8 +253,12 @@
// Add views
if (mNumNotifications > 0) {
// Add notification entries
- View.inflate(getContext(), R.layout.notification_content, this);
- mNotificationItemView = new NotificationItemView(this);
+ if (mNotificationContainer == null) {
+ mNotificationContainer = findViewById(R.id.notification_container);
+ mNotificationContainer.setVisibility(VISIBLE);
+ }
+ View.inflate(getContext(), R.layout.notification_content, mNotificationContainer);
+ mNotificationItemView = new NotificationItemView(this, mNotificationContainer);
if (mNumNotifications == 1) {
mNotificationItemView.removeFooter();
}
@@ -359,34 +342,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 +360,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 +410,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);
@@ -499,18 +459,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 */));
@@ -620,8 +568,9 @@
// No more notifications, remove the notification views and expand all shortcuts.
mNotificationItemView.removeAllViews();
mNotificationItemView = null;
+ mNotificationContainer.setVisibility(GONE);
updateHiddenShortcuts();
- updateDividers();
+ assignMarginsAndBackgrounds();
} else {
mNotificationItemView.trimNotifications(
NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
@@ -630,6 +579,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..e5424cf 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,11 @@
protected final T mTarget;
protected final ItemInfo mItemInfo;
+ /**
+ * Indicates if it's invokable or not through some disabled UI
+ */
+ private boolean isEnabled = true;
+
public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) {
mIconResId = iconResId;
mLabelResId = labelResId;
@@ -85,6 +88,14 @@
mAccessibilityActionId, context.getText(mLabelResId));
}
+ public void setEnabled(boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
public boolean hasHandlerForAction(int action) {
return mAccessibilityActionId == action;
}
@@ -99,7 +110,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 +128,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 +148,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 +181,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/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 289e0d8..a191df4 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -20,6 +20,8 @@
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_PROVIDER;
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
import android.app.Activity;
import android.app.Fragment;
import android.app.SearchManager;
@@ -34,6 +36,7 @@
import android.os.Bundle;
import android.provider.Settings;
import android.util.AttributeSet;
+import android.util.SizeF;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -50,6 +53,8 @@
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.FragmentWithPreview;
+import java.util.ArrayList;
+
/**
* A frame layout which contains a QSB. This internally uses fragment to bind the view, which
* allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}.
@@ -294,12 +299,16 @@
InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
Bundle opts = new Bundle();
- Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(),
- idp.numColumns, 1, null);
+ ArrayList<SizeF> sizes = AppWidgetResizeFrame
+ .getWidgetSizes(getContext(), idp.numColumns, 1);
+ Rect size = AppWidgetResizeFrame.getMinMaxSizes(sizes, null /* outRect */);
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);
+ if (ATLEAST_S) {
+ opts.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizes);
+ }
return opts;
}
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/allapps/search/SearchAlgorithm.java b/src/com/android/launcher3/search/SearchAlgorithm.java
similarity index 69%
rename from src/com/android/launcher3/allapps/search/SearchAlgorithm.java
rename to src/com/android/launcher3/search/SearchAlgorithm.java
index c409b1c..a1720c7 100644
--- a/src/com/android/launcher3/allapps/search/SearchAlgorithm.java
+++ b/src/com/android/launcher3/search/SearchAlgorithm.java
@@ -13,20 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3.allapps.search;
+package com.android.launcher3.search;
/**
* An interface for handling search.
+ *
+ * @param <T> Search Result type
*/
-public interface SearchAlgorithm {
+public interface SearchAlgorithm<T> {
/**
- * Performs search and sends the result to the callback.
+ * Performs search and sends the result to {@link SearchCallback}.
*/
- void doSearch(String query, AllAppsSearchBarController.Callbacks callback);
+ 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..f5e74b7 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;
@@ -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/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index b12d04f..8d676c9 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;
@@ -99,6 +106,50 @@
maybeAddSandboxCategory();
}
+ 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);
+ }
+ });
+ }
+
@Override
public void onDestroy() {
super.onDestroy();
@@ -161,7 +212,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 +224,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 +235,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 +243,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);
@@ -224,7 +275,8 @@
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[] {"RIGHT_EDGE_BACK_NAVIGATION"}));
return true;
});
sandboxCategory.addPreference(launchBackTutorialPreference);
@@ -233,7 +285,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 +296,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 +307,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 +341,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 +408,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..afcf882 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,29 @@
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();
+ }
+
+ 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/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index d3213a1..5b42ac7 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -18,14 +18,11 @@
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.SharedPreferences;
import android.os.Bundle;
-import android.provider.Settings;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -45,8 +42,8 @@
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;
/**
* Settings activity for Launcher. Currently implements the following setting: Allow rotation
@@ -59,8 +56,6 @@
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";
@@ -126,10 +121,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) {
@@ -172,24 +166,7 @@
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)) {
@@ -205,18 +182,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) {
@@ -252,14 +248,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..1c1418c 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -41,7 +41,6 @@
private BubbleTextView mBubbleText;
private View mIconView;
- private View mDivider;
private WorkspaceItemInfo mInfo;
private ShortcutInfo mDetail;
@@ -63,11 +62,6 @@
super.onFinishInflate();
mBubbleText = findViewById(R.id.bubble_text);
mIconView = findViewById(R.id.icon);
- mDivider = findViewById(R.id.divider);
- }
-
- public void setDividerVisibility(int visibility) {
- mDivider.setVisibility(visibility);
}
public BubbleTextView getBubbleText() {
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..14ef2dc 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -16,7 +16,9 @@
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.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
@@ -24,15 +26,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 +77,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 +97,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;
}
@@ -179,7 +189,7 @@
private void goToState(STATE_TYPE state, boolean animated, long delay,
final Runnable onCompleteRunnable) {
- animated &= Utilities.areAnimationsEnabled(mActivity);
+ animated &= areAnimatorsEnabled();
if (mActivity.isInState(state)) {
if (mConfig.currentAnimation == null) {
// Run any queued runnable
@@ -279,14 +289,14 @@
*/
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);
}
@@ -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) {
@@ -396,6 +403,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.
*
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..22c9d5b 100644
--- a/src/com/android/launcher3/states/HintState.java
+++ b/src/com/android/launcher3/states/HintState.java
@@ -15,11 +15,12 @@
*/
package com.android.launcher3.states;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+
import android.content.Context;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
/**
* Scale down workspace/hotseat to hint at going to either overview (on pause) or first home screen.
@@ -30,7 +31,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,7 +49,7 @@
}
@Override
- public float getOverviewScrimAlpha(Launcher launcher) {
+ public float getWorkspaceScrimAlpha(Launcher launcher) {
return 0.4f;
}
@@ -52,10 +57,4 @@
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..2da06e9 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -21,14 +21,9 @@
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.Utilities;
@@ -43,16 +38,6 @@
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.
@@ -66,7 +51,7 @@
public static final int REQUEST_ROTATE = 1;
public static final int REQUEST_LOCK = 2;
- private final Activity mActivity;
+ private Activity mActivity;
private final SharedPreferences mSharedPrefs;
private boolean mIgnoreAutoRotateSettings;
@@ -91,7 +76,8 @@
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) {
mActivity = activity;
@@ -106,30 +92,16 @@
} 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);
- }
-
- 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());
if (mHomeRotationEnabled != wasRotationEnabled) {
notifyChange();
- updateAutoRotateSetting();
}
}
@@ -165,21 +137,16 @@
if (!mInitialized) {
mInitialized = true;
notifyChange();
-
- mContentResolver.registerContentObserver(
- Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
- false, mSystemAutoRotateObserver);
- updateAutoRotateSetting();
}
}
public void destroy() {
if (!mDestroyed) {
mDestroyed = true;
+ mActivity = null;
if (mSharedPrefs != null) {
mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
}
- mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
}
}
@@ -225,9 +192,8 @@
@Override
public String toString() {
return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d,"
- + " mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, mHomeRotationEnabled=%b,"
- + " mSystemAutoRotateEnabled=%b]",
+ + " mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, mHomeRotationEnabled=%b]",
mStateHandlerRequest, mCurrentStateRequest, mLastActivityFlags,
- mIgnoreAutoRotateSettings, mHomeRotationEnabled, mSystemAutoRotateEnabled);
+ mIgnoreAutoRotateSettings, mHomeRotationEnabled);
}
}
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 2a4f887..39bcdc5 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) {
+ public float getWorkspaceBackgroundAlpha(Launcher launcher) {
return 0.3f;
}
+
+ @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..0dbfb0b 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -27,32 +27,21 @@
*/
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,
})
@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 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 +56,11 @@
ANIM_OVERVIEW_TRANSLATE_Y,
ANIM_OVERVIEW_FADE,
ANIM_ALL_APPS_FADE,
- ANIM_OVERVIEW_SCRIM_FADE,
+ ANIM_WORKSPACE_SCRIM_FADE,
ANIM_ALL_APPS_HEADER_FADE,
ANIM_OVERVIEW_MODAL,
ANIM_DEPTH,
+ ANIM_OVERVIEW_ACTIONS_FADE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AnimType {}
@@ -85,14 +75,15 @@
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_WORKSPACE_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_OVERVIEW_ACTIONS_FADE = 15;
- private static final int ANIM_TYPES_COUNT = 15;
+ private static final int ANIM_TYPES_COUNT = 16;
- 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..943d129 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,6 +84,7 @@
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";
@@ -97,13 +101,16 @@
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 TIS_NO_EVENTS = "b/180915942";
+ public static final String GET_RECENTS_FAILED = "b/177472267";
}
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 3c78b08..a437293 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,19 +50,13 @@
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;
+ protected final AnimatorListener mClearStateOnCancelListener =
+ newCancelListener(this::clearState);
+
private boolean mNoIntercept;
private boolean mIsLogContainerSet;
protected int mStartContainerType;
@@ -82,30 +65,14 @@
protected LauncherState mFromState;
protected LauncherState mToState;
protected AnimatorPlaybackController mCurrentAnimation;
- protected PendingAnimation mPendingAnimation;
+ protected boolean mGoingBetweenStates = true;
private float mStartProgress;
// Ratio of transition process [0, 1] to drag displacement (px)
private float mProgressMultiplier;
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 final FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
mLauncher = l;
@@ -113,14 +80,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 +131,6 @@
@Override
public final boolean onControllerTouchEvent(MotionEvent ev) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PAUSE_NOT_DETECTED, "onControllerTouchEvent");
- }
return mDetector.onTouchEvent(ev);
}
@@ -185,61 +145,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 +190,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
@@ -302,11 +227,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 +243,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 +255,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 +278,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 +303,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 +317,18 @@
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);
}
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 +337,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.getDragLayer().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..2e54904 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,9 @@
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
import android.app.AlertDialog;
+import android.app.PendingIntent;
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 +39,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 +49,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 +73,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 +85,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 +180,7 @@
LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
try {
launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
- launcher.getActivityLaunchOptionsAsBundle(v));
+ launcher.getActivityLaunchOptions(v).toBundle());
return;
} catch (Exception e) {
Log.e(TAG, "Unable to launch market intent for package=" + packageName, e);
@@ -191,7 +190,7 @@
// Fallback to using custom market intent.
Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName);
- launcher.startActivitySafely(v, intent, item, null);
+ launcher.startActivitySafely(v, intent, item);
}
/**
@@ -199,8 +198,7 @@
*
* @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) {
+ public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
if (shortcut.isDisabled()) {
final int disabledFlags = shortcut.runtimeStatusFlags
& WorkspaceItemInfo.FLAG_DISABLED_MASK;
@@ -234,24 +232,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 +304,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..a241e63 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -17,9 +17,13 @@
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;
@@ -33,19 +37,25 @@
import android.view.accessibility.AccessibilityEvent;
import android.widget.LinearLayout;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+
+import java.util.Collections;
+import java.util.List;
public class LandscapePagedViewHandler implements PagedOrientationHandler {
@Override
- public int getPrimaryValue(int x, int y) {
+ public <T> T getPrimaryValue(T x, T y) {
return y;
}
@Override
- public int getSecondaryValue(int x, int y) {
+ public <T> T getSecondaryValue(T x, T y) {
return x;
}
@@ -65,19 +75,6 @@
}
@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;
- }
-
- @Override
public boolean isLayoutNaturalToLauncher() {
return false;
}
@@ -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);
}
@@ -125,7 +127,7 @@
}
@Override
- public int getClearAllScrollOffset(View view, boolean isRtl) {
+ public int getClearAllSidePadding(View view, boolean isRtl) {
return (isRtl ? view.getPaddingBottom() : - view.getPaddingTop()) / 2;
}
@@ -145,12 +147,6 @@
}
@Override
- public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
- view.setTranslationX(0);
- view.setTranslationY(translation);
- }
-
- @Override
public int getPrimaryScroll(View view) {
return view.getScrollY();
}
@@ -207,18 +203,26 @@
}
@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 int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ return translationOffset;
}
@Override
@@ -237,7 +241,8 @@
}
@Override
- public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
+ public int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate,
+ LinearLayout taskMenuLayout) {
return LinearLayout.HORIZONTAL;
}
@@ -248,6 +253,31 @@
lp.weight = 1;
}
+ /* ---------- 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) {
@@ -260,4 +290,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..b85d08a 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -32,6 +32,10 @@
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
@@ -57,15 +61,15 @@
<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);
float getPrimarySize(RectF rect);
- int getClearAllScrollOffset(View view, boolean isRtl);
+ int getClearAllSidePadding(View view, boolean isRtl);
int getSecondaryDimension(View view);
FloatProperty<View> getPrimaryViewTranslate();
FloatProperty<View> getSecondaryViewTranslate();
- void setPrimaryAndResetSecondaryTranslate(View view, float translation);
int getPrimaryScroll(View view);
float getPrimaryScale(View view);
int getChildStart(View view);
@@ -73,29 +77,42 @@
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();
- int getPrimaryValue(int x, int y);
- int getSecondaryValue(int x, int y);
+ <T> T getPrimaryValue(T x, T y);
+ <T> T getSecondaryValue(T x, T 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);
boolean isLayoutNaturalToLauncher();
float getTaskMenuX(float x, View thumbnailView);
float getTaskMenuY(float y, View thumbnailView);
int getTaskMenuWidth(View view);
- int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout);
+ int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate, LinearLayout taskMenuLayout);
void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
+ int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
+ List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
+ FloatProperty getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary,
+ DeviceProfile deviceProfile);
+
+ // 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 +120,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..2fb5952 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -19,6 +19,9 @@
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;
@@ -32,19 +35,25 @@
import android.view.accessibility.AccessibilityEvent;
import android.widget.LinearLayout;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+
+import java.util.ArrayList;
+import java.util.List;
public class PortraitPagedViewHandler implements PagedOrientationHandler {
@Override
- public int getPrimaryValue(int x, int y) {
+ public <T> T getPrimaryValue(T x, T y) {
return x;
}
@Override
- public int getSecondaryValue(int x, int y) {
+ public <T> T getSecondaryValue(T x, T y) {
return y;
}
@@ -64,20 +73,6 @@
}
@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;
- }
-
- @Override
public boolean isLayoutNaturalToLauncher() {
return true;
}
@@ -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);
}
@@ -123,7 +123,7 @@
}
@Override
- public int getClearAllScrollOffset(View view, boolean isRtl) {
+ public int getClearAllSidePadding(View view, boolean isRtl) {
return (isRtl ? view.getPaddingRight() : - view.getPaddingLeft()) / 2;
}
@@ -143,12 +143,6 @@
}
@Override
- public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
- view.setTranslationX(translation);
- view.setTranslationY(0);
- }
-
- @Override
public int getPrimaryScroll(View view) {
return view.getScrollX();
}
@@ -205,19 +199,29 @@
}
@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 int getSplitAnimationTranslation(int translationOffset, DeviceProfile dp) {
+ if (dp.isLandscape) {
+ return translationOffset;
+ }
+ return 0;
}
@Override
@@ -236,8 +240,9 @@
}
@Override
- public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
- return taskMenuLayout.getOrientation();
+ public int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate,
+ LinearLayout taskMenuLayout) {
+ return canRecentsActivityRotate ? taskMenuLayout.getOrientation() : LinearLayout.VERTICAL;
}
@Override
@@ -245,6 +250,33 @@
// no-op, defaults are fine
}
+ /* ---------- 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) {
@@ -257,4 +289,41 @@
}
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);
+ // TODO: Add in correct icons
+ if (dp.isSeascape()) { // or seascape
+ // Add left/right options
+ options.add(new SplitPositionOption(
+ R.drawable.ic_split_screen, R.string.split_screen_position_right,
+ STAGE_POSITION_TOP_OR_LEFT, 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..bd6e31b 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -16,23 +16,44 @@
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 com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+
+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 +72,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;
@@ -73,8 +89,40 @@
}
@Override
- public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
- view.setTranslationX(0);
- view.setTranslationY(translation);
+ 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..8c3c115 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;
@@ -105,11 +113,6 @@
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()));
- }
}
public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
@@ -117,10 +120,6 @@
mIgnoreSlopWhenSettling = ignoreSlop;
}
- public int getScrollDirections() {
- return mScrollDirections;
- }
-
/**
* Returns if the start drag was towards the positive direction or negative.
*
@@ -161,10 +160,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/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
index 0f81520..b3b69f6 100644
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ b/src/com/android/launcher3/util/ConfigMonitor.java
@@ -26,14 +26,16 @@
import android.graphics.Point;
import android.util.Log;
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
+import com.android.launcher3.util.DisplayController.Info;
+
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 {
+public class ConfigMonitor extends BroadcastReceiver implements DisplayInfoChangeListener {
private static final String TAG = "ConfigMonitor";
@@ -57,9 +59,9 @@
mFontScale = config.fontScale;
mDensity = config.densityDpi;
- DefaultDisplay display = DefaultDisplay.INSTANCE.get(context);
+ DisplayController.DisplayHolder display = DisplayController.getDefaultDisplay(context);
display.addChangeListener(this);
- DefaultDisplay.Info displayInfo = display.getInfo();
+ Info displayInfo = display.getInfo();
mDisplayId = displayInfo.id;
mRealSize = new Point(displayInfo.realSize);
@@ -82,7 +84,7 @@
}
@Override
- public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
+ public void onDisplayInfoChanged(Info info, int flags) {
if (info.id != mDisplayId) {
return;
}
@@ -113,8 +115,7 @@
public void unregister() {
try {
mContext.unregisterReceiver(this);
- DefaultDisplay display = DefaultDisplay.INSTANCE.get(mContext);
- display.removeChangeListener(this);
+ DisplayController.getDefaultDisplay(mContext).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..d0e8bb1
--- /dev/null
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -0,0 +1,310 @@
+/*
+ * 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.MAIN_EXECUTOR;
+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.util.DisplayMetrics;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.Utilities;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to cache properties of default display to avoid a system RPC on every call.
+ */
+public class DisplayController implements DisplayListener {
+
+ private static final String TAG = "DisplayController";
+
+ public static final MainThreadInitializedObject<DisplayController> INSTANCE =
+ new MainThreadInitializedObject<>(DisplayController::new);
+
+ private final SparseArray<DisplayHolder> mOtherDisplays = new SparseArray<>(0);
+ // We store the default display separately, to avoid null checks for primary use case.
+ private final DisplayHolder mDefaultDisplay;
+
+ private final ArrayList<DisplayListChangeListener> mListListeners = new ArrayList<>();
+
+ private DisplayController(Context context) {
+ mDefaultDisplay = DisplayHolder.create(context, DEFAULT_DISPLAY);
+
+ DisplayManager dm = context.getSystemService(DisplayManager.class);
+ dm.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
+ }
+
+ @Override
+ public final void onDisplayAdded(int displayId) {
+ DisplayHolder holder = DisplayHolder.create(mDefaultDisplay.mDisplayContext, displayId);
+ if (holder == null) {
+ // Display is already removed by the time we dot this.
+ return;
+ }
+ synchronized (mOtherDisplays) {
+ mOtherDisplays.put(displayId, holder);
+ }
+ MAIN_EXECUTOR.execute(() -> mListListeners.forEach(l-> l.onDisplayAdded(holder)));
+ }
+
+ @Override
+ public final void onDisplayRemoved(int displayId) {
+ synchronized (mOtherDisplays) {
+ mOtherDisplays.remove(displayId);
+ }
+ MAIN_EXECUTOR.execute(() -> mListListeners.forEach(l-> l.onDisplayRemoved(displayId)));
+ }
+
+ /**
+ * Returns the holder corresponding to the given display
+ */
+ public DisplayHolder getHolder(int displayId) {
+ if (displayId == mDefaultDisplay.mId) {
+ return mDefaultDisplay;
+ } else {
+ synchronized (mOtherDisplays) {
+ return mOtherDisplays.get(displayId);
+ }
+ }
+ }
+
+ /**
+ * Adds a listener for display list changes
+ */
+ public void addListChangeListener(DisplayListChangeListener listener) {
+ mListListeners.add(listener);
+ }
+
+ /**
+ * Removes a previously added display list change listener
+ */
+ public void removeListChangeListener(DisplayListChangeListener listener) {
+ mListListeners.remove(listener);
+ }
+
+ @Override
+ public final void onDisplayChanged(int displayId) {
+ DisplayHolder holder = getHolder(displayId);
+ if (holder != null) {
+ holder.handleOnChange();
+ }
+ }
+
+ public static int getSingleFrameMs(Context context) {
+ return getDefaultDisplay(context).getInfo().singleFrameMs;
+ }
+
+ public static DisplayHolder getDefaultDisplay(Context context) {
+ return INSTANCE.get(context).mDefaultDisplay;
+ }
+
+ /**
+ * A listener to receiving addition or removal of new displays
+ */
+ public interface DisplayListChangeListener {
+
+ /**
+ * Called when a new display is added
+ */
+ void onDisplayAdded(DisplayHolder holder);
+
+ /**
+ * Called when a previously added display is removed
+ */
+ void onDisplayRemoved(int displayId);
+ }
+
+ /**
+ * Interface for listening for display changes
+ */
+ public interface DisplayInfoChangeListener {
+
+ void onDisplayInfoChanged(Info info, int flags);
+ }
+
+ public static class DisplayHolder {
+
+ 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;
+
+ final Context mDisplayContext;
+ final int mId;
+ private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
+ private DisplayController.Info mInfo;
+
+ private DisplayHolder(Context displayContext) {
+ mDisplayContext = displayContext;
+ // 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 DisplayController.Info(mDisplayContext);
+ mId = mInfo.id;
+ }
+
+ public void addChangeListener(DisplayInfoChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeChangeListener(DisplayInfoChangeListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public DisplayController.Info getInfo() {
+ return mInfo;
+ }
+
+ /** Creates and up-to-date DisplayController.Info for the given context. */
+ @Nullable
+ public Info createInfoForContext(Context context) {
+ Display display = Utilities.ATLEAST_R ? context.getDisplay() : null;
+ if (display == null) {
+ display = context.getSystemService(DisplayManager.class).getDisplay(mId);
+ }
+ if (display == null) {
+ return null;
+ }
+ // Refresh the Context the prevent stale DisplayMetrics.
+ Context displayContext = context.getApplicationContext().createDisplayContext(display);
+ return new Info(displayContext, display);
+ }
+
+ public Context getDisplayContext() {
+ return mDisplayContext;
+ }
+
+ protected void handleOnChange() {
+ Info oldInfo = mInfo;
+ Info newInfo = createInfoForContext(mDisplayContext);
+ if (newInfo == null) {
+ return;
+ }
+
+ int change = 0;
+ if (newInfo.hasDifferentSize(oldInfo)) {
+ change |= CHANGE_SIZE;
+ }
+ if (newInfo.rotation != oldInfo.rotation) {
+ change |= CHANGE_ROTATION;
+ }
+ if (newInfo.singleFrameMs != oldInfo.singleFrameMs) {
+ change |= CHANGE_FRAME_DELAY;
+ }
+
+ if (change != 0) {
+ mInfo = newInfo;
+ final int flags = change;
+ MAIN_EXECUTOR.execute(() -> notifyChange(flags));
+ }
+ }
+
+ private void notifyChange(int flags) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ mListeners.get(i).onDisplayInfoChanged(mInfo, flags);
+ }
+ }
+
+ private static DisplayHolder create(Context context, int id) {
+ DisplayManager dm = context.getSystemService(DisplayManager.class);
+ Display display = dm.getDisplay(id);
+ if (display == null) {
+ return null;
+ }
+ // Use application context to create display context so that it can have its own
+ // Resources.
+ Context displayContext = context.getApplicationContext().createDisplayContext(display);
+ return new DisplayHolder(displayContext);
+ }
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index 0a32734..a85ae45 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -20,8 +20,10 @@
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
@@ -83,4 +85,29 @@
*/
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/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/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index d4e074c..2e279fa 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -33,19 +33,18 @@
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";
/**
* 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 {}
@@ -55,8 +54,6 @@
*/
@StringDef(value = {
HOME_BOUNCE_COUNT,
- SHELF_BOUNCE_COUNT,
- ALL_APPS_COUNT,
HOTSEAT_DISCOVERY_TIP_COUNT
})
@Retention(RetentionPolicy.SOURCE)
@@ -64,10 +61,8 @@
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);
MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
}
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/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..55add14
--- /dev/null
+++ b/src/com/android/launcher3/util/RunnableList.java
@@ -0,0 +1,64 @@
+/*
+ * 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 (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/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..50166c3 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;
/**
@@ -33,6 +31,7 @@
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_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..e8a0635 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -28,6 +28,7 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.uioverrides.WallpaperColorInfo;
/**
@@ -81,11 +82,18 @@
return getAttrColor(context, android.R.attr.colorAccent);
}
+ /** Returns the floating 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 +118,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/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/SafeCloseable.java b/src/com/android/launcher3/util/VelocityUtils.java
similarity index 66%
copy from src/com/android/launcher3/util/SafeCloseable.java
copy to src/com/android/launcher3/util/VelocityUtils.java
index ba8ee04..d5962ed 100644
--- a/src/com/android/launcher3/util/SafeCloseable.java
+++ b/src/com/android/launcher3/util/VelocityUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -13,14 +13,16 @@
* 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
+ * Contains some constants and functions to standardize velocity usage.
*/
-public interface SafeCloseable extends AutoCloseable {
+public class VelocityUtils {
- @Override
- void close();
+ /**
+ * 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/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 11c1029..f79313f 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -64,7 +64,7 @@
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
@@ -216,8 +216,7 @@
return mLauncher.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);
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..71aa4ac 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -17,7 +17,8 @@
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;
@@ -49,6 +50,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 +86,13 @@
DeviceProfile getDeviceProfile();
- static ActivityContext lookupContext(Context context) {
+ /**
+ * 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..1f12a2f 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -87,10 +87,6 @@
}
@Override
- public void logActionCommand(int command) {
- }
-
- @Override
protected boolean isOfType(int type) {
return (type & TYPE_ON_BOARD_POPUP) != 0;
}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index b010b4b..1939d15 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -20,7 +20,7 @@
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;
@@ -33,7 +33,6 @@
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;
@@ -49,7 +48,6 @@
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;
@@ -191,12 +189,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 +206,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 +459,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) {
@@ -602,13 +592,13 @@
*/
private boolean computeAllowSysuiScrims(@Nullable WallpaperInfo newWallpaperInfo) {
if (newWallpaperInfo == null) {
- // New wallpaper is static, not live. Thus, blacklist isn't applicable.
+ // Static wallpapers need scrim unless determined otherwise by wallpaperColors.
return true;
}
ComponentName newWallpaper = newWallpaperInfo.getComponent();
for (String wallpaperWithoutScrim : mWallpapersWithoutSysuiScrims) {
if (newWallpaper.equals(ComponentName.unflattenFromString(wallpaperWithoutScrim))) {
- // New wallpaper is blacklisted from showing a scrim.
+ // New wallpaper does not need a scrim.
return false;
}
}
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 1a8e11b..4e82336 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -36,6 +36,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 +45,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 +93,6 @@
}
};
- private final Launcher mLauncher;
private final int mBlurSizeOutline;
private final boolean mIsRtl;
@@ -128,7 +126,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 +140,46 @@
.setStiffness(SpringForce.STIFFNESS_LOW));
}
- void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
- boolean isOpening, float scale, float minSize, LayoutParams parentLp,
- boolean isVerticalBarLayout) {
- DeviceProfile dp = mLauncher.getDeviceProfile();
+ /**
+ * Update the icon UI to match the provided parameters during an animation frame
+ */
+ public void update(RectF rect, float progress, float shapeProgressStart,
+ float cornerRadius, boolean isOpening, View container,
+ DeviceProfile dp, boolean isVerticalBarLayout) {
+
+ 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, 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,
+ 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();
@@ -228,8 +261,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 +300,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/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 177aff4..23c3722 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,11 +51,12 @@
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.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.
@@ -74,7 +71,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 +85,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,7 +97,6 @@
private final Rect mFinalDrawableBounds = new Rect();
- private AnimatorSet mFadeAnimatorSet;
private ListenerView mListenerView;
private Runnable mFastFinishRunnable;
@@ -116,6 +114,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);
}
@@ -144,32 +144,8 @@
public void update(RectF rect, float alpha, 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, isOpening,
+ this, mLauncher.getDeviceProfile(), mIsVerticalBarLayout);
}
@Override
@@ -200,6 +176,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,13 +197,18 @@
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();
@@ -239,17 +221,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};
+ float[] points = new float[] {outViewBounds.left, outViewBounds.top, outViewBounds.right,
+ outViewBounds.bottom};
Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points,
false, ignoreTransform);
outRect.set(
@@ -273,13 +254,26 @@
@SuppressWarnings("WrongThread")
private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos,
IconLoadResult iconLoadResult) {
- Drawable drawable = null;
+ Drawable drawable;
+ Drawable btvIcon;
Drawable badge = null;
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;
+
+ if (originalView instanceof BubbleTextView) {
+ BubbleTextView btv = (BubbleTextView) originalView;
+
+ if (info instanceof ItemInfoWithIcon
+ && (((ItemInfoWithIcon) info).runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
+ btvIcon = btv.makePreloadIcon();
+ } else {
+ btvIcon = btv.getIcon();
+ }
+ } else {
+ btvIcon = null;
+ }
+
if (info instanceof SystemShortcut) {
if (originalView instanceof ImageView) {
drawable = ((ImageView) originalView).getDrawable();
@@ -288,6 +282,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 +310,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 +331,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,6 +363,10 @@
mBadge.setBounds(0, 0, clipViewOgWidth, clipViewOgHeight);
}
}
+
+ if (!mIsOpening && btvIcon != null) {
+ mBtvDrawable.setBackground(btvIcon);
+ }
invalidate();
}
@@ -380,8 +385,8 @@
synchronized (mIconLoadResult) {
if (mIconLoadResult.isIconLoaded) {
setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
- mIconLoadResult.iconOffset);
- hideOriginalView(originalView);
+ mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset);
+ setIconAndDotVisible(originalView, false);
} else {
mIconLoadResult.onIconLoaded = () -> {
if (cancellationSignal.isCanceled()) {
@@ -389,30 +394,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;
}
@@ -464,10 +459,6 @@
mEndRunnable.run();
mEndRunnable = null;
}
- if (mFadeAnimatorSet != null) {
- mFadeAnimatorSet.end();
- mFadeAnimatorSet = null;
- }
}
@Override
@@ -477,7 +468,7 @@
}
if (!mIsOpening) {
// When closing an app, we want the item on the workspace to be invisible immediately
- hideOriginalView(mOriginalIcon);
+ setIconAndDotVisible(mOriginalIcon, false);
}
}
@@ -561,11 +552,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 +559,19 @@
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);
+ }
+ if (originalView instanceof BubbleTextView) {
+ BubbleTextView btv = (BubbleTextView) originalView;
+ btv.setIconVisible(true);
+ btv.setForceHideDot(true);
+ }
+ view.finish(dragLayer);
}
} else {
view.finish(dragLayer);
@@ -599,56 +588,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 +607,7 @@
mLoadIconSignal = null;
mEndRunnable = null;
mFinalDrawableBounds.setEmpty();
- if (mFadeAnimatorSet != null) {
- mFadeAnimatorSet.cancel();
- }
mPositionOut = null;
- mFadeAnimatorSet = null;
mListenerView.setListener(null);
mOriginalIcon = null;
mOnTargetChangeRunnable = null;
@@ -677,11 +615,13 @@
sTmpObjArray[0] = null;
mIconLoadResult = null;
mClipIconView.recycle();
+ mBtvDrawable.setBackground(null);
mFastFinishRunnable = null;
}
private static class IconLoadResult {
final ItemInfo itemInfo;
+ Drawable btvDrawable;
Drawable drawable;
Drawable badge;
int iconOffset;
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
new file mode 100644
index 0000000..011f6de
--- /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.getDefaultDisplay(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(
+ 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/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..98cc876 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,22 +130,24 @@
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) {
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);
+ 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.show();
+ return popup;
}
@VisibleForTesting
@@ -152,28 +162,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 +209,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 +222,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 +231,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 +242,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..ae34257 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -16,6 +16,8 @@
package com.android.launcher3.views;
+import static android.view.HapticFeedbackConstants.CLOCK_TICK;
+
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
@@ -32,6 +34,7 @@
import android.view.ViewConfiguration;
import android.widget.TextView;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BaseRecyclerView;
@@ -97,6 +100,7 @@
private boolean mIsThumbDetached;
private final boolean mCanThumbDetach;
private boolean mIgnoreDragGesture;
+ private boolean mIsRecyclerViewFirstChildInParent = true;
// 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.
@@ -110,6 +114,7 @@
protected BaseRecyclerView mRv;
private RecyclerView.OnScrollListener mOnScrollListener;
+ @Nullable private OnFastScrollChangeListener mOnFastScrollChangeListener;
private int mDownX;
private int mDownY;
@@ -186,6 +191,9 @@
updatePopupY(y);
mThumbOffsetY = y;
invalidate();
+ if (mOnFastScrollChangeListener != null) {
+ mOnFastScrollChangeListener.onThumbOffsetYChanged(mThumbOffsetY);
+ }
}
public int getThumbOffsetY() {
@@ -288,6 +296,7 @@
if (!sectionName.equals(mPopupSectionName)) {
mPopupSectionName = sectionName;
mPopupView.setText(sectionName);
+ performHapticFeedback(CLOCK_TICK);
}
animatePopupVisibility(!sectionName.isEmpty());
mLastTouchY = boundedY;
@@ -388,7 +397,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 +412,23 @@
// 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;
+ }
+
+ /**
+ * A callback that is invoked when there is a scroll change in {@link RecyclerViewFastScroller}.
+ */
+ public interface OnFastScrollChangeListener {
+ /**
+ * Called when the thumb offset vertical position, in pixels, has changed to {@code y}.
+ */
+ void onThumbOffsetYChanged(int y);
+ }
}
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index 22faf97..c9424de 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -15,206 +15,39 @@
*/
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.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.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.SystemUiController;
import com.android.launcher3.util.Themes;
-import com.android.launcher3.widget.WidgetsFullSheet;
-
-import java.util.List;
/**
* 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 boolean mIsScrimDark;
+ private SystemUiController mSystemUiController;
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);
+ mIsScrimDark = ColorUtils.calculateLuminance(
+ Themes.getAttrColor(context, R.attr.allAppsScrimColor)) < 0.5f;
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);
- }
+ public void setInsets(Rect insets) { }
@Override
public boolean hasOverlappingRendering() {
@@ -222,324 +55,33 @@
}
@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();
+ return super.onSetAlpha(alpha);
}
- public void setProgress(float progress) {
- if (mProgress != progress) {
- mProgress = progress;
- stopDragHandleEducationAnim();
- updateColors();
- updateSysUiColors();
- updateDragHandleAlpha();
- invalidate();
- }
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ updateSysUiColors();
}
- public void reInitUi() { }
-
- protected void updateColors() {
- mCurrentFlatColor = mProgress >= 1 ? 0 : setColorAlphaBound(
- mEndFlatColor, Math.round((1 - mProgress) * mEndFlatColorAlpha));
- }
-
- protected void updateSysUiColors() {
+ private 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;
+ boolean forceChange =
+ getVisibility() == VISIBLE && getAlpha() > STATUS_BAR_COLOR_FORCE_UPDATE_THRESHOLD;
if (forceChange) {
- mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !mIsScrimDark);
+ getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !mIsScrimDark);
} else {
- mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
+ getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
}
}
- protected void updateDragHandleAlpha() {
- if (mDragHandle != null) {
- mDragHandle.setAlpha(mDragHandleAlpha);
+ private SystemUiController getSystemUiController() {
+ if (mSystemUiController == null) {
+ mSystemUiController = Launcher.getLauncher(getContext()).getSystemUiController();
}
- }
-
- private void setDragHandleAlpha(int alpha) {
- if (alpha != mDragHandleAlpha) {
- mDragHandleAlpha = alpha;
- if (mDragHandle != null) {
- mDragHandle.setAlpha(mDragHandleAlpha);
- invalidate();
- }
- }
- }
-
- @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);
- }
- }
-
- @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();
- }
- return superHandledTouch;
- }
-
- /**
- * Animates the drag handle to demonstrate how to get to all apps.
- * @return Whether the animation was started (false if drag handle is invisible).
- */
- 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;
- }
- 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;
- }
- }
-
- /**
- * @return The top of this scrim view, or {@link Float#MAX_VALUE} if there's no distinct top.
- */
- public float getVisualTop() {
- return Float.MAX_VALUE;
+ return mSystemUiController;
}
}
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..9701389 100644
--- a/src/com/android/launcher3/views/SpringRelativeLayout.java
+++ b/src/com/android/launcher3/views/SpringRelativeLayout.java
@@ -15,51 +15,25 @@
*/
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;
+ private final EdgeEffect mEdgeGlowTop;
+ private final EdgeEffect mEdgeGlowBottom;
public SpringRelativeLayout(Context context) {
this(context, null);
@@ -71,98 +45,73 @@
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();
- }
- protected void finishWithShiftAndVelocity(float shift, float velocity,
- DynamicAnimation.OnAnimationEndListener listener) {
- setDampedScrollShift(shift);
- mSpring.addEndListener(listener);
- finishScrollWithVelocity(velocity);
+ /**
+ * Absorbs the velocity as a result for swipe-up fling
+ */
+ protected void absorbSwipeUpVelocity(int velocity) {
+ mEdgeGlowBottom.onAbsorb(velocity);
+ invalidate();
}
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);
+ return new EdgeEffectProxy(getContext(), mEdgeGlowTop);
case DIRECTION_BOTTOM:
- return new SpringEdgeEffect(getContext(), -VELOCITY_MULTIPLIER);
+ return new EdgeEffectProxy(getContext(), mEdgeGlowBottom);
}
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 +119,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/WorkEduView.java b/src/com/android/launcher3/views/WorkEduView.java
index d35a38f..c8cf627 100644
--- a/src/com/android/launcher3/views/WorkEduView.java
+++ b/src/com/android/launcher3/views/WorkEduView.java
@@ -38,7 +38,6 @@
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
@@ -61,7 +60,6 @@
private View mViewWrapper;
private Button mProceedButton;
private TextView mContentText;
- private AllAppsPagedView mAllAppsPagedView;
private int mNextWorkEduStep = WORK_EDU_PERSONAL_APPS;
@@ -89,16 +87,6 @@
}
@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;
}
@@ -112,13 +100,10 @@
// 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);
+ if (getAllAppsPagedView() != null) {
+ getAllAppsPagedView().snapToPage(AllAppsContainerView.AdapterHolder.WORK);
}
goToWorkTab(true);
});
@@ -166,8 +151,8 @@
}
private void goToFirstPage() {
- if (mAllAppsPagedView != null) {
- mAllAppsPagedView.snapToPageImmediately(AllAppsContainerView.AdapterHolder.MAIN);
+ if (getAllAppsPagedView() != null) {
+ getAllAppsPagedView().snapToPageImmediately(AllAppsContainerView.AdapterHolder.MAIN);
}
}
@@ -182,6 +167,11 @@
mOpenCloseAnimator.start();
}
+ private AllAppsPagedView getAllAppsPagedView() {
+ View v = mLauncher.getAppsView().getContentView();
+ return (v instanceof AllAppsPagedView) ? (AllAppsPagedView) v : null;
+ }
+
/**
* Checks if user has not seen onboarding UI yet and shows it when user navigates to all apps
*/
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 3e5113a..95b887c 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -16,10 +16,10 @@
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;
@@ -31,24 +31,19 @@
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;
-
/**
* Base class for various widgets popup
*/
-abstract class BaseWidgetSheet extends AbstractSlideInView
+public abstract class BaseWidgetSheet extends AbstractSlideInView
implements OnClickListener, OnLongClickListener, DragSource,
PopupDataProvider.PopupDataChangeListener {
@@ -80,16 +75,18 @@
@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
@@ -100,6 +97,8 @@
if (v instanceof WidgetCell) {
return beginDraggingWidget((WidgetCell) v);
+ } else if (v.getParent() instanceof WidgetCell) {
+ return beginDraggingWidget((WidgetCell) v.getParent());
}
return true;
}
@@ -110,16 +109,29 @@
// 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.getPreview());
+ 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 +161,41 @@
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();
}
+
+ /**
+ * 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;
+ }
}
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
similarity index 82%
rename from src/com/android/launcher3/LauncherAppWidgetHost.java
rename to src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index 7ea6851..d745754 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.launcher3;
+package com.android.launcher3.widget;
import static android.app.Activity.RESULT_CANCELED;
@@ -29,12 +29,13 @@
import android.util.SparseArray;
import android.widget.Toast;
+import com.android.launcher3.BaseActivity;
+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.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;
@@ -49,8 +50,11 @@
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;
+ 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;
public static final int APPWIDGET_HOST_ID = 1024;
@@ -59,7 +63,7 @@
private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
private final Context mContext;
- private int mFlags = FLAG_RESUMED;
+ private int mFlags = FLAG_STATE_IS_NORMAL;
private IntConsumer mAppWidgetRemovedCallback = null;
@@ -130,49 +134,45 @@
}
/**
- * 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)
+ * Sets or unsets a flag the can change whether the widget host should be in the listening
+ * state.
*/
- 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();
- }
+ private void setShouldListenFlag(int flag, boolean on) {
+ if (on) {
+ mFlags |= flag;
} else {
- mFlags &= ~FLAG_RESUMED;
+ 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();
}
}
/**
- * Updates the listening state of the host. If the host is not resumed, startListening is
- * deferred until next resume.
- *
- * @see #setResumed(boolean)
+ * Registers an "entering/leaving Normal state" event.
*/
- 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();
- }
+ 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
@@ -200,7 +200,7 @@
}
}
- void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
+ public void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
mPendingViews.put(appWidgetId, view);
}
@@ -248,7 +248,7 @@
super.onProviderChanged(appWidgetId, info);
// The super method updates the dimensions of the providerInfo. Update the
// launcher spans accordingly.
- info.initSpans(mContext);
+ info.initSpans(mContext, LauncherAppState.getIDP(mContext));
}
/**
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 6f2e179..687318f 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -19,36 +19,52 @@
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.Log;
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.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.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;
@@ -61,37 +77,81 @@
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 size during onLayout.
+ private final Rect mCurrentWidgetSize = new Rect();
+ private final Rect mWidgetSizeAtDrag = new Rect();
+ private final RectF mTempRectF = new RectF();
+ private final boolean mIsRtl;
+ 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);
+ }
+ }
+ };
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);
}
+ mIsRtl = Utilities.isRtl(context.getResources());
+ mColorExtractor = LocalColorExtractor.newInstance(getContext());
+ mColorExtractor.setListener(this);
+
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
+ }
+
+ @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();
@@ -126,7 +186,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)) {
@@ -140,7 +200,7 @@
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);
}
@@ -172,6 +232,7 @@
// state is updated. So isAttachedToWindow() will return true until next frame.
mIsAttachedToWindow = false;
checkIfAutoAdvance();
+ mColorExtractor.removeLocations();
}
@Override
@@ -218,6 +279,99 @@
}
mIsScrollable = checkScrollableRecursively(this);
+
+ if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) {
+ mCurrentWidgetSize.left = left;
+ mCurrentWidgetSize.top = top;
+ mCurrentWidgetSize.right = right;
+ mCurrentWidgetSize.bottom = bottom;
+ LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
+ int pageId = mWorkspace.getPageIndexForScreenId(info.screenId);
+ updateColorExtraction(mCurrentWidgetSize, pageId);
+ }
+
+ 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 rect, int pageId) {
+ mWidgetSizeAtDrag.set(rect);
+ updateColorExtraction(mWidgetSizeAtDrag, pageId);
+ }
+
+ /** Ends the drag mode. */
+ public void endDrag() {
+ mIsInDragMode = false;
+ mDragListener = null;
+ mWidgetSizeAtDrag.setEmpty();
+ requestLayout();
+ }
+
+ private void updateColorExtraction(Rect widgetLocation, int pageId) {
+ // If the widget hasn't been measured and laid out, we cannot do this.
+ if (widgetLocation.isEmpty()) {
+ return;
+ }
+ int screenWidth = mLauncher.getDeviceProfile().widthPx;
+ int screenHeight = mLauncher.getDeviceProfile().heightPx;
+ int numScreens = mWorkspace.getNumPagesForWallpaperParallax();
+ pageId = mIsRtl ? numScreens - pageId - 1 : pageId;
+ float relativeScreenWidth = 1f / numScreens;
+ float absoluteTop = widgetLocation.top;
+ float absoluteBottom = widgetLocation.bottom;
+ for (View v = (View) getParent();
+ v != null && v.getId() != R.id.launcher;
+ v = (View) v.getParent()) {
+ absoluteBottom += v.getTop();
+ absoluteTop += v.getTop();
+ }
+ float xOffset = 0;
+ View parentView = (View) getParent();
+ // The layout depends on the orientation.
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ int parentViewWidth = parentView == null ? 0 : parentView.getWidth();
+ xOffset = screenHeight - mWorkspace.getPaddingRight() - parentViewWidth;
+ } else {
+ int parentViewPaddingLeft = parentView == null ? 0 : parentView.getPaddingLeft();
+ xOffset = mWorkspace.getPaddingLeft() + parentViewPaddingLeft;
+ }
+ // This is the position of the widget relative to the wallpaper, as expected by the
+ // local color extraction of the WallpaperManager.
+ // The coordinate system is such that, on the horizontal axis, each screen has a
+ // distinct range on the [0,1] segment. So if there are 3 screens, they will have the
+ // ranges [0, 1/3], [1/3, 2/3] and [2/3, 1]. The position on the subrange should be
+ // the position of the widget relative to the screen. For the vertical axis, this is
+ // simply the location of the widget relative to the screen.
+ mTempRectF.left = ((widgetLocation.left + xOffset) / screenWidth + pageId)
+ * relativeScreenWidth;
+ mTempRectF.right = ((widgetLocation.right + xOffset) / screenWidth + pageId)
+ * relativeScreenWidth;
+ mTempRectF.top = absoluteTop / screenHeight;
+ mTempRectF.bottom = absoluteBottom / screenHeight;
+ if (mTempRectF.left < 0 || mTempRectF.right > 1 || mTempRectF.top < 0
+ || mTempRectF.bottom > 1) {
+ Log.e(LOG_TAG, " Error, invalid relative position");
+ return;
+ }
+ if (!mTempRectF.equals(mLastLocationRegistered)) {
+ if (mLastLocationRegistered != null) {
+ mColorExtractor.removeLocations();
+ }
+ mLastLocationRegistered = new RectF(mTempRectF);
+ mColorExtractor.addLocation(List.of(mLastLocationRegistered));
+ }
+ }
+
+ @Override
+ public void onColorsChanged(RectF rectF, SparseIntArray colors) {
+ // setColorResources will reapply the view, which must happen in the UI thread.
+ post(() -> setColorResources(colors));
}
@Override
@@ -230,6 +384,14 @@
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
maybeRegisterAutoAdvance();
+
+ if (visibility == View.VISIBLE) {
+ if (mLastLocationRegistered != null) {
+ mColorExtractor.addLocation(List.of(mLastLocationRegistered));
+ }
+ } else {
+ mColorExtractor.removeLocations();
+ }
}
private void checkIfAutoAdvance() {
@@ -326,4 +488,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..ad61495
--- /dev/null
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -0,0 +1,160 @@
+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.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-";
+
+ public int spanX;
+ public int spanY;
+ public int minSpanX;
+ public int minSpanY;
+ public int maxSpanX;
+ public int maxSpanY;
+
+ 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) {
+ 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.
+ // If grids supports insetting widgets, we do not account for widget padding.
+ Rect widgetPadding = new Rect();
+ if (!idp.landscapeProfile.shouldInsetWidgets()
+ || !idp.portraitProfile.shouldInsetWidgets()) {
+ AppWidgetHostView.getDefaultPaddingForWidget(context, provider, widgetPadding);
+ }
+
+ minSpanX = getSpanX(widgetPadding, minResizeWidth, smallestCellWidth);
+ minSpanY = getSpanY(widgetPadding, minResizeHeight, smallestCellHeight);
+
+ // Use maxResizeWidth/Height if they are defined and we're on S or above.
+ maxSpanX =
+ (ATLEAST_S && maxResizeWidth > 0)
+ ? getSpanX(widgetPadding, maxResizeWidth, smallestCellWidth)
+ : idp.numColumns;
+ maxSpanY =
+ (ATLEAST_S && maxResizeHeight > 0)
+ ? getSpanY(widgetPadding, maxResizeHeight, smallestCellHeight)
+ : idp.numRows;
+
+ // Use targetCellWidth/Height if it is within the min/max ranges and we're on S or above.
+ // Otherwise, fall back to minWidth/Height.
+ if (ATLEAST_S && targetCellWidth >= minSpanX && targetCellWidth <= maxSpanX
+ && targetCellHeight >= minSpanY && targetCellHeight <= maxSpanY) {
+ spanX = targetCellWidth;
+ spanY = targetCellHeight;
+ } else {
+ spanX = getSpanX(widgetPadding, minWidth, smallestCellWidth);
+ spanY = getSpanY(widgetPadding, minHeight, smallestCellHeight);
+ }
+ }
+
+ private int getSpanX(Rect widgetPadding, int widgetWidth, float cellWidth) {
+ return Math.max(1, (int) Math.ceil(
+ (widgetWidth + widgetPadding.left + widgetPadding.right) / cellWidth));
+ }
+
+ private int getSpanY(Rect widgetPadding, int widgetHeight, float cellHeight) {
+ return Math.max(1, (int) Math.ceil(
+ (widgetHeight + widgetPadding.top + widgetPadding.bottom) / cellHeight));
+ }
+
+ 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;
+ }
+
+ @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..097158b
--- /dev/null
+++ b/src/com/android/launcher3/widget/LocalColorExtractor.java
@@ -0,0 +1,65 @@
+/*
+ * 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.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.graphics.RectF;
+import android.util.SparseIntArray;
+
+import androidx.annotation.Nullable;
+
+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);
+ }
+
+ 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
+ }
+}
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..ee0b84e 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -20,7 +20,6 @@
import android.appwidget.AppWidgetHostView;
import android.os.Bundle;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.PendingAddItemInfo;
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 9021d9e..3308eec 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,7 +16,6 @@
package com.android.launcher3.widget;
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import android.content.Context;
@@ -29,6 +28,7 @@
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;
@@ -36,8 +36,8 @@
import android.widget.RemoteViews;
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 +46,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;
@@ -110,13 +111,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
@@ -146,12 +151,12 @@
// 2) Preload icon in the center
// 3) Setup icon in the center and app icon in the top right corner.
if (mDisabledForSafeMode) {
- FastBitmapDrawable disabledIcon = newIcon(getContext(), info);
+ FastBitmapDrawable disabledIcon = info.newIcon(getContext());
disabledIcon.setIsDisabled(true);
mCenterDrawable = disabledIcon;
mSettingIconDrawable = null;
} else if (isReadyForClickSetup()) {
- mCenterDrawable = newIcon(getContext(), info);
+ mCenterDrawable = info.newIcon(getContext());
mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate();
updateSettingColor(info.bitmap.color);
} else {
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 3c11274..e78d517 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -25,17 +25,22 @@
import android.view.View;
import android.widget.RemoteViews;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DragSource;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.AppWidgetHostViewDrawable;
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 LauncherAppWidgetHostView 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 LauncherAppWidgetHostView} which shows a preview layout of an app widget. */
+ public void setAppWidgetHostViewPreview(
+ @Nullable LauncherAppWidgetHostView appWidgetHostViewPreview) {
+ mAppWidgetHostViewPreview = appWidgetHostViewPreview;
}
/**
@@ -73,7 +92,7 @@
final Launcher launcher = Launcher.getLauncher(mView.getContext());
LauncherAppState app = LauncherAppState.getInstance(launcher);
- Bitmap preview = null;
+ Drawable preview = null;
final float scale;
final Point dragOffset;
final Rect dragRegion;
@@ -89,13 +108,26 @@
int[] previewSizeBeforeScale = new int[1];
- if (mPreview != null) {
- preview = LivePreviewWidgetCell.generateFromRemoteViews(launcher, mPreview,
- createWidgetInfo.info, maxWidth, previewSizeBeforeScale);
+ if (mRemoteViewsPreview != null) {
+ preview = new FastBitmapDrawable(
+ WidgetCell.generateFromRemoteViews(launcher, mRemoteViewsPreview,
+ createWidgetInfo.info, maxWidth, previewSizeBeforeScale));
+ }
+ if (mAppWidgetHostViewPreview != null) {
+ preview = new AppWidgetHostViewDrawable(mAppWidgetHostViewPreview);
+ previewSizeBeforeScale[0] = mAppWidgetHostViewPreview.getMeasuredWidth();
+ launcher.getDragController()
+ .addDragListener(new AppWidgetHostViewDragListener(launcher));
}
if (preview == null) {
- preview = app.getWidgetCache().generateWidgetPreview(launcher,
- createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale).first;
+ 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 +140,7 @@
previewBounds.left += padding;
previewBounds.right -= padding;
}
- scale = previewBounds.width() / (float) preview.getWidth();
+ scale = previewBounds.width() / (float) preview.getIntrinsicWidth();
launcher.getDragController().addDragListener(new WidgetHostViewLoader(launcher, mView));
dragOffset = null;
@@ -118,9 +150,10 @@
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));
li.recycle();
- scale = ((float) launcher.getDeviceProfile().iconSizePx) / preview.getWidth();
+ scale = ((float) launcher.getDeviceProfile().iconSizePx) / preview.getIntrinsicWidth();
dragOffset = new Point(previewPadding / 2, previewPadding / 2);
@@ -148,9 +181,9 @@
launcher.getWorkspace().prepareDragWithProvider(this);
int dragLayerX = screenPos.x + previewBounds.left
- + (int) ((scale * preview.getWidth() - preview.getWidth()) / 2);
+ + (int) ((scale * preview.getIntrinsicWidth() - preview.getIntrinsicWidth()) / 2);
int dragLayerY = screenPos.y + previewBounds.top
- + (int) ((scale * preview.getHeight() - preview.getHeight()) / 2);
+ + (int) ((scale * preview.getIntrinsicHeight() - preview.getIntrinsicHeight()) / 2);
// Start the drag
launcher.getDragController().startDrag(preview, draggableView, dragLayerX, dragLayerY,
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..5e7c961 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,8 +16,12 @@
package com.android.launcher3.widget;
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
import android.content.Context;
+import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
import android.os.CancellationSignal;
import android.util.AttributeSet;
import android.util.Log;
@@ -27,15 +31,23 @@
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
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.R;
import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BaseIconFactory;
+import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.FastBitmapDrawable;
+import com.android.launcher3.icons.RoundDrawableWrapper;
import com.android.launcher3.model.WidgetItem;
/**
@@ -60,12 +72,18 @@
/** 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 ImageView mWidgetBadge;
private TextView mWidgetName;
private TextView mWidgetDims;
+ private TextView mWidgetDescription;
protected WidgetItem mItem;
@@ -75,11 +93,15 @@
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 RemoteViews mPreview;
+ private LauncherAppWidgetHostView mAppWidgetHostViewPreview;
public WidgetCell(Context context) {
this(context, null);
@@ -101,20 +123,33 @@
setWillNotDraw(false);
setClipToPadding(false);
setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
}
private void setContainerWidth() {
mCellSize = (int) (mDeviceProfile.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);
+ mWidgetBadge = findViewById(R.id.widget_badge);
+ mWidgetName = findViewById(R.id.widget_name);
+ mWidgetDims = findViewById(R.id.widget_dims);
+ mWidgetDescription = findViewById(R.id.widget_description);
+ }
+
+ public void setPreview(RemoteViews view) {
+ mPreview = view;
+ }
+
+ public RemoteViews getPreview() {
+ return mPreview;
}
/**
@@ -125,25 +160,46 @@
Log.d(TAG, "reset called on:" + mWidgetName.getText());
}
mWidgetImage.animate().cancel();
- mWidgetImage.setBitmap(null, null);
+ mWidgetImage.setDrawable(null);
+ mWidgetImage.setVisibility(View.VISIBLE);
+ mWidgetBadge.setImageDrawable(null);
mWidgetName.setText(null);
mWidgetDims.setText(null);
+ mWidgetDescription.setText(null);
+ mWidgetDescription.setVisibility(GONE);
+ mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
if (mActiveRequest != null) {
mActiveRequest.cancel();
mActiveRequest = null;
}
+ mPreview = null;
+ if (mAppWidgetHostViewPreview != null) {
+ mWidgetImageContainer.removeView(mAppWidgetHostViewPreview);
+ }
+ mAppWidgetHostViewPreview = null;
}
public void applyFromCellItem(WidgetItem item, WidgetPreviewLoader loader) {
+ applyPreviewLayout(item);
+
mItem = item;
mWidgetName.setText(mItem.label);
mWidgetDims.setText(getContext().getString(R.string.widget_dims_format,
mItem.spanX, mItem.spanY));
mWidgetDims.setContentDescription(getContext().getString(
R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
- mWidgetPreviewLoader = loader;
+ if (ATLEAST_S && mItem.widgetInfo != null) {
+ CharSequence description = mItem.widgetInfo.loadDescription(getContext());
+ 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 {
@@ -151,10 +207,43 @@
}
}
+ private void applyPreviewLayout(WidgetItem item) {
+ if (ATLEAST_S
+ && mPreview == null
+ && item.widgetInfo != null
+ && item.widgetInfo.previewLayout != Resources.ID_NULL) {
+ mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(getContext());
+ LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+ LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(),
+ 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;
+ mAppWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1,
+ launcherAppWidgetProviderInfo);
+ mAppWidgetHostViewPreview.setPadding(/* left= */ 0, /* top= */0, /* right= */
+ 0, /* bottom= */ 0);
+ mAppWidgetHostViewPreview.updateAppWidget(/* remoteViews= */ null);
+ // Gravity 77 = "fill"
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT, /* gravity= */ 77);
+ mAppWidgetHostViewPreview.setLayoutParams(params);
+ mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0);
+ mWidgetImage.setVisibility(View.GONE);
+ }
+ }
+
public WidgetImageView getWidgetView() {
return mWidgetImage;
}
+ @Nullable
+ public LauncherAppWidgetHostView 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 +253,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 +265,86 @@
}
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) {
+ setContainerSize(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ mWidgetImage.setDrawable(drawable);
+ mWidgetImage.setVisibility(View.VISIBLE);
+ if (mAppWidgetHostViewPreview != null) {
+ removeView(mAppWidgetHostViewPreview);
+ mAppWidgetHostViewPreview = null;
}
}
+ Drawable badge = mWidgetPreviewLoader.getBadgeForUser(mItem.user,
+ BaseIconFactory.getBadgeSizeForIconSize(mDeviceProfile.allAppsIconSizePx));
+ if (badge == null) {
+ mWidgetBadge.setVisibility(View.GONE);
+ } else {
+ mWidgetBadge.setVisibility(View.VISIBLE);
+ mWidgetBadge.setImageDrawable(badge);
+ }
+ 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 (mPreview != null && mActiveRequest == null) {
+ Bitmap preview = generateFromRemoteViews(
+ mActivity, mPreview, mItem.widgetInfo, mPresetPreviewSize, new int[1]);
+ if (preview != null) {
+ applyPreview(new FastBitmapDrawable(preview));
+ return;
+ }
+ }
+
+ if (mAppWidgetHostViewPreview != null) {
+ DeviceProfile dp = mActivity.getDeviceProfile();
+ int viewWidth = dp.cellWidthPx * mItem.spanX;
+ int viewHeight = dp.cellHeightPx * mItem.spanY;
+
+ setContainerSize(viewWidth, viewHeight);
+ applyPreview((Drawable) null);
+ return;
+ }
if (mActiveRequest != null) {
return;
}
- mActiveRequest = mWidgetPreviewLoader.getPreview(
- mItem, mPresetPreviewSize, mPresetPreviewSize, this);
+ mActiveRequest = mWidgetPreviewLoader.getPreview(mItem, mPreviewWidth, mPreviewHeight,
+ this);
+ }
+
+ /** Sets the widget preview image size in number of cells. */
+ public void setPreviewSize(int spanX, int spanY) {
+ setPreviewSize(spanX, spanY, 1f);
+ }
+
+ /** Sets the widget preview image size, in number of cells, and preview scale. */
+ public void setPreviewSize(int spanX, int spanY, float previewScale) {
+ int padding = 2 * getResources()
+ .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
+ mPreviewWidth = mDeviceProfile.cellWidthPx * spanX + padding;
+ mPreviewHeight = mDeviceProfile.cellHeightPx * spanY + padding;
+ mPreviewScale = previewScale;
}
@Override
@@ -233,12 +379,6 @@
}
@Override
- public void setLayoutParams(ViewGroup.LayoutParams params) {
- params.width = params.height = mCellSize;
- super.setLayoutParams(params);
- }
-
- @Override
public CharSequence getAccessibilityClassName() {
return WidgetCell.class.getName();
}
@@ -248,4 +388,53 @@
super.onInitializeAccessibilityNodeInfo(info);
info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
}
+
+ /**
+ * 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) {
+ try {
+ return generateFromView(activity, views.apply(activity, new FrameLayout(activity)),
+ info, previewSize, preScaledWidthOut);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static Bitmap generateFromView(BaseActivity activity, View v,
+ LauncherAppWidgetProviderInfo info, int previewSize, int[] preScaledWidthOut) {
+
+ DeviceProfile dp = activity.getDeviceProfile();
+ int viewWidth = dp.cellWidthPx * info.spanX;
+ int viewHeight = dp.cellHeightPx * info.spanY;
+
+ v.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
+
+ viewWidth = v.getMeasuredWidth();
+ viewHeight = v.getMeasuredHeight();
+ v.layout(0, 0, viewWidth, viewHeight);
+
+ 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/widget/WidgetCellPreview.java b/src/com/android/launcher3/widget/WidgetCellPreview.java
new file mode 100644
index 0000000..ad3a61a
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetCellPreview.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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+
+/**
+ * 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;
+ }
+
+}
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index c022374..12e0d43 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -7,17 +7,20 @@
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
+import android.util.SizeF;
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;
import com.android.launcher3.util.Thunk;
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
public class WidgetHostViewLoader implements DragController.DragListener {
private static final String TAG = "WidgetHostViewLoader";
private static final boolean LOGD = false;
@@ -152,24 +155,28 @@
}
public static Bundle getDefaultOptionsForWidget(Context context, PendingAddWidgetInfo info) {
- Rect rect = new Rect();
- AppWidgetResizeFrame.getWidgetSizeRanges(context, info.spanX, info.spanY, rect);
+ ArrayList<SizeF> sizes = AppWidgetResizeFrame
+ .getWidgetSizes(context, info.spanX, info.spanY);
+
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);
+ float xPaddingDips = (padding.left + padding.right) / density;
+ float yPaddingDips = (padding.top + padding.bottom) / density;
+
+ ArrayList<SizeF> paddedSizes = sizes.stream().map(
+ size -> new SizeF(Math.max(0.f, size.getWidth() - xPaddingDips),
+ Math.max(0.f, size.getHeight() - yPaddingDips))).collect(
+ Collectors.toCollection(ArrayList::new));
+
+ Rect rect = AppWidgetResizeFrame.getMinMaxSizes(paddedSizes, null /* outRect */);
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);
+ 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;
}
}
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/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 30be7a6..bbb0d92 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -26,19 +26,24 @@
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.DeviceProfile;
import com.android.launcher3.Insettable;
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;
@@ -64,6 +69,8 @@
private static final int DEFAULT_CLOSE_DURATION = 200;
private ItemInfo mOriginalItemInfo;
private Rect mInsets;
+ private final int mMaxTableHeight;
+ private int mMaxHorizontalSpan = 4;
public WidgetsBottomSheet(Context context, AttributeSet attrs) {
this(context, attrs, 0);
@@ -74,18 +81,46 @@
setWillNotDraw(false);
mInsets = new Rect();
mContent = this;
+ DeviceProfile deviceProfile = mLauncher.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;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int paddingPx = 2 * getResources().getDimensionPixelOffset(
+ R.dimen.widget_cell_horizontal_padding);
+ int maxHorizontalSpan = findViewById(R.id.widgets_table).getMeasuredWidth()
+ / (mLauncher.getDeviceProfile().cellWidthPx + paddingPx);
+
+ if (mMaxHorizontalSpan != maxHorizontalSpan) {
+ // Ensure the table layout is showing widgets in the right column after measure.
+ mMaxHorizontalSpan = maxHorizontalSpan;
+ onWidgetsBound();
+ }
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
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);
+ }
}
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();
@@ -100,47 +135,44 @@
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.spanX, widgetItem.spanY);
+ widget.applyFromCellItem(widgetItem, LauncherAppState.getInstance(mLauncher)
+ .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);
+ WidgetImageView preview = widget.findViewById(R.id.widget_preview);
+ preview.setOnClickListener(this);
+ preview.setOnLongClickListener(this);
widget.setAnimatePreview(false);
parent.addView(widget);
@@ -180,11 +212,6 @@
}
@Override
- protected int getElementsRowCount() {
- return 1;
- }
-
- @Override
protected Pair<View, String> getAccessibilityTarget() {
return Pair.create(findViewById(R.id.title), getContext().getString(
mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
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/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..66bb363
--- /dev/null
+++ b/src/com/android/launcher3/widget/dragndrop/AppWidgetHostViewDragListener.java
@@ -0,0 +1,61 @@
+/*
+ * 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.AppWidgetHostViewDrawable;
+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 AppWidgetHostViewDrawable mAppWidgetHostViewDrawable;
+ private LauncherAppWidgetHostView mAppWidgetHostView;
+
+ public AppWidgetHostViewDragListener(Launcher launcher) {
+ mLauncher = launcher;
+ }
+
+ @Override
+ public void onDragStart(DropTarget.DragObject dragObject, DragOptions unused) {
+ if (dragObject.dragView.getDrawable() instanceof AppWidgetHostViewDrawable) {
+ mDragObject = dragObject;
+ mAppWidgetHostViewDrawable =
+ (AppWidgetHostViewDrawable) mDragObject.dragView.getDrawable();
+ mAppWidgetHostView = mAppWidgetHostViewDrawable.getAppWidgetHostView();
+ 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..73bae6f
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.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.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();
+
+ @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..0328cf6
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
@@ -0,0 +1,52 @@
+/*
+ * 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 {
+
+ public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ super(pkgItem, titleSectionName, items);
+ }
+
+ @Override
+ public String toString() {
+ return "Content:" + mPkgItem.packageName + ":" + mWidgets.size();
+ }
+
+ @Override
+ @Rank
+ public int getRank() {
+ return RANK_WIDGETS_LIST_CONTENT;
+ }
+
+ @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);
+ }
+}
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..1fdc399
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -0,0 +1,77 @@
+/*
+ * 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 {
+
+ public final int widgetsCount;
+ public final int shortcutsCount;
+
+ private boolean mIsWidgetListShown = false;
+ private boolean mHasEntryUpdated = false;
+
+ public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ super(pkgItem, titleSectionName, items);
+ widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
+ shortcutsCount = Math.max(0, items.size() - widgetsCount);
+ }
+
+ /** Sets if the widgets list associated with this header is shown. */
+ public void setIsWidgetListShown(boolean isWidgetListShown) {
+ if (mIsWidgetListShown != isWidgetListShown) {
+ this.mIsWidgetListShown = isWidgetListShown;
+ mHasEntryUpdated = true;
+ } else {
+ mHasEntryUpdated = false;
+ }
+ }
+
+ /** Returns {@code true} if the widgets list associated with this header is shown. */
+ public boolean isWidgetListShown() {
+ return mIsWidgetListShown;
+ }
+
+ /** Returns {@code true} if this entry has been updated due to user interactions. */
+ public boolean hasEntryUpdated() {
+ return mHasEntryUpdated;
+ }
+
+ @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);
+ }
+}
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..2aec3f8
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
@@ -0,0 +1,72 @@
+/*
+ * 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 {
+
+ private boolean mIsWidgetListShown = false;
+ private boolean mHasEntryUpdated = false;
+
+ public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items) {
+ super(pkgItem, titleSectionName, items);
+ }
+
+ /** Sets if the widgets list associated with this header is shown. */
+ public void setIsWidgetListShown(boolean isWidgetListShown) {
+ if (mIsWidgetListShown != isWidgetListShown) {
+ this.mIsWidgetListShown = isWidgetListShown;
+ mHasEntryUpdated = true;
+ } else {
+ mHasEntryUpdated = false;
+ }
+ }
+
+ /** Returns {@code true} if the widgets list associated with this header is shown. */
+ public boolean isWidgetListShown() {
+ return mIsWidgetListShown;
+ }
+
+ /** Returns {@code true} if this entry has been updated due to user interactions. */
+ public boolean hasEntryUpdated() {
+ return mHasEntryUpdated;
+ }
+
+ @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);
+ }
+}
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..7f84077
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -0,0 +1,257 @@
+/*
+ * 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.View;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.RelativeLayout;
+
+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 {
+ private final boolean mHasWorkProfile;
+ private final SearchAndRecommendationViewHolder mViewHolder;
+ private final WidgetsRecyclerView mPrimaryRecyclerView;
+ private final WidgetsRecyclerView mSearchRecyclerView;
+ private final int mTabsHeight;
+
+ // 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;
+
+ /**
+ * 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;
+
+ SearchAndRecommendationsScrollController(
+ boolean hasWorkProfile,
+ int tabsHeight,
+ SearchAndRecommendationViewHolder viewHolder,
+ WidgetsRecyclerView primaryRecyclerView,
+ @Nullable WidgetsRecyclerView workRecyclerView,
+ WidgetsRecyclerView searchRecyclerView,
+ @Nullable View personalWorkTabsView,
+ @Nullable PersonalWorkPagedView primaryWorkViewPager) {
+ mHasWorkProfile = hasWorkProfile;
+ mViewHolder = viewHolder;
+ mPrimaryRecyclerView = primaryRecyclerView;
+ mCurrentRecyclerView = mPrimaryRecyclerView;
+ mWorkRecyclerView = workRecyclerView;
+ mSearchRecyclerView = searchRecyclerView;
+ mPrimaryWorkTabsView = personalWorkTabsView;
+ mPrimaryWorkViewPager = primaryWorkViewPager;
+ mCurrentRecyclerView = mPrimaryRecyclerView;
+ mTabsHeight = tabsHeight;
+ }
+
+ /** Sets the current active {@link WidgetsRecyclerView}. */
+ public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
+ mCurrentRecyclerView = currentRecyclerView;
+ mCurrentRecyclerView = currentRecyclerView;
+ mViewHolder.mHeaderTitle.setTranslationY(0);
+ mViewHolder.mRecommendedWidgetsTable.setTranslationY(0);
+ mViewHolder.mSearchBar.setTranslationY(0);
+
+ if (mHasWorkProfile) {
+ mPrimaryWorkTabsView.setTranslationY(0);
+ }
+ }
+
+ /**
+ * 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.mSearchBar)
+ + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable);
+
+ int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer);
+ 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;
+
+ 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;
+ }
+ return hasMarginOrPaddingUpdated;
+ }
+
+ /**
+ * Changes the displacement of collapsible views (e.g. title & widget recommendations) and fixed
+ * views (e.g. recycler views, tabs) upon scrolling.
+ */
+ @Override
+ public void onThumbOffsetYChanged(int unused) {
+ // Always use the recycler view offset because fast scroller offset has a different scale.
+ int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY();
+ if (recyclerViewYOffset < 0) return;
+
+ if (mCollapsibleHeightForRecommendation > 0) {
+ int yDisplacement = Math.max(-recyclerViewYOffset,
+ -mCollapsibleHeightForRecommendation);
+ mViewHolder.mHeaderTitle.setTranslationY(yDisplacement);
+ mViewHolder.mRecommendedWidgetsTable.setTranslationY(yDisplacement);
+ }
+
+ if (mCollapsibleHeightForSearch > 0) {
+ int searchYDisplacement = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForSearch);
+ mViewHolder.mSearchBar.setTranslationY(searchYDisplacement);
+ }
+
+ if (mHasWorkProfile && mCollapsibleHeightForTabs > 0) {
+ int yDisplacementForTabs = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForTabs);
+ mPrimaryWorkTabsView.setTranslationY(yDisplacementForTabs);
+ }
+ }
+
+ /** Resets any previous view translation. */
+ public void reset() {
+ mViewHolder.mHeaderTitle.setTranslationY(0);
+ mViewHolder.mSearchBar.setTranslationY(0);
+ if (mHasWorkProfile) {
+ mPrimaryWorkTabsView.setTranslationY(0);
+ }
+ }
+
+ /** 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;
+ }
+}
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..2366609
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -0,0 +1,194 @@
+/*
+ * 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)
+ || hasWidgetsListChanged(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 of widgets.
+ */
+ private boolean hasWidgetsListChanged(WidgetsListBaseEntry curRow,
+ WidgetsListBaseEntry newRow) {
+ if (!(curRow instanceof WidgetsListContentEntry)
+ || !(newRow instanceof WidgetsListContentEntry)) {
+ return false;
+ }
+ WidgetsListContentEntry orgRowEntry = (WidgetsListContentEntry) curRow;
+ WidgetsListContentEntry newRowEntry = (WidgetsListContentEntry) newRow;
+ return !orgRowEntry.mWidgets.equals(newRowEntry.mWidgets);
+ }
+
+ /**
+ * 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 ((WidgetsListHeaderEntry) newRow).hasEntryUpdated() || !curRow.equals(newRow);
+ }
+ if (newRow instanceof WidgetsListSearchHeaderEntry
+ && curRow instanceof WidgetsListSearchHeaderEntry) {
+ return ((WidgetsListSearchHeaderEntry) newRow).hasEntryUpdated()
+ || !curRow.equals(newRow);
+ }
+ 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..a4257a2
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -0,0 +1,638 @@
+/*
+ * 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 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.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.RecyclerView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.views.RecyclerViewFastScroller;
+import com.android.launcher3.views.TopRoundedCornerView;
+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.picker.search.WidgetsSearchBarUIHelper;
+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;
+
+/**
+ * Popup for showing the full list of available widgets
+ */
+public class WidgetsFullSheet extends BaseWidgetSheet
+ implements Insettable, ProviderChangedListener, OnActivePageChangedListener,
+ WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener,
+ WidgetsSearchBarUIHelper {
+ 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 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 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();
+ private final int mTabsHeight;
+ private final int mWidgetCellHorizontalPadding;
+
+ @Nullable private PersonalWorkPagedView mViewPager;
+ private boolean mIsInSearchMode;
+ 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_tab_height)
+ : 0;
+ 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);
+ 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);
+ } else {
+ mViewPager = null;
+ }
+
+ layoutInflater.inflate(R.layout.widgets_full_sheet_search_and_recommendations, springLayout,
+ true);
+ mSearchAndRecommendationViewHolder = new SearchAndRecommendationViewHolder(
+ findViewById(R.id.search_and_recommendations_container));
+ 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);
+ fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController);
+
+ mNoWidgetsView = findViewById(R.id.no_widgets_text);
+
+ onRecommendedWidgetsBound();
+ onWidgetsBound();
+
+ mSearchAndRecommendationViewHolder.mSearchBar.initialize(
+ mLauncher.getPopupDataProvider(), /* searchModeListener= */ this);
+ }
+
+ @Override
+ public void onActivePageChanged(int currentActivePage) {
+ AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
+ WidgetsRecyclerView currentRecyclerView =
+ mAdapters.get(currentActivePage).mWidgetsRecyclerView;
+
+ updateNoWidgetsView(currentAdapterHolder);
+ attachScrollbarToRecyclerView(currentRecyclerView);
+ resetExpandedHeaders();
+ }
+
+ private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
+ recyclerView.bindFastScrollbar();
+ mSearchAndRecommendationsScrollController.setCurrentRecyclerView(recyclerView);
+ reset();
+ }
+
+ private void updateNoWidgetsView(AdapterHolder adapterHolder) {
+ boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.getItemCount() > 0;
+ adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
+
+ // Always resets the text in case this is updated by search.
+ mNoWidgetsView.setText(R.string.no_widgets_available);
+ mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE);
+ }
+
+ private void updateNoSearchResultsView(boolean isVisible) {
+ mNoWidgetsView.setVisibility(isVisible ? VISIBLE : GONE);
+ if (isVisible) {
+ mNoWidgetsView.setText(R.string.no_search_results);
+ }
+ }
+
+ 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();
+ }
+
+ @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();
+ mLauncher.getAppWidgetHost().addProviderChangeListener(this);
+ notifyWidgetProvidersChanged();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mLauncher.getAppWidgetHost().removeProviderChangeListener(this);
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ mInsets.set(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);
+ }
+ if (insets.bottom > 0) {
+ setupNavBarColor();
+ } else {
+ clearNavBarColor();
+ }
+
+ ((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom);
+ 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);
+ }
+ }
+ }
+
+ private void doMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ DeviceProfile deviceProfile = mLauncher.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));
+ }
+
+ /** Returns {@code true} if the max spans have been updated. */
+ private boolean updateMaxSpansPerRow() {
+ if (getMeasuredWidth() == 0) return false;
+
+ int previousMaxSpansPerRow = mMaxSpansPerRow;
+ mMaxSpansPerRow = getMeasuredWidth()
+ / (mLauncher.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() {
+ mLauncher.refreshAndBindWidgetsForPackageUser(null);
+ }
+
+ @Override
+ public void onWidgetsBound() {
+ List<WidgetsListBaseEntry> allWidgets = mLauncher.getPopupDataProvider().getAllWidgets();
+
+ AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
+ primaryUserAdapterHolder.setup(findViewById(R.id.primary_widgets_list_view));
+ AdapterHolder searchAdapterHolder = mAdapters.get(AdapterHolder.SEARCH);
+ searchAdapterHolder.setup(findViewById(R.id.search_widgets_list_view));
+ primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+ updateNoWidgetsView(primaryUserAdapterHolder);
+
+ if (mHasWorkProfile) {
+ AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
+ workUserAdapterHolder.setup(findViewById(R.id.work_widgets_list_view));
+ workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+ onActivePageChanged(mViewPager.getCurrentPage());
+ }
+ }
+
+ @Override
+ public void enterSearchMode() {
+ if (mIsInSearchMode) return;
+ setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
+ attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
+ resetExpandedHeaders();
+ }
+
+ @Override
+ public void exitSearchMode() {
+ 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);
+ updateNoSearchResultsView(
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.getItemCount() == 0);
+ }
+
+ private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
+ mIsInSearchMode = isInSearchMode;
+ mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable
+ .setVisibility(isInSearchMode ? GONE : VISIBLE);
+ if (mHasWorkProfile) {
+ mViewPager.setVisibility(isInSearchMode ? GONE : VISIBLE);
+ mTabsView.setVisibility(isInSearchMode ? GONE : VISIBLE);
+ } else {
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView
+ .setVisibility(isInSearchMode ? GONE : VISIBLE);
+ }
+ mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView
+ .setVisibility(mIsInSearchMode ? VISIBLE : GONE);
+ mNoWidgetsView.setVisibility(GONE);
+ }
+
+ private void resetExpandedHeaders() {
+ mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader();
+ mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader();
+ }
+
+ @Override
+ public void onRecommendedWidgetsBound() {
+ List<WidgetItem> recommendedWidgets =
+ mLauncher.getPopupDataProvider().getRecommendedWidgets();
+ WidgetsRecommendationTableLayout table =
+ mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable;
+ if (recommendedWidgets.size() > 0) {
+ float maxTableHeight =
+ (mLauncher.getDeviceProfile().heightPx - mTabsHeight - getHeaderViewHeight())
+ * 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());
+ }
+ }
+ 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();
+ AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
+ }
+
+ @Override
+ public int getHeaderViewHeight() {
+ return measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mCollapseHandle)
+ + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mHeaderTitle)
+ + measureHeightWithVerticalMargins(
+ (View) mSearchAndRecommendationViewHolder.mSearchBar);
+ }
+
+ /** 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());
+ }
+
+ @Override
+ public void clearSearchBarFocus() {
+ mSearchAndRecommendationViewHolder.mSearchBar.clearSearchBarFocus();
+ }
+
+ /** 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 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,
+ /* WidgetsSearchBarUIHelper= */
+ mAdapterType == SEARCH ? WidgetsFullSheet.this : null);
+ mWidgetsListAdapter.setHasStableIds(true);
+ switch (mAdapterType) {
+ case PRIMARY:
+ mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
+ break;
+ case WORK:
+ mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
+ break;
+ default:
+ break;
+ }
+ }
+
+ void setup(WidgetsRecyclerView recyclerView) {
+ mWidgetsRecyclerView = recyclerView;
+ mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
+ // Disables animation because it disrupts the item focus upon adapter item change.
+ mWidgetsRecyclerView.setItemAnimator(null);
+ mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
+ mWidgetsRecyclerView.setEdgeEffectFactory(
+ ((TopRoundedCornerView) mContent).createEdgeEffectFactory());
+ mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView);
+ mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow);
+ }
+ }
+
+ final class SearchAndRecommendationViewHolder {
+ final ViewGroup mContainer;
+ final View mCollapseHandle;
+ final WidgetsSearchBar mSearchBar;
+ final TextView mHeaderTitle;
+ final WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
+
+ SearchAndRecommendationViewHolder(ViewGroup searchAndRecommendationContainer) {
+ mContainer = searchAndRecommendationContainer;
+ mCollapseHandle = mContainer.findViewById(R.id.collapse_handle);
+ 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..d9c9d4d
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -0,0 +1,291 @@
+/*
+ * 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.os.Process;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.TableRow;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.LabelComparator;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.WidgetCell;
+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.search.WidgetsSearchBarUIHelper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * 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;
+
+ @Nullable private final WidgetsSearchBarUIHelper mSearchBarUIHelper;
+ 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;
+
+ public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
+ WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
+ OnClickListener iconClickListener, OnLongClickListener iconLongClickListener,
+ @Nullable WidgetsSearchBarUIHelper searchBarUIHelper) {
+ mSearchBarUIHelper = searchBarUIHelper;
+ mDiffReporter = new WidgetsDiffReporter(iconCache, this);
+ mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
+ layoutInflater, iconClickListener, iconLongClickListener,
+ widgetPreviewLoader, /* listAdapter= */ this);
+ mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
+ mViewHolderBinders.put(
+ VIEW_TYPE_WIDGETS_HEADER,
+ new WidgetsListHeaderViewHolderBinder(
+ layoutInflater, /* onHeaderClickListener= */this, /* listAdapter= */ this));
+ mViewHolderBinders.put(
+ VIEW_TYPE_WIDGETS_SEARCH_HEADER,
+ new WidgetsListSearchHeaderViewHolderBinder(
+ layoutInflater, /*onHeaderClickListener=*/ this, /* listAdapter= */ this));
+ }
+
+ 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) {
+ mAllEntries = tempEntries.stream().sorted(mRowComparator)
+ .collect(Collectors.toList());
+ 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;
+ setWidgets(searchResults);
+ }
+
+ private void updateVisibleEntries() {
+ mAllEntries.forEach(entry -> {
+ if (entry instanceof WidgetsListHeaderEntry) {
+ ((WidgetsListHeaderEntry) entry).setIsWidgetListShown(
+ new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+ .equals(mWidgetsContentVisiblePackageUserKey));
+ } else if (entry instanceof WidgetsListSearchHeaderEntry) {
+ ((WidgetsListSearchHeaderEntry) entry).setIsWidgetListShown(
+ new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+ .equals(mWidgetsContentVisiblePackageUserKey));
+ }
+ });
+ List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
+ .filter(entry -> (mFilter == null || mFilter.test(entry))
+ && mHeaderAndSelectedContentFilter.test(entry))
+ .collect(Collectors.toList());
+ mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
+ }
+
+ /**
+ * Resets any expanded widget header.
+ */
+ public void resetExpandedHeader() {
+ mWidgetsContentVisiblePackageUserKey = null;
+ updateVisibleEntries();
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int pos) {
+ ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
+ viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), pos);
+ }
+
+ @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) {
+ if (mSearchBarUIHelper != null) {
+ mSearchBarUIHelper.clearSearchBarFocus();
+ }
+ if (showWidgets) {
+ mWidgetsContentVisiblePackageUserKey = packageUserKey;
+ updateVisibleEntries();
+ } else if (packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) {
+ mWidgetsContentVisiblePackageUserKey = null;
+ updateVisibleEntries();
+ }
+ }
+
+ /**
+ * Sets the max horizontal spans that are allowed for grouping more than one widgets in a table
+ * row.
+ *
+ * <p>If there is only one widget in a row, that widget horizontal span is allowed to exceed
+ * {@code maxHorizontalSpans}.
+ * <p>Let's say the max horizontal spans is set to 5. 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 void setMaxHorizontalSpansPerRow(int maxHorizontalSpans) {
+ mWidgetsListTableViewHolderBinder.setMaxSpansPerRow(maxHorizontalSpans);
+ }
+
+ /** 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/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
new file mode 100644
index 0000000..497c72e
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -0,0 +1,258 @@
+/*
+ * 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.util.AttributeSet;
+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 androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+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.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 final int mBottomMarginSize;
+
+ private ImageView mAppIcon;
+ private TextView mTitle;
+ private TextView mSubtitle;
+
+ private CheckBox mExpandToggle;
+ private boolean mIsExpanded = false;
+
+ 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);
+ mBottomMarginSize =
+ getResources().getDimensionPixelSize(R.dimen.widget_list_entry_bottom_margin);
+ }
+
+ @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);
+ }
+
+ /**
+ * 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);
+ onExpandChangeListener.onExpansionChange(mIsExpanded);
+ });
+ }
+
+ /** Sets the expand toggle to expand / collapse. */
+ @UiThread
+ public void setExpanded(boolean isExpanded) {
+ this.mIsExpanded = isExpanded;
+ mExpandToggle.setChecked(isExpanded);
+ if (getLayoutParams() instanceof RecyclerView.LayoutParams) {
+ int bottomMargin = isExpanded ? 0 : mBottomMarginSize;
+ RecyclerView.LayoutParams layoutParams =
+ ((RecyclerView.LayoutParams) getLayoutParams());
+ layoutParams.bottomMargin = bottomMargin;
+ setLayoutParams(layoutParams);
+ }
+ }
+
+ /** 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) {
+ FastBitmapDrawable 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);
+
+ 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;
+ }
+ }
+
+ /** 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..e57f4d8
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
@@ -0,0 +1,72 @@
+/*
+ * 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 WidgetsListAdapter mWidgetsListAdapter;
+
+ public WidgetsListHeaderViewHolderBinder(LayoutInflater layoutInflater,
+ OnHeaderClickListener onHeaderClickListener,
+ WidgetsListAdapter listAdapter) {
+ mLayoutInflater = layoutInflater;
+ mOnHeaderClickListener = onHeaderClickListener;
+ mWidgetsListAdapter = listAdapter;
+ }
+
+ @Override
+ public WidgetsListHeaderHolder newViewHolder(ViewGroup parent) {
+ WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+ R.layout.widgets_list_row_header, parent, false);
+
+ return new WidgetsListHeaderHolder(header);
+ }
+
+ @Override
+ public void bindViewHolder(WidgetsListHeaderHolder viewHolder, WidgetsListHeaderEntry data,
+ int position) {
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ if (mWidgetsListAdapter.getItemCount() == 1) {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_single_item_ripple);
+ } else if (position == 0) {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_top_ripple);
+ } else if (position == mWidgetsListAdapter.getItemCount() - 1) {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_bottom_ripple);
+ } else {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_middle_ripple);
+ }
+ widgetsListHeader.applyFromItemInfoWithIcon(data);
+ widgetsListHeader.setExpanded(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/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..b98f5e1
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.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;
+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 WidgetsListAdapter mWidgetsListAdapter;
+
+ public WidgetsListSearchHeaderViewHolderBinder(LayoutInflater layoutInflater,
+ OnHeaderClickListener onHeaderClickListener,
+ WidgetsListAdapter listAdapter) {
+ mLayoutInflater = layoutInflater;
+ mOnHeaderClickListener = onHeaderClickListener;
+ mWidgetsListAdapter = listAdapter;
+ }
+
+ @Override
+ public WidgetsListSearchHeaderHolder newViewHolder(ViewGroup parent) {
+ WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+ R.layout.widgets_list_row_header, parent, false);
+
+ return new WidgetsListSearchHeaderHolder(header);
+ }
+
+ @Override
+ public void bindViewHolder(WidgetsListSearchHeaderHolder viewHolder,
+ WidgetsListSearchHeaderEntry data, int position) {
+ WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+ if (mWidgetsListAdapter.getItemCount() == 1) {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_single_item_ripple);
+ } else if (position == 0) {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_top_ripple);
+ } else if (position == mWidgetsListAdapter.getItemCount() - 1) {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_bottom_ripple);
+ } else {
+ widgetsListHeader.setBackgroundResource(R.drawable.widgets_list_middle_ripple);
+ }
+ widgetsListHeader.applyFromItemInfoWithIcon(data);
+ widgetsListHeader.setExpanded(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/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
new file mode 100644
index 0000000..1524ab3
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -0,0 +1,193 @@
+/*
+ * 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.Log;
+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.WidgetPreviewLoader;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+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 int mMaxSpansPerRow = 4;
+ private final LayoutInflater mLayoutInflater;
+ private final int mIndent;
+ private final OnClickListener mIconClickListener;
+ private final OnLongClickListener mIconLongClickListener;
+ private final WidgetPreviewLoader mWidgetPreviewLoader;
+ private final WidgetsListAdapter mWidgetsListAdapter;
+ private boolean mApplyBitmapDeferred = false;
+
+ public WidgetsListTableViewHolderBinder(
+ Context context,
+ LayoutInflater layoutInflater,
+ OnClickListener iconClickListener,
+ OnLongClickListener iconLongClickListener,
+ WidgetPreviewLoader widgetPreviewLoader,
+ WidgetsListAdapter listAdapter) {
+ mLayoutInflater = layoutInflater;
+ mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
+ mIconClickListener = iconClickListener;
+ mIconLongClickListener = iconLongClickListener;
+ mWidgetPreviewLoader = widgetPreviewLoader;
+ mWidgetsListAdapter = listAdapter;
+ }
+
+ /**
+ * Defers applying bitmap on all the {@link WidgetCell} at
+ * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry)} if
+ * {@code applyBitmapDeferred} is {@code true}.
+ */
+ public void setApplyBitmapDeferred(boolean applyBitmapDeferred) {
+ mApplyBitmapDeferred = applyBitmapDeferred;
+ }
+
+ public void setMaxSpansPerRow(int maxSpansPerRow) {
+ mMaxSpansPerRow = maxSpansPerRow;
+ }
+
+ @Override
+ public WidgetsRowViewHolder newViewHolder(ViewGroup parent) {
+ if (DEBUG) {
+ Log.v(TAG, "\nonCreateViewHolder");
+ }
+
+ ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
+ R.layout.widgets_table_container, 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_table).setPaddingRelative(mIndent, 0, 1, 0);
+
+ return new WidgetsRowViewHolder(container);
+ }
+
+ @Override
+ public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry,
+ int position) {
+ TableLayout table = holder.mTableContainer;
+ if (DEBUG) {
+ Log.d(TAG, String.format("onBindViewHolder [widget#=%d, table.getChildCount=%d]",
+ entry.mWidgets.size(), table.getChildCount()));
+ }
+
+ if (position == mWidgetsListAdapter.getItemCount() - 1) {
+ table.setBackgroundResource(R.drawable.widgets_list_bottom_ripple);
+ } else {
+ // WidgetsListContentEntry is never shown in position 0. There must be a header above
+ // it.
+ table.setBackgroundResource(R.drawable.widgets_list_middle_ripple);
+ }
+
+ List<ArrayList<WidgetItem>> widgetItemsTable =
+ WidgetsTableUtils.groupWidgetItemsIntoTable(entry.mWidgets, mMaxSpansPerRow);
+ 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);
+ widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
+ widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
+ widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
+ widget.ensurePreview();
+ 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..6569fb0
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.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.widget.picker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+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.WidgetImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A {@link TableLayout} for showing recommended widgets. */
+public final class WidgetsRecommendationTableLayout extends TableLayout {
+ private static final float SCALE_DOWN_RATIO = 0.9f;
+ private final DeviceProfile mDeviceProfile;
+ 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);
+ mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
+ // There are 1 row for title, 1 row for dimension and 2 rows for description.
+ 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.spanX, widgetItem.spanY, 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);
+ WidgetImageView preview = widget.findViewById(R.id.widget_preview);
+ preview.setOnClickListener(mWidgetCellOnClickListener);
+ preview.setOnLongClickListener(mWidgetCellOnLongClickListener);
+ widget.setAnimatePreview(false);
+
+ parent.addView(widget);
+ return widget;
+ }
+
+ private RecommendationTableData fitRecommendedWidgetsToTableSpace(
+ float previewScale,
+ List<ArrayList<WidgetItem>> recommendedWidgetsInTable) {
+ // A naive estimation of the widgets recommendation table height without inflation.
+ float totalHeight = 0;
+ 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++) {
+ float previewHeight = widgetItems.get(j).spanY * mDeviceProfile.allAppsCellHeightPx
+ * previewScale;
+ rowHeight = Math.max(rowHeight, previewHeight + mWidgetCellTextViewsHeight);
+ }
+ 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 * SCALE_DOWN_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..b016b4f
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -0,0 +1,246 @@
+/*
+ * 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.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;
+
+/**
+ * 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 final int mEstimatedWidgetListHeaderHeight;
+ private boolean mTouchDownOnScroller;
+ private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
+ private int mLastVisibleWidgetContentTableHeight = 0;
+
+ 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();
+ mEstimatedWidgetListHeaderHeight = grid.iconSizePx
+ + 2 * context.getResources().getDimensionPixelSize(
+ R.dimen.widget_list_header_view_vertical_padding);
+ }
+
+ @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);
+ 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();
+ }
+ }
+
+ 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;
+ }
+
+ /**
+ * 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 += mEstimatedWidgetListHeaderHeight;
+ } 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/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
similarity index 68%
rename from src/com/android/launcher3/widget/WidgetsRowViewHolder.java
rename to src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
index d26edb6..aef1103 100644
--- a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
@@ -13,25 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3.widget;
+package com.android.launcher3.widget.picker;
import android.view.ViewGroup;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.R;
+import android.widget.TableLayout;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-public class WidgetsRowViewHolder extends ViewHolder {
+import com.android.launcher3.R;
- public final ViewGroup cellContainer;
- public final BubbleTextView title;
+/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
+public final class WidgetsRowViewHolder extends ViewHolder {
+
+ public final TableLayout mTableContainer;
public WidgetsRowViewHolder(ViewGroup v) {
super(v);
- cellContainer = v.findViewById(R.id.widgets_cell_list);
- title = v.findViewById(R.id.section);
- title.setAccessibilityDelegate(null);
+ 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..42f1bb2
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
@@ -0,0 +1,82 @@
+/*
+ * 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 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..0ac47ce
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.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.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();
+
+ /**
+ * 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..d35a75b
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
@@ -0,0 +1,139 @@
+/*
+ * 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() {
+ mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
+ mInput.getText().clear();
+ clearFocus();
+ mSearchModeListener.exitSearchMode();
+ }
+
+ /**
+ * 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/util/SafeCloseable.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarUIHelper.java
similarity index 66%
copy from src/com/android/launcher3/util/SafeCloseable.java
copy to src/com/android/launcher3/widget/picker/search/WidgetsSearchBarUIHelper.java
index ba8ee04..edfdc65 100644
--- a/src/com/android/launcher3/util/SafeCloseable.java
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarUIHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,13 +14,14 @@
* limitations under the License.
*/
-package com.android.launcher3.util;
+package com.android.launcher3.widget.picker.search;
/**
- * Extension of closeable which does not throw an exception
+ * UI helper for {@link WidgetsSearchBar}.
*/
-public interface SafeCloseable extends AutoCloseable {
-
- @Override
- void close();
+public interface WidgetsSearchBarUIHelper {
+ /**
+ * Clears focus from the search bar.
+ */
+ void clearSearchBarFocus();
}
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..e73d661
--- /dev/null
+++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.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.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}.
+ * 3. The order shortcuts are grouped together in the same row until their total horizontal
+ * spans exceed the {@code maxSpansPerRow}.
+ */
+ 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);
+ if (numOfWidgetItems == 0) {
+ widgetItemsAtRow.add(widgetItem);
+ } else if (widgetItem.spanX + totalHorizontalSpan <= maxSpansPerRow
+ && 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/allapps/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
similarity index 85%
rename from src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
rename to src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
index 2de425e..3a3028f 100644
--- a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3.allapps;
+package com.android.launcher3.workprofile;
import android.content.Context;
import android.graphics.Canvas;
@@ -35,9 +35,6 @@
* 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;
@@ -47,7 +44,7 @@
private float mScrollOffset;
private int mSelectedPosition = 0;
- private AllAppsContainerView mContainerView;
+ private OnActivePageChangedListener mOnActivePageChangedListener;
private int mLastActivePage = 0;
private boolean mIsRtl;
@@ -123,7 +120,7 @@
float y = getHeight() - mDividerPaint.getStrokeWidth();
canvas.drawLine(getPaddingLeft(), y, getWidth() - getPaddingRight(), y, mDividerPaint);
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
- mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
+ mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
@Override
@@ -135,15 +132,15 @@
@Override
public void setActiveMarker(int activePage) {
updateTabTextColor(activePage);
- if (mContainerView != null && mLastActivePage != activePage) {
+ if (mOnActivePageChangedListener != null && mLastActivePage != activePage) {
updateIndicatorPosition(activePage);
- mContainerView.onTabChanged(activePage);
+ mOnActivePageChangedListener.onActivePageChanged(activePage);
}
mLastActivePage = activePage;
}
- public void setContainerView(AllAppsContainerView containerView) {
- mContainerView = containerView;
+ public void setOnActivePageChangedListener(OnActivePageChangedListener listener) {
+ mOnActivePageChangedListener = listener;
}
@Override
@@ -153,4 +150,12 @@
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..f82f2cc 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -5,11 +5,12 @@
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;
@@ -18,7 +19,6 @@
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 +26,23 @@
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.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 +53,53 @@
// 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 AppFilter mAppFilter;
+ 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.
@@ -139,83 +156,28 @@
// Temporary list 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<>();
+ HashMap<PackageUserKey, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
- // clear the lists.
+ // Clear the lists only if this is an update on all widgets and shortcuts. If packageUser
+ // isn't null, only updates the shortcuts and widgets for the app represented in
+ // packageUser.
if (packageUser == null) {
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();
- }
- }
- }
}
-
- 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));
+ mWidgetsList.putAll(rawWidgetsShortcuts.stream()
+ .filter(new WidgetValidityCheck(app))
+ .collect(Collectors.groupingBy(item -> {
+ PackageUserKey packageUserKey = new PackageUserKey(
+ item.componentName.getPackageName(), item.user);
+ PackageItemInfo pInfo = tmpPackageItemInfos.get(packageUserKey);
+ if (pInfo == null) {
+ pInfo = new PackageItemInfo(packageUserKey.mPackageName);
+ pInfo.user = item.user;
+ tmpPackageItemInfos.put(packageUserKey, pInfo);
}
- 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);
- }
+ return pInfo;
+ })));
// Update each package entry
IconCache iconCache = app.getIconCache();
@@ -226,9 +188,9 @@
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 +210,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 +223,46 @@
}
return null;
}
+
+ 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
+ int minSpanX = Math.min(item.widgetInfo.spanX, item.widgetInfo.minSpanX);
+ int minSpanY = Math.min(item.widgetInfo.spanY, item.widgetInfo.minSpanY);
+ if (minSpanX > mIdp.numColumns || minSpanY > mIdp.numRows) {
+ if (DEBUG) {
+ Log.d(TAG, String.format(
+ "Widget %s : (%d X %d) can't fit on this device",
+ item.componentName, minSpanX, minSpanY));
+ }
+ 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;
+ }
+ }
}
\ 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
index 5a1f9ca..780a0f0 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java
@@ -21,16 +21,16 @@
import android.util.Pair;
import android.util.Range;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
+
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
**/
@@ -69,7 +69,7 @@
// 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.
+ // Get the most preeminent, non-disallowed color.
Integer bestColor = 0;
final float[] hsl = new float[3];
for (int i = 0; i < mainColorsSize; i++) {
@@ -78,7 +78,7 @@
Color.blue(colorValue), hsl);
// Stop when we find a color that meets our criteria
- if (!isBlacklisted(hsl)) {
+ if (!isDisallowed(hsl)) {
bestColor = colorValue;
break;
}
@@ -167,12 +167,12 @@
}
/**
- * Checks if a given color exists in the blacklist
+ * Checks if a given color exists in the disallowed_colors list.
* @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) {
+ private boolean isDisallowed(float[] hsl) {
+ for (ColorRange badRange: DISALLOWED_COLORS) {
if (badRange.containsColor(hsl[0], hsl[1], hsl[2])) {
return true;
}
@@ -592,7 +592,7 @@
);
@SuppressWarnings("WeakerAccess")
- static final ColorRange[] BLACKLISTED_COLORS = new ColorRange[] {
+ static final ColorRange[] DISALLOWED_COLORS = new ColorRange[] {
// Red
new ColorRange(
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
index 0fd0a35..9dbe47c 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
@@ -17,8 +17,7 @@
package com.android.launcher3.uioverrides.dynamicui;
import android.content.Context;
-
-import com.android.launcher3.Utilities;
+import android.os.Build;
import androidx.annotation.Nullable;
@@ -32,7 +31,7 @@
if (sInstance == null) {
context = context.getApplicationContext();
- if (Utilities.ATLEAST_OREO_MR1) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
try {
sInstance = new WallpaperManagerCompatVOMR1(context);
} catch (Throwable e) {
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..53748b7 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,13 @@
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;
/**
* Definition for AllApps state
@@ -41,7 +41,7 @@
};
public AllAppsState(int id) {
- super(id, ContainerType.ALLAPPS, STATE_FLAGS);
+ super(id, LAUNCHER_STATE_ALLAPPS, STATE_FLAGS);
}
@Override
@@ -56,7 +56,7 @@
@Override
public int getVisibleElements(Launcher launcher) {
- return ALL_APPS_HEADER | ALL_APPS_CONTENT;
+ return ALL_APPS_CONTENT;
}
@Override
@@ -74,4 +74,9 @@
public float getVerticalProgress(Launcher launcher) {
return 0f;
}
+
+ @Override
+ public float getWorkspaceScrimAlpha(Launcher launcher) {
+ return 1;
+ }
}
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..e85e505 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,11 @@
*/
package com.android.launcher3.uioverrides.states;
+import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
+
import android.content.Context;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
/**
* Definition for overview state
@@ -26,7 +27,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 +39,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 +49,11 @@
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);
+ }
}
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..2c7d30a 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -23,6 +23,7 @@
androidx.annotation_annotation \
androidx.test.runner \
androidx.test.rules \
+ androidx.preference_preference \
androidx.test.uiautomator_uiautomator
ifneq (,$(wildcard frameworks/base))
@@ -31,11 +32,13 @@
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_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../NOTICE
LOCAL_SDK_VERSION := system_current
include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 1c8f095..7f6c8f8 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"
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/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..21625c6
--- /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="prim"/>
+ <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="second"/>
+ <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="neutral"/>
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:background="@android:color/system_neutral2_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/src/com/android/launcher3/util/SafeCloseable.java b/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java
similarity index 66%
copy from src/com/android/launcher3/util/SafeCloseable.java
copy to tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java
index ba8ee04..5fb3454 100644
--- a/src/com/android/launcher3/util/SafeCloseable.java
+++ b/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.launcher3.util;
+package com.android.launcher3.testcomponent;
+
+import android.appwidget.AppWidgetProvider;
/**
- * Extension of closeable which does not throw an exception
+ * A simple app widget showing a primary, secondary and neutral color.
*/
-public interface SafeCloseable extends AutoCloseable {
-
- @Override
- void close();
+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..d4e8f1f 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,7 +63,6 @@
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.ShellCommandRule;
@@ -101,6 +99,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 +115,7 @@
if (TestHelpers.isInLauncherProcess()) {
StrictMode.VmPolicy.Builder builder =
new StrictMode.VmPolicy.Builder()
-// b/154772063
-// .detectActivityLeaks()
+ .detectActivityLeaks()
.penaltyLog()
.penaltyListener(Runnable::run, violation -> {
if (sStrictmodeDetectedActivityLeak == null) {
@@ -152,16 +150,31 @@
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) {
+ 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;
+ 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() {
@@ -226,9 +239,8 @@
}
@Rule
- public TestRule mOrderSensitiveRules = RuleChain.
- outerRule(new FailureRewriterRule())
- .around(new TestStabilityRule())
+ public TestRule mOrderSensitiveRules = RuleChain
+ .outerRule(new TestStabilityRule())
.around(mActivityMonitor)
.around(getRulesInsideActivityMonitor());
@@ -236,25 +248,11 @@
return mDevice;
}
- private boolean hasSystemUiObject(String resId) {
- return mDevice.hasObject(By.res(SYSTEMUI_PACKAGE, resId));
- }
-
@Before
public void setUp() throws Exception {
- Log.d(TAG, "Before disabling battery defender");
- mDevice.executeShellCommand("setprop vendor.battery.defender.disable 1");
- Log.d(TAG, "Before enabling stay awake");
- mDevice.executeShellCommand("settings put global stay_on_while_plugged_in 3");
- for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
- Log.d(TAG, "Before unlocking the phone");
- mDevice.executeShellCommand("input keyevent 82");
- mDevice.waitForIdle();
- }
- Assert.assertTrue("Keyguard still visible",
- mDevice.wait(
+ Assert.assertTrue("Keyguard is visible",
+ TestHelpers.wait(
Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
- Log.d(TAG, "Keyguard is not visible");
final String launcherPackageName = mDevice.getLauncherPackageName();
try {
@@ -272,8 +270,6 @@
}
mLauncherPid = 0;
- // Disable app tracker
- AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
mTargetContext = InstrumentationRegistry.getTargetContext();
mTargetPackage = mTargetContext.getPackageName();
@@ -283,6 +279,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());
}
}
@@ -486,8 +484,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 +503,7 @@
// Destroy Launcher activity.
executeOnLauncher(launcher -> {
if (launcher != null) {
+ onLauncherActivityClose(launcher);
launcher.finish();
}
});
@@ -514,7 +512,7 @@
}
protected boolean isInBackground(Launcher launcher) {
- return !launcher.hasBeenResumed();
+ return launcher == null || !launcher.hasBeenResumed();
}
protected boolean isInState(Supplier<LauncherState> state) {
@@ -527,7 +525,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 +536,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 +560,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 +585,20 @@
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/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 34e425d..6f47df0 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -37,10 +37,11 @@
import com.android.launcher3.tapl.Widgets;
import com.android.launcher3.tapl.Workspace;
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;
@@ -101,7 +102,7 @@
mLauncher.pressHome();
}
- @Test
+ @Ignore
public void testOpenHomeSettingsFromWorkspace() {
mDevice.pressMenu();
mDevice.waitForIdle();
@@ -298,7 +299,9 @@
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 +334,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)
+ .openMenu();
+ 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/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 9d4ccff..b421b0e 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();
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index f146db5..714b11b 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;
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..822fefc 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;
@@ -126,9 +125,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/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_common/com/android/launcher3/common/WidgetUtils.java b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
index c0913bf..ffad93f 100644
--- a/tests/src_common/com/android/launcher3/common/WidgetUtils.java
+++ b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
@@ -23,12 +23,12 @@
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;
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src_disabled/WorkTabTest.java
similarity index 75%
rename from tests/src/com/android/launcher3/ui/WorkTabTest.java
rename to tests/src_disabled/WorkTabTest.java
index 8d571ff..bfacc74 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src_disabled/WorkTabTest.java
@@ -17,6 +17,7 @@
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;
@@ -24,6 +25,7 @@
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;
@@ -34,15 +36,19 @@
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)
@@ -53,7 +59,9 @@
private static final int WORK_PAGE = AllAppsContainerView.AdapterHolder.WORK;
@Before
- public void createWorkProfile() throws Exception {
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
String output =
mDevice.executeShellCommand(
"pm create-user --profileOf 0 --managed TestProfile");
@@ -61,26 +69,42 @@
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();
@@ -112,6 +136,7 @@
l -> l.getSystemService(UserManager.class).isQuietModeEnabled(workProfile));
}
+ @Ignore("b/182844465")
@Test
public void testWorkEduFlow() {
mDevice.pressHome();
@@ -120,9 +145,13 @@
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);
+ 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();
@@ -132,14 +161,26 @@
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 ->
- ((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)));
+ 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();
@@ -164,6 +205,10 @@
// 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);
@@ -183,7 +228,7 @@
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));
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index b6c17df..e32250e 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -27,7 +27,6 @@
import androidx.test.uiautomator.Direction;
import androidx.test.uiautomator.UiObject2;
-import com.android.launcher3.ResourceUtils;
import com.android.launcher3.testing.TestProtocol;
import java.util.stream.Collectors;
@@ -108,26 +107,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/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index ce94a3e..4a666b1 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,
@@ -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;
}
@@ -147,68 +146,97 @@
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")) {
+ 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;
+ if (toRight) {
+ 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;
+ }
+ } else {
+ if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
+ // Swipe from the bottom right to the bottom left of the screen.
+ startX = mLauncher.getDevice().getDisplayWidth();
+ startY = getSwipeStartY();
+ endX = 0;
+ endY = startY;
+ } else {
+ // Swipe from the bottom left to the top left of the screen.
+ startX = getSwipeStartX();
+ startY = 0;
+ endX = startX;
+ endY = mLauncher.getRealDisplaySize().y - 1;
+ }
+ }
- 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");
+ 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);
+ 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");
+ 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..c4a566b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -56,17 +56,19 @@
private Background launch(BySelector selector) {
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);
mLauncher.assertTrue(
- "App didn't start: " + selector,
- mLauncher.getDevice().wait(Until.hasObject(selector),
- LauncherInstrumentation.WAIT_TIME_MS));
+ "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..475a4ff 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -52,6 +52,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 +65,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;
@@ -103,6 +105,8 @@
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 final boolean mIsLauncher3;
// 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 +116,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 +154,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 WIDGETS_RES_ID = "primary_widgets_list_view";
private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
- public static final int WAIT_TIME_MS = 10000;
+ 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 +177,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");
@@ -201,6 +206,7 @@
public LauncherInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
mDevice = UiDevice.getInstance(instrumentation);
+ mIsLauncher3 = "com.android.launcher3".equals(getLauncherPackageName());
// Launcher should run in test harness so that custom accessibility protocol between
// Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call
@@ -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());
}
@@ -358,9 +365,11 @@
if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
- if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
+ if (!mDevice.wait(Until.hasObject(By.textStartsWith("")), 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);
@@ -448,7 +457,7 @@
}
}
- dumpDiagnostics();
+ dumpDiagnostics(message);
log("Hierarchy dump for: " + message);
dumpViewHierarchy();
@@ -456,10 +465,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,10 +477,10 @@
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);
}
}
@@ -527,17 +537,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 +569,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 +586,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 +604,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);
@@ -632,6 +639,14 @@
fail("Launcher didn't initialize");
}
+ Parcelable executeAndWaitForLauncherEvent(Runnable command,
+ UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
+ return executeAndWaitForEvent(
+ command,
+ e -> mLauncherPackage.equals(e.getPackageName()) && eventFilter.accept(e),
+ message);
+ }
+
Parcelable executeAndWaitForEvent(Runnable command,
UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
try {
@@ -693,7 +708,7 @@
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
launcherWasVisible
? GestureScope.INSIDE_TO_OUTSIDE
- : GestureScope.OUTSIDE);
+ : GestureScope.OUTSIDE_WITH_PILFER);
}
}
} else {
@@ -853,6 +868,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 {
@@ -925,6 +950,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);
@@ -976,7 +1009,7 @@
void runToState(Runnable command, int expectedState) {
final List<Integer> actualEvents = new ArrayList<>();
- executeAndWaitForEvent(
+ executeAndWaitForLauncherEvent(
command,
event -> isSwitchToStateEvent(event, expectedState, actualEvents),
() -> "Failed to receive an event for the state change: expected ["
@@ -1001,16 +1034,20 @@
expectedState);
}
- 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) {
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);
@@ -1033,6 +1070,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,7 +1133,7 @@
return;
}
- executeAndWaitForEvent(
+ executeAndWaitForLauncherEvent(
() -> linearGesture(
startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
@@ -1159,7 +1201,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 +1210,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);
@@ -1182,7 +1230,7 @@
final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
assertTrue("injectInputEvent failed",
- mInstrumentation.getUiAutomation().injectInputEvent(event, true));
+ mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
event.recycle();
}
@@ -1273,25 +1321,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);
}
@@ -1360,7 +1396,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 +1409,7 @@
}
boolean isLauncher3() {
- return "com.android.launcher3".equals(getLauncherPackageName());
+ return mIsLauncher3;
}
void expectEvent(String sequence, Pattern expected) {
@@ -1392,4 +1428,4 @@
return null;
}
}
-}
+}
\ 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..ab6465c 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -57,6 +57,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,6 +85,7 @@
String verify(long waitForExpectedCountMs, boolean successfulGesture) {
final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs);
+ if (actualEvents == null) return "null event sequences because launcher likely died";
final StringBuilder sb = new StringBuilder();
boolean hasMismatches = false;
@@ -91,8 +94,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 +105,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,
@@ -117,13 +118,6 @@
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;
- }
-
// If the list of actual events matches the list of expected events, returns -1, otherwise
// the position of the mismatch.
private static int getMismatchPosition(List<Pattern> expected, List<String> actual) {
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..e3e0f42
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -0,0 +1,151 @@
+/*
+ * 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/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
new file mode 100644
index 0000000..3507418
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.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.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);
+ }
+ }
+ }
+
+ /**
+ * Click feedback button.
+ */
+ @NonNull
+ public Background clickFeedback() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c =
+ mLauncher.addContextLayer("want to click feedback button")) {
+ UiObject2 feedback = mLauncher.waitForObjectInContainer(mSelectModeButtons, "feedback");
+ mLauncher.clickLauncherObject(feedback);
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "clicked feedback button")) {
+ return new Background(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/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 39ac645..a3f9ade 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,114 @@
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;
+ for (int i = 0; i < SCROLL_ATTEMPTS; i++) {
+ UiObject2 fullWidgetsPicker = verifyActiveContainer();
+
+ UiObject2 header = mLauncher.waitForObjectInContainer(fullWidgetsPicker,
+ headerSelector);
+ int headerHeight = 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.
+ if (!hasHeaderExpanded) {
+ 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;
+ }
+ }
+ mLauncher.scrollDownByDistance(fullWidgetsPicker, headerHeight);
+ }
+
+ return null;
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index f0e686f..d43e235 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -110,7 +110,8 @@
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(