Merge "Revert "Enable ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE for TEAMFOOD"" into udc-qpr-dev
diff --git a/Android.bp b/Android.bp
index 9b696a2..316f9c0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -153,7 +153,8 @@
"androidx.cardview_cardview",
"com.google.android.material_material",
"iconloader_base",
- "view_capture"
+ "view_capture",
+ "animationlib"
],
manifest: "AndroidManifest-common.xml",
sdk_version: "current",
diff --git a/OWNERS b/OWNERS
index b684460..7834396 100644
--- a/OWNERS
+++ b/OWNERS
@@ -13,6 +13,8 @@
winsonc@google.com
jonmiranda@google.com
alexchau@google.com
+patmanning@google.com
+tsuharesu@google.com
per-file FeatureFlags.java, globs = set noparent
per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index 63ea20c..f8b08f8 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -135,7 +135,7 @@
}
}
-// Next value 51
+// Next value 52
enum Attribute {
option allow_alias = true;
@@ -187,6 +187,7 @@
ALL_APPS_SEARCH_RESULT_SYSTEM_POINTER = 42;
ALL_APPS_SEARCH_RESULT_EDUCARD = 43;
ALL_APPS_SEARCH_RESULT_LOCATION = 50;
+ ALL_APPS_SEARCH_RESULT_TEXT_HEADER = 51;
// Result sources
DATA_SOURCE_APPSEARCH_APP_PREVIEW = 45;
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
index f5a8253..638ce27 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -42,5 +42,6 @@
"tests/src/com/android/quickstep/NavigationModeSwitchRule.java",
"tests/src/com/android/quickstep/AbstractQuickStepTest.java",
"tests/src/com/android/quickstep/TaplTestsQuickstep.java",
+ "tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
]
}
diff --git a/res/interpolator/standard_decelerate.xml b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
similarity index 65%
rename from res/interpolator/standard_decelerate.xml
rename to quickstep/res/drawable/bg_bubble_dismiss_circle.xml
index 579f4f5..b793eec 100644
--- a/res/interpolator/standard_decelerate.xml
+++ b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2022 The Android Open Source Project
+ ~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,8 +15,13 @@
~ limitations under the License.
-->
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
- android:controlX1="0"
- android:controlY1="0"
- android:controlX2="0"
- android:controlY2="1"/>
\ No newline at end of file
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <stroke
+ android:width="2dp"
+ android:color="@android:color/system_accent1_600" />
+
+ <solid android:color="@android:color/system_accent1_600" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_bubble_dismiss_white.xml b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
new file mode 100644
index 0000000..b15111b
--- /dev/null
+++ b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+ android:fillColor="@android:color/system_neutral1_50"/>
+</vector>
diff --git a/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml b/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml
deleted file mode 100644
index 70c4231..0000000
--- a/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?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.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
index 4e67629..69e1574 100644
--- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
android:importantForAccessibility="yes"
android:background="@drawable/keyboard_quick_switch_task_view_background"
android:clipToOutline="true"
- launcher:borderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
diff --git a/quickstep/res/layout/fallback_recents_activity.xml b/quickstep/res/layout/fallback_recents_activity.xml
index bfeb82d..f0ea09c 100644
--- a/quickstep/res/layout/fallback_recents_activity.xml
+++ b/quickstep/res/layout/fallback_recents_activity.xml
@@ -15,6 +15,7 @@
-->
<com.android.launcher3.LauncherRootView
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
diff --git a/quickstep/res/layout/gesture_tutorial_activity.xml b/quickstep/res/layout/gesture_tutorial_activity.xml
index 0e763ec..82caedf 100644
--- a/quickstep/res/layout/gesture_tutorial_activity.xml
+++ b/quickstep/res/layout/gesture_tutorial_activity.xml
@@ -13,80 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<androidx.constraintlayout.widget.ConstraintLayout
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/gesture_tutorial_fragment_container"
android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <FrameLayout
- android:id="@+id/gesture_tutorial_fragment_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
-
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent" />
-
- <RelativeLayout
- android:id="@+id/rotation_prompt"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?attr/surfaceHome"
- android:visibility="gone"
-
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/background"
- android:layout_width="300dp"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- android:background="@drawable/rotate_prompt_bg"
- android:padding="24dp">
-
- <ImageView
- android:id="@+id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:src="@drawable/rotate_tutorial_warning"
-
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintEnd_toEndOf="parent" />
-
- <TextView
- android:id="@+id/rotate_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="20dp"
- android:lineSpacingExtra="2sp"
- android:text="@string/gesture_tutorial_rotation_prompt_title"
- android:textAppearance="@style/rotate_prompt_title"
-
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/icon"
- app:layout_constraintEnd_toEndOf="parent" />
-
- <TextView
- android:id="@+id/rotate_screen_subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:gravity="center"
- android:text="@string/gesture_tutorial_rotation_prompt"
- android:textAppearance="@style/rotate_prompt_subtitle"
-
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/rotate_title"
- app:layout_constraintEnd_toEndOf="parent" />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
- </RelativeLayout>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+ android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_rotation_prompt.xml b/quickstep/res/layout/gesture_tutorial_rotation_prompt.xml
new file mode 100644
index 0000000..b41a96d
--- /dev/null
+++ b/quickstep/res/layout/gesture_tutorial_rotation_prompt.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/surfaceHome">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/background"
+ android:layout_width="300dp"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:background="@drawable/rotate_prompt_bg"
+ android:padding="24dp"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/rotate_tutorial_warning"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <TextView
+ android:id="@+id/rotate_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:lineSpacingExtra="2sp"
+ android:text="@string/gesture_tutorial_rotation_prompt_title"
+ android:textAppearance="@style/rotate_prompt_title"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/icon"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:gravity="center"
+ android:text="@string/gesture_tutorial_rotation_prompt"
+ android:textAppearance="@style/rotate_prompt_subtitle"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/rotate_title"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/keyboard_quick_switch_overview.xml b/quickstep/res/layout/keyboard_quick_switch_overview.xml
index e7b1f23..4a9b023 100644
--- a/quickstep/res/layout/keyboard_quick_switch_overview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_overview.xml
@@ -22,7 +22,7 @@
android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
android:clipToOutline="true"
android:importantForAccessibility="yes"
- launcher:borderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
index 4d213fa..6ed3c6e 100644
--- a/quickstep/res/layout/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
android:importantForAccessibility="yes"
android:background="@drawable/keyboard_quick_switch_task_view_background"
android:clipToOutline="true"
- launcher:borderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index 5f037f8..c663bf4 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -32,7 +32,7 @@
android:layout_width="wrap_content"
android:gravity="center"
android:textColor="?androidprv:attr/textColorOnAccent"
- android:drawableEnd="@drawable/ic_split_horizontal"
+ android:drawableEnd="@drawable/ic_split_exit"
android:drawablePadding="@dimen/split_instructions_drawable_padding"
android:text="@string/toast_split_select_app" />
</com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 4865aef..29c9992 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -24,7 +24,8 @@
android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
- launcher:borderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+ launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
<com.android.quickstep.views.TaskThumbnailView
android:id="@+id/snapshot"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index fd82c66..06f4d06 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -25,7 +25,8 @@
android:clipToOutline="true"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
- launcher:borderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+ launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
<View
android:id="@+id/background"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index c9fa9c0..75ff626 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -29,7 +29,8 @@
android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
- launcher:borderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+ launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
<com.android.quickstep.views.TaskThumbnailView
android:id="@+id/snapshot"
diff --git a/quickstep/res/layout/transient_taskbar.xml b/quickstep/res/layout/transient_taskbar.xml
index 7a6d16a..0890a4e 100644
--- a/quickstep/res/layout/transient_taskbar.xml
+++ b/quickstep/res/layout/transient_taskbar.xml
@@ -49,6 +49,7 @@
android:visibility="gone"
android:gravity="center"
android:clipChildren="false"
+ android:elevation="@dimen/bubblebar_elevation"
/>
<FrameLayout
diff --git a/quickstep/res/values-af/strings.xml b/quickstep/res/values-af/strings.xml
index 8007646..b589e32 100644
--- a/quickstep/res/values-af/strings.xml
+++ b/quickstep/res/values-af/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Skermkiekie"</string>
<string name="action_split" msgid="2098009717623550676">"Verdeel"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tik op ’n ander app om verdeelde skerm te gebruik"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Verlaat verdeeldeskermkeuse"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Kies nog ’n app as jy verdeelde skerm wil gebruik"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Jou organisasie laat nie hierdie program toe nie"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Slaan navigasietutoriaal oor?"</string>
diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml
index d0269b8..d9e9691 100644
--- a/quickstep/res/values-am/strings.xml
+++ b/quickstep/res/values-am/strings.xml
@@ -33,12 +33,12 @@
<string name="all_apps_prediction_tip" msgid="2672336544844936186">"የእርስዎ የሚገመቱ መተግበሪያዎች"</string>
<string name="hotseat_edu_title_migrate" msgid="306578144424489980">"በመነሻ ገጽዎ ታችኛው ረድፍ ላይ የመተግበሪያ አስተያየት ጥቆማዎችን ያግኙ"</string>
<string name="hotseat_edu_title_migrate_landscape" msgid="3633942953997845243">"በመነሻ ማያ ገጽዎ የተወዳጆች ረድፍ ላይ የመተግበሪያ አስተያየት ጥቆማዎችን ያግኙ"</string>
- <string name="hotseat_edu_message_migrate" msgid="8927179260533775320">"በጣም ስራ ላይ የዋሉ መተግበሪያዎችዎን በቀላሉ ከመነሻ ገጹ ሆነው ይድረሱባቸው። የአስተያየት ጥቆማዎች በእርስዎ ዕለት ተዕለት ተግባራት ላይ በመመስረት ይቀየራሉ። በታችኛው ረድፍ ላይ ያሉ መተግበሪያዎች ወደ መነሻ ገጽዎ ይወሰዳሉ።"</string>
+ <string name="hotseat_edu_message_migrate" msgid="8927179260533775320">"በጣም ሥራ ላይ የዋሉ መተግበሪያዎችዎን በቀላሉ ከመነሻ ገጹ ሆነው ይድረሱባቸው። የአስተያየት ጥቆማዎች በእርስዎ ዕለት ተዕለት ተግባራት ላይ በመመስረት ይቀየራሉ። በታችኛው ረድፍ ላይ ያሉ መተግበሪያዎች ወደ መነሻ ገጽዎ ይወሰዳሉ።"</string>
<string name="hotseat_edu_message_migrate_landscape" msgid="4248943380443387697">"በጣም ሥራ ላይ የዋሉ መተግበሪያዎችዎን በቀላሉ ከመነሻ ገጹ ሆነው ይድረሱባቸው። የአስተያየት ጥቆማዎች በእርስዎ ዕለት ተዕለት ተግባራት ላይ በመመሥረት ይቀየራሉ። በተወዳጆች ረድፍ ውስጥ ያሉ መተግበሪያዎች ወደ የእርስዎ መነሻ ማያ ገፅ ይንቀሳቀሳሉ።"</string>
<string name="hotseat_edu_accept" msgid="1611544083278999837">"የመተግበሪያ አስተያየት ጥቆማዎችን አግኝ"</string>
<string name="hotseat_edu_dismiss" msgid="2781161822780201689">"አይ፣ አመሰግናለሁ"</string>
<string name="hotseat_prediction_settings" msgid="6246554993566070818">"ቅንብሮች"</string>
- <string name="hotseat_auto_enrolled" msgid="522100018967146807">"በብዛት ስራ ላይ የዋሉ መተግበሪያዎች እዚህ ይመጣሉ፣ እና በዕለት ተዕለት ተግባራት ላይ በመመስረት ይቀየራሉ"</string>
+ <string name="hotseat_auto_enrolled" msgid="522100018967146807">"በብዛት ሥራ ላይ የዋሉ መተግበሪያዎች እዚህ ይመጣሉ፣ እና በዕለት ተዕለት ተግባራት ላይ በመመስረት ይቀየራሉ"</string>
<string name="hotseat_tip_no_empty_slots" msgid="1325212677738179185">"የመተግበሪያ ጥቆማዎችን ለማግኘት መተግበሪያዎችን ከታችኛው ረድፍ ይጎትቱ"</string>
<string name="hotseat_tip_gaps_filled" msgid="3035673010274223538">"የመተግበሪያ አስተያየት ጥቆማዎች ወደ ባዶ ቦታ ታክለዋል"</string>
<string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"የመተግበሪያ አስተያየት ጥቆማዎች ነቅቷል"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ቅጽበታዊ ገፅ እይታ"</string>
<string name="action_split" msgid="2098009717623550676">"ክፈል"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"የተከፈለ ማያ ገጽን ለመጠቀም ሌላ መተግበሪያ መታ ያድርጉ"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ከተከፈለ ማያ ገፅ ምርጫ ይውጡ"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"የተከፈለ ማያ ገጽን ለመቀበል ሌላ መተግበሪያ ይምረጡ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ይህ ድርጊት በመተግበሪያው ወይም በእርስዎ ድርጅት አይፈቀድም"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"የአሰሳ አጋዥ ሥልጠናን ይዝለሉ?"</string>
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index 3f711de..6d7effc 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"لقطة شاشة"</string>
<string name="action_split" msgid="2098009717623550676">"تقسيم"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"انقر على تطبيق آخر لاستخدام وضع تقسيم الشاشة."</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"الخروج من وضع تقسيم الشاشة"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"اختَر تطبيقًا آخر لاستخدام وضع تقسيم الشاشة."</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"لا يسمح التطبيق أو لا تسمح مؤسستك بهذا الإجراء."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"هل تريد تخطي الدليل التوجيهي للتنقّل؟"</string>
diff --git a/quickstep/res/values-as/strings.xml b/quickstep/res/values-as/strings.xml
index 9ab8c30..a68aee5 100644
--- a/quickstep/res/values-as/strings.xml
+++ b/quickstep/res/values-as/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"স্ক্ৰীনশ্বট"</string>
<string name="action_split" msgid="2098009717623550676">"বিভাজন কৰক"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপত টিপক"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"বিভাজিত স্ক্ৰীনৰ বাছনিৰ পৰা বাহিৰ হওক"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"বিভাজিত স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ অন্য এটা এপ্ বাছক"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"এপ্টোৱে অথবা আপোনাৰ প্ৰতিষ্ঠানে এই কাৰ্যটোৰ অনুমতি নিদিয়ে"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"নেভিগেশ্বনৰ টিউট’ৰিয়েল এৰিব বিচাৰে নেকি?"</string>
diff --git a/quickstep/res/values-az/strings.xml b/quickstep/res/values-az/strings.xml
index 4b45d50..9fff2d8 100644
--- a/quickstep/res/values-az/strings.xml
+++ b/quickstep/res/values-az/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Skrinşot"</string>
<string name="action_split" msgid="2098009717623550676">"Ayırın"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Bölünmüş ekran üçün başqa tətbiqə toxunun"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Bölünmüş ekran seçimindən çıxın"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Bölünmüş ekrandan istifadə üçün başqa tətbiq seçin"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Bu əməliyyata tətbiq və ya təşkilatınız tərəfindən icazə verilmir"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Naviqasiya dərsliyi ötürülsün?"</string>
diff --git a/quickstep/res/values-b+sr+Latn/strings.xml b/quickstep/res/values-b+sr+Latn/strings.xml
index b818edc..9a7aa4b 100644
--- a/quickstep/res/values-b+sr+Latn/strings.xml
+++ b/quickstep/res/values-b+sr+Latn/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Snimak ekrana"</string>
<string name="action_split" msgid="2098009717623550676">"Podeli"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu aplikaciju za podeljeni ekran"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izlazak iz biranja podeljenog ekrana"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu aplikaciju za podeljeni ekran"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ili organizacija ne dozvoljavaju ovu radnju"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Želite da preskočite vodič za kretanje?"</string>
diff --git a/quickstep/res/values-be/strings.xml b/quickstep/res/values-be/strings.xml
index 6438392..ca62108 100644
--- a/quickstep/res/values-be/strings.xml
+++ b/quickstep/res/values-be/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Здымак экрана"</string>
<string name="action_split" msgid="2098009717623550676">"Падзелены экран"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Каб падзяліць экран, націсніце на іншую праграму"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Выйсці з рэжыму падзеленага экрана"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Каб падзяліць экран, выберыце іншую праграму"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Гэта дзеянне не дазволена праграмай ці вашай арганізацыяй"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Прапусціць дапаможнік па навігацыі?"</string>
diff --git a/quickstep/res/values-bg/strings.xml b/quickstep/res/values-bg/strings.xml
index c8f19f1..72e731a 100644
--- a/quickstep/res/values-bg/strings.xml
+++ b/quickstep/res/values-bg/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Екранна снимка"</string>
<string name="action_split" msgid="2098009717623550676">"Разделяне на екрана"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Докоснете друго прил., за да ползвате разд. екран"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Изход от избора на разделен екран"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"За разделен екран изберете още едно приложение"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Това действие не е разрешено от приложението или организацията ви"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Пропускане на урока за навигиране?"</string>
diff --git a/quickstep/res/values-bn/strings.xml b/quickstep/res/values-bn/strings.xml
index b114620..0973a50 100644
--- a/quickstep/res/values-bn/strings.xml
+++ b/quickstep/res/values-bn/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"স্ক্রিনশট নিন"</string>
<string name="action_split" msgid="2098009717623550676">"স্প্লিট"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"স্প্লিট স্ক্রিন ব্যবহারের জন্য অ্যাপে ট্যাপ করুন"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"স্প্লিট স্ক্রিন বেছে নেওয়ার বিকল্প থেকে বেরিয়ে আসুন"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"স্প্লিট স্ক্রিন ব্যবহার করতে অন্য অ্যাপ বেছে নিন"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"এই অ্যাপ বা আপনার প্রতিষ্ঠান এই অ্যাকশনটি পারফর্ম করার অনুমতি দেয়নি"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"নেভিগেশন টিউটোরিয়াল এড়িয়ে যেতে চান?"</string>
diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml
index c186550..a2e4e33 100644
--- a/quickstep/res/values-bs/strings.xml
+++ b/quickstep/res/values-bs/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Snimak ekrana"</string>
<string name="action_split" msgid="2098009717623550676">"Podijeli"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu apl. da koristite podijeljeni ekran"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izlaz iz odabira podijeljenog ekrana"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu apl. da koristite podijeljeni ekran"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Ovu radnju ne dozvoljava aplikacija ili vaša organizacija"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Preskočiti vodič za navigiranje?"</string>
diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml
index d446190..0cfe820 100644
--- a/quickstep/res/values-ca/strings.xml
+++ b/quickstep/res/values-ca/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Captura de pantalla"</string>
<string name="action_split" msgid="2098009717623550676">"Divideix"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Toca una altra app per utilitzar pantalla dividida"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Surt de la selecció de pantalla dividida"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Tria una altra app per utilitzar pantalla dividida"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"L\'aplicació o la teva organització no permeten aquesta acció"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vols ometre el tutorial de navegació?"</string>
diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml
index afb026e..3582005 100644
--- a/quickstep/res/values-cs/strings.xml
+++ b/quickstep/res/values-cs/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Snímek obrazovky"</string>
<string name="action_split" msgid="2098009717623550676">"Rozdělit"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Obrazovku rozdělíte klepnutím na jinou aplikaci"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Výběr opuštění rozdělené obrazovky"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Vyberte podporovanou aplikaci"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Aplikace nebo organizace zakazuje tuto akci"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Přeskočit výukový program k navigaci?"</string>
diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml
index bfce9a1..1603fac 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -72,7 +72,7 @@
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Prøv at holde fingeren nede på vinduet i længere tid, inden du løfter den"</string>
<string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Stryg lige opad, og hold derefter fingeren stille"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Du har lært, hvordan du bruger bevægelser. Du kan aktivere bevægelser i Indstillinger."</string>
- <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har fuldført bevægelsen for Skift app"</string>
+ <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har fuldført bevægelsen for at skifte mellem apps"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Stryg for at skifte app"</string>
<string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Skift mellem apps ved at stryge opad fra bunden af skærmen, holde fingeren stille og løfte den."</string>
<string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Skift mellem apps ved at stryge opad fra bunden af skærmen med 2 fingre, holde dem nede og slippe."</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Opdel"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tryk på en anden app for at bruge opdelt skærm"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Luk valg af opdelt skærm"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Vælg en anden app for at bruge opdelt skærm"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller din organisation tillader ikke denne handling"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vil du springe vejledningen for navigation over?"</string>
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index 1eedbb8..828b85f 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -60,12 +60,12 @@
<string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Wische vom unteren Displayrand nach oben"</string>
<string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Achte darauf, nicht innezuhalten, bevor du loslässt"</string>
<string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Wische gerade nach oben"</string>
- <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Du hast den Schritt für die „Startbildschirm“-Geste abgeschlossen. Jetzt lernst du, wie du zurückgehst."</string>
+ <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Du hast den Schritt für die „Zum Startbildschirm“-Geste abgeschlossen. Jetzt lernst du, wie du zurückgehst."</string>
<string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Du hast den Schritt für die „Startbildschirm“-Touch-Geste abgeschlossen"</string>
<string name="home_gesture_intro_title" msgid="836590312858441830">"Den Startbildschirm aufrufen"</string>
<string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Wenn du zum Startbildschirm gehen möchtest, wische einfach vom unteren Displayrand nach oben."</string>
<string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Wische mit zwei Fingern vom unteren Displayrand nach oben. So gelangst du immer zum Startbildschirm."</string>
- <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Zum Startbildschirm"</string>
+ <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Zum StartU+00ADbildschirm"</string>
<string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Wische vom unteren Displayrand nach oben"</string>
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Gut gemacht!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Wische vom unteren Displayrand nach oben"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Teilen"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Für Splitscreen auf weitere App tippen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Splitscreen-Auswahl beenden"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Für Splitscreen andere App auswählen"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Die App oder deine Organisation lässt diese Aktion nicht zu"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Tutorial zur Bedienung überspringen?"</string>
diff --git a/quickstep/res/values-el/strings.xml b/quickstep/res/values-el/strings.xml
index 3ea7c1e..ab9fe6d 100644
--- a/quickstep/res/values-el/strings.xml
+++ b/quickstep/res/values-el/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Στιγμιότυπο οθόνης"</string>
<string name="action_split" msgid="2098009717623550676">"Διαχωρισμός"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Πατήστε άλλη εφαρμογή για διαχωρισμό οθόνης"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Έξοδος από την επιλογή διαχωρισμού οθόνης"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Επιλέξτε άλλη εφαρμογή για διαχωρισμό οθόνης"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Αυτή η ενέργεια δεν επιτρέπεται από την εφαρμογή ή τον οργανισμό σας."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Παράβλεψη οδηγού πλοήγησης;"</string>
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index db6a6cc..878c062 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -70,7 +70,7 @@
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Split"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
diff --git a/quickstep/res/values-en-rCA/strings.xml b/quickstep/res/values-en-rCA/strings.xml
index 2e3010e..eed32b3 100644
--- a/quickstep/res/values-en-rCA/strings.xml
+++ b/quickstep/res/values-en-rCA/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Split"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organization"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index db6a6cc..878c062 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -70,7 +70,7 @@
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Split"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index db6a6cc..878c062 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -70,7 +70,7 @@
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Split"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organisation"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml
index 342d580..ae825c1 100644
--- a/quickstep/res/values-en-rXC/strings.xml
+++ b/quickstep/res/values-en-rXC/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Split"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tap another app to use split screen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Exit split screen selection"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choose another app to use split screen"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"This action isn\'t allowed by the app or your organization"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Skip navigation tutorial?"</string>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index 35ab3db..cf5d6e9 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -70,7 +70,7 @@
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"¡Bien hecho!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Asegúrate de deslizar hacia arriba desde el borde inferior de la pantalla"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Intenta mantener presionada la ventana más tiempo antes de soltarla"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Asegúrate de deslizar directamente hacia arriba y, luego, detenerte"</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Asegúrate de deslizar directamente hacia arriba y detenerte"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Ya sabes cómo usar los gestos. Para desactivarlos, ve a Configuración."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Completaste el gesto para cambiar de app"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Desliza para cambiar de app"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Captura de pantalla"</string>
<string name="action_split" msgid="2098009717623550676">"Pantalla dividida"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Presiona otra app para usar la pantalla dividida"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Salir de la selección de pantalla dividida"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Elige otra app para usar la pantalla dividida"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"La app o tu organización no permiten realizar esta acción"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"¿Omitir el instructivo de navegación?"</string>
diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml
index 3377381..d40780c 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -65,12 +65,12 @@
<string name="home_gesture_intro_title" msgid="836590312858441830">"Desliza para ir a la pantalla de inicio"</string>
<string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Desliza hacia arriba desde la parte inferior de la pantalla. Este gesto siempre te lleva a la pantalla de inicio."</string>
<string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Desliza dos dedos hacia arriba desde la parte inferior de la pantalla. Si haces este gesto, siempre irás a la pantalla de inicio."</string>
- <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Ir a Inicio"</string>
+ <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Ir a inicio"</string>
<string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Desliza hacia arriba desde la parte inferior de la pantalla"</string>
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"¡Bien hecho!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Asegúrate de deslizar hacia arriba desde el borde inferior de la pantalla"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Prueba a mantener pulsada la ventana durante más tiempo antes de soltarla"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Asegúrate de deslizar directamente hacia arriba y, luego, detenerte"</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Asegúrate de deslizar directamente hacia arriba y luego detenerte"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Ya sabes cómo utilizar gestos. Para desactivarlos, ve a Ajustes."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Has completado el gesto para cambiar de aplicación"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Desliza el dedo para cambiar de aplicación"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Hacer captura"</string>
<string name="action_split" msgid="2098009717623550676">"Dividir"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Toca otra aplicación para usar la pantalla dividida"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Salir de la selección de pantalla dividida"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Elige otra app para usar la pantalla dividida"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"No puedes hacerlo porque la aplicación o tu organización no lo permiten"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"¿Saltar tutorial de navegación?"</string>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index 1bf92c4..176741a 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Ekraanipilt"</string>
<string name="action_split" msgid="2098009717623550676">"Eralda"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Jagatud ekraanikuva kasutamiseks puudutage muud rakendust"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Jagatud ekraanikuva valikust väljumine"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Valige jagatud ekraanikuva jaoks muu rakendus"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Rakendus või teie organisatsioon on selle toimingu keelanud"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Kas jätta navigeerimise õpetused vahele?"</string>
diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml
index c801297..0d6c664 100644
--- a/quickstep/res/values-eu/strings.xml
+++ b/quickstep/res/values-eu/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Atera pantaila-argazki bat"</string>
<string name="action_split" msgid="2098009717623550676">"Zatitu"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Sakatu beste aplikazio bat pantaila zatitzeko"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Irten Pantaila zatitzea eginbidearen hautapenetik"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pantaila zatitua ikusteko, aukeratu beste aplikazio bat"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Aplikazioak edo erakundeak ez du eman ekintza hori gauzatzeko baimena"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Nabigazio-tutoriala saltatu nahi duzu?"</string>
diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml
index 879796a..85b4862 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -72,7 +72,7 @@
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"قبلاز رها کردن پنجره، آن را برای مدت طولانیتری نگه دارید"</string>
<string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"دقت کنید که مستقیماً تند به بالا بکشید و سپس توقف کنید"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"با نحوه استفاده از اشارهها آشنا شدید. برای خاموش کردن اشارهها، به «تنظیمات» بروید."</string>
- <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"اشاره جابهجا شدن بین برنامهها را تکمیل کردید"</string>
+ <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"اشاره جابهجایی بین برنامهها را تکمیل کردید"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"برای جابهجا شدن بین برنامهها، تند بهبالا بکشید"</string>
<string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"برای جابهجا شدن بین برنامهها، از پایین صفحه تند بهبالا بکشید، نگه دارید، و سپس رها کنید."</string>
<string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"برای جابهجایی بین برنامهها، با ۲ انگشت از پایین صفحه تند بهبالا بکشید، نگه دارید، و سپس رها کنید."</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"نماگرفت"</string>
<string name="action_split" msgid="2098009717623550676">"دونیمه"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"زدن روی برنامهای دیگر برای استفاده از صفحه دونیمه"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"خروج از انتخاب صفحهٔ دونیمه"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"انتخاب برنامهای دیگر برای استفاده از صفحه دونیمه"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"برنامه یا سازمان شما اجازه نمیدهد این کنش انجام شود."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"آموزش گامبهگام پیمایش رد شود؟"</string>
diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml
index 02c4ad3..60d6618 100644
--- a/quickstep/res/values-fi/strings.xml
+++ b/quickstep/res/values-fi/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Kuvakaappaus"</string>
<string name="action_split" msgid="2098009717623550676">"Jaa"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Avaa jaettu näyttö napauttamalla toista sovellusta"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Poistu jaetun näytön valinnasta"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Käytä jaettua näyttöä valitsemalla toinen sovellus"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Sovellus tai organisaatio ei salli tätä toimintoa"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ohitetaanko navigointiohje?"</string>
diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index b280d48..a98e9c5 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Capture d\'écran"</string>
<string name="action_split" msgid="2098009717623550676">"Partager"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Toucher une autre appli pour partager l\'écran"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Quitter la sélection d\'écran divisé"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choisir une autre application pour utiliser l\'écran partagé"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"L\'application ou votre organisation n\'autorise pas cette action"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorer le tutoriel sur la navigation?"</string>
diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml
index 868188b..2e2e0f1 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Capture d\'écran"</string>
<string name="action_split" msgid="2098009717623550676">"Partager"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Appuyez sur autre appli pour l\'écran partagé"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Quitter la sélection de l\'écran partagé"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Sélect. autre appli pour utiliser l\'écran partagé"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Cette action n\'est pas autorisée par l\'application ou par votre organisation"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorer le tutoriel de navigation ?"</string>
diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml
index 8248aa1..11479e1 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Facer captura"</string>
<string name="action_split" msgid="2098009717623550676">"Dividir"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Para usar a pantalla dividida, toca outra app"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Saír da selección de pantalla dividida"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolle outra app para usar a pantalla dividida"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"A aplicación ou a túa organización non permite realizar esta acción"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Queres omitir o titorial de navegación?"</string>
diff --git a/quickstep/res/values-gu/strings.xml b/quickstep/res/values-gu/strings.xml
index db41a1e..ef44fb9 100644
--- a/quickstep/res/values-gu/strings.xml
+++ b/quickstep/res/values-gu/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"સ્ક્રીનશૉટ"</string>
<string name="action_split" msgid="2098009717623550676">"વિભાજિત કરો"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"વિભાજિત સ્ક્રીન વાપરવા, કોઈ અન્ય ઍપ પર ટૅપ કરો"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"\'સ્ક્રીનને વિભાજિત કરો\' પસંદગીમાંથી બહાર નીકળો"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"સ્ક્રીન વિભાજનનો ઉપયોગ કરવા કોઈ અન્ય ઍપ પસંદ કરો"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ઍપ કે તમારી સંસ્થા દ્વારા આ ક્રિયા કરવાની મંજૂરી નથી"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"નૅવિગેશન ટ્યૂટૉરિઅલ છોડી દઈએ?"</string>
diff --git a/quickstep/res/values-hi/strings.xml b/quickstep/res/values-hi/strings.xml
index 6affb00..28551ad 100644
--- a/quickstep/res/values-hi/strings.xml
+++ b/quickstep/res/values-hi/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"स्क्रीनशॉट लें"</string>
<string name="action_split" msgid="2098009717623550676">"स्प्लिट स्क्रीन मोड"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिट स्क्रीन के लिए दूसरे ऐप्लिकेशन पर टैप करें"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"स्प्लिट स्क्रीन मोड से बाहर निकलें"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"स्प्लिट स्क्रीन के लिए, दूसरा ऐप्लिकेशन चुनें"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ऐप्लिकेशन या आपका संगठन इस कार्रवाई की अनुमति नहीं देता"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"क्या आपको अभी नेविगेशन ट्यूटोरियल नहीं देखना है?"</string>
diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml
index 6c9172f..abad768 100644
--- a/quickstep/res/values-hr/strings.xml
+++ b/quickstep/res/values-hr/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Snimka zaslona"</string>
<string name="action_split" msgid="2098009717623550676">"Podijeli"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Dodirnite drugu aplikaciju za podijeljeni zaslon"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Zatvori odabir podijeljenog zaslona"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Odaberite drugu aplikaciju za upotrebu podijeljenog zaslona"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ili vaša organizacija ne dopuštaju ovu radnju"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Želite li preskočiti vodič za kretanje?"</string>
diff --git a/quickstep/res/values-hu/strings.xml b/quickstep/res/values-hu/strings.xml
index 3f4ad18..16cd5f5 100644
--- a/quickstep/res/values-hu/strings.xml
+++ b/quickstep/res/values-hu/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Képernyőkép"</string>
<string name="action_split" msgid="2098009717623550676">"Felosztás"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Koppintson másik appra az osztott képernyőhöz"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Kilépés az osztott képernyő elemeinek kiválasztásából"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Válasszon másik appot a képernyő felosztásához"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Az alkalmazás vagy az Ön szervezete nem engedélyezi ezt a műveletet"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Kihagyja a navigáció bemutatóját?"</string>
diff --git a/quickstep/res/values-hy/strings.xml b/quickstep/res/values-hy/strings.xml
index 1521c53..48eecd6 100644
--- a/quickstep/res/values-hy/strings.xml
+++ b/quickstep/res/values-hy/strings.xml
@@ -80,7 +80,7 @@
<string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Մատը սահեցրեք էկրանի ներքևից վերև, պահեք և բաց թողեք"</string>
<string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Հիանալի՛ է"</string>
<string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Պատրաստ է"</string>
- <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Պատրաստ է"</string>
+ <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Ավարտել"</string>
<string name="gesture_tutorial_action_button_label_settings" msgid="2923621047916486604">"Կարգավորումներ"</string>
<string name="gesture_tutorial_try_again" msgid="65962545858556697">"Նորից փորձեք"</string>
<string name="gesture_tutorial_nice" msgid="2936275692616928280">"Գերազանց է"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Սքրինշոթ անել"</string>
<string name="action_split" msgid="2098009717623550676">"Տրոհել"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Հպեք այլ հավելվածի՝ տրոհված էկրանից օգտվելու համար"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Դուրս գալ տրոհված էկրանի ռեժիմից"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Ընտրեք այլ հավելված՝ կիսված էկրանից օգտվելու համար"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Այս գործողությունն արգելված է հավելվածի կամ ձեր կազմակերպության կողմից"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Բաց թողնե՞լ նավիգացիայի ուղեցույցը"</string>
diff --git a/quickstep/res/values-in/strings.xml b/quickstep/res/values-in/strings.xml
index 02fead4..2852fc0 100644
--- a/quickstep/res/values-in/strings.xml
+++ b/quickstep/res/values-in/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Pisahkan"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Ketuk aplikasi lain untuk memakai layar terpisah"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Keluar dari pemilihan layar terpisah"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pilih aplikasi lain untuk memakai layar terpisah"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Tindakan ini tidak diizinkan oleh aplikasi atau organisasi Anda"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Lewati tutorial gestur?"</string>
diff --git a/quickstep/res/values-is/strings.xml b/quickstep/res/values-is/strings.xml
index 8b840ce..998c23f 100644
--- a/quickstep/res/values-is/strings.xml
+++ b/quickstep/res/values-is/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Skjámynd"</string>
<string name="action_split" msgid="2098009717623550676">"Skipta"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Ýttu á annað forrit til að nota skjáskiptingu"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Loka skjáskiptingu"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Veldu annað forrit til að nota skjáskiptingu"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Forritið eða fyrirtækið leyfir ekki þessa aðgerð"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Sleppa flettileiðsögn?"</string>
diff --git a/quickstep/res/values-it/strings.xml b/quickstep/res/values-it/strings.xml
index f5b4c5c..3755dbb 100644
--- a/quickstep/res/values-it/strings.xml
+++ b/quickstep/res/values-it/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Dividi"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tocca un\'altra app per usare lo schermo diviso"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Esci dalla selezione dello schermo diviso"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Scegli un\'altra app per usare lo schermo diviso"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Questa azione non è consentita dall\'app o dall\'organizzazione"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Saltare il tutorial di navigazione?"</string>
diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml
index 27b053a..2ec3cbf 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"צילום מסך"</string>
<string name="action_split" msgid="2098009717623550676">"פיצול"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"צריך להקיש על אפליקציה אחרת כדי להשתמש במסך מפוצל"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"יציאה מתצוגת מסך מפוצל"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"כדי להשתמש במסך מפוצל צריך לבחור אפליקציה אחרת"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"האפליקציה או הארגון שלך אינם מתירים את הפעולה הזאת"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"לדלג על המדריך לניווט?"</string>
diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml
index f8ad440..7517f9d 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"スクリーンショット"</string>
<string name="action_split" msgid="2098009717623550676">"分割"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"分割画面を使用するには、他のアプリをタップします"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"分割画面の選択を終了します"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"分割画面にするには、別のアプリを選択してください"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"この操作はアプリまたは組織で許可されていません"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"操作チュートリアルをスキップしますか?"</string>
diff --git a/quickstep/res/values-ka/strings.xml b/quickstep/res/values-ka/strings.xml
index 57b8947..64aa870 100644
--- a/quickstep/res/values-ka/strings.xml
+++ b/quickstep/res/values-ka/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ეკრანის ანაბეჭდი"</string>
<string name="action_split" msgid="2098009717623550676">"გაყოფა"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"შეეხეთ სხვა აპს ეკრანის გასაყოფად"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ეკრანის გაყოფის არჩევანიდან გასვლა"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"აირჩიეთ სხვა აპი ეკრანის გასაყოფად"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ეს მოქმედება არ არის დაშვებული აპის ან თქვენი ორგანიზაციის მიერ"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"გსურთ, გამოტოვოთ ნავიგაციის სახელმძღვანელო?"</string>
diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml
index cfdf461..cde0dfd 100644
--- a/quickstep/res/values-kk/strings.xml
+++ b/quickstep/res/values-kk/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
<string name="action_split" msgid="2098009717623550676">"Бөлу"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Экранды бөлу режимін пайдалану үшін басқа қолданбаны түртіңіз."</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Экранды бөлу режимінен шығу"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Экранды бөлу үшін басқа қолданбаны таңдаңыз."</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Бұл әрекетке қолданба не ұйым рұқсат етпейді."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Қимылдар оқулығын өткізіп жіберу керек пе?"</string>
diff --git a/quickstep/res/values-km/strings.xml b/quickstep/res/values-km/strings.xml
index 4f11f16..89d8335 100644
--- a/quickstep/res/values-km/strings.xml
+++ b/quickstep/res/values-km/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"រូបថតអេក្រង់"</string>
<string name="action_split" msgid="2098009717623550676">"បំបែក"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"ចុចកម្មវិធីផ្សេងទៀត ដើម្បីប្រើមុខងារបំបែកអេក្រង់"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ចាកចេញពីការជ្រើសរើសរបស់មុខងារបំបែកអេក្រង់"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"ជ្រើសរើសកម្មវិធីផ្សេងទៀត ដើម្បីប្រើមុខងារបំបែកអេក្រង់"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"សកម្មភាពនេះមិនត្រូវបានអនុញ្ញាតដោយកម្មវិធី ឬស្ថាប័នរបស់អ្នកទេ"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"រំលងមេរៀនអំពីការរុករកឬ?"</string>
diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml
index acf66a8..6c84e6f 100644
--- a/quickstep/res/values-kn/strings.xml
+++ b/quickstep/res/values-kn/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ಸ್ಕ್ರೀನ್ಶಾಟ್"</string>
<string name="action_split" msgid="2098009717623550676">"ವಿಭಜಿಸಿ"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಆಯ್ಕೆಯಿಂದ ನಿರ್ಗಮಿಸಿ"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"\"ಪರದೆ ಬೇರ್ಪಡಿಸಿ\" ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ಆ್ಯಪ್ ಅಥವಾ ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ಈ ಕ್ರಿಯೆಯನ್ನು ಅನುಮತಿಸುವುದಿಲ್ಲ"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ನ್ಯಾವಿಗೇಶನ್ ಟ್ಯುಟೋರಿಯಲ್ ಸ್ಕಿಪ್ ಮಾಡಬೇಕೇ?"</string>
diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml
index 9f44a35..c00eeed 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"스크린샷"</string>
<string name="action_split" msgid="2098009717623550676">"분할"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"다른 앱을 탭하여 화면 분할 사용"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"화면 분할 선택 종료"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"화면 분할을 사용하려면 다른 앱을 선택하세요."</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"이 작업은 앱 또는 조직에서 허용되지 않습니다."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"이동 방법 튜토리얼을 건너뛰시겠습니까?"</string>
diff --git a/quickstep/res/values-ky/strings.xml b/quickstep/res/values-ky/strings.xml
index 3b3f92f..f5d8782 100644
--- a/quickstep/res/values-ky/strings.xml
+++ b/quickstep/res/values-ky/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
<string name="action_split" msgid="2098009717623550676">"Бөлүү"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Экранды бөлүү үчүн башка колдонмону таптап коюңуз"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Тандалган экранды бөлүүдөн чыгуу"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Экранды бөлүү үчүн башка колдонмону тандаңыз"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Бул аракетти аткарууга колдонмо же ишканаңыз тыюу салган"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Жаңсоолор үйрөткүчүн өткөрүп жибересизби?"</string>
diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml
index 2ab0d74..2f67c30 100644
--- a/quickstep/res/values-lo/strings.xml
+++ b/quickstep/res/values-lo/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ຮູບໜ້າຈໍ"</string>
<string name="action_split" msgid="2098009717623550676">"ແບ່ງ"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"ແຕະແອັບອື່ນເພື່ອໃຊ້ໜ້າຈໍແຍກ"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ອອກຈາກາກນເລືອກການແບ່ງໜ້າຈໍ"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"ເລືອກແອັບອື່ນເພື່ອໃຊ້ການແບ່ງໜ້າຈໍ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ແອັບ ຫຼື ອົງການຂອງທ່ານບໍ່ອະນຸຍາດໃຫ້ໃຊ້ຄຳສັ່ງນີ້"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ຂ້າມການສອນການນຳໃຊ້ການນຳທາງບໍ?"</string>
diff --git a/quickstep/res/values-lt/strings.xml b/quickstep/res/values-lt/strings.xml
index 5dd1a3b..8bb1252 100644
--- a/quickstep/res/values-lt/strings.xml
+++ b/quickstep/res/values-lt/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Ekrano kopija"</string>
<string name="action_split" msgid="2098009717623550676">"Išskaidymo režimas"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Išskaidyto ekrano režimas palietus kitą programą"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Išeiti iš išskaidyto ekrano pasirinkimo"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Išskaidyto ekrano režimą naudokite kita programa"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Jūsų organizacijoje arba naudojant šią programą neleidžiama atlikti šio veiksmo"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Praleisti naršymo mokymo programą?"</string>
diff --git a/quickstep/res/values-lv/strings.xml b/quickstep/res/values-lv/strings.xml
index a69fe5f..a8bff55 100644
--- a/quickstep/res/values-lv/strings.xml
+++ b/quickstep/res/values-lv/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Veikt ekrānuzņēmumu"</string>
<string name="action_split" msgid="2098009717623550676">"Sadalīt"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Lai sadalītu ekrānu, pieskarieties citai lietotnei"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Izejiet no ekrāna sadalīšanas režīma atlases."</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Izvēlieties citu lietotni, lai sadalītu ekrānu"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Lietotne vai jūsu organizācija neatļauj veikt šo darbību."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vai izlaist navigācijas mācības?"</string>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index b3ee4ea..258a544 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Слика од екранот"</string>
<string name="action_split" msgid="2098009717623550676">"Раздели"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Допрете друга аплик. за да користите поделен екран"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Излези од изборот на поделен екран"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Изберете друга апликација за да користите поделен екран"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Апликацијата или вашата организација не го дозволува дејствово"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Да се прескокне упатството за навигација?"</string>
diff --git a/quickstep/res/values-ml/strings.xml b/quickstep/res/values-ml/strings.xml
index 6d81f05..fffcd54 100644
--- a/quickstep/res/values-ml/strings.xml
+++ b/quickstep/res/values-ml/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"സ്ക്രീൻഷോട്ട്"</string>
<string name="action_split" msgid="2098009717623550676">"വിഭജിക്കുക"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"സ്പ്ലിറ്റ് സ്ക്രീനിന് മറ്റൊരു ആപ്പിൽ ടാപ്പ് ചെയ്യൂ"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"സ്ക്രീൻ വിഭജന തിരഞ്ഞെടുപ്പിൽ നിന്ന് പുറത്തുകടക്കുക"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"സ്ക്രീൻ വിഭജന മോഡിന് മറ്റൊരു ആപ്പ് തിരഞ്ഞെടുക്കൂ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ഈ നടപടി എടുക്കുന്നത് ആപ്പോ നിങ്ങളുടെ സ്ഥാപനമോ അനുവദിക്കുന്നില്ല"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"നാവിഗേഷൻ ട്യൂട്ടോറിയൽ ഒഴിവാക്കണോ?"</string>
diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml
index 703bf7b..8c7ae78 100644
--- a/quickstep/res/values-mn/strings.xml
+++ b/quickstep/res/values-mn/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Дэлгэцийн агшин дарах"</string>
<string name="action_split" msgid="2098009717623550676">"Хуваах"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Дэлгэцийг хуваахыг ашиглахын тулд өөр аппыг товш"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Дэлгэцийг хуваах сонголтоос гарах"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Дэлгэцийг хуваах горим ашиглах өөр апп сонгоно уу"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Энэ үйлдлийг апп эсвэл танай байгууллага зөвшөөрдөггүй"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Навигацын практик хичээлийг алгасах уу?"</string>
diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml
index 8cefec4..5bf47ce 100644
--- a/quickstep/res/values-mr/strings.xml
+++ b/quickstep/res/values-mr/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"स्क्रीनशॉट"</string>
<string name="action_split" msgid="2098009717623550676">"स्प्लिट"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिट स्क्रीन वापरण्यासाठी दुसऱ्या ॲपवर टॅप करा"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"स्प्लिट स्क्रीन निवडीतून बाहेर पडा"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"स्प्लिट स्क्रीन वापरण्यासाठी दुसरे ॲप निवडा"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"अॅप किंवा तुमच्या संस्थेद्वारे ही क्रिया करण्याची अनुमती नाही"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"नेव्हिगेशन ट्यूटोरियल वगळायचे आहे का?"</string>
diff --git a/quickstep/res/values-ms/strings.xml b/quickstep/res/values-ms/strings.xml
index 9ae478b..5e61b88 100644
--- a/quickstep/res/values-ms/strings.xml
+++ b/quickstep/res/values-ms/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Tangkapan skrin"</string>
<string name="action_split" msgid="2098009717623550676">"Pisah"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Ketik apl lain untuk menggunakan skrin pisah"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Keluar daripada pilihan skrin pisah"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pilih apl lain untuk menggunakan skrin pisah"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Tindakan ini tidak dibenarkan oleh apl atau organisasi anda"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Langkau tutorial navigasi?"</string>
diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml
index a2927d4..61b1c58 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ဖန်သားပြင်ဓာတ်ပုံ"</string>
<string name="action_split" msgid="2098009717623550676">"ခွဲထုတ်ရန်"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"မျက်နှာပြင် ခွဲ၍ပြသရန် အက်ပ်နောက်တစ်ခုကို တို့ပါ"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"မျက်နှာပြင် ခွဲ၍ပြသခြင်း ရွေးချယ်မှုမှ ထွက်ရန်"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"မျက်နှာပြင်ခွဲ၍ပြသခြင်းသုံးရန် နောက်အက်ပ်တစ်ခုရွေးပါ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ဤလုပ်ဆောင်ချက်ကို အက်ပ် သို့မဟုတ် သင်၏အဖွဲ့အစည်းက ခွင့်မပြုပါ"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"လမ်းညွှန်ခြင်း ရှင်းလင်းပို့ချချက်ကို ကျော်မလား။"</string>
diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml
index fe679e7..d4b8de9 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Skjermdump"</string>
<string name="action_split" msgid="2098009717623550676">"Del opp"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Trykk på en annen app for å bruke delt skjerm"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Avslutt valg av delt skjerm"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Velg en annen app for å bruke delt skjerm"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller organisasjonen din tillater ikke denne handlingen"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vil du hoppe over navigeringsveiledningen?"</string>
diff --git a/quickstep/res/values-ne/strings.xml b/quickstep/res/values-ne/strings.xml
index 01b79d4..b326cb4 100644
--- a/quickstep/res/values-ne/strings.xml
+++ b/quickstep/res/values-ne/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"स्क्रिनसट"</string>
<string name="action_split" msgid="2098009717623550676">"स्प्लिट गर्नुहोस्"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"स्प्लिटस्क्रिन प्रयोग गर्न अर्को एपमा ट्याप गर्नु…"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"स्प्लिट स्क्रिन मोडबाट बाहिरिनुहोस्"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"स्प्लिट स्क्रिन प्रयोग गर्न अर्को एप रोज्नुहोस्"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"यो एप वा तपाईंको सङ्गठनले यो कारबाही गर्ने अनुमति दिँदैन"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"नेभिगेसन ट्युटोरियल स्किप गर्ने हो?"</string>
diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml
index 774a66c..6a7efe5 100644
--- a/quickstep/res/values-nl/strings.xml
+++ b/quickstep/res/values-nl/strings.xml
@@ -46,7 +46,7 @@
<string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Voorspelde app: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
<string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Het apparaat draaien"</string>
<string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Draai het apparaat om de tutorial voor navigatie met gebaren af te ronden"</string>
- <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Swipe helemaal vanaf de rechter- of linkerrand"</string>
+ <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Swipe vanaf de rechter- of linkerrand"</string>
<string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Swipe vanaf de rechter- of linkerrand naar het midden van het scherm en laat los"</string>
<string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Je weet nu hoe je vanaf rechts kunt swipen om terug te gaan. Ontdek nu hoe je tussen apps schakelt."</string>
<string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Je weet nu hoe je het gebaar Terug maakt"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Splitsen"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tik op nog een app om je scherm te splitsen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Sluit de selectie voor gesplitst scherm"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Kies andere app om gesplitst scherm te gebruiken"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Deze actie wordt niet toegestaan door de app of je organisatie"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Navigatietutorial overslaan?"</string>
diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml
index ac45415..f0a5053 100644
--- a/quickstep/res/values-or/strings.xml
+++ b/quickstep/res/values-or/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ସ୍କ୍ରିନସଟ୍"</string>
<string name="action_split" msgid="2098009717623550676">"ସ୍ପ୍ଲିଟ୍"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"ସ୍ପ୍ଲିଟସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପରେ ଟାପ କର"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଚୟନରୁ ବାହାରି ଯାଆନ୍ତୁ"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ଆପ୍ କିମ୍ବା ଆପଣଙ୍କ ସଂସ୍ଥା ଦ୍ୱାରା ଏହି କାର୍ଯ୍ୟକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ନାଭିଗେସନ୍ ଟ୍ୟୁଟୋରିଆଲକୁ ବାଦ୍ ଦେବେ?"</string>
diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml
index d0f17b0..c66e26f 100644
--- a/quickstep/res/values-pa/strings.xml
+++ b/quickstep/res/values-pa/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ਸਕ੍ਰੀਨਸ਼ਾਟ"</string>
<string name="action_split" msgid="2098009717623550676">"ਸਪਲਿਟ"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨੂੰ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ \'ਤੇ ਟੈਪ ਕਰੋ"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੀ ਚੋਣ ਤੋਂ ਬਾਹਰ ਜਾਓ"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ਐਪ ਜਾਂ ਤੁਹਾਡੀ ਸੰਸਥਾ ਵੱਲੋਂ ਇਸ ਕਾਰਵਾਈ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ਕੀ ਨੈਵੀਗੇਸ਼ਨ ਟਿਊਟੋਰੀਅਲ ਨੂੰ ਛੱਡਣਾ ਹੈ?"</string>
diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml
index 5f609b2..7f0600f 100644
--- a/quickstep/res/values-pl/strings.xml
+++ b/quickstep/res/values-pl/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Zrzut ekranu"</string>
<string name="action_split" msgid="2098009717623550676">"Podziel"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Aby podzielić ekran, kliknij drugą aplikację"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Wyjdź z wyboru podzielonego ekranu"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Wybierz drugą aplikację, aby podzielić ekran"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Nie możesz wykonać tego działania, bo nie zezwala na to aplikacja lub Twoja organizacja"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Pominąć samouczek nawigacji?"</string>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index a82f29a..791a127 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Fazer captura de ecrã"</string>
<string name="action_split" msgid="2098009717623550676">"Dividir"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Toque noutra app para usar o ecrã dividido"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Saia da seleção de ecrã dividido"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolher outra app para usar o ecrã dividido"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Esta ação não é permitida pela app ou a sua entidade."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorar o tutorial de navegação?"</string>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index 8ff0a04..c943da5 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -72,7 +72,7 @@
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Mantenha a janela pressionada por mais tempo antes de soltar"</string>
<string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Deslize para cima e pare"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Você aprendeu. Para desativar os gestos, acesse as Configurações."</string>
- <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Você concluiu o gesto para trocar de app"</string>
+ <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Você concluiu o gesto para mudar de app"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Deslizar para trocar de app"</string>
<string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Para mudar de app, deslize de baixo para cima, mantenha a tela pressionada por um tempo e solte."</string>
<string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Para mudar de app, deslize de baixo para cima na tela com dois dedos, segure por um tempo e solte."</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Capturar tela"</string>
<string name="action_split" msgid="2098009717623550676">"Dividir"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Toque em outro app para usar a tela dividida"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Sair da seleção de tela dividida"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Escolha outro app para usar na tela dividida"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Essa ação não é permitida pelo app ou pela organização"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Pular o tutorial de navegação?"</string>
diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml
index 459abee..5db7859 100644
--- a/quickstep/res/values-ro/strings.xml
+++ b/quickstep/res/values-ro/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Captură de ecran"</string>
<string name="action_split" msgid="2098009717623550676">"Împărțit"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Atinge altă aplicație pentru ecranul împărțit"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ieși din selecția cu ecran împărțit"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Alege altă aplicație pentru ecranul împărțit"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Această acțiune nu este permisă de aplicație sau de organizația ta"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Omiți tutorialul de navigare?"</string>
diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml
index 2cece97..a11175e 100644
--- a/quickstep/res/values-ru/strings.xml
+++ b/quickstep/res/values-ru/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Скриншот"</string>
<string name="action_split" msgid="2098009717623550676">"Разделить"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Для разделения экрана выберите другое приложение."</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Выйдите из режима разделения экрана."</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Выберите другое приложение для разделения экрана."</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Это действие заблокировано приложением или организацией."</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Пропустить руководство по жестам?"</string>
diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml
index b8d5110..3722b2f 100644
--- a/quickstep/res/values-si/strings.xml
+++ b/quickstep/res/values-si/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"තිර රුව"</string>
<string name="action_split" msgid="2098009717623550676">"බෙදන්න"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"බෙදුම් තිරය භාවිතා කිරීමට තවත් යෙදුමක් තට්ටු කරන්න"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"බෙදීම් තිර තේරීමෙන් පිටවන්න"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"බෙදීම් තිරය භාවිතා කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"මෙම ක්රියාව යෙදුම හෝ ඔබේ සංවිධානය මගින් ඉඩ නොදේ"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"නිබන්ධනය සංචාලනය මඟ හරින්නද?"</string>
diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml
index 11b091e..aee281c 100644
--- a/quickstep/res/values-sk/strings.xml
+++ b/quickstep/res/values-sk/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Snímka obrazovky"</string>
<string name="action_split" msgid="2098009717623550676">"Rozdeliť"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Obrazovku rozdelíte klepnutím na inú aplikáciu"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ukončite výber rozdelenej obrazovky"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Na použitie rozd. obrazovky vyberte inú aplikáciu"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Aplikácia alebo vaša organizácia túto akciu nepovoľuje"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Chcete preskočiť návod na navigáciu?"</string>
diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml
index a1b2021..4787f04 100644
--- a/quickstep/res/values-sl/strings.xml
+++ b/quickstep/res/values-sl/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Posnetek zaslona"</string>
<string name="action_split" msgid="2098009717623550676">"Razdeli"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Za razdeljeni zaslon se dotaknite še 1 aplikacije"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Zapri izbiro razdeljenega zaslona"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Izberite drugo aplikacijo za uporabo razdeljenega zaslona."</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Aplikacija ali vaša organizacija ne dovoljuje tega dejanja"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Želite preskočiti vadnico za krmarjenje?"</string>
diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml
index 392c7fd..a9e8b94 100644
--- a/quickstep/res/values-sq/strings.xml
+++ b/quickstep/res/values-sq/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Pamja e ekranit"</string>
<string name="action_split" msgid="2098009717623550676">"Ndaj"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Trokit një apl. tjetër; përdor ekranin e ndarë"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Dil nga zgjedhja e ekranit të ndarë"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Ky veprim nuk lejohet nga aplikacioni ose organizata jote"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Të kapërcehet udhëzuesi i navigimit?"</string>
diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml
index cc12985..da057d8 100644
--- a/quickstep/res/values-sr/strings.xml
+++ b/quickstep/res/values-sr/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Снимак екрана"</string>
<string name="action_split" msgid="2098009717623550676">"Подели"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Додирните другу апликацију за подељени екран"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Излазак из бирања подељеног екрана"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Одаберите другу апликацију за подељени екран"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Апликација или организација не дозвољавају ову радњу"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Желите да прескочите водич за кретање?"</string>
diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml
index 6164140..31853f9 100644
--- a/quickstep/res/values-sv/strings.xml
+++ b/quickstep/res/values-sv/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Skärmbild"</string>
<string name="action_split" msgid="2098009717623550676">"Delat"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Tryck på en annan app för att använda delad skärm"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Avsluta val av delad skärm"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Välj en annan app för att använda delad skärm"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Appen eller organisationen tillåter inte den här åtgärden"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Vill du hoppa över självstudierna?"</string>
diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml
index 5dbd510..8c438f4 100644
--- a/quickstep/res/values-sw/strings.xml
+++ b/quickstep/res/values-sw/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Picha ya skrini"</string>
<string name="action_split" msgid="2098009717623550676">"Iliyogawanywa"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Gusa programu nyingine ili utumie kipengele cha kugawa skrini"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ondoka kwenye hali ya skrini iliyogawanywa"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Chagua programu nyingine ili utumie hali ya kugawa skrini"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Kitendo hiki hakiruhusiwi na programu au shirika lako"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ungependa kuruka mafunzo ya usogezaji?"</string>
diff --git a/quickstep/res/values-sw720dp/dimens.xml b/quickstep/res/values-sw720dp/dimens.xml
index 9e832bc..1caffb8 100644
--- a/quickstep/res/values-sw720dp/dimens.xml
+++ b/quickstep/res/values-sw720dp/dimens.xml
@@ -43,4 +43,7 @@
<dimen name="taskbar_app_window_threshold">100dp</dimen>
<dimen name="taskbar_home_overview_threshold">180dp</dimen>
<dimen name="taskbar_catch_up_threshold">300dp</dimen>
+
+ <!-- Taskbar swipe up threshold multipliers -->
+ <item name="taskbar_nav_threshold_mult" format="float" type="dimen">3</item>
</resources>
diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml
index 6846677..ef5bd3f 100644
--- a/quickstep/res/values-ta/strings.xml
+++ b/quickstep/res/values-ta/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ஸ்கிரீன்ஷாட்"</string>
<string name="action_split" msgid="2098009717623550676">"பிரி"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தட்டவும்"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"திரைப் பிரிப்பு தேர்வில் இருந்து வெளியேறும்"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"திரைப் பிரிப்பை பயன்படுத்த வேறு ஆப்ஸை தேர்வுசெய்க"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ஆப்ஸோ உங்கள் நிறுவனமோ இந்த செயலை அனுமதிப்பதில்லை"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"வழிகாட்டுதல் பயிற்சியைத் தவிர்க்கவா?"</string>
diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml
index ddebce9..a2adfe1 100644
--- a/quickstep/res/values-te/strings.xml
+++ b/quickstep/res/values-te/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"స్క్రీన్షాట్"</string>
<string name="action_split" msgid="2098009717623550676">"స్ప్లిట్ చేయండి"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్ను ట్యాప్ చేయండి"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"స్ప్లిట్ స్క్రీన్ ఎంపిక నుండి ఎగ్జిట్ అవ్వండి"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"స్ప్లిట్ స్క్రీన్ ఉపయోగానికి మరొక యాప్ ఎంచుకోండి"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ఈ చర్యను యాప్ గానీ, మీ సంస్థ గానీ అనుమతించవు"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"నావిగేషన్ ట్యుటోరియల్ను స్కిప్ చేయాలా?"</string>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index 2b9906e..0339046 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"ภาพหน้าจอ"</string>
<string name="action_split" msgid="2098009717623550676">"แยก"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"แตะแอปอื่นเพื่อใช้การแยกหน้าจอ"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ออกจากการเลือกโหมดแยกหน้าจอ"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"แอปหรือองค์กรของคุณไม่อนุญาตการดำเนินการนี้"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ข้ามบทแนะนำการนำทางไหม"</string>
diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml
index f1f3d3c..7bb38c2 100644
--- a/quickstep/res/values-tl/strings.xml
+++ b/quickstep/res/values-tl/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Screenshot"</string>
<string name="action_split" msgid="2098009717623550676">"Split"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Mag-tap ng ibang app para gamitin ang split screen"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Lumabas sa pagpili ng split screen"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Pumili ng ibang app para gamitin ang split screen"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Hindi pinapayagan ng app o ng iyong organisasyon ang pagkilos na ito"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Laktawan ang tutorial sa pag-navigate?"</string>
diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml
index 242ee0c..905183a 100644
--- a/quickstep/res/values-tr/strings.xml
+++ b/quickstep/res/values-tr/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Ekran görüntüsü"</string>
<string name="action_split" msgid="2098009717623550676">"Böl"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Bölünmüş ekran için başka bir uygulamaya dokunun"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Bölünmüş ekran seçiminden çıkın"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Bölünmüş ekran kullanmak için başka bir uygulama seçin"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Uygulamanız veya kuruluşunuz bu işleme izin vermiyor"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Gezinme eğitimi atlansın mı?"</string>
diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml
index 011aadb..6cbf71f 100644
--- a/quickstep/res/values-uk/strings.xml
+++ b/quickstep/res/values-uk/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Знімок екрана"</string>
<string name="action_split" msgid="2098009717623550676">"Розділити"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Щоб розділити екран, виберіть ще один додаток"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Вийти з режиму розділення екрана"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Щоб розділити екран, виберіть ще один додаток"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Ця дія заборонена додатком або адміністратором організації"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Пропустити посібник із навігації?"</string>
diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml
index 0d58d48..2392c6c 100644
--- a/quickstep/res/values-ur/strings.xml
+++ b/quickstep/res/values-ur/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"اسکرین شاٹ"</string>
<string name="action_split" msgid="2098009717623550676">"اسپلٹ"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"اسپلٹ اسکرین کا استعمال کرنے کیلئے دوسری ایپ پر تھپتھپائیں"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"اسپلٹ اسکرین کے انتخاب سے باہر نکلیں"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"ایپ یا آپ کی تنظیم کی جانب سے اس کارروائی کی اجازت نہیں ہے"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"نیویگیشن کا ٹیوٹوریل نظر انداز کریں؟"</string>
diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml
index b450c6f..da2d363 100644
--- a/quickstep/res/values-uz/strings.xml
+++ b/quickstep/res/values-uz/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Skrinshot"</string>
<string name="action_split" msgid="2098009717623550676">"Ajratish"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Ekranni ikkiga ajratish uchun boshqa ilovani bosing"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Ekranni ikkiga ajratish tanlovidan chiqish"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Ekranni ikkiga ajratish uchun boshqa ilovani tanlang"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Bu amal ilova yoki tashkilotingiz tomonidan taqiqlangan"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Navigatsiya darsi yopilsinmi?"</string>
diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml
index 2f0e0b9..dae4212 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Chụp ảnh màn hình"</string>
<string name="action_split" msgid="2098009717623550676">"Chia đôi màn hình"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Nhấn vào ứng dụng khác để chia đôi màn hình"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Thoát khỏi lựa chọn chia đôi màn hình"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Ứng dụng hoặc tổ chức của bạn không cho phép thực hiện hành động này"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Bỏ qua phần hướng dẫn thao tác?"</string>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index 5db869d..3980b21 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -72,7 +72,7 @@
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"尝试按住窗口较长时间,然后再松开手指"</string>
<string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"确保笔直向上滑动,然后停住"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"您已了解如何使用手势了。如要关闭手势,请前往“设置”。"</string>
- <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"您完成了“切换应用”手势"</string>
+ <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"您完成了应用切换手势"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"滑动即可切换应用"</string>
<string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"如需在应用之间切换,请从屏幕底部向上滑动,按住,然后松开。"</string>
<string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"如需在应用之间切换,请从屏幕底部向上滑动,按住,然后松开。"</string>
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"屏幕截图"</string>
<string name="action_split" msgid="2098009717623550676">"拆分"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"点按另一个应用即可使用分屏"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"退出分屏选择模式"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"另外选择一个应用才可使用分屏模式"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"该应用或您所在的单位不允许执行此操作"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"要跳过导航教程吗?"</string>
diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml
index 523ab65..0532ae5 100644
--- a/quickstep/res/values-zh-rHK/strings.xml
+++ b/quickstep/res/values-zh-rHK/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"螢幕截圖"</string>
<string name="action_split" msgid="2098009717623550676">"分割"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"輕按其他應用程式以使用分割螢幕"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"退出分割螢幕選取頁面"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"選擇其他應用程式才能使用分割螢幕"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"應用程式或你的機構不允許此操作"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"要略過手勢操作教學課程嗎?"</string>
diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml
index 06ff8e0..214fcfd 100644
--- a/quickstep/res/values-zh-rTW/strings.xml
+++ b/quickstep/res/values-zh-rTW/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"螢幕截圖"</string>
<string name="action_split" msgid="2098009717623550676">"分割"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"輕觸另一個應用程式即可使用分割畫面"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"退出分割畫面選擇器"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"必須選擇另一個應用程式才能使用分割畫面"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"這個應用程式或貴機構不允許執行這個動作"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"要略過手勢操作教學課程嗎?"</string>
diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml
index fc9385f..9655a44 100644
--- a/quickstep/res/values-zu/strings.xml
+++ b/quickstep/res/values-zu/strings.xml
@@ -95,6 +95,7 @@
<string name="action_screenshot" msgid="8171125848358142917">"Isithombe-skrini"</string>
<string name="action_split" msgid="2098009717623550676">"Hlukanisa"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Thepha enye i-app ukuze usebenzise isikrini sokuhlukanisa"</string>
+ <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Phuma ekukhetheni ukuhlukaniswa kwesikrini"</string>
<string name="toast_split_app_unsupported" msgid="2360229567007828914">"Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini"</string>
<string name="blocked_by_policy" msgid="2071401072261365546">"Lesi senzo asivunyelwanga uhlelo lokusebenza noma inhlangano yakho"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Yeqa isifundo sokuzulazula?"</string>
diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml
index fb51919..7288774 100644
--- a/quickstep/res/values/attrs.xml
+++ b/quickstep/res/values/attrs.xml
@@ -26,7 +26,8 @@
-->
<declare-styleable name="TaskView">
<!-- Border color for a keyboard quick switch task views -->
- <attr name="borderColor" format="color" />
+ <attr name="focusBorderColor" format="color" />
+ <attr name="hoverBorderColor" format="color" />
</declare-styleable>
<!--
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index e4f6555..b024418 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -38,6 +38,8 @@
<dimen name="task_thumbnail_icon_size">48dp</dimen>
<!-- The icon size for the focused task, placed in center of touch target -->
<dimen name="task_thumbnail_icon_drawable_size">44dp</dimen>
+ <!-- The border width shown when task is hovered -->
+ <dimen name="task_hover_border_width">4dp</dimen>
<!-- The space under the focused task icon -->
<dimen name="overview_task_margin">16dp</dimen>
<!-- The horizontal space between tasks -->
@@ -326,6 +328,12 @@
<!-- Taskbar swipe down threshold -->
<dimen name="taskbar_to_nav_threshold">24dp</dimen>
+ <!-- Taskbar swipe up threshold multipliers -->
+ <item name="taskbar_nav_threshold_mult" format="float" type="dimen">4.5</item>
+ <item name="taskbar_app_window_threshold_mult" format="float" type="dimen">10</item>
+ <item name="taskbar_home_overview_threshold_mult" format="float" type="dimen">18</item>
+ <item name="taskbar_catch_up_threshold_mult" format="float" type="dimen">30</item>
+
<!-- Taskbar 3 button spacing -->
<dimen name="taskbar_button_space_inbetween">24dp</dimen>
<dimen name="taskbar_button_space_inbetween_phone">40dp</dimen>
@@ -359,6 +367,7 @@
<dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
<dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
<dimen name="bubblebar_pointer_size">8dp</dimen>
+ <dimen name="bubblebar_elevation">1dp</dimen>
<dimen name="bubblebar_icon_size">50dp</dimen>
<dimen name="bubblebar_badge_size">24dp</dimen>
@@ -367,6 +376,13 @@
<dimen name="bubblebar_icon_spacing">3dp</dimen>
<dimen name="bubblebar_icon_elevation">1dp</dimen>
+ <!-- Bubble bar dismiss view -->
+ <dimen name="bubblebar_dismiss_target_size">96dp</dimen>
+ <dimen name="bubblebar_dismiss_target_small_size">60dp</dimen>
+ <dimen name="bubblebar_dismiss_target_icon_size">24dp</dimen>
+ <dimen name="bubblebar_dismiss_target_bottom_margin">50dp</dimen>
+ <dimen name="bubblebar_dismiss_floating_gradient_height">548dp</dimen>
+
<!-- Launcher splash screen -->
<!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
<!-- starting_surface_exit_animation_window_shift_length -->
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 28bd701..f64b5cf 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -15,23 +15,16 @@
*/
package com.android.launcher3;
-import android.animation.AnimatorSet;
import android.annotation.TargetApi;
import android.os.Build;
-import android.os.CancellationSignal;
-import android.view.RemoteAnimationTarget;
-import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.RemoteAnimationProvider;
import java.util.function.BiPredicate;
@TargetApi(Build.VERSION_CODES.P)
public class LauncherInitListener extends ActivityInitListener<Launcher> {
- private RemoteAnimationProvider mRemoteAnimationProvider;
-
/**
* @param onInitListener a callback made when the activity is initialized. The callback should
* return true to continue receiving callbacks (ie. for if the activity is
@@ -43,37 +36,7 @@
@Override
public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
- if (mRemoteAnimationProvider != null) {
- QuickstepTransitionManager appTransitionManager =
- ((QuickstepLauncher) launcher).getAppTransitionManager();
-
- // Set a one-time animation provider. After the first call, this will get cleared.
- // TODO: Probably also check the intended target id.
- CancellationSignal cancellationSignal = new CancellationSignal();
- appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
- @Override
- public AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets,
- RemoteAnimationTarget[] wallpaperTargets) {
-
- // On the first call clear the reference.
- cancellationSignal.cancel();
- RemoteAnimationProvider provider = mRemoteAnimationProvider;
- mRemoteAnimationProvider = null;
-
- if (provider != null && launcher.getStateManager().getState().overviewUi) {
- return provider.createWindowAnimation(appTargets, wallpaperTargets);
- }
- return null;
- }
- }, cancellationSignal);
- }
launcher.deferOverlayCallbacksUntilNextResumeOrStop();
return super.handleInit(launcher, alreadyOnHome);
}
-
- @Override
- public void unregister() {
- mRemoteAnimationProvider = null;
- super.unregister();
- }
}
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 153c1ac..c6c4dde 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -60,6 +60,8 @@
import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
+import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME;
+import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME_FALLBACK;
import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
@@ -82,7 +84,6 @@
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.os.CancellationSignal;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -140,7 +141,6 @@
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig;
import com.android.quickstep.util.RectFSpringAnim.TaskbarHotseatSpringConfig;
-import com.android.quickstep.util.RemoteAnimationProvider;
import com.android.quickstep.util.StaggeredWorkspaceAnim;
import com.android.quickstep.util.SurfaceTransaction;
import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
@@ -158,6 +158,7 @@
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import com.android.wm.shell.startingsurface.IStartingWindowListener;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -182,7 +183,7 @@
private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
"android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
- private static final long APP_LAUNCH_DURATION = 500;
+ public static final long APP_LAUNCH_DURATION = 500;
private static final long APP_LAUNCH_ALPHA_DURATION = 50;
private static final long APP_LAUNCH_ALPHA_START_DELAY = 25;
@@ -228,7 +229,6 @@
private DeviceProfile mDeviceProfile;
- private RemoteAnimationProvider mRemoteAnimationProvider;
// Strong refs to runners which are cleared when the launcher activity is destroyed
private RemoteAnimationFactory mWallpaperOpenRunner;
private RemoteAnimationFactory mAppLaunchRunner;
@@ -285,7 +285,7 @@
mOpeningXInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.app_open_x);
mOpeningInterpolator = AnimationUtils.loadInterpolator(context,
- R.interpolator.three_point_fast_out_extra_slow_in);
+ R.interpolator.emphasized_interpolator);
}
@Override
@@ -467,15 +467,8 @@
return bounds;
}
- public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider,
- CancellationSignal cancellationSignal) {
- mRemoteAnimationProvider = animationProvider;
- cancellationSignal.setOnCancelListener(() -> {
- if (animationProvider == mRemoteAnimationProvider) {
- mRemoteAnimationProvider = null;
- }
- });
- }
+ /** Dump debug logs to bug report. */
+ public void dump(@NonNull String prefix, @NonNull PrintWriter printWriter) {}
/**
* Content is everything on screen except the background and the floating view (if any).
@@ -1046,7 +1039,7 @@
boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW
&& BlurUtils.supportsBlursOnWindows();
- MyDepthController depthController = new MyDepthController(mLauncher);
+ LaunchDepthController depthController = new LaunchDepthController(mLauncher);
ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController.stateDepth,
MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(mLauncher))
.setDuration(APP_LAUNCH_DURATION);
@@ -1230,7 +1223,7 @@
* ie. pressing home, swiping up from nav bar.
*/
RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) {
- return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock);
+ return new WallpaperOpenLauncherAnimationRunner(fromUnlock);
}
/**
@@ -1348,7 +1341,7 @@
/**
* Closing animator that animates the window into its final location on the workspace.
*/
- private RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation,
+ protected RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation,
RemoteAnimationTarget[] targets, View launcherView, PointF velocityPxPerS,
RectF closingWindowStartRect, float startWindowCornerRadius) {
FloatingIconView floatingIconView = null;
@@ -1588,89 +1581,80 @@
RectF startRect,
float startWindowCornerRadius,
boolean fromPredictiveBack) {
- AnimatorSet anim = null;
+ AnimatorSet anim = new AnimatorSet();
RectFSpringAnim rectFSpringAnim = null;
- RemoteAnimationProvider provider = mRemoteAnimationProvider;
- if (provider != null) {
- anim = provider.createWindowAnimation(appTargets, wallpaperTargets);
+ final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
+ || launcherIsATargetWithMode(appTargets, MODE_OPENING);
+
+ View launcherView = findLauncherView(appTargets);
+ boolean playFallBackAnimation = (launcherView == null
+ && launcherIsForceInvisibleOrOpening)
+ || mLauncher.getWorkspace().isOverlayShown()
+ || shouldPlayFallbackClosingAnimation(appTargets);
+
+ boolean playWorkspaceReveal = true;
+ boolean skipAllAppsScale = false;
+ if (fromUnlock) {
+ anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
+ } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
+ && !playFallBackAnimation) {
+ // Use a fixed velocity to start the animation.
+ float velocityPxPerS = DynamicResource.provider(mLauncher)
+ .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
+ PointF velocity = new PointF(0, -velocityPxPerS);
+ rectFSpringAnim = getClosingWindowAnimators(
+ anim, appTargets, launcherView, velocity, startRect,
+ startWindowCornerRadius);
+ if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+ // Skip scaling all apps, otherwise FloatingIconView will get wrong
+ // layout bounds.
+ skipAllAppsScale = true;
+ } else if (!fromPredictiveBack) {
+ anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
+ true /* animateOverviewScrim */, launcherView).getAnimators());
+
+ if (!areAllTargetsTranslucent(appTargets)) {
+ anim.play(ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
+ MULTI_PROPERTY_VALUE,
+ BACKGROUND_APP.getDepth(mLauncher), NORMAL.getDepth(mLauncher)));
+ }
+
+ // We play StaggeredWorkspaceAnim as a part of the closing window animation.
+ playWorkspaceReveal = false;
+ }
+ } else {
+ anim.play(getFallbackClosingWindowAnimators(appTargets));
}
- if (anim == null) {
- anim = new AnimatorSet();
+ // Normally, we run the launcher content animation when we are transitioning
+ // home, but if home is already visible, then we don't want to animate the
+ // contents of launcher unless we know that we are animating home as a result
+ // of the home button press with quickstep, which will result in launcher being
+ // started on touch down, prior to the animation home (and won't be in the
+ // targets list because it is already visible). In that case, we force
+ // invisibility on touch down, and only reset it after the animation to home
+ // is initialized.
+ if (launcherIsForceInvisibleOrOpening) {
+ addCujInstrumentation(anim, playFallBackAnimation
+ ? CUJ_APP_CLOSE_TO_HOME_FALLBACK : CUJ_APP_CLOSE_TO_HOME);
+ // Only register the content animation for cancellation when state changes
+ mLauncher.getStateManager().setCurrentAnimation(anim);
- final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
- || launcherIsATargetWithMode(appTargets, MODE_OPENING);
-
- View launcherView = findLauncherView(appTargets);
- boolean playFallBackAnimation = (launcherView == null
- && launcherIsForceInvisibleOrOpening)
- || mLauncher.getWorkspace().isOverlayShown()
- || shouldPlayFallbackClosingAnimation(appTargets);
-
- boolean playWorkspaceReveal = true;
- boolean skipAllAppsScale = false;
- if (fromUnlock) {
- anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
- } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
- && !playFallBackAnimation) {
- // Use a fixed velocity to start the animation.
- float velocityPxPerS = DynamicResource.provider(mLauncher)
- .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
- PointF velocity = new PointF(0, -velocityPxPerS);
- rectFSpringAnim = getClosingWindowAnimators(
- anim, appTargets, launcherView, velocity, startRect,
- startWindowCornerRadius);
- if (mLauncher.isInState(LauncherState.ALL_APPS)) {
- // Skip scaling all apps, otherwise FloatingIconView will get wrong
- // layout bounds.
- skipAllAppsScale = true;
- } else if (!fromPredictiveBack) {
- anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
- true /* animateOverviewScrim */, launcherView).getAnimators());
-
- if (!areAllTargetsTranslucent(appTargets)) {
- anim.play(ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
- MULTI_PROPERTY_VALUE,
- BACKGROUND_APP.getDepth(mLauncher), NORMAL.getDepth(mLauncher)));
+ if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+ Pair<AnimatorSet, Runnable> contentAnimator =
+ getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY,
+ skipAllAppsScale);
+ anim.play(contentAnimator.first);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ contentAnimator.second.run();
}
-
- // We play StaggeredWorkspaceAnim as a part of the closing window animation.
- playWorkspaceReveal = false;
- }
+ });
} else {
- anim.play(getFallbackClosingWindowAnimators(appTargets));
- }
-
- // Normally, we run the launcher content animation when we are transitioning
- // home, but if home is already visible, then we don't want to animate the
- // contents of launcher unless we know that we are animating home as a result
- // of the home button press with quickstep, which will result in launcher being
- // started on touch down, prior to the animation home (and won't be in the
- // targets list because it is already visible). In that case, we force
- // invisibility on touch down, and only reset it after the animation to home
- // is initialized.
- if (launcherIsForceInvisibleOrOpening) {
- addCujInstrumentation(
- anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
- // Only register the content animation for cancellation when state changes
- mLauncher.getStateManager().setCurrentAnimation(anim);
-
- if (mLauncher.isInState(LauncherState.ALL_APPS)) {
- Pair<AnimatorSet, Runnable> contentAnimator =
- getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY,
- skipAllAppsScale);
- anim.play(contentAnimator.first);
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- contentAnimator.second.run();
- }
- });
- } else {
- if (playWorkspaceReveal) {
- anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
- }
+ if (playWorkspaceReveal) {
+ anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
}
}
}
@@ -1683,11 +1667,9 @@
*/
protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory {
- private final Handler mHandler;
private final boolean mFromUnlock;
- public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean fromUnlock) {
- mHandler = handler;
+ public WallpaperOpenLauncherAnimationRunner(boolean fromUnlock) {
mFromUnlock = fromUnlock;
}
@@ -2047,11 +2029,14 @@
}
}
- private static class MyDepthController extends DepthController {
- MyDepthController(Launcher l) {
- super(l);
+ private static class LaunchDepthController extends DepthController {
+ LaunchDepthController(QuickstepLauncher launcher) {
+ super(launcher);
setCrossWindowBlursEnabled(
CrossWindowBlurListeners.getInstance().isCrossWindowBlurEnabled());
+ // Make sure that the starting value matches the current depth set by the main
+ // controller.
+ stateDepth.setValue(launcher.getDepthController().stateDepth.getValue());
}
}
}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
index bd47923..db225be 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -19,7 +19,6 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN;
-import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
@@ -30,7 +29,6 @@
import android.widget.LinearLayout;
import android.widget.TextView;
-import com.android.app.animation.Interpolators;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
@@ -160,14 +158,11 @@
}
private void animateOpen() {
- if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
return;
}
mIsOpen = true;
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- mOpenCloseAnimator.start();
+ setUpDefaultOpenAnimation().start();
}
@Override
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index 85d0ab5..619bef2 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -114,8 +114,7 @@
WorkspaceItemInfo dragItem = new WorkspaceItemInfo((WorkspaceItemInfo) v.getTag());
v.setVisibility(View.INVISIBLE);
mLauncher.getWorkspace().beginDragShared(
- v, null, this, dragItem, new DragPreviewProvider(v),
- mLauncher.getDefaultWorkspaceDragOptions());
+ v, null, this, dragItem, new DragPreviewProvider(v), new DragOptions());
return true;
};
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index d379d6d..882682d 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -193,5 +193,6 @@
writer.println(prefix + "\tmIgnoreStateChangesDuringMultiWindowAnimation="
+ mIgnoreStateChangesDuringMultiWindowAnimation);
writer.println(prefix + "\tmPauseBlurs=" + mPauseBlurs);
+ writer.println(prefix + "\tmWaitingOnSurfaceValidity=" + mWaitingOnSurfaceValidity);
}
}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 7283a18..ecf483c 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -27,6 +27,7 @@
import com.android.launcher3.LauncherState;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.quickstep.GestureState;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.views.DesktopAppSelectView;
import com.android.wm.shell.desktopmode.IDesktopTaskListener;
@@ -39,7 +40,8 @@
private static final String TAG = "DesktopVisController";
private static final boolean DEBUG = false;
-
+ private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_stashing", false);
private final Launcher mLauncher;
private boolean mFreeformTasksVisible;
@@ -73,6 +75,9 @@
@Override
public void onStashedChanged(int displayId, boolean stashed) {
+ if (!IS_STASHING_ENABLED) {
+ return;
+ }
MAIN_EXECUTOR.execute(() -> {
if (displayId == mLauncher.getDisplayId()) {
if (DEBUG) {
@@ -166,20 +171,40 @@
/**
* Whether recents gesture is currently in progress.
*/
- public boolean isGestureInProgress() {
+ public boolean isRecentsGestureInProgress() {
return mGestureInProgress;
}
/**
- * Sets whether recents gesture is in progress.
+ * Notify controller that recents gesture has started.
*/
- public void setGestureInProgress(boolean gestureInProgress) {
- if (DEBUG) {
- Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress);
- }
+ public void setRecentsGestureStart() {
if (!isDesktopModeSupported()) {
return;
}
+ setRecentsGestureInProgress(true);
+ }
+
+ /**
+ * Notify controller that recents gesture finished with the given
+ * {@link com.android.quickstep.GestureState.GestureEndTarget}
+ */
+ public void setRecentsGestureEnd(@Nullable GestureState.GestureEndTarget endTarget) {
+ if (!isDesktopModeSupported()) {
+ return;
+ }
+ setRecentsGestureInProgress(false);
+
+ if (endTarget == null) {
+ // Gesture did not result in a new end target. Ensure launchers gets paused again.
+ markLauncherPaused();
+ }
+ }
+
+ private void setRecentsGestureInProgress(boolean gestureInProgress) {
+ if (DEBUG) {
+ Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress);
+ }
if (gestureInProgress != mGestureInProgress) {
mGestureInProgress = gestureInProgress;
}
@@ -189,7 +214,7 @@
* Handle launcher moving to home due to home gesture or home button press.
*/
public void onHomeActionTriggered() {
- if (areFreeformTasksVisible()) {
+ if (IS_STASHING_ENABLED && areFreeformTasksVisible()) {
SystemUiProxy.INSTANCE.get(mLauncher).stashDesktopApps(mLauncher.getDisplayId());
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 8a11b57..3e1a6ae 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -80,7 +80,7 @@
setWillNotDraw(false);
mBorderColor = ta.getColor(
- R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR);
+ R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR);
ta.recycle();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 1705f11..fcd8c80 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -736,6 +736,7 @@
dp, mNavButtonsView, res, isInKidsMode, isInSetup, isThreeButtonNav,
TaskbarManager.isPhoneMode(dp), mDisplayController.getInfo().rotation);
navButtonLayoutter.layoutButtons(dp, isContextualButtonShowing());
+ updateNavButtonColor();
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 42cb290..9fe0c00 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -90,6 +90,8 @@
import com.android.launcher3.taskbar.bubbles.BubbleBarView;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
+import com.android.launcher3.taskbar.bubbles.BubbleDragController;
import com.android.launcher3.taskbar.bubbles.BubbleStashController;
import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
@@ -117,6 +119,7 @@
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
import java.io.PrintWriter;
+import java.util.Collections;
import java.util.Optional;
/**
@@ -144,7 +147,7 @@
private int mLastRequestedNonFullscreenHeight;
private NavigationMode mNavMode;
- private final boolean mImeDrawsImeNavBar;
+ private boolean mImeDrawsImeNavBar;
private final ViewCache mViewCache = new ViewCache();
private final boolean mIsSafeModeEnabled;
@@ -216,7 +219,9 @@
new BubbleBarController(this, bubbleBarView),
new BubbleBarViewController(this, bubbleBarView),
new BubbleStashController(this),
- new BubbleStashedHandleViewController(this, bubbleHandleView)));
+ new BubbleStashedHandleViewController(this, bubbleHandleView),
+ new BubbleDragController(this),
+ new BubbleDismissController(this, mDragLayer)));
}
// Construct controllers.
@@ -295,6 +300,7 @@
public void init(@NonNull TaskbarSharedState sharedState) {
+ mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false);
mLastRequestedNonFullscreenHeight = getDefaultTaskbarWindowHeight();
mWindowLayoutParams = createAllWindowParams();
@@ -329,6 +335,11 @@
mControllers.taskbarStashController.showTaskbarFromBroadcast();
}
+ /** Toggles Taskbar All Apps overlay. */
+ public void toggleAllApps() {
+ mControllers.taskbarAllAppsController.toggle();
+ }
+
@Override
public DeviceProfile getDeviceProfile() {
return mDeviceProfile;
@@ -992,9 +1003,10 @@
if (recents == null) {
return;
}
- recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
- info.getComponentKey(),
- foundTask -> {
+ recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
+ Collections.singletonList(info.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
TaskView foundTaskView =
recents.getTaskViewByTaskId(foundTask.key.id);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index f6de926..e521154 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.RectF;
+import android.media.permission.SafeCloseable;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.KeyEvent;
@@ -31,6 +32,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.app.viewcapture.SettingsAwareViewCapture;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -68,6 +70,7 @@
// Initialized in init.
private TaskbarDragLayerController.TaskbarDragLayerCallbacks mControllerCallbacks;
+ private SafeCloseable mViewCaptureCloseable;
private float mTaskbarBackgroundOffset;
@@ -128,12 +131,14 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnComputeInternalInsetsListener(mTaskbarInsetsComputer);
+ mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+ .startCapture(getRootView(), ".Taskbar");
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
-
+ mViewCaptureCloseable.close();
onDestroy(true);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index 363f915..c3ec1e5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -35,6 +35,8 @@
import android.view.MotionEvent;
import android.view.View;
+import androidx.annotation.VisibleForTesting;
+
import com.android.app.animation.Interpolators;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
@@ -49,7 +51,7 @@
*/
public class TaskbarHoverToolTipController implements View.OnHoverListener {
- private static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400;
+ @VisibleForTesting protected static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400;
private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 300;
private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150;
@@ -145,7 +147,6 @@
}
private void startRevealHoverToolTip() {
- mActivity.setTaskbarWindowFullscreen(true);
mHoverToolTipHandler.postDelayed(mRevealHoverToolTipRunnable,
HOVER_TOOL_TIP_REVEAL_START_DELAY);
}
@@ -157,6 +158,7 @@
if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
return;
}
+ mActivity.setTaskbarWindowFullscreen(true);
Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
mTaskbarView.getTop(), /* shouldAutoClose= */ false);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 68ea475..a935bac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -94,26 +94,12 @@
} else {
0
}
- if (context.isGestureNav) {
- windowLayoutParams.providedInsets =
- arrayOf(
- InsetsFrameProvider(insetsOwner, 0, navigationBars())
- .setFlags(
- FLAG_SUPPRESS_SCRIM or insetsRoundedCornerFlag,
- FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER
- ),
- InsetsFrameProvider(insetsOwner, 0, tappableElement()),
- InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
- InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
- .setSource(SOURCE_DISPLAY),
- InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
- .setSource(SOURCE_DISPLAY)
- )
- } else {
- windowLayoutParams.providedInsets = getButtonNavInsets(insetsRoundedCornerFlag)
+
+ windowLayoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag)
+ if (!context.isGestureNav) {
if (windowLayoutParams.paramsForRotation != null) {
for (layoutParams in windowLayoutParams.paramsForRotation) {
- layoutParams.providedInsets = getButtonNavInsets(insetsRoundedCornerFlag)
+ layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag)
}
}
}
@@ -165,15 +151,28 @@
context.notifyUpdateLayoutParams()
}
- private fun getButtonNavInsets(insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> {
+ /**
+ * The inset types and number of insets provided have to match for both gesture nav and button
+ * nav. The values and the order of the elements in array are allowed to differ.
+ * Reason being WM does not allow types and number of insets changing for a given window once it
+ * is added into the hierarchy for performance reasons.
+ */
+ private fun getProvidedInsets(insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> {
+ val navBarsFlag =
+ (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
return arrayOf(
- InsetsFrameProvider(insetsOwner, 0, navigationBars())
+ InsetsFrameProvider(insetsOwner, 0, navigationBars())
.setFlags(
- insetsRoundedCornerFlag,
- (FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER)
+ navBarsFlag,
+ FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER
),
- InsetsFrameProvider(insetsOwner, 0, tappableElement()),
- InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()))
+ InsetsFrameProvider(insetsOwner, 0, tappableElement()),
+ InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
+ InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
+ .setSource(SOURCE_DISPLAY),
+ InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
+ .setSource(SOURCE_DISPLAY)
+ )
}
private fun setProviderInsets(provider: InsetsFrameProvider, gravity: Int) {
@@ -181,47 +180,45 @@
val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
val res = context.resources
if (provider.type == navigationBars() || provider.type == mandatorySystemGestures()) {
- provider.insetsSize = getInsetsByNavMode(contentHeight, gravity)
+ provider.insetsSize = getInsetsForGravity(contentHeight, gravity)
} else if (provider.type == tappableElement()) {
- provider.insetsSize = getInsetsByNavMode(tappableHeight, gravity)
+ provider.insetsSize = getInsetsForGravity(tappableHeight, gravity)
} else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) {
- provider.insetsSize =
- Insets.of(
- gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res),
- 0,
- 0,
- 0
- )
+ val leftIndexInset =
+ if (context.isThreeButtonNav) 0
+ else gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res)
+ provider.insetsSize = Insets.of(leftIndexInset, 0, 0, 0)
} else if (provider.type == systemGestures() && provider.index == INDEX_RIGHT) {
- provider.insetsSize =
- Insets.of(
- 0,
- 0,
- gestureNavSettingsObserver.getRightSensitivityForCallingUser(res),
- 0
- )
+ val rightIndexInset =
+ if (context.isThreeButtonNav) 0
+ else gestureNavSettingsObserver.getRightSensitivityForCallingUser(res)
+ provider.insetsSize = Insets.of(0, 0, rightIndexInset, 0)
}
- val imeInsetsSize = getInsetsByNavMode(taskbarHeightForIme, gravity)
- val insetsSizeOverride =
+
+ val imeInsetsSize = getInsetsForGravity(taskbarHeightForIme, gravity)
+ val imeInsetsSizeOverride =
arrayOf(
InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
)
// Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
- val visInsetsSizeForGestureNavTappableElement = getInsetsByNavMode(0, gravity)
- val insetsSizeOverrideForGestureNavTappableElement =
+ val visInsetsSizeForTappableElement =
+ if (context.isGestureNav) getInsetsForGravity(0, gravity)
+ else getInsetsForGravity(tappableHeight, gravity)
+ val insetsSizeOverrideForTappableElement =
arrayOf(
InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
InsetsFrameProvider.InsetsSizeOverride(
TYPE_VOICE_INTERACTION,
- visInsetsSizeForGestureNavTappableElement
+ visInsetsSizeForTappableElement
),
)
- if (context.isGestureNav && provider.type == tappableElement()) {
- provider.insetsSizeOverrides = insetsSizeOverrideForGestureNavTappableElement
+ if ((context.isGestureNav || TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW)
+ && provider.type == tappableElement()) {
+ provider.insetsSizeOverrides = insetsSizeOverrideForTappableElement
} else if (provider.type != systemGestures()) {
// We only override insets at the bottom of the screen
- provider.insetsSizeOverrides = insetsSizeOverride
+ provider.insetsSizeOverrides = imeInsetsSizeOverride
}
}
@@ -229,14 +226,14 @@
* @return [Insets] where the [inset] is either used as a bottom inset or
* right/left inset if using 3 button nav
*/
- private fun getInsetsByNavMode(inset: Int, gravity: Int): Insets {
- if ((gravity and Gravity.BOTTOM) != 0) {
+ private fun getInsetsForGravity(inset: Int, gravity: Int): Insets {
+ if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
// Taskbar or portrait phone mode
return Insets.of(0, 0, 0, inset)
}
// TODO(b/230394142): seascape
- val isSeascape = (gravity and Gravity.START) != 0
+ val isSeascape = (gravity and Gravity.START) == Gravity.START
val leftInset = if (isSeascape) inset else 0
val rightInset = if (isSeascape) 0 else inset
return Insets.of(leftInset , 0, rightInset, 0)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 296e0db..90f7bea 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -204,7 +204,7 @@
updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false);
// TODO(b/279514548) Cleans up bad state that can occur when user interacts with
// taskbar on top of transparent activity.
- if (finalState == LauncherState.NORMAL && mLauncher.isResumed()) {
+ if (finalState == LauncherState.NORMAL && mLauncher.hasBeenResumed()) {
updateStateForFlag(FLAG_RESUMED, true);
}
applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 1809d40..c423fb3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -22,6 +22,7 @@
import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY;
+import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.util.DisplayController.TASKBAR_NOT_DESTROYED_TAG;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
@@ -40,6 +41,7 @@
import android.net.Uri;
import android.os.Handler;
import android.os.SystemProperties;
+import android.os.Trace;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
@@ -180,6 +182,8 @@
@Override
public void onConfigurationChanged(Configuration newConfig) {
+ Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
+ "onConfigurationChanged: " + newConfig);
debugWhyTaskbarNotDestroyed(
"TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
DeviceProfile dp = mUserUnlocked
@@ -268,6 +272,25 @@
}
/**
+ * Toggles All Apps for Taskbar or Launcher depending on the current state.
+ *
+ * @param homeAllAppsIntent Intent used if Taskbar is not enabled or Launcher is resumed.
+ */
+ public void toggleAllApps(Intent homeAllAppsIntent) {
+ if (mTaskbarActivityContext == null) {
+ mContext.startActivity(homeAllAppsIntent);
+ return;
+ }
+
+ if (mActivity != null && mActivity.isResumed() && !mActivity.isInState(OVERVIEW)) {
+ mContext.startActivity(homeAllAppsIntent);
+ return;
+ }
+
+ mTaskbarActivityContext.toggleAllApps();
+ }
+
+ /**
* Displays a frame of the first Launcher reveal animation.
*
* This should be used to run a first Launcher reveal animation whose progress matches a swipe
@@ -347,38 +370,44 @@
*/
@VisibleForTesting
public void recreateTaskbar() {
- DeviceProfile dp = mUserUnlocked ?
+ Trace.beginSection("recreateTaskbar");
+ try {
+ DeviceProfile dp = mUserUnlocked ?
LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
- destroyExistingTaskbar();
+ destroyExistingTaskbar();
- boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
- debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
+ debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
+ " FLAG_HIDE_NAVBAR_WINDOW=" + FLAG_HIDE_NAVBAR_WINDOW
+ " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
- if (!isTaskbarEnabled) {
- SystemUiProxy.INSTANCE.get(mContext)
+ if (!isTaskbarEnabled) {
+ SystemUiProxy.INSTANCE.get(mContext)
.notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
- return;
- }
+ return;
+ }
- if (mTaskbarActivityContext == null) {
- mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp, mNavButtonController,
+ if (mTaskbarActivityContext == null) {
+ mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp,
+ mNavButtonController,
mUnfoldProgressProvider);
- } else {
- mTaskbarActivityContext.updateDeviceProfile(dp);
- }
- mTaskbarActivityContext.init(mSharedState);
+ } else {
+ mTaskbarActivityContext.updateDeviceProfile(dp);
+ }
+ mTaskbarActivityContext.init(mSharedState);
- if (mActivity != null) {
- mTaskbarActivityContext.setUIController(
+ if (mActivity != null) {
+ mTaskbarActivityContext.setUIController(
createTaskbarUIControllerForActivity(mActivity));
- }
+ }
- // We to wait until user unlocks the device to attach listener.
- LauncherPrefs.get(mContext).addListener(mTaskbarPinningPreferenceChangeListener,
+ // We to wait until user unlocks the device to attach listener.
+ LauncherPrefs.get(mContext).addListener(mTaskbarPinningPreferenceChangeListener,
TASKBAR_PINNING);
+ } finally {
+ Trace.endSection();
+ }
}
public void onSystemUiFlagsChanged(int systemUiStateFlags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 5e37cf4..b5b453b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -338,7 +338,6 @@
// us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
updateStateForFlag(FLAG_IN_APP, true);
applyState(/* duration = */ 0);
-
notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp());
}
@@ -675,7 +674,10 @@
.setDuration(duration));
mAnimator.play(mTaskbarImeBgAlpha.animateToValue(
hasAnyFlag(FLAG_STASHED_IN_APP_IME) ? 0 : 1).setDuration(duration));
- mAnimator.addListener(AnimatorListeners.forEndCallback(() -> mAnimator = null));
+ mAnimator.addListener(AnimatorListeners.forEndCallback(() -> {
+ mAnimator = null;
+ mIsStashed = isStashed;
+ }));
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
new file mode 100644
index 0000000..5b6fbef
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static com.android.launcher3.Utilities.dpToPx;
+import static com.android.launcher3.Utilities.dpiFromPx;
+
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+
+import androidx.core.content.res.ResourcesCompat;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+
+/**
+ * Utility class that contains the different taskbar thresholds logic.
+ */
+public class TaskbarThresholdUtils {
+
+ // We divide the screen into this many parts, and use the result to scale the thresholds to
+ // any size device. Note that this value was calculated arbitrarily by using two tablet devices
+ // as data points.
+ private static final float SCREEN_UNITS = 1 / 80f;
+
+ private static int getThreshold(Resources r, DeviceProfile dp, int thresholdDimen,
+ int multiplierDimen) {
+ if (!FeatureFlags.ENABLE_DYNAMIC_TASKBAR_THRESHOLDS.get()) {
+ return r.getDimensionPixelSize(thresholdDimen);
+ }
+
+ float landscapeScreenHeight = dp.isLandscape ? dp.heightPx : dp.widthPx;
+ float screenPart = (landscapeScreenHeight * SCREEN_UNITS);
+ float defaultDp = dpiFromPx(screenPart, DisplayMetrics.DENSITY_DEVICE_STABLE);
+ float thisDp = dpToPx(defaultDp);
+ float multiplier = ResourcesCompat.getFloat(r, multiplierDimen);
+ float value = (thisDp) * multiplier;
+
+ return Math.round(value);
+ }
+
+ /**
+ * Returns the threshold that determines if we should show taskbar.
+ */
+ public static int getFromNavThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_from_nav_threshold,
+ R.dimen.taskbar_nav_threshold_mult);
+ }
+
+ /**
+ * Returns the threshold that we start moving the app window.
+ */
+ public static int getAppWindowThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_app_window_threshold,
+ R.dimen.taskbar_app_window_threshold_mult);
+ }
+
+ /**
+ * Returns the threshold for whether we land in home or overview.
+ */
+ public static int getHomeOverviewThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_home_overview_threshold,
+ R.dimen.taskbar_home_overview_threshold_mult);
+ }
+
+ /**
+ * Returns the threshold that we use to allow swipe to catch up to finger.
+ */
+ public static int getCatchUpThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_catch_up_threshold,
+ R.dimen.taskbar_catch_up_threshold_mult);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 7154731..6fad655 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -40,8 +40,11 @@
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
+import com.android.systemui.shared.recents.model.Task;
import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.stream.Stream;
/**
@@ -204,9 +207,10 @@
return;
}
- recentsView.getSplitSelectController().findLastActiveTaskAndRunCallback(
- splitSelectSource.itemInfo.getComponentKey(),
- foundTask -> {
+ recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback(
+ Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
splitSelectSource.alreadyRunningTaskId = foundTask == null
? INVALID_TASK_ID
: foundTask.key.id;
@@ -221,9 +225,10 @@
*/
public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) {
RecentsView recents = getRecentsView();
- recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
- info.getComponentKey(),
- foundTask -> {
+ recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
+ Collections.singletonList(info.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
// TODO (b/266482558): This additional null check is needed because there
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index 3e1fef9..544f9bf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -164,7 +164,7 @@
cleanUpOverlay();
});
TaskbarAllAppsViewController viewController = new TaskbarAllAppsViewController(
- mOverlayContext, mSlideInView, mControllers);
+ mOverlayContext, mSlideInView, mControllers, mSearchSessionController);
viewController.show(animate);
mAppsView = mOverlayContext.getAppsView();
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 84cc002..537d2c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -17,18 +17,21 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
-import android.animation.PropertyValuesHolder;
+import android.animation.Animator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
+import android.view.View;
import android.view.animation.Interpolator;
import android.window.OnBackInvokedDispatcher;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
@@ -58,20 +61,50 @@
/** Opens the all apps view. */
void show(boolean animate) {
- if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
return;
}
mIsOpen = true;
attachToContainer();
- if (animate) {
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setInterpolator(EMPHASIZED);
- mOpenCloseAnimator.setDuration(mAllAppsCallbacks.getOpenDuration()).start();
- } else {
+ addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ removeOnAttachStateChangeListener(this);
+ // Wait for view and its descendants to be fully attached before starting open.
+ post(() -> showOnFullyAttachedToWindow(animate));
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ removeOnAttachStateChangeListener(this);
+ }
+ });
+ }
+
+ private void showOnFullyAttachedToWindow(boolean animate) {
+ mAllAppsCallbacks.onAllAppsTransitionStart(true);
+ if (!animate) {
+ mAllAppsCallbacks.onAllAppsTransitionEnd(true);
mTranslationShift = TRANSLATION_SHIFT_OPENED;
+ return;
}
+
+ setUpOpenAnimation(mAllAppsCallbacks.getOpenDuration());
+ Animator animator = mOpenCloseAnimation.getAnimationPlayer();
+ animator.setInterpolator(EMPHASIZED);
+ animator.addListener(AnimatorListeners.forEndCallback(() -> {
+ if (mIsOpen) {
+ mAllAppsCallbacks.onAllAppsTransitionEnd(true);
+ }
+ }));
+ animator.start();
+ }
+
+ @Override
+ protected void onOpenCloseAnimationPending(PendingAnimation animation) {
+ mAllAppsCallbacks.onAllAppsAnimationPending(
+ animation, mToTranslationShift == TRANSLATION_SHIFT_OPENED);
}
/** The apps container inside this view. */
@@ -81,10 +114,19 @@
@Override
protected void handleClose(boolean animate) {
+ if (mIsOpen) {
+ mAllAppsCallbacks.onAllAppsTransitionStart(false);
+ }
handleClose(animate, mAllAppsCallbacks.getCloseDuration());
}
@Override
+ protected void onCloseComplete() {
+ mAllAppsCallbacks.onAllAppsTransitionEnd(false);
+ super.onCloseComplete();
+ }
+
+ @Override
protected Interpolator getIdleInterpolator() {
return EMPHASIZED;
}
@@ -194,4 +236,11 @@
protected boolean isEventOverContent(MotionEvent ev) {
return getPopupContainer().isEventOverView(mAppsView.getVisibleContainerView(), ev);
}
+
+ @Override
+ public void onBackInvoked() {
+ if (!mAllAppsCallbacks.handleSearchBackInvoked()) {
+ super.onBackInvoked();
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
index a851734..85633e9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
@@ -19,6 +19,8 @@
import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT;
import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.allapps.AllAppsTransitionListener;
+import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.appprediction.AppsDividerView;
import com.android.launcher3.taskbar.NavbarButtonsViewController;
import com.android.launcher3.taskbar.TaskbarControllers;
@@ -43,7 +45,8 @@
TaskbarAllAppsViewController(
TaskbarOverlayContext context,
TaskbarAllAppsSlideInView slideInView,
- TaskbarControllers taskbarControllers) {
+ TaskbarControllers taskbarControllers,
+ TaskbarSearchSessionController searchSessionController) {
mContext = context;
mSlideInView = slideInView;
@@ -52,7 +55,7 @@
mNavbarButtonsViewController = taskbarControllers.navbarButtonsViewController;
mOverlayController = taskbarControllers.taskbarOverlayController;
- mSlideInView.init(new TaskbarAllAppsCallbacks());
+ mSlideInView.init(new TaskbarAllAppsCallbacks(searchSessionController));
setUpAppDivider();
setUpTaskbarStashing();
}
@@ -94,7 +97,13 @@
});
}
- class TaskbarAllAppsCallbacks {
+ class TaskbarAllAppsCallbacks implements AllAppsTransitionListener {
+ private final TaskbarSearchSessionController mSearchSessionController;
+
+ private TaskbarAllAppsCallbacks(TaskbarSearchSessionController searchSessionController) {
+ mSearchSessionController = searchSessionController;
+ }
+
int getOpenDuration() {
return mOverlayController.getOpenDuration();
}
@@ -102,5 +111,24 @@
int getCloseDuration() {
return mOverlayController.getCloseDuration();
}
+
+ @Override
+ public void onAllAppsTransitionStart(boolean toAllApps) {
+ mSearchSessionController.onAllAppsTransitionStart(toAllApps);
+ }
+
+ @Override
+ public void onAllAppsTransitionEnd(boolean toAllApps) {
+ mSearchSessionController.onAllAppsTransitionEnd(toAllApps);
+ }
+
+ /** Invoked on back press, returning {@code true} if the search session handled it. */
+ boolean handleSearchBackInvoked() {
+ return mSearchSessionController.handleBackInvoked();
+ }
+
+ void onAllAppsAnimationPending(PendingAnimation animation, boolean toAllApps) {
+ mSearchSessionController.onAllAppsAnimationPending(animation, toAllApps);
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
index 52e2ce1..8a2041f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarSearchSessionController.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.view.View
import com.android.launcher3.R
+import com.android.launcher3.allapps.AllAppsTransitionListener
+import com.android.launcher3.anim.PendingAnimation
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.dragndrop.DragOptions.PreDragCondition
import com.android.launcher3.model.data.ItemInfo
@@ -26,23 +28,31 @@
import com.android.launcher3.util.ResourceBasedOverride.Overrides
/** Stub for managing the Taskbar search session. */
-open class TaskbarSearchSessionController : ResourceBasedOverride {
+open class TaskbarSearchSessionController : ResourceBasedOverride, AllAppsTransitionListener {
/** Start the search session lifecycle. */
- open fun startLifecycle() {}
+ open fun startLifecycle() = Unit
/** Destroy the search session. */
- open fun onDestroy() {}
+ open fun onDestroy() = Unit
/** Updates the predicted items shown in the zero-state. */
- open fun setZeroStatePredictedItems(items: List<ItemInfo>) {}
+ open fun setZeroStatePredictedItems(items: List<ItemInfo>) = Unit
/** Updates the search suggestions shown in the zero-state. */
- open fun setZeroStateSearchSuggestions(items: List<ItemInfo>) {}
+ open fun setZeroStateSearchSuggestions(items: List<ItemInfo>) = Unit
+
+ override fun onAllAppsTransitionStart(toAllApps: Boolean) = Unit
+
+ override fun onAllAppsTransitionEnd(toAllApps: Boolean) = Unit
/** Creates a [PreDragCondition] for [view], if it is a search result that requires one. */
open fun createPreDragConditionForSearch(view: View): PreDragCondition? = null
+ open fun handleBackInvoked(): Boolean = false
+
+ open fun onAllAppsAnimationPending(animation: PendingAnimation, toAllApps: Boolean) = Unit
+
companion object {
@JvmStatic
fun newInstance(context: Context): TaskbarSearchSessionController {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 6818db6..24db380 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -277,6 +277,7 @@
private void applyViewChanges(BubbleBarViewUpdate update) {
final boolean isCollapsed = (update.expandedChanged && !update.expanded)
|| (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
+ BubbleBarItem previouslySelectedBubble = mSelectedBubble;
BubbleBarBubble bubbleToSelect = null;
if (!update.removedBubbles.isEmpty()) {
for (int i = 0; i < update.removedBubbles.size(); i++) {
@@ -321,6 +322,11 @@
mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
+ if (mBubbles.isEmpty()) {
+ // all bubbles were removed. clear the selected bubble
+ mSelectedBubble = null;
+ }
+
if (update.updatedBubble != null) {
// Updates mean the dot state may have changed; any other changes were updated in
// the populateBubble step.
@@ -356,7 +362,11 @@
}
if (bubbleToSelect != null) {
setSelectedBubble(bubbleToSelect);
+ if (previouslySelectedBubble == null) {
+ mBubbleStashController.animateToInitialState(update.expanded);
+ }
}
+
if (update.expandedChanged) {
if (update.expanded != mBubbleBarViewController.isExpanded()) {
mBubbleBarViewController.setExpandedFromSysui(update.expanded);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index eec334a..ffe077b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -95,6 +95,8 @@
private View.OnClickListener mOnClickListener;
private final Rect mTempRect = new Rect();
+ private float mRelativePivotX = 1f;
+ private float mRelativePivotY = 1f;
// An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
// collapsed state and 1 to the fully expanded state.
@@ -109,6 +111,9 @@
@Nullable
private Consumer<String> mUpdateSelectedBubbleAfterCollapse;
+ @Nullable
+ private BubbleView mDraggedBubbleView;
+
public BubbleBarView(Context context) {
this(context, null);
}
@@ -181,9 +186,10 @@
mBubbleBarBounds.right = right;
mBubbleBarBounds.bottom = bottom;
- // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
- setPivotX(getWidth());
- setPivotY(getHeight());
+ // The bubble bar handle is aligned according to the relative pivot,
+ // by default it's aligned to the bottom edge of the screen so scale towards that
+ setPivotX(mRelativePivotX * getWidth());
+ setPivotY(mRelativePivotY * getHeight());
// Position the views
updateChildrenRenderNodeProperties();
@@ -198,6 +204,32 @@
return mBubbleBarBounds;
}
+ /**
+ * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
+ * respectively. If the value is not in range of 0 to 1 it will be normalized.
+ * @param x relative X pivot value in range 0..1
+ * @param y relative Y pivot value in range 0..1
+ */
+ public void setRelativePivot(float x, float y) {
+ mRelativePivotX = Float.max(Float.min(x, 1), 0);
+ mRelativePivotY = Float.max(Float.min(y, 1), 0);
+ requestLayout();
+ }
+
+ /**
+ * Get current relative pivot for X axis
+ */
+ public float getRelativePivotX() {
+ return mRelativePivotX;
+ }
+
+ /**
+ * Get current relative pivot for Y axis
+ */
+ public float getRelativePivotY() {
+ return mRelativePivotY;
+ }
+
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -254,9 +286,9 @@
// where the bubble will end up when the animation ends
final float targetX = currentWidth - expandedWidth + expandedX;
bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
- // if we're fully expanded, set the z level to 0
+ // if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
if (widthState == 1f) {
- bv.setZ(0);
+ bv.setZ(bv == mDraggedBubbleView ? mBubbleElevation : 0);
}
// When we're expanded, we're not stacked so we're not behind the stack
bv.setBehindStack(false, animate);
@@ -306,7 +338,10 @@
if (!isExpanded()) {
for (int i = 0; i < viewOrder.size(); i++) {
View child = viewOrder.get(i);
- if (child != null) {
+ // this child view may have already been removed so verify that it still exists
+ // before reordering it, otherwise it will be re-added.
+ int indexOfChild = indexOfChild(child);
+ if (child != null && indexOfChild >= 0) {
removeViewInLayout(child);
addViewInLayout(child, i, child.getLayoutParams());
}
@@ -329,6 +364,14 @@
}
/**
+ * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top
+ */
+ public void setDraggedBubble(@Nullable BubbleView view) {
+ mDraggedBubbleView = view;
+ requestLayout();
+ }
+
+ /**
* Update the arrow position to match the selected bubble.
*
* @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 8e7fda8..20b8e3b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -24,6 +24,8 @@
import android.view.View;
import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -54,6 +56,7 @@
// Initialized in init.
private BubbleStashController mBubbleStashController;
private BubbleBarController mBubbleBarController;
+ private BubbleDragController mBubbleDragController;
private TaskbarStashController mTaskbarStashController;
private TaskbarInsetsController mTaskbarInsetsController;
private View.OnClickListener mBubbleClickListener;
@@ -85,6 +88,7 @@
public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleBarController = bubbleControllers.bubbleBarController;
+ mBubbleDragController = bubbleControllers.bubbleDragController;
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
@@ -95,6 +99,7 @@
mBubbleBarScale.updateValue(1f);
mBubbleClickListener = v -> onBubbleClicked(v);
mBubbleBarClickListener = v -> setExpanded(true);
+ mBubbleDragController.setupBubbleBarView(mBarView);
mBarView.setOnClickListener(mBubbleBarClickListener);
mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -152,6 +157,11 @@
return mBarView.getBubbleBarBounds();
}
+ /** The horizontal margin of the bubble bar from the edge of the screen. */
+ public int getHorizontalMargin() {
+ return mBarView.getHorizontalMargin();
+ }
+
/**
* When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or
* expanded (the icons are in a row). This indicates whether the bubble bar is expanded.
@@ -206,10 +216,12 @@
// TODO: (b/273592694) animate it
private void updateVisibilityForStateChange() {
- if (!mHiddenForSysui && !mBubbleStashController.isStashed() && !mHiddenForNoBubbles) {
+ if (!mHiddenForSysui && !mHiddenForNoBubbles) {
mBarView.setVisibility(VISIBLE);
} else {
mBarView.setVisibility(INVISIBLE);
+ mBarView.setAlpha(0);
+ mBarView.setExpanded(false);
}
}
@@ -261,6 +273,7 @@
if (b != null) {
mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
b.getView().setOnClickListener(mBubbleClickListener);
+ mBubbleDragController.setupBubbleView(b.getView());
} else {
Log.w(TAG, "addBubble, bubble was null!");
}
@@ -312,4 +325,46 @@
mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
}
}
+
+ /**
+ * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
+ * that a bubble is being dragged to dismiss.
+ * @param bubbleView dragged bubble view
+ */
+ public void onDragStart(@NonNull BubbleView bubbleView) {
+ if (bubbleView.getBubble() == null) return;
+ mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ true);
+ mBarView.setDraggedBubble(bubbleView);
+ }
+
+ /**
+ * Notifies SystemUI to expand the selected bubble when the bubble is released.
+ * @param bubbleView dragged bubble view
+ */
+ public void onDragRelease(@NonNull BubbleView bubbleView) {
+ if (bubbleView.getBubble() == null) return;
+ mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ false);
+ }
+
+ /**
+ * Removes the dragged bubble view in the bubble bar view
+ */
+ public void onDragEnd() {
+ mBarView.setDraggedBubble(null);
+ }
+
+ /**
+ * Called when bubble was dragged into the dismiss target. Notifies System
+ * @param bubble dismissed bubble item
+ */
+ public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+ mSystemUiProxy.removeBubble(bubble.getKey());
+ }
+
+ /**
+ * Called when bubble stack was dragged into the dismiss target
+ */
+ public void onDismissAllBubblesWhileDragging() {
+ mSystemUiProxy.removeAllBubbles();
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 6417f3c..c47427d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -27,6 +27,8 @@
public final BubbleBarViewController bubbleBarViewController;
public final BubbleStashController bubbleStashController;
public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
+ public final BubbleDragController bubbleDragController;
+ public final BubbleDismissController bubbleDismissController;
private final RunnableList mPostInitRunnables = new RunnableList();
@@ -39,11 +41,15 @@
BubbleBarController bubbleBarController,
BubbleBarViewController bubbleBarViewController,
BubbleStashController bubbleStashController,
- BubbleStashedHandleViewController bubbleStashedHandleViewController) {
+ BubbleStashedHandleViewController bubbleStashedHandleViewController,
+ BubbleDragController bubbleDragController,
+ BubbleDismissController bubbleDismissController) {
this.bubbleBarController = bubbleBarController;
this.bubbleBarViewController = bubbleBarViewController;
this.bubbleStashController = bubbleStashController;
this.bubbleStashedHandleViewController = bubbleStashedHandleViewController;
+ this.bubbleDragController = bubbleDragController;
+ this.bubbleDismissController = bubbleDismissController;
}
/**
@@ -56,6 +62,8 @@
bubbleBarViewController.init(taskbarControllers, this);
bubbleStashedHandleViewController.init(taskbarControllers, this);
bubbleStashController.init(taskbarControllers, this);
+ bubbleDragController.init(/* bubbleControllers = */ this);
+ bubbleDismissController.init(/* bubbleControllers = */ this);
mPostInitRunnables.executeAllAndDestroy();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
new file mode 100644
index 0000000..0ff0469
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar.bubbles;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarDragLayer;
+import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
+/**
+ * Controls dismiss view presentation for the bubble bar dismiss functionality.
+ * Provides the dragged view snapping to the target dismiss area and animates it.
+ * When the dragged bubble/bubble stack is released inside of the target area, it gets dismissed.
+ *
+ * @see BubbleDragController
+ */
+public class BubbleDismissController {
+ private static final String TAG = BubbleDismissController.class.getSimpleName();
+ private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
+ // LINT.IfChange
+ private static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
+ SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
+ // LINT.ThenChange(com/android/wm/shell/bubbles/BubbleStackView.java)
+ private final TaskbarActivityContext mActivity;
+ private final TaskbarDragLayer mDragLayer;
+ @Nullable
+ private BubbleBarViewController mBubbleBarViewController;
+
+ // Dismiss view that's attached to drag layer. It consists of the scrim view and the circular
+ // dismiss view used as a dismiss target.
+ @Nullable
+ private DismissView mDismissView;
+
+ // The currently magnetized object, which is being dragged and will be attracted to the magnetic
+ // dismiss target. This is either the stack itself, or an individual bubble.
+ @Nullable
+ private MagnetizedObject<View> mMagnetizedObject;
+
+ // The MagneticTarget instance for our circular dismiss view. This is added to the
+ // MagnetizedObject instances for the stack and any dragged-out bubbles.
+ @Nullable
+ private MagnetizedObject.MagneticTarget mMagneticTarget;
+
+ // The bubble drag animator that synchronizes bubble drag and dismiss view animations
+ // A new instance is provided when the dismiss view is setup
+ @Nullable
+ private BubbleDragAnimator mAnimator;
+
+ public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
+ mActivity = activity;
+ mDragLayer = dragLayer;
+ }
+
+ /**
+ * Initializes dependencies when bubble controllers are created.
+ * Should be careful to only access things that were created in constructors for now, as some
+ * controllers may still be waiting for init().
+ */
+ public void init(@NonNull BubbleControllers bubbleControllers) {
+ mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+ }
+
+ /**
+ * Setup the dismiss view and magnetized object that will be attracted to magnetic target.
+ * Should be called before handling events or showing/hiding dismiss view.
+ *
+ * @param magnetizedView the view to be pulled into target dismiss area
+ * @param animator the bubble animator to be used for the magnetized view, it syncs bubble
+ * dragging and dismiss animations with the dismiss view provided.
+ */
+ public void setupDismissView(@NonNull View magnetizedView,
+ @NonNull BubbleDragAnimator animator) {
+ setupDismissView();
+ setupMagnetizedObject(magnetizedView);
+ if (mDismissView != null) {
+ animator.setDismissView(mDismissView);
+ mAnimator = animator;
+ }
+ }
+
+ /**
+ * Handle the touch event and pass it to the magnetized object.
+ * It should be called after {@code setupDismissView}
+ */
+ public boolean handleTouchEvent(@NonNull MotionEvent event) {
+ return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+ }
+
+ /**
+ * Show dismiss view with animation
+ * It should be called after {@code setupDismissView}
+ */
+ public void showDismissView() {
+ if (mDismissView == null) return;
+ mDismissView.show();
+ }
+
+ /**
+ * Hide dismiss view with animation
+ * It should be called after {@code setupDismissView}
+ */
+ public void hideDismissView() {
+ if (mDismissView == null) return;
+ mDismissView.hide();
+ }
+
+ /**
+ * Dismiss magnetized object when it's released in the dismiss target area
+ */
+ private void dismissMagnetizedObject() {
+ if (mMagnetizedObject == null || mBubbleBarViewController == null) return;
+ if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
+ BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
+ if (bubbleView.getBubble() != null) {
+ mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+ }
+ } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
+ mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+ }
+ }
+
+ private void setupDismissView() {
+ if (mDismissView != null) return;
+ mDismissView = new DismissView(mActivity.getApplicationContext());
+ BubbleDismissViewUtils.setup(mDismissView);
+ mDragLayer.addView(mDismissView, /* index = */ 0,
+ new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ mDismissView.setElevation(mDismissView.getResources().getDimensionPixelSize(
+ R.dimen.bubblebar_elevation));
+ setupMagneticTarget(mDismissView.getCircle());
+ }
+
+ private void setupMagneticTarget(@NonNull View view) {
+ int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize(
+ R.dimen.bubblebar_dismiss_target_size);
+ mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius);
+ }
+
+ private void setupMagnetizedObject(@NonNull View magnetizedView) {
+ mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
+ magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
+ @Override
+ public float getWidth(@NonNull View underlyingObject) {
+ return underlyingObject.getWidth() * underlyingObject.getScaleX();
+ }
+
+ @Override
+ public float getHeight(@NonNull View underlyingObject) {
+ return underlyingObject.getHeight() * underlyingObject.getScaleY();
+ }
+
+ @Override
+ public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
+ underlyingObject.getLocationOnScreen(loc);
+ }
+ };
+
+ mMagnetizedObject.setHapticsEnabled(true);
+ mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
+ mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ if (mMagneticTarget != null) {
+ mMagnetizedObject.addTarget(mMagneticTarget);
+ } else {
+ Log.e(TAG,"Requires MagneticTarget to add target to MagnetizedObject!");
+ }
+ mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ if (mAnimator == null) return;
+ mAnimator.animateDismissCaptured();
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ float velX, float velY, boolean wasFlungOut) {
+ if (mAnimator == null) return;
+ mAnimator.animateDismissReleased();
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ dismissMagnetizedObject();
+ }
+ });
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
new file mode 100644
index 0000000..4b235a9
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("BubbleDismissViewUtils")
+
+package com.android.launcher3.taskbar.bubbles
+
+import com.android.launcher3.R
+import com.android.wm.shell.common.bubbles.DismissView
+
+/**
+ * Dismiss view is shared from WMShell. It requires setup with local resources.
+ *
+ * Usage:
+ * - Kotlin `dismissView.setup()`
+ * - Java `BubbleDismissViewUtils.setup(dismissView)`
+ */
+fun DismissView.setup() {
+ setup(
+ DismissView.Config(
+ targetSizeResId = R.dimen.bubblebar_dismiss_target_size,
+ iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size,
+ bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin,
+ floatingGradientHeightResId = R.dimen.bubblebar_dismiss_floating_gradient_height,
+ floatingGradientColorResId = android.R.color.system_neutral1_900,
+ backgroundResId = R.drawable.bg_bubble_dismiss_circle,
+ iconResId = R.drawable.ic_bubble_dismiss_white
+ )
+ )
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
new file mode 100644
index 0000000..24dca5e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles;
+
+import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
+
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+
+import com.android.launcher3.R;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.common.bubbles.DismissCircleView;
+import com.android.wm.shell.common.bubbles.DismissView;
+
+/**
+ * The animator performs the bubble animations while dragging and coordinates bubble and dismiss
+ * view animations when it gets magnetized, released or dismissed.
+ */
+public class BubbleDragAnimator {
+ private static final float SCALE_BUBBLE_FOCUSED = 1.2f;
+ private static final float SCALE_BUBBLE_CAPTURED = 0.9f;
+ private static final float SCALE_BUBBLE_BAR_FOCUSED = 1.1f;
+
+ private final PhysicsAnimator.SpringConfig mDefaultConfig =
+ new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY);
+ private final PhysicsAnimator.SpringConfig mTranslationConfig =
+ new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_LOW_BOUNCY);
+ @NonNull
+ private final View mView;
+ @NonNull
+ private final PhysicsAnimator<View> mBubbleAnimator;
+ @Nullable
+ private DismissView mDismissView;
+ @Nullable
+ private PhysicsAnimator<DismissCircleView> mDismissAnimator;
+ private final float mBubbleFocusedScale;
+ private final float mBubbleCapturedScale;
+ private final float mDismissCapturedScale;
+
+ /**
+ * Should be initialised for each dragged view
+ *
+ * @param view the dragged view to animate
+ */
+ public BubbleDragAnimator(@NonNull View view) {
+ mView = view;
+ mBubbleAnimator = PhysicsAnimator.getInstance(view);
+ mBubbleAnimator.setDefaultSpringConfig(mDefaultConfig);
+
+ Resources resources = view.getResources();
+ final int collapsedSize = resources.getDimensionPixelSize(
+ R.dimen.bubblebar_dismiss_target_small_size);
+ final int expandedSize = resources.getDimensionPixelSize(
+ R.dimen.bubblebar_dismiss_target_size);
+ mDismissCapturedScale = (float) collapsedSize / expandedSize;
+
+ if (view instanceof BubbleBarView) {
+ mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED;
+ mBubbleCapturedScale = mDismissCapturedScale;
+ } else {
+ mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED;
+ mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED;
+ }
+ }
+
+ /**
+ * Sets dismiss view to be animated alongside the dragged bubble
+ */
+ public void setDismissView(@NonNull DismissView dismissView) {
+ mDismissView = dismissView;
+ mDismissAnimator = PhysicsAnimator.getInstance(dismissView.getCircle());
+ mDismissAnimator.setDefaultSpringConfig(mDefaultConfig);
+ }
+
+ /**
+ * Animates the focused state of the bubble when the dragging starts
+ */
+ public void animateFocused() {
+ mBubbleAnimator.cancel();
+ mBubbleAnimator
+ .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
+ .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
+ .start();
+ }
+
+ /**
+ * Animates the dragged bubble movement back to the initial position.
+ *
+ * @param initialPosition the position to animate to
+ * @param velocity the initial velocity to use for the spring animation
+ * @param endActions gets called when the animation completes or gets cancelled
+ */
+ public void animateToInitialState(@NonNull PointF initialPosition, @NonNull PointF velocity,
+ @Nullable Runnable endActions) {
+ mBubbleAnimator.cancel();
+ mBubbleAnimator
+ .spring(DynamicAnimation.SCALE_X, 1f)
+ .spring(DynamicAnimation.SCALE_Y, 1f)
+ .spring(DynamicAnimation.TRANSLATION_X, initialPosition.x, velocity.x,
+ mTranslationConfig)
+ .spring(DynamicAnimation.TRANSLATION_Y, initialPosition.y, velocity.y,
+ mTranslationConfig)
+ .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
+ boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
+ boolean allRelevantPropertyAnimationsEnded) -> {
+ if (canceled || allRelevantPropertyAnimationsEnded) {
+ resetAnimatedViews(initialPosition);
+ if (endActions != null) {
+ endActions.run();
+ }
+ }
+ })
+ .start();
+ }
+
+ /**
+ * Animates the dragged view alongside the dismiss view when it gets captured in the dismiss
+ * target area.
+ */
+ public void animateDismissCaptured() {
+ mBubbleAnimator.cancel();
+ mBubbleAnimator
+ .spring(DynamicAnimation.SCALE_X, mBubbleCapturedScale)
+ .spring(DynamicAnimation.SCALE_Y, mBubbleCapturedScale)
+ .spring(DynamicAnimation.ALPHA, mDismissCapturedScale)
+ .start();
+
+ if (mDismissAnimator != null) {
+ mDismissAnimator.cancel();
+ mDismissAnimator
+ .spring(DynamicAnimation.SCALE_X, mDismissCapturedScale)
+ .spring(DynamicAnimation.SCALE_Y, mDismissCapturedScale)
+ .start();
+ }
+ }
+
+ /**
+ * Animates the dragged view alongside the dismiss view when it gets released from the dismiss
+ * target area.
+ */
+ public void animateDismissReleased() {
+ mBubbleAnimator.cancel();
+ mBubbleAnimator
+ .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
+ .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
+ .spring(DynamicAnimation.ALPHA, 1f)
+ .start();
+
+ if (mDismissAnimator != null) {
+ mDismissAnimator.cancel();
+ mDismissAnimator
+ .spring(DynamicAnimation.SCALE_X, 1f)
+ .spring(DynamicAnimation.SCALE_Y, 1f)
+ .start();
+ }
+ }
+
+ /**
+ * Animates the dragged bubble dismiss when it's released in the dismiss target area.
+ *
+ * @param initialPosition the initial position to move the bubble too after animation finishes
+ * @param endActions gets called when the animation completes or gets cancelled
+ */
+ public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) {
+ float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f;
+ float translationY = mView.getTranslationY() + dismissHeight;
+ mBubbleAnimator
+ .spring(DynamicAnimation.TRANSLATION_Y, translationY)
+ .spring(DynamicAnimation.SCALE_X, 0f)
+ .spring(DynamicAnimation.SCALE_Y, 0f)
+ .spring(DynamicAnimation.ALPHA, 0f)
+ .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
+ boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
+ boolean allRelevantPropertyAnimationsEnded) -> {
+ if (canceled || allRelevantPropertyAnimationsEnded) {
+ resetAnimatedViews(initialPosition);
+ if (endActions != null) endActions.run();
+ }
+ })
+ .start();
+ }
+
+ /**
+ * Reset the animated views to the initial state
+ *
+ * @param initialPosition position of the bubble
+ */
+ private void resetAnimatedViews(@NonNull PointF initialPosition) {
+ mView.setScaleX(1f);
+ mView.setScaleY(1f);
+ mView.setAlpha(1f);
+ mView.setTranslationX(initialPosition.x);
+ mView.setTranslationY(initialPosition.y);
+
+ if (mDismissView != null) {
+ mDismissView.getCircle().setScaleX(1f);
+ mDismissView.getCircle().setScaleY(1f);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
new file mode 100644
index 0000000..08fd681
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar.bubbles;
+
+import android.annotation.SuppressLint;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+
+/**
+ * Controls bubble bar drag to dismiss interaction.
+ * Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}.
+ * Supported interactions:
+ * - Drag a single bubble view into dismiss target to remove it.
+ * - Drag the bubble stack into dismiss target to remove all.
+ * Restores initial position of dragged view if released outside of the dismiss target.
+ */
+public class BubbleDragController {
+ private final TaskbarActivityContext mActivity;
+ private BubbleBarViewController mBubbleBarViewController;
+ private BubbleDismissController mBubbleDismissController;
+
+ public BubbleDragController(TaskbarActivityContext activity) {
+ mActivity = activity;
+ }
+
+ /**
+ * Initializes dependencies when bubble controllers are created.
+ * Should be careful to only access things that were created in constructors for now, as some
+ * controllers may still be waiting for init().
+ */
+ public void init(@NonNull BubbleControllers bubbleControllers) {
+ mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+ mBubbleDismissController = bubbleControllers.bubbleDismissController;
+ }
+
+ /**
+ * Setup the bubble view for dragging and attach touch listener to it
+ */
+ @SuppressLint("ClickableViewAccessibility")
+ public void setupBubbleView(@NonNull BubbleView bubbleView) {
+ if (!(bubbleView.getBubble() instanceof BubbleBarBubble)) {
+ // Don't setup dragging for overflow bubble view
+ return;
+ }
+
+ bubbleView.setOnTouchListener(new BubbleTouchListener() {
+ @Override
+ void onDragStart() {
+ mBubbleBarViewController.onDragStart(bubbleView);
+ }
+
+ @Override
+ void onDragEnd() {
+ mBubbleBarViewController.onDragEnd();
+ }
+
+ @Override
+ protected void onDragRelease() {
+ mBubbleBarViewController.onDragRelease(bubbleView);
+ }
+ });
+ }
+
+ /**
+ * Setup the bubble bar view for dragging and attach touch listener to it
+ */
+ @SuppressLint("ClickableViewAccessibility")
+ public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) {
+ PointF initialRelativePivot = new PointF();
+ bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
+ @Override
+ protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
+ if (bubbleBarView.isExpanded()) return false;
+ return super.onTouchDown(view, event);
+ }
+
+ @Override
+ void onDragStart() {
+ initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
+ bubbleBarView.getRelativePivotY());
+ // By default the bubble bar view pivot is in bottom right corner, while dragging
+ // it should be centered in order to align it with the dismiss target view
+ bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
+ }
+
+ @Override
+ void onDragEnd() {
+ // Restoring the initial pivot for the bubble bar view
+ bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
+ }
+ });
+ }
+
+ /**
+ * Bubble touch listener for handling a single bubble view or bubble bar view while dragging.
+ * The dragging starts after "shorter" long click (the long click duration might change):
+ * - When the touch gesture moves out of the {@code ACTION_DOWN} location the dragging
+ * interaction is cancelled.
+ * - When {@code ACTION_UP} happens before long click is registered and there was no significant
+ * movement the view will perform click.
+ * - When the listener registers long click it starts dragging interaction, all the subsequent
+ * {@code ACTION_MOVE} events will drag the view, and the interaction finishes when
+ * {@code ACTION_UP} or {@code ACTION_CANCEL} are received.
+ * Lifecycle methods can be overridden do add extra setup/clean up steps.
+ */
+ private abstract class BubbleTouchListener implements View.OnTouchListener {
+ /**
+ * The internal state of the touch listener
+ */
+ private enum State {
+ // Idle and ready for the touch events.
+ // Changes to:
+ // - TOUCHED, when the {@code ACTION_DOWN} is handled
+ IDLE,
+
+ // Touch down was handled and the lister is recognising the gestures.
+ // Changes to:
+ // - IDLE, when performs the click
+ // - DRAGGING, when registers the long click and starts dragging interaction
+ // - CANCELLED, when the touch events move out of the initial location before the long
+ // click is recognised
+
+ TOUCHED,
+
+ // The long click was registered and the view is being dragged.
+ // Changes to:
+ // - IDLE, when the gesture ends with the {@code ACTION_UP} or {@code ACTION_CANCEL}
+ DRAGGING,
+
+ // The dragging was cancelled.
+ // Changes to:
+ // - IDLE, when the current gesture completes
+ CANCELLED
+ }
+
+ private final PointF mTouchDownLocation = new PointF();
+ private final PointF mViewInitialPosition = new PointF();
+ private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
+ private State mState = State.IDLE;
+ private int mTouchSlop = -1;
+ private BubbleDragAnimator mAnimator;
+ @Nullable
+ private Runnable mLongClickRunnable;
+
+ /**
+ * Called when the dragging interaction has started
+ */
+ abstract void onDragStart();
+
+ /**
+ * Called when the dragging interaction has ended and all the animations have completed
+ */
+ abstract void onDragEnd();
+
+ /**
+ * Called when the dragged bubble is released outside of the dismiss target area and will
+ * move back to its initial position
+ */
+ protected void onDragRelease() {
+ }
+
+ /**
+ * Called when the dragged bubble is released inside of the dismiss target area and will get
+ * dismissed with animation
+ */
+ protected void onDragDismiss() {
+ }
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility")
+ public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
+ updateVelocity(event);
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ return onTouchDown(view, event);
+ case MotionEvent.ACTION_MOVE:
+ onTouchMove(view, event);
+ break;
+ case MotionEvent.ACTION_UP:
+ onTouchUp(view, event);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ onTouchCancel(view, event);
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * The touch down starts the interaction and schedules the long click handler.
+ *
+ * @param view the view that received the event
+ * @param event the motion event
+ * @return true if the gesture should be intercepted and handled, false otherwise. Note if
+ * the false is returned subsequent events in the gesture won't get reported.
+ */
+ protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
+ mState = State.TOUCHED;
+ mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
+ mTouchDownLocation.set(event.getRawX(), event.getRawY());
+ mViewInitialPosition.set(view.getTranslationX(), view.getTranslationY());
+ setupLongClickHandler(view);
+ return true;
+ }
+
+ /**
+ * The move event drags the view or cancels the interaction if hasn't long clicked yet.
+ *
+ * @param view the view that received the event
+ * @param event the motion event
+ */
+ protected void onTouchMove(@NonNull View view, @NonNull MotionEvent event) {
+ final float dx = event.getRawX() - mTouchDownLocation.x;
+ final float dy = event.getRawY() - mTouchDownLocation.y;
+ switch (mState) {
+ case TOUCHED:
+ final boolean movedOut = Math.hypot(dx, dy) > mTouchSlop;
+ if (movedOut) {
+ // Moved out of the initial location before the long click was registered
+ mState = State.CANCELLED;
+ cleanUpLongClickHandler(view);
+ }
+ break;
+ case DRAGGING:
+ drag(view, event, dx, dy);
+ break;
+ }
+ }
+
+ /**
+ * On touch up performs click or finishes the dragging depending on the state.
+ *
+ * @param view the view that received the event
+ * @param event the motion event
+ */
+ protected void onTouchUp(@NonNull View view, @NonNull MotionEvent event) {
+ switch (mState) {
+ case TOUCHED:
+ view.performClick();
+ cleanUp(view);
+ break;
+ case DRAGGING:
+ stopDragging(view, event);
+ break;
+ default:
+ cleanUp(view);
+ break;
+ }
+ }
+
+ /**
+ * The gesture is cancelled and the interaction should clean up and complete.
+ *
+ * @param view the view that received the event
+ * @param event the motion event
+ */
+ protected void onTouchCancel(@NonNull View view, @NonNull MotionEvent event) {
+ if (mState == State.DRAGGING) {
+ stopDragging(view, event);
+ } else {
+ cleanUp(view);
+ }
+ }
+
+ private void startDragging(@NonNull View view) {
+ onDragStart();
+ mActivity.setTaskbarWindowFullscreen(true);
+ mAnimator = new BubbleDragAnimator(view);
+ mAnimator.animateFocused();
+ mBubbleDismissController.setupDismissView(view, mAnimator);
+ mBubbleDismissController.showDismissView();
+ }
+
+ private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy) {
+ if (mBubbleDismissController.handleTouchEvent(event)) return;
+ view.setTranslationX(mViewInitialPosition.x + dx);
+ view.setTranslationY(mViewInitialPosition.y + dy);
+ }
+
+ private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
+ Runnable onComplete = () -> {
+ mActivity.setTaskbarWindowFullscreen(false);
+ cleanUp(view);
+ onDragEnd();
+ };
+
+ if (mBubbleDismissController.handleTouchEvent(event)) {
+ onDragDismiss();
+ mAnimator.animateDismiss(mViewInitialPosition, onComplete);
+ } else {
+ onDragRelease();
+ mAnimator.animateToInitialState(mViewInitialPosition, getCurrentVelocity(),
+ onComplete);
+ }
+ mBubbleDismissController.hideDismissView();
+ }
+
+ private void setupLongClickHandler(@NonNull View view) {
+ cleanUpLongClickHandler(view);
+ mLongClickRunnable = () -> {
+ // Register long click and start dragging interaction
+ mState = State.DRAGGING;
+ startDragging(view);
+ };
+ view.getHandler().postDelayed(mLongClickRunnable, mPressToDragTimeout);
+ }
+
+ private void cleanUpLongClickHandler(@NonNull View view) {
+ if (mLongClickRunnable == null || view.getHandler() == null) return;
+ view.getHandler().removeCallbacks(mLongClickRunnable);
+ mLongClickRunnable = null;
+ }
+
+ private void cleanUp(@NonNull View view) {
+ cleanUpLongClickHandler(view);
+ mVelocityTracker.clear();
+ mState = State.IDLE;
+ }
+
+ private void updateVelocity(MotionEvent event) {
+ final float deltaX = event.getRawX() - event.getX();
+ final float deltaY = event.getRawY() - event.getY();
+ event.offsetLocation(deltaX, deltaY);
+ mVelocityTracker.addMovement(event);
+ event.offsetLocation(-deltaX, -deltaY);
+ }
+
+ private PointF getCurrentVelocity() {
+ mVelocityTracker.computeCurrentVelocity(/* units = */ 1000);
+ return new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 8af4ff9..00c2ca1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -71,6 +71,7 @@
private int mUnstashedHeight;
private boolean mBubblesShowingOnHome;
private boolean mBubblesShowingOnOverview;
+ private boolean mIsSysuiLocked;
@Nullable
private AnimatorSet mAnimator;
@@ -95,14 +96,6 @@
mStashedHeight = mHandleViewController.getStashedHeight();
mUnstashedHeight = mHandleViewController.getUnstashedHeight();
-
- bubbleControllers.runAfterInit(() -> {
- if (mTaskbarStashController.isStashed()) {
- stashBubbleBar();
- } else {
- showBubbleBar(false /* expandBubbles */);
- }
- });
}
/**
@@ -120,6 +113,40 @@
}
/**
+ * Animates the bubble bar and handle to their initial state, transitioning from the state where
+ * both views are invisible. Called when the first bubble is added or when the device is
+ * unlocked.
+ *
+ * <p>Normally either the bubble bar or the handle is visible,
+ * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
+ * between these two states. But the transition from the state where both the bar and handle
+ * are invisible is slightly different.
+ *
+ * <p>The initial state will depend on the current state of the device, i.e. overview, home etc
+ * and whether bubbles are requested to be expanded.
+ */
+ public void animateToInitialState(boolean expanding) {
+ AnimatorSet animatorSet = new AnimatorSet();
+ if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) {
+ mIsStashed = false;
+ animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
+ mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
+ mIconAlphaForStash.animateToValue(1));
+ } else {
+ mIsStashed = true;
+ animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1));
+ }
+
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onIsStashedChanged();
+ }
+ });
+ animatorSet.setDuration(BAR_STASH_DURATION).start();
+ }
+
+ /**
* Called when launcher enters or exits the home page. Bubbles are unstashed on home.
*/
public void setBubblesShowingOnHome(boolean onHome) {
@@ -172,12 +199,11 @@
/** Called when sysui locked state changes, when locked, bubble bar is stashed. */
public void onSysuiLockedStateChange(boolean isSysuiLocked) {
- if (isSysuiLocked) {
- // TODO: should the normal path flip mBubblesOnHome / check if this is needed
- // If we're locked, we're no longer showing on home.
- mBubblesShowingOnHome = false;
- mBubblesShowingOnOverview = false;
- stashBubbleBar();
+ if (isSysuiLocked != mIsSysuiLocked) {
+ mIsSysuiLocked = isSysuiLocked;
+ if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
+ animateToInitialState(false /* expanding */);
+ }
}
}
@@ -256,11 +282,7 @@
firstHalfDurationScale = 0.5f;
secondHalfDurationScale = 0.75f;
- // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
- // Otherwise we're either showing in an app or in overview. In either case adjust it so
- // the bubble bar aligns with the taskbar.
- final float translationY = mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
- : getBubbleBarTranslationYForTaskbar();
+ final float translationY = getBubbleBarTranslationY();
fullLengthAnimatorSet.playTogether(
mIconScaleForStash.animateToValue(1),
@@ -317,6 +339,9 @@
}
float getBubbleBarTranslationY() {
+ // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
+ // Otherwise we're either showing in an app or in overview. In either case adjust it so
+ // the bubble bar aligns with the taskbar.
return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
: getBubbleBarTranslationYForTaskbar();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 26756d4..fbab595 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -52,6 +52,7 @@
private BubbleStashController mBubbleStashController;
private RegionSamplingHelper mRegionSamplingHelper;
private int mBarSize;
+ private int mStashedTaskbarHeight;
private int mStashedHandleWidth;
private int mStashedHandleHeight;
@@ -92,7 +93,7 @@
mTaskbarStashedHandleAlpha.get(0).setValue(0);
- final int stashedTaskbarHeight = resources.getDimensionPixelSize(
+ mStashedTaskbarHeight = resources.getDimensionPixelSize(
R.dimen.bubblebar_stashed_size);
mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
@Override
@@ -115,22 +116,27 @@
}
}, Executors.UI_HELPER_EXECUTOR);
- mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> {
- // As more bubbles get added, the icon bounds become larger. To ensure a consistent
- // handle bar position, we pin it to the edge of the screen.
- Rect bubblebarRect = mBarViewController.getBubbleBarBounds();
- final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+ mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
+ updateBounds());
+ }
- mStashedHandleBounds.set(
- bubblebarRect.right - mStashedHandleWidth,
- stashedCenterY - mStashedHandleHeight / 2,
- bubblebarRect.right,
- stashedCenterY + mStashedHandleHeight / 2);
- mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+ private void updateBounds() {
+ // As more bubbles get added, the icon bounds become larger. To ensure a consistent
+ // handle bar position, we pin it to the edge of the screen.
+ final int right =
+ mActivity.getDeviceProfile().widthPx - mBarViewController.getHorizontalMargin();
- view.setPivotX(view.getWidth());
- view.setPivotY(view.getHeight() - stashedTaskbarHeight / 2f);
- });
+ final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2;
+
+ mStashedHandleBounds.set(
+ right - mStashedHandleWidth,
+ stashedCenterY - mStashedHandleHeight / 2,
+ right,
+ stashedCenterY + mStashedHandleHeight / 2);
+ mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+
+ mStashedHandleView.setPivotX(mStashedHandleView.getWidth());
+ mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f);
}
public void onDestroy() {
@@ -188,6 +194,7 @@
mStashedHandleView.setVisibility(VISIBLE);
} else {
mStashedHandleView.setVisibility(INVISIBLE);
+ mStashedHandleView.setAlpha(0);
}
updateRegionSampling();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 7db2320..5c7f2be 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -67,6 +67,7 @@
val startContextualContainer =
navButtonsView.findViewById<ViewGroup>(ID_START_CONTEXTUAL_BUTTONS)
val isPhoneNavMode = phoneMode && isThreeButtonNav
+ val isPhoneGestureMode = phoneMode && !isThreeButtonNav
return when {
isPhoneNavMode -> {
if (!deviceProfile.isLandscape) {
@@ -92,6 +93,14 @@
)
}
}
+ isPhoneGestureMode ->{
+ PhoneGestureLayoutter(
+ resources,
+ navButtonContainer,
+ endContextualContainer,
+ startContextualContainer
+ )
+ }
deviceProfile.isTaskbarPresent -> {
return when {
isInSetup -> {
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
new file mode 100644
index 0000000..8525c6c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.launcher3.taskbar.navbutton
+
+import android.content.res.Resources
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import com.android.launcher3.DeviceProfile
+
+/** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */
+class PhoneGestureLayoutter(
+ resources: Resources,
+ navBarContainer: LinearLayout,
+ endContextualContainer: ViewGroup,
+ startContextualContainer: ViewGroup
+) :
+ AbstractNavButtonLayoutter(
+ resources,
+ navBarContainer,
+ endContextualContainer,
+ startContextualContainer
+ ) {
+
+ override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
+ // no-op
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index 92715a7..2acd5d4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -42,17 +42,15 @@
override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
// TODO(b/230395757): Polish pending, this is just to make it usable
- val navContainerParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
val taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(dp, resources,
TaskbarManager.isPhoneMode(dp))
navButtonContainer.removeAllViews()
navButtonContainer.orientation = LinearLayout.VERTICAL
+ val navContainerParams = FrameLayout.LayoutParams(
+ taskbarDimensions.x, ViewGroup.LayoutParams.MATCH_PARENT)
navContainerParams.apply {
- width = taskbarDimensions.x
- height = ViewGroup.LayoutParams.MATCH_PARENT
- gravity = Gravity.CENTER
topMargin = endStartMargins
bottomMargin = endStartMargins
marginEnd = 0
@@ -65,6 +63,7 @@
navButtonContainer.addView(backButton)
navButtonContainer.layoutParams = navContainerParams
+ navButtonContainer.gravity = Gravity.CENTER
// Add the spaces in between the nav buttons
val spaceInBetween: Int =
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
index 7f7fda7..c763115 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
@@ -41,28 +41,31 @@
override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
// TODO(b/230395757): Polish pending, this is just to make it usable
- val navContainerParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
val taskbarDimensions =
DimensionUtils.getTaskbarPhoneDimensions(dp, resources,
TaskbarManager.isPhoneMode(dp))
val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
- navContainerParams.width = taskbarDimensions.x
- navContainerParams.height = ViewGroup.LayoutParams.MATCH_PARENT
- navContainerParams.gravity = Gravity.CENTER_VERTICAL
// Ensure order of buttons is correct
navButtonContainer.removeAllViews()
navButtonContainer.orientation = LinearLayout.HORIZONTAL
- navContainerParams.topMargin = 0
- navContainerParams.bottomMargin = 0
- navContainerParams.marginEnd = endStartMargins
- navContainerParams.marginStart = endStartMargins
+
+ val navContainerParams = FrameLayout.LayoutParams(
+ taskbarDimensions.x, ViewGroup.LayoutParams.MATCH_PARENT)
+ navContainerParams.apply {
+ topMargin = 0
+ bottomMargin = 0
+ marginEnd = endStartMargins
+ marginStart = endStartMargins
+ }
+
// Swap recents and back button in case we were landscape prior to this
navButtonContainer.addView(backButton)
navButtonContainer.addView(homeButton)
navButtonContainer.addView(recentsButton)
navButtonContainer.layoutParams = navContainerParams
+ navButtonContainer.gravity = Gravity.CENTER
// Add the spaces in between the nav buttons
val spaceInBetween =
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
index 5ec7ca0..8332b7d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
@@ -40,7 +40,6 @@
override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
// Add spacing after the end of the last nav button
- val navButtonParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
var navMarginEnd = resources.getDimension(dp.inv.inlineNavButtonsEndSpacing).toInt()
val contextualWidth = endContextualContainer.width
// If contextual buttons are showing, we check if the end margin is enough for the
@@ -50,10 +49,10 @@
navMarginEnd += resources.getDimensionPixelSize(R.dimen.taskbar_hotseat_nav_spacing) / 2
}
+ val navButtonParams = FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
navButtonParams.apply {
- gravity = Gravity.END
- width = FrameLayout.LayoutParams.WRAP_CONTENT
- height = ViewGroup.LayoutParams.MATCH_PARENT
+ gravity = Gravity.END or Gravity.CENTER_VERTICAL
marginEnd = navMarginEnd
}
navButtonContainer.orientation = LinearLayout.HORIZONTAL
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index add7279..ff00560 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.graphics.Insets;
+import android.media.permission.SafeCloseable;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@@ -29,6 +30,7 @@
import androidx.annotation.NonNull;
+import com.android.app.viewcapture.SettingsAwareViewCapture;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -44,6 +46,7 @@
BaseDragLayer<TaskbarOverlayContext> implements
ViewTreeObserver.OnComputeInternalInsetsListener {
+ private SafeCloseable mViewCaptureCloseable;
private final List<OnClickListener> mOnClickListeners = new CopyOnWriteArrayList<>();
private final TouchController mClickListenerTouchController = new TouchController() {
@Override
@@ -77,12 +80,15 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+ .startCapture(getRootView(), ".TaskbarOverlay");
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+ mViewCaptureCloseable.close();
}
@Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index a53dc15..475f465 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -16,13 +16,16 @@
package com.android.launcher3.uioverrides;
+import android.app.ActivityOptions;
import android.app.Person;
import android.content.Context;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
+import android.window.RemoteTransition;
import com.android.launcher3.Utilities;
+import com.android.quickstep.util.FadeOutRemoteTransition;
import java.util.Map;
@@ -41,4 +44,13 @@
public static Map<String, LauncherActivityInfo> getActivityOverrides(Context context) {
return context.getSystemService(LauncherApps.class).getActivityOverrides();
}
+
+ /**
+ * Creates an ActivityOptions to play fade-out animation on closing targets
+ */
+ public static ActivityOptions createFadeOutAnimOptions(Context context) {
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setRemoteTransition(new RemoteTransition(new FadeOutRemoteTransition()));
+ return options;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 6c73a2d..f2d4660 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -58,7 +58,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.content.Context;
@@ -75,13 +74,11 @@
import android.media.permission.SafeCloseable;
import android.os.Build;
import android.os.Bundle;
-import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.SystemProperties;
import android.os.Trace;
import android.view.Display;
import android.view.HapticFeedbackConstants;
-import android.view.RemoteAnimationTarget;
import android.view.View;
import android.window.BackEvent;
import android.window.OnBackAnimationCallback;
@@ -109,7 +106,6 @@
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.appprediction.PredictionRowView;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.hybridhotseat.HotseatPredictionController;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.StatsLogManager;
@@ -117,6 +113,7 @@
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.WellbeingModel;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter;
import com.android.launcher3.statehandlers.DepthController;
@@ -162,8 +159,6 @@
import com.android.quickstep.util.LauncherUnfoldAnimationController;
import com.android.quickstep.util.ProxyScreenStatusProvider;
import com.android.quickstep.util.QuickstepOnboardingPrefs;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.util.RemoteFadeOutAnimationListener;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.util.SplitToWorkspaceController;
import com.android.quickstep.util.SplitWithKeyboardShortcutController;
@@ -173,6 +168,7 @@
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.unfold.RemoteUnfoldSharedComponent;
import com.android.systemui.unfold.UnfoldSharedComponent;
@@ -200,9 +196,15 @@
public static final boolean ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true);
+ private static final boolean TRACE_LAYOUTS =
+ SystemProperties.getBoolean("persist.debug.trace_layouts", false);
+ private static final String TRACE_RELAYOUT_CLASS =
+ SystemProperties.get("persist.debug.trace_request_layout_class", null);
public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
+ protected static final String RING_APPEAR_ANIMATION_PREFIX = "RingAppearAnimation\t";
+
private FixedContainerItems mAllAppsPredictions;
private HotseatPredictionController mHotseatPredictionController;
private DepthController mDepthController;
@@ -212,7 +214,6 @@
private TISBindHelper mTISBindHelper;
private @Nullable LauncherTaskbarUIController mTaskbarUIController;
// Will be updated when dragging from taskbar.
- private @Nullable DragOptions mNextWorkspaceDragOptions = null;
private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider;
private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController;
@@ -257,6 +258,7 @@
mDesktopVisibilityController = new DesktopVisibilityController(this);
if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
mDesktopVisibilityController.registerSystemUiListener();
+ mSplitSelectStateController.initSplitFromDesktopController(this);
}
mHotseatPredictionController = new HotseatPredictionController(this);
@@ -609,6 +611,8 @@
mViewCapture = SettingsAwareViewCapture.getInstance(this).startCapture(getWindow());
}
getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE);
+ View.setTraceLayoutSteps(TRACE_LAYOUTS);
+ View.setTracedRequestLayoutClassClass(TRACE_RELAYOUT_CLASS);
}
@Override
@@ -616,9 +620,10 @@
RecentsView recentsView = getOverviewPanel();
// Check if there is already an instance of this app running, if so, initiate the split
// using that.
- mSplitSelectStateController.findLastActiveTaskAndRunCallback(
- splitSelectSource.itemInfo.getComponentKey(),
- foundTask -> {
+ mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+ Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
boolean taskWasFound = foundTask != null;
splitSelectSource.alreadyRunningTaskId = taskWasFound
? foundTask.key.id
@@ -662,6 +667,8 @@
@Override
public void onAnimationCancel(Animator animation) {
getDragLayer().removeView(floatingTaskView);
+ mSplitSelectStateController.getSplitAnimationController()
+ .removeSplitInstructionsView(QuickstepLauncher.this);
mSplitSelectStateController.resetState();
}
});
@@ -863,7 +870,7 @@
if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
DesktopVisibilityController controller = mDesktopVisibilityController;
if (controller != null && controller.areFreeformTasksVisible()
- && !controller.isGestureInProgress()) {
+ && !controller.isRecentsGestureInProgress()) {
// Return early to skip setting activity to appear as resumed
// TODO(b/255649902): shouldn't be needed when we have a separate launcher state
// for desktop that we can use to control other parts of launcher
@@ -1034,41 +1041,6 @@
}
@Override
- public DragOptions getDefaultWorkspaceDragOptions() {
- if (mNextWorkspaceDragOptions != null) {
- DragOptions options = mNextWorkspaceDragOptions;
- mNextWorkspaceDragOptions = null;
- return options;
- }
- return super.getDefaultWorkspaceDragOptions();
- }
-
- public void setNextWorkspaceDragOptions(DragOptions dragOptions) {
- mNextWorkspaceDragOptions = dragOptions;
- }
-
- @Override
- public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
- QuickstepTransitionManager appTransitionManager = getAppTransitionManager();
- appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
- @Override
- public AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets,
- RemoteAnimationTarget[] wallpaperTargets) {
-
- // On the first call clear the reference.
- signal.cancel();
-
- ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0);
- fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(appTargets,
- wallpaperTargets));
- AnimatorSet anim = new AnimatorSet();
- anim.play(fadeAnimation);
- return anim;
- }
- }, signal);
- }
-
- @Override
public float[] getNormalOverviewScaleAndOffset() {
return DisplayController.getNavigationMode(this).hasGestures
? new float[] {1, 1} : new float[] {1.1f, NO_OFFSET};
@@ -1322,6 +1294,13 @@
: groupTask.mSplitBounds.leftTaskPercent);
}
+ /**
+ * Launches two apps as an app pair.
+ */
+ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2);
+ }
+
public boolean canStartHomeSafely() {
OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely();
@@ -1363,5 +1342,8 @@
if (recentsView != null) {
recentsView.getSplitSelectController().dump(prefix, writer);
}
+ if (mAppTransitionManager != null) {
+ mAppTransitionManager.dump(prefix + "\t" + RING_APPEAR_ANIMATION_PREFIX, writer);
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 41bcb79..6651c73 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -55,12 +55,14 @@
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
import android.animation.ValueAnimator;
+import android.util.Log;
import com.android.launcher3.CellLayout;
import com.android.launcher3.Hotseat;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Workspace;
import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.touch.AllAppsSwipeController;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.DisplayController;
@@ -94,7 +96,8 @@
@Override
public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
StateAnimationConfig config) {
-
+ Log.d(TestProtocol.OVERVIEW_OVER_HOME, "creating animation fromState: "
+ + fromState + " toState: " + toState);
RecentsView overview = mActivity.getOverviewPanel();
if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) {
overview.switchToScreenshot(() ->
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index d3ef589..6f421eb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -16,7 +16,6 @@
package com.android.launcher3.uioverrides.touchcontrollers;
import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_MOVE;
import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
import static com.android.app.animation.Interpolators.DECELERATE_3;
@@ -87,6 +86,7 @@
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.LauncherRecentsView;
import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
/**
* Handles quick switching to a recent task from the home screen. To give as much flexibility to
@@ -191,6 +191,9 @@
public void onDragStart(boolean start) {
mMotionPauseDetector.clear();
if (start) {
+ InteractionJankMonitorWrapper.begin(mRecentsView,
+ InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+
mStartState = mLauncher.getStateManager().getState();
mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
@@ -325,6 +328,7 @@
if (mMotionPauseDetector.isPaused() && noFling) {
// Going to Overview.
cancelAnimations();
+ InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
StateAnimationConfig config = new StateAnimationConfig();
config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
@@ -441,6 +445,8 @@
RecentsView.SCROLL_VIBRATION_PRIMITIVE,
RecentsView.SCROLL_VIBRATION_PRIMITIVE_SCALE,
RecentsView.SCROLL_VIBRATION_FALLBACK);
+ } else {
+ InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
}
nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
@@ -462,6 +468,11 @@
: targetState.ordinal > mStartState.ordinal
? LAUNCHER_UNKNOWN_SWIPEUP
: LAUNCHER_UNKNOWN_SWIPEDOWN));
+
+ if (targetState == QUICK_SWITCH_FROM_HOME) {
+ InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+ }
+
mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index ff757b1..796840d 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -106,6 +106,7 @@
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarThresholdUtils;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
@@ -379,12 +380,12 @@
mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed();
mIsTaskbarAllAppsOpen = controller != null && controller.isTaskbarAllAppsOpen();
mTaskbarAppWindowThreshold =
- res.getDimensionPixelSize(R.dimen.taskbar_app_window_threshold);
+ TaskbarThresholdUtils.getAppWindowThreshold(res, mDp);
boolean swipeWillNotShowTaskbar = mTaskbarAlreadyOpen || mGestureState.isTrackpadGesture();
mTaskbarHomeOverviewThreshold = swipeWillNotShowTaskbar
? 0
- : res.getDimensionPixelSize(R.dimen.taskbar_home_overview_threshold);
- mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold);
+ : TaskbarThresholdUtils.getHomeOverviewThreshold(res, mDp);
+ mTaskbarCatchUpThreshold = TaskbarThresholdUtils.getCatchUpThreshold(res, mDp);
}
@Nullable
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index ab37493..c5a88bc 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -139,6 +139,7 @@
mTmpMatrix.setScale(scale, scale,
app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
builder.setMatrix(mTmpMatrix).setAlpha(alpha);
+ builder.setShow();
}
@Override
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index c18ad5a..f1660ee 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -129,7 +129,7 @@
mWindowMaxDeltaY = mLauncher.getResources().getDimensionPixelSize(
R.dimen.swipe_back_window_max_delta_y);
mCancelInterpolator =
- AnimationUtils.loadInterpolator(mLauncher, R.interpolator.back_cancel);
+ AnimationUtils.loadInterpolator(mLauncher, R.interpolator.standard_interpolator);
}
/**
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 60784f5..e73b525 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -84,6 +84,7 @@
import com.android.wm.shell.recents.IRecentTasksListener;
import com.android.wm.shell.splitscreen.ISplitScreen;
import com.android.wm.shell.splitscreen.ISplitScreenListener;
+import com.android.wm.shell.splitscreen.ISplitSelectListener;
import com.android.wm.shell.startingsurface.IStartingWindow;
import com.android.wm.shell.startingsurface.IStartingWindowListener;
import com.android.wm.shell.transition.IShellTransitions;
@@ -128,6 +129,7 @@
private IPipAnimationListener mPipAnimationListener;
private IBubblesListener mBubblesListener;
private ISplitScreenListener mSplitScreenListener;
+ private ISplitSelectListener mSplitSelectListener;
private IStartingWindowListener mStartingWindowListener;
private ILauncherUnlockAnimationController mLauncherUnlockAnimationController;
private IRecentTasksListener mRecentTasksListener;
@@ -239,6 +241,7 @@
setPipAnimationListener(mPipAnimationListener);
setBubblesListener(mBubblesListener);
registerSplitScreenListener(mSplitScreenListener);
+ registerSplitSelectListener(mSplitSelectListener);
setStartingWindowListener(mStartingWindowListener);
setLauncherUnlockAnimationController(mLauncherUnlockAnimationController);
new LinkedHashMap<>(mRemoteTransitions).forEach(this::registerRemoteTransition);
@@ -662,6 +665,31 @@
}
/**
+ * Tells SysUI to remove the bubble with the provided key.
+ * @param key the key of the bubble to show.
+ */
+ public void removeBubble(String key) {
+ if (mBubbles == null) return;
+ try {
+ mBubbles.removeBubble(key);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call removeBubble");
+ }
+ }
+
+ /**
+ * Tells SysUI to remove all bubbles.
+ */
+ public void removeAllBubbles() {
+ if (mBubbles == null) return;
+ try {
+ mBubbles.removeAllBubbles();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call removeAllBubbles");
+ }
+ }
+
+ /**
* Tells SysUI to collapse the bubbles.
*/
public void collapseBubbles() {
@@ -674,6 +702,21 @@
}
}
+ /**
+ * Tells SysUI when the bubble is being dragged.
+ * Should be called only when the bubble bar is expanded.
+ * @param bubbleKey the key of the bubble to collapse/expand
+ * @param isBeingDragged whether the bubble is being dragged
+ */
+ public void onBubbleDrag(@Nullable String bubbleKey, boolean isBeingDragged) {
+ if (mBubbles == null) return;
+ try {
+ mBubbles.onBubbleDrag(bubbleKey, isBeingDragged);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call onBubbleDrag");
+ }
+ }
+
//
// Splitscreen
//
@@ -700,6 +743,28 @@
mSplitScreenListener = null;
}
+ public void registerSplitSelectListener(ISplitSelectListener listener) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.registerSplitSelectListener(listener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call registerSplitSelectListener");
+ }
+ }
+ mSplitSelectListener = listener;
+ }
+
+ public void unregisterSplitSelectListener(ISplitSelectListener listener) {
+ if (mSplitScreen != null) {
+ try {
+ mSplitScreen.unregisterSplitSelectListener(listener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call unregisterSplitSelectListener");
+ }
+ }
+ mSplitSelectListener = null;
+ }
+
/** Start multiple tasks in split-screen simultaneously. */
public void startTasks(int taskId1, Bundle options1, int taskId2, Bundle options2,
@SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio,
@@ -1241,6 +1306,17 @@
}
}
+ /** Perform cleanup transactions after animation to split select is complete */
+ public void onDesktopSplitSelectAnimComplete(ActivityManager.RunningTaskInfo taskInfo) {
+ if (mDesktopMode != null) {
+ try {
+ mDesktopMode.onDesktopSplitSelectAnimComplete(taskInfo);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call onDesktopSplitSelectAnimComplete", e);
+ }
+ }
+ }
+
//
// Unfold transition
//
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 739f10e..6dbb5bf 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
@@ -37,6 +38,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.DisplayController;
import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.views.DesktopTaskView;
@@ -169,10 +171,16 @@
for (RemoteAnimationTarget compat : appearedTaskTargets) {
if (compat.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME
- && activityInterface.getCreatedActivity() instanceof RecentsActivity) {
- // When receive opening home activity while recents is running, enter home
- // and dismiss recents.
- ((RecentsActivity) activityInterface.getCreatedActivity()).startHome();
+ && activityInterface.getCreatedActivity() instanceof RecentsActivity
+ && DisplayController.getNavigationMode(mCtx) != NO_BUTTON) {
+ // The only time we get onTasksAppeared() in button navigation with a
+ // 3p launcher is if the user goes to overview first, and in this case we
+ // can immediately finish the transition
+ RecentsView recentsView =
+ activityInterface.getCreatedActivity().getOverviewPanel();
+ if (recentsView != null) {
+ recentsView.finishRecentsAnimation(true, null);
+ }
return;
}
}
@@ -206,7 +214,8 @@
if (mLastAppearedTaskTargets != null) {
for (RemoteAnimationTarget lastTarget : mLastAppearedTaskTargets) {
for (RemoteAnimationTarget appearedTarget : appearedTaskTargets) {
- if (appearedTarget.taskId != lastTarget.taskId) {
+ if (lastTarget != null &&
+ appearedTarget.taskId != lastTarget.taskId) {
mController.removeTaskTarget(lastTarget.taskId);
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 1744b08..06f1f9a 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -159,6 +159,10 @@
return mActionsView;
}
+ public TaskThumbnailView getThumbnailView() {
+ return mThumbnailView;
+ }
+
/**
* Called when the current task is interactive for the user
*/
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 56f407c..901690b 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -140,6 +140,7 @@
@Override
public void onClick(View view) {
+ dismissTaskMenuView(mTarget);
((RecentsView) mTarget.getOverviewPanel())
.getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 538aba9..af49774 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -38,6 +38,8 @@
import static com.android.launcher3.QuickstepTransitionManager.SPLIT_DIVIDER_ANIM_DURATION;
import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION;
import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED;
@@ -411,26 +413,21 @@
}
/**
- * TODO: This doesn't animate at present. Feel free to blow out everyhing in this method
- * if needed
+ * If {@param launchingTaskView} is not null, then this will play the tasks launch animation
+ * from the position of the GroupedTaskView (when user taps on the TaskView to start it).
+ * Technically this case should be taken care of by
+ * {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether
+ * it's a single task or multiple tasks results in different entry-points.
*
- * 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
+ * If it is null, then it will simply fade in the starting apps and fade out launcher (for the
+ * case where launcher handles animating starting split tasks from app icon)
*/
public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
@NonNull StateManager stateManager, @Nullable DepthController depthController,
int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
if (launchingTaskView != null) {
+ testLogD(LAUNCH_SPLIT_PAIR, "composeRecentsSplitLaunchAnimator taskView not-null");
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
@@ -458,12 +455,15 @@
return;
}
- // TODO: consider initialTaskPendingIntent
TransitionInfo.Change splitRoot1 = null;
TransitionInfo.Change splitRoot2 = null;
+ final ArrayList<SurfaceControl> openingTargets = new ArrayList<>();
for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
- if (change.getTaskInfo() == null) continue;
+ if (change.getTaskInfo() == null) {
+ testLogD(LAUNCH_SPLIT_PAIR, "changeTaskInfo null; change: " + change);
+ continue;
+ }
final int taskId = change.getTaskInfo().taskId;
final int mode = change.getMode();
@@ -478,31 +478,48 @@
if (taskId == initialTaskId) {
splitRoot1 = change.getParent() == null ? change :
transitionInfo.getChange(change.getParent());
+ openingTargets.add(splitRoot1.getLeash());
}
if (taskId == secondTaskId) {
splitRoot2 = change.getParent() == null ? change :
transitionInfo.getChange(change.getParent());
+ openingTargets.add(splitRoot2.getLeash());
}
}
- // This is where we should animate the split roots. For now, though, just make them visible.
- animateSplitRoot(t, splitRoot1);
- animateSplitRoot(t, splitRoot2);
+ SurfaceControl.Transaction animTransaction = new SurfaceControl.Transaction();
+ ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+ animator.setDuration(SPLIT_LAUNCH_DURATION);
+ animator.addUpdateListener(valueAnimator -> {
+ float progress = valueAnimator.getAnimatedFraction();
+ for (SurfaceControl leash: openingTargets) {
+ animTransaction.setAlpha(leash, progress);
+ }
+ animTransaction.apply();
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ for (SurfaceControl leash: openingTargets) {
+ animTransaction.show(leash)
+ .setAlpha(leash, 0.0f);
+ }
+ animTransaction.apply();
+ }
- // This contains the initial state (before animation), so apply this at the beginning of
- // the animation.
- t.apply();
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finishCallback.run();
+ }
+ });
- // Once there is an animation, this should be called AFTER the animation completes.
- finishCallback.run();
- }
-
- private static void animateSplitRoot(SurfaceControl.Transaction t,
- TransitionInfo.Change splitRoot) {
- if (splitRoot != null) {
- t.show(splitRoot.getLeash());
- t.setAlpha(splitRoot.getLeash(), 1.f);
+ if (splitRoot1 != null && splitRoot1.getParent() != null) {
+ // Set the highest level split root alpha; we could technically use the parent of either
+ // splitRoot1 or splitRoot2
+ t.setAlpha(transitionInfo.getChange(splitRoot1.getParent()).getLeash(), 1f);
}
+ t.apply();
+ animator.start();
}
/**
@@ -515,7 +532,9 @@
* it's a single task or multiple tasks results in different entry-points.
*
* If it is null, then it will simply fade in the starting apps and fade out launcher (for the
- * case where launcher handles animating starting split tasks from app icon) */
+ * case where launcher handles animating starting split tasks from app icon)
+ * @deprecated with shell transitions
+ */
public static void composeRecentsSplitLaunchAnimatorLegacy(
@Nullable GroupedTaskView launchingTaskView, int initialTaskId, int secondTaskId,
@NonNull RemoteAnimationTarget[] appTargets,
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index bb12356..7a9f88a 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -56,6 +56,8 @@
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.app.Service;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@@ -571,15 +573,7 @@
AccessibilityManager am = getSystemService(AccessibilityManager.class);
if (isHomeAndOverviewSame) {
- Intent intent = new Intent(mOverviewComponentObserver.getHomeIntent())
- .setAction(INTENT_ACTION_ALL_APPS_TOGGLE);
- RemoteAction allAppsAction = new RemoteAction(
- Icon.createWithResource(this, R.drawable.ic_apps),
- getString(R.string.all_apps_label),
- getString(R.string.all_apps_label),
- PendingIntent.getActivity(this, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, intent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
- am.registerSystemAction(allAppsAction, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
+ am.registerSystemAction(createAllAppsAction(), GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
} else {
am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
}
@@ -592,6 +586,35 @@
mTISBinder.onOverviewTargetChange();
}
+ private RemoteAction createAllAppsAction() {
+ final Intent homeIntent = new Intent(mOverviewComponentObserver.getHomeIntent())
+ .setAction(INTENT_ACTION_ALL_APPS_TOGGLE);
+ final PendingIntent actionPendingIntent;
+
+ if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
+ actionPendingIntent = new PendingIntent(new IIntentSender.Stub() {
+ @Override
+ public void send(int code, Intent intent, String resolvedType,
+ IBinder allowlistToken, IIntentReceiver finishedReceiver,
+ String requiredPermission, Bundle options) {
+ MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllApps(homeIntent));
+ }
+ });
+ } else {
+ actionPendingIntent = PendingIntent.getActivity(
+ this,
+ GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
+ homeIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ return new RemoteAction(
+ Icon.createWithResource(this, R.drawable.ic_apps),
+ getString(R.string.all_apps_label),
+ getString(R.string.all_apps_label),
+ actionPendingIntent);
+ }
+
@UiThread
private void onSystemUiFlagsChanged(int lastSysUIFlags) {
if (LockedUserState.get(this).isUserUnlocked()) {
@@ -888,7 +911,8 @@
.append(SUBSTRING_PREFIX)
.append("TaskbarActivityContext != null, "
+ "using TaskbarUnstashInputConsumer");
- base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac);
+ base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
+ mOverviewCommandHelper);
}
} else if (canStartSystemGesture && FeatureFlags.ENABLE_LONG_PRESS_NAV_HANDLE.get()) {
base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index e9a0761..4b13cd1 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -15,6 +15,7 @@
*/
package com.android.quickstep.inputconsumers;
+import static android.view.MotionEvent.ACTION_BUTTON_RELEASE;
import static android.view.MotionEvent.INVALID_POINTER_ID;
import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -36,11 +37,13 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarThresholdUtils;
import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.InputConsumer;
+import com.android.quickstep.OverviewCommandHelper;
import com.android.systemui.shared.system.InputMonitorCompat;
/**
@@ -51,6 +54,7 @@
public class TaskbarUnstashInputConsumer extends DelegateInputConsumer {
private final TaskbarActivityContext mTaskbarActivityContext;
+ private final OverviewCommandHelper mOverviewCommandHelper;
private final GestureDetector mLongPressDetector;
private final float mSquaredTouchSlop;
@@ -80,16 +84,19 @@
private final @Nullable TransitionCallback mTransitionCallback;
public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
- InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
+ InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext,
+ OverviewCommandHelper overviewCommandHelper) {
super(delegate, inputMonitor);
mTaskbarActivityContext = taskbarActivityContext;
+ mOverviewCommandHelper = overviewCommandHelper;
// TODO(b/270395798): remove this when cleaning up old Persistent Taskbar code.
mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
Resources res = context.getResources();
mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
- mTaskbarNavThreshold = res.getDimensionPixelSize(R.dimen.taskbar_from_nav_threshold);
+ mTaskbarNavThreshold = TaskbarThresholdUtils.getFromNavThreshold(res,
+ taskbarActivityContext.getDeviceProfile());
mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx
- mTaskbarNavThreshold;
mIsTaskbarAllAppsOpen =
@@ -123,7 +130,11 @@
public void onMotionEvent(MotionEvent ev) {
mLongPressDetector.onTouchEvent(ev);
if (mState != STATE_ACTIVE) {
- mDelegate.onMotionEvent(ev);
+ boolean isStashedTaskbarHovered =
+ isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY());
+ if (!isStashedTaskbarHovered) {
+ mDelegate.onMotionEvent(ev);
+ }
// Only show the transient task bar if the touch events are on the screen.
if (mTaskbarActivityContext != null && !isTrackpadMotionEvent(ev)) {
@@ -218,6 +229,11 @@
mHasPassedTaskbarNavThreshold = false;
mIsInBubbleBarArea = false;
break;
+ case ACTION_BUTTON_RELEASE:
+ if (isStashedTaskbarHovered) {
+ mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME);
+ }
+ break;
}
}
}
@@ -274,19 +290,17 @@
private void updateHoveredTaskbarState(int x, int y) {
DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
- mStashedTaskbarHandleBounds.set(
+ mBottomEdgeBounds.set(
(dp.widthPx - (int) mUnstashArea) / 2,
- dp.heightPx - dp.stashedTaskbarHeight,
+ dp.heightPx - mStashedTaskbarBottomEdge,
(int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
dp.heightPx);
- mBottomEdgeBounds.set(mStashedTaskbarHandleBounds);
- mBottomEdgeBounds.top = dp.heightPx - mStashedTaskbarBottomEdge;
if (mBottomEdgeBounds.contains(x, y)) {
// If hovering stashed taskbar and then hover screen bottom edge, unstash it.
mTaskbarActivityContext.onSwipeToUnstashTaskbar();
mIsStashedTaskbarHovered = false;
- } else if (!mStashedTaskbarHandleBounds.contains(x, y)) {
+ } else if (!isStashedTaskbarHovered(x, y)) {
// If exit hovering stashed taskbar, remove hint.
startStashedTaskbarHover(/* isHovered = */ false);
}
@@ -294,18 +308,13 @@
private void updateUnhoveredTaskbarState(int x, int y) {
DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
- mStashedTaskbarHandleBounds.set(
- (dp.widthPx - (int) mUnstashArea) / 2,
- dp.heightPx - dp.stashedTaskbarHeight,
- (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
- dp.heightPx);
mBottomEdgeBounds.set(
0,
dp.heightPx - mBottomScreenEdge,
dp.widthPx,
dp.heightPx);
- if (mStashedTaskbarHandleBounds.contains(x, y)) {
+ if (isStashedTaskbarHovered(x, y)) {
// If enter hovering stashed taskbar, start hint.
startStashedTaskbarHover(/* isHovered = */ true);
} else if (mBottomEdgeBounds.contains(x, y)) {
@@ -318,4 +327,19 @@
mTaskbarActivityContext.startTaskbarUnstashHint(isHovered, /* forceUnstash = */ true);
mIsStashedTaskbarHovered = isHovered;
}
+
+ private boolean isStashedTaskbarHovered(int x, int y) {
+ if (!mTaskbarActivityContext.isTaskbarStashed()
+ || mTaskbarActivityContext.isTaskbarAllAppsOpen()
+ || !ENABLE_CURSOR_HOVER_STATES.get()) {
+ return false;
+ }
+ DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+ mStashedTaskbarHandleBounds.set(
+ (dp.widthPx - (int) mUnstashArea) / 2,
+ dp.heightPx - dp.stashedTaskbarHeight,
+ (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+ dp.heightPx);
+ return mStashedTaskbarHandleBounds.contains(x, y);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index f11bc81..49814df 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -95,9 +95,10 @@
private static final float ANIMATION_PAUSE_ALPHA_THRESHOLD = 0.1f;
+ private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate);
+
private TISBindHelper mTISBindHelper;
- private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate);
private BgDrawable mBackground;
private View mRootView;
private float mSwipeUpShift;
@@ -172,7 +173,7 @@
LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary),
getTheme());
- startBackgroundAnimation();
+ startBackgroundAnimation(dp.isTablet);
}
private void runOnUiHelperThread(Runnable runnable) {
@@ -183,7 +184,7 @@
Executors.UI_HELPER_EXECUTOR.execute(runnable);
}
- private void startBackgroundAnimation() {
+ private void startBackgroundAnimation(boolean forTablet) {
if (!Utilities.ATLEAST_S || mVibrator == null) {
return;
}
@@ -199,7 +200,7 @@
.addPrimitive(supportsThud
? VibrationEffect.Composition.PRIMITIVE_THUD
: VibrationEffect.Composition.PRIMITIVE_TICK,
- /* scale= */ 1.0f,
+ /* scale= */ forTablet ? 1.0f : 0.3f,
/* delay= */ 50)
.compose();
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 135cb72..631cff7 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -154,7 +154,7 @@
@Override
public void onBackGestureAttempted(BackGestureResult result) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
switch (mTutorialType) {
@@ -172,7 +172,7 @@
@Override
public void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
@@ -241,7 +241,7 @@
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index a16b239..b379baa 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -42,6 +42,12 @@
super(fromTutorialMenu);
}
+ @NonNull
+ @Override
+ TutorialType getDefaultTutorialType() {
+ return TutorialType.BACK_NAVIGATION;
+ }
+
@Nullable
@Override
Integer getEdgeAnimationResId() {
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 2189a24..d102502 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -53,16 +53,14 @@
static final String KEY_USE_TUTORIAL_MENU = "use_tutorial_menu";
@Nullable private TutorialType[] mTutorialSteps;
- private GestureSandboxFragment mFragment;
+ private GestureSandboxFragment mCurrentFragment;
+ private GestureSandboxFragment mPendingFragment;
private int mCurrentStep;
private int mNumSteps;
- private boolean mShowRotationPrompt;
private SharedPreferences mSharedPrefs;
private StatsLogManager mStatsLogManager;
-
- private View mRotationPrompt;
private TISBindHelper mTISBindHelper;
@Override
@@ -82,7 +80,7 @@
&& args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) {
mTutorialSteps = null;
TutorialType tutorialTypeOverride = (TutorialType) args.get(KEY_TUTORIAL_TYPE);
- mFragment = tutorialTypeOverride == null
+ mCurrentFragment = tutorialTypeOverride == null
? new MenuFragment()
: makeTutorialFragment(
tutorialTypeOverride,
@@ -90,16 +88,15 @@
/* fromMenu= */ true);
} else {
mTutorialSteps = getTutorialSteps(args);
- mFragment = makeTutorialFragment(
+ mCurrentFragment = makeTutorialFragment(
mTutorialSteps[mCurrentStep - 1],
gestureComplete,
/* fromMenu= */ false);
}
getSupportFragmentManager().beginTransaction()
- .add(R.id.gesture_tutorial_fragment_container, mFragment)
+ .add(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
.commit();
- mRotationPrompt = findViewById(R.id.rotation_prompt);
if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
correctUserOrientation();
}
@@ -127,34 +124,45 @@
DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.get(
getApplicationContext()).getDeviceProfile(this);
if (deviceProfile.isTablet) {
- mShowRotationPrompt = getResources().getConfiguration().orientation
+ boolean showRotationPrompt = getResources().getConfiguration().orientation
== ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
- updateVisibility(mRotationPrompt, mShowRotationPrompt ? View.VISIBLE : View.GONE);
+
+ GestureSandboxFragment recreatedFragment =
+ showRotationPrompt || mPendingFragment == null
+ ? null : mPendingFragment.recreateFragment();
+ showFragment(showRotationPrompt
+ ? new RotationPromptFragment()
+ : recreatedFragment == null
+ ? mCurrentFragment : recreatedFragment);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
- void updateVisibility(View view, int visibility) {
- if (view == null || view.getVisibility() == visibility) {
- return;
+ private void showFragment(@NonNull GestureSandboxFragment fragment) {
+ if (mCurrentFragment.recreateFragment() != null) {
+ mPendingFragment = mCurrentFragment;
}
- view.setVisibility(visibility);
+ mCurrentFragment = fragment;
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
+ .runOnCommit(() -> mCurrentFragment.onAttachedToWindow())
+ .commit();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
- if (mFragment.shouldDisableSystemGestures()) {
+ if (mCurrentFragment.shouldDisableSystemGestures()) {
disableSystemGestures();
}
- mFragment.onAttachedToWindow();
+ mCurrentFragment.onAttachedToWindow();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
- mFragment.onDetachedFromWindow();
+ mCurrentFragment.onDetachedFromWindow();
}
@Override
@@ -169,14 +177,10 @@
protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
savedInstanceState.putStringArray(KEY_TUTORIAL_STEPS, getTutorialStepNames());
savedInstanceState.putInt(KEY_CURRENT_STEP, mCurrentStep);
- mFragment.onSaveInstanceState(savedInstanceState);
+ mCurrentFragment.onSaveInstanceState(savedInstanceState);
super.onSaveInstanceState(savedInstanceState);
}
- protected boolean isRotationPromptShowing() {
- return mShowRotationPrompt;
- }
-
protected SharedPreferences getSharedPrefs() {
return mSharedPrefs;
}
@@ -205,7 +209,7 @@
*/
public void continueTutorial() {
if (isTutorialComplete() || mTutorialSteps == null) {
- mFragment.close();
+ mCurrentFragment.close();
return;
}
launchTutorialStep(mTutorialSteps[mCurrentStep], false);
@@ -224,20 +228,12 @@
* the menu when complete.
*/
public void launchTutorialStep(@NonNull TutorialType tutorialType, boolean fromMenu) {
- mFragment = makeTutorialFragment(tutorialType, false, fromMenu);
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.gesture_tutorial_fragment_container, mFragment)
- .runOnCommit(() -> mFragment.onAttachedToWindow())
- .commit();
+ showFragment(makeTutorialFragment(tutorialType, false, fromMenu));
}
/** Launches the gesture nav tutorial menu page */
public void launchTutorialMenu() {
- mFragment = new MenuFragment();
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.gesture_tutorial_fragment_container, mFragment)
- .runOnCommit(() -> mFragment.onAttachedToWindow())
- .commit();
+ showFragment(new MenuFragment());
}
private String[] getTutorialStepNames() {
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java
index d52f19a..08f2989 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxFragment.java
@@ -17,6 +17,7 @@
import android.app.Activity;
+import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -27,6 +28,11 @@
void onDetachedFromWindow() {}
+ @Nullable
+ GestureSandboxFragment recreateFragment() {
+ return null;
+ }
+
boolean shouldDisableSystemGestures() {
return true;
}
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
index 0aa80d3..daac99b 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -147,7 +147,7 @@
@Override
public void onBackGestureAttempted(BackGestureResult result) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
switch (mTutorialType) {
@@ -174,7 +174,7 @@
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
switch (mTutorialType) {
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
index bced8c4..3e924d7 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
@@ -41,6 +41,12 @@
super(fromTutorialMenu);
}
+ @NonNull
+ @Override
+ TutorialType getDefaultTutorialType() {
+ return TutorialType.HOME_NAVIGATION;
+ }
+
@Nullable
@Override
Integer getEdgeAnimationResId() {
diff --git a/quickstep/src/com/android/quickstep/interaction/MenuFragment.java b/quickstep/src/com/android/quickstep/interaction/MenuFragment.java
index c19d44a..dbf141b 100644
--- a/quickstep/src/com/android/quickstep/interaction/MenuFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/MenuFragment.java
@@ -32,6 +32,12 @@
/** Displays the gesture nav tutorial menu. */
public final class MenuFragment extends GestureSandboxFragment {
+ @NonNull
+ @Override
+ GestureSandboxFragment recreateFragment() {
+ return new MenuFragment();
+ }
+
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
index 75b80b3..afdc1e5 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
@@ -179,7 +179,7 @@
@Override
public void onBackGestureAttempted(BackGestureResult result) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
switch (mTutorialType) {
@@ -206,7 +206,7 @@
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
switch (mTutorialType) {
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
index 01074dd..ee1c460 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialFragment.java
@@ -41,6 +41,12 @@
super(fromTutorialMenu);
}
+ @NonNull
+ @Override
+ TutorialType getDefaultTutorialType() {
+ return TutorialType.OVERVIEW_NAVIGATION;
+ }
+
@Nullable
@Override
Integer getEdgeAnimationResId() {
diff --git a/quickstep/src/com/android/quickstep/interaction/RotationPromptFragment.java b/quickstep/src/com/android/quickstep/interaction/RotationPromptFragment.java
new file mode 100644
index 0000000..89af647
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/RotationPromptFragment.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+
+/** Displays the prompt requesting that the user rotates their device. */
+public class RotationPromptFragment extends GestureSandboxFragment {
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.gesture_tutorial_rotation_prompt, container, false);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index d0d7534..87defc5 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -282,7 +282,7 @@
@Override
public void setNavBarGestureProgress(@Nullable Float displacement) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
if (mTutorialType == HOME_NAVIGATION_COMPLETE
@@ -303,7 +303,7 @@
@Override
public void onMotionPaused(boolean unused) {
- if (skipGestureAttempt()) {
+ if (isGestureCompleted()) {
return;
}
if (mShowTasks) {
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 4a1fec3..ed2d18a 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -489,10 +489,6 @@
return mGestureCompleted;
}
- public boolean skipGestureAttempt() {
- return isGestureCompleted() || mTutorialFragment.isRotationPromptShowing();
- }
-
void hideFeedback() {
if (mFeedbackView.getVisibility() != View.VISIBLE) {
return;
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 84326f5..a28cf58 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -100,6 +100,19 @@
return fragment;
}
+ @Nullable
+ @Override
+ GestureSandboxFragment recreateFragment() {
+ TutorialType tutorialType = mTutorialController == null
+ ? (mTutorialType == null
+ ? getDefaultTutorialType() : mTutorialType)
+ : mTutorialController.mTutorialType;
+ return newInstance(tutorialType, isGestureComplete(), mFromTutorialMenu);
+ }
+
+ @NonNull
+ abstract TutorialType getDefaultTutorialType();
+
TutorialFragment(boolean fromTutorialMenu) {
mFromTutorialMenu = fromTutorialMenu;
}
@@ -498,11 +511,6 @@
return activity != null ? activity.getStatsLogManager() : null;
}
- protected boolean isRotationPromptShowing() {
- GestureSandboxActivity activity = getGestureSandboxActivity();
- return activity != null && activity.isRotationPromptShowing();
- }
-
@Nullable
private SharedPreferences getSharedPreferences() {
GestureSandboxActivity activity = getGestureSandboxActivity();
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 1f06f94..be66637 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -62,14 +62,11 @@
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.LogConfig;
import com.android.launcher3.views.ActivityContext;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.SysUiStatsLog;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -564,14 +561,16 @@
* Helps to construct and log statsd compatible impression events.
*/
private static class StatsCompatImpressionLogger implements StatsImpressionLogger {
- private int[] mResultTypeList = new int[]{};
- private int[] mResultCountList = new int[]{};
- private final List<Boolean> mAboveKeyboardList = new ArrayList<>();
- private int[] mUidList = new int[]{};
private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
private State mLauncherState = State.UNKNOWN;
private int mQueryLength = -1;
+ // Fields used for Impression Logging V2.
+ private int mResultType;
+ private boolean mAboveKeyboard = false;
+ private int mUid;
+ private int mResultSource;
+
@Override
public StatsImpressionLogger withInstanceId(InstanceId instanceId) {
this.mInstanceId = instanceId;
@@ -591,69 +590,60 @@
}
@Override
- public StatsImpressionLogger withResultType(IntArray resultType) {
- mResultTypeList = resultType.toArray();
+ public StatsImpressionLogger withResultType(int resultType) {
+ mResultType = resultType;
+ return this;
+ }
+
+
+ @Override
+ public StatsImpressionLogger withAboveKeyboard(boolean aboveKeyboard) {
+ mAboveKeyboard = aboveKeyboard;
return this;
}
@Override
- public StatsImpressionLogger withResultCount(IntArray resultCount) {
- mResultCountList = resultCount.toArray();
+ public StatsImpressionLogger withUid(int uid) {
+ mUid = uid;
return this;
}
@Override
- public StatsImpressionLogger withAboveKeyboard(List<Boolean> aboveKeyboard) {
- mAboveKeyboardList.clear();
- this.mAboveKeyboardList.addAll(aboveKeyboard);
- return this;
- }
-
- @Override
- public StatsImpressionLogger withUids(IntArray uid) {
- mUidList = uid.toArray();
+ public StatsImpressionLogger withResultSource(int resultSource) {
+ mResultSource = resultSource;
return this;
}
@Override
public void log(EventEnum event) {
- boolean[] mAboveKeyboard = new boolean[mAboveKeyboardList.size()];
- for (int i = 0; i < mAboveKeyboardList.size(); i++) {
- mAboveKeyboard[i] = mAboveKeyboardList.get(i);
- }
if (IS_VERBOSE) {
String name = (event instanceof Enum) ? ((Enum) event).name() :
event.getId() + "";
StringBuilder logStringBuilder = new StringBuilder("\n");
logStringBuilder.append(String.format("InstanceId:%s ", mInstanceId));
logStringBuilder.append(String.format("ImpressionEvent:%s ", name));
- logStringBuilder.append(String.format("LauncherState = %s ", mLauncherState));
- logStringBuilder.append(String.format("QueryLength = %s ", mQueryLength));
- for (int i = 0; i < mResultTypeList.length; i++) {
- logStringBuilder.append(String.format(
- "\n ResultType = %s with ResultCount = %s with is_above_keyboard = %s"
- + " with uid = %s",
- mResultTypeList[i], mResultCountList[i],
- mAboveKeyboard[i], mUidList[i]));
- }
+ logStringBuilder.append(String.format("\n\tLauncherState = %s ", mLauncherState));
+ logStringBuilder.append(String.format("\tQueryLength = %s ", mQueryLength));
+ logStringBuilder.append(String.format(
+ "\n\t ResultType = %s is_above_keyboard = %s"
+ + " uid = %s result_source = %s",
+ mResultType,
+ mAboveKeyboard, mUid, mResultSource));
+
Log.d(IMPRESSION_TAG, logStringBuilder.toString());
}
-
- SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT,
+ SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT_V2,
event.getId(), // event_id
mInstanceId.getId(), // instance_id
mLauncherState.getLauncherState(), // state
mQueryLength, // query_length
- //result type list
- mResultTypeList,
- // result count list
- mResultCountList,
- // above keyboard list
- mAboveKeyboard,
- // uid list
- mUidList
+ mResultType, //result type
+ mAboveKeyboard, // above keyboard
+ mUid, // uid
+ mResultSource // result source
+
);
}
}
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index cbde257..1a7099d 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -17,19 +17,30 @@
package com.android.quickstep.util;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import android.app.ActivityTaskManager;
import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.Arrays;
/**
* Mini controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -52,10 +63,13 @@
private final Context mContext;
private final SplitSelectStateController mSplitSelectStateController;
+ private final StatsLogManager mStatsLogManager;
public AppPairsController(Context context,
- SplitSelectStateController splitSelectStateController) {
+ SplitSelectStateController splitSelectStateController,
+ StatsLogManager statsLogManager) {
mContext = context;
mSplitSelectStateController = splitSelectStateController;
+ mStatsLogManager = statsLogManager;
}
/**
@@ -84,11 +98,51 @@
LauncherAccessibilityDelegate delegate =
Launcher.getLauncher(mContext).getAccessibilityDelegate();
if (delegate != null) {
- MAIN_EXECUTOR.execute(() -> delegate.addToWorkspace(newAppPair, true));
+ delegate.addToWorkspace(newAppPair, true);
+ mStatsLogManager.logger().withItemInfo(newAppPair)
+ .log(StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_SAVE);
}
});
});
+ }
+ /**
+ * Launches an app pair by searching the RecentsModel for running instances of each app, and
+ * staging either those running instances or launching the apps as new Intents.
+ */
+ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
+ ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
+ mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+ Arrays.asList(app1Key, app2Key),
+ foundTasks -> {
+ @Nullable Task foundTask1 = foundTasks.get(0);
+ Intent task1Intent;
+ int task1Id;
+ if (foundTask1 != null) {
+ task1Id = foundTask1.key.id;
+ task1Intent = null;
+ } else {
+ task1Id = ActivityTaskManager.INVALID_TASK_ID;
+ task1Intent = app1.intent;
+ }
+
+ mSplitSelectStateController.setInitialTaskSelect(task1Intent,
+ SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
+ app1,
+ LAUNCHER_APP_PAIR_LAUNCH,
+ task1Id);
+
+ @Nullable Task foundTask2 = foundTasks.get(1);
+ if (foundTask2 != null) {
+ mSplitSelectStateController.setSecondTask(foundTask2);
+ } else {
+ mSplitSelectStateController.setSecondTask(
+ app2.intent, app2.user);
+ }
+
+ mSplitSelectStateController.launchSplitTasks();
+ });
}
/**
diff --git a/quickstep/src/com/android/quickstep/util/BaseDepthController.java b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
index 931e468..99f564c 100644
--- a/quickstep/src/com/android/quickstep/util/BaseDepthController.java
+++ b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
@@ -18,6 +18,7 @@
import android.app.WallpaperManager;
import android.os.IBinder;
import android.util.FloatProperty;
+import android.util.Log;
import android.view.AttachedSurfaceControl;
import android.view.SurfaceControl;
@@ -50,6 +51,9 @@
private static final int DEPTH_INDEX_WIDGET = 1;
private static final int DEPTH_INDEX_COUNT = 2;
+ // b/291401432
+ private static final String TAG = "BaseDepthController";
+
protected final Launcher mLauncher;
/** Property to set the depth for state transition. */
public final MultiProperty stateDepth;
@@ -88,7 +92,7 @@
*/
protected boolean mInEarlyWakeUp;
- private boolean mWaitingOnSurfaceValidity;
+ protected boolean mWaitingOnSurfaceValidity;
public BaseDepthController(Launcher activity) {
mLauncher = activity;
@@ -133,9 +137,11 @@
return;
}
if (mSurface == null) {
+ Log.d(TAG, "mSurface is null and mCurrentBlur is: " + mCurrentBlur);
return;
}
if (!mSurface.isValid()) {
+ Log.d(TAG, "mSurface is not valid");
mWaitingOnSurfaceValidity = true;
onInvalidSurface();
return;
@@ -186,6 +192,8 @@
protected void setSurface(SurfaceControl surface) {
if (mSurface != surface || mWaitingOnSurfaceValidity) {
mSurface = surface;
+ Log.d(TAG, "setSurface:\n\tmWaitingOnSurfaceValidity: " + mWaitingOnSurfaceValidity
+ + "\n\tmSurface: " + mSurface);
applyDepthAndBlur();
}
}
diff --git a/quickstep/src/com/android/quickstep/util/FadeOutRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/FadeOutRemoteTransition.kt
new file mode 100644
index 0000000..59ff81d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/FadeOutRemoteTransition.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util
+
+import android.animation.ValueAnimator
+import android.os.IBinder
+import android.os.RemoteException
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.window.IRemoteTransition
+import android.window.IRemoteTransitionFinishedCallback
+import android.window.TransitionInfo
+import com.android.launcher3.anim.AnimatorListeners.forEndCallback
+import com.android.launcher3.util.Executors
+import com.android.wm.shell.util.TransitionUtil
+
+/** Remote animation which fades out the closing targets */
+class FadeOutRemoteTransition : IRemoteTransition.Stub() {
+
+ override fun mergeAnimation(
+ iBinder: IBinder,
+ transitionInfo: TransitionInfo,
+ transaction: Transaction,
+ mergeTarget: IBinder,
+ finishCB: IRemoteTransitionFinishedCallback
+ ) {
+
+ try {
+ finishCB.onTransitionFinished(null, Transaction())
+ } catch (e: RemoteException) {
+ // Ignore
+ }
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startT: Transaction,
+ finishCB: IRemoteTransitionFinishedCallback
+ ) {
+ val anim = ValueAnimator.ofFloat(1f, 0f)
+
+ val closingControls: MutableList<SurfaceControl> = mutableListOf()
+ for (chg in info.changes) {
+ startT.show(chg.leash)
+ if (TransitionUtil.isClosingType(chg.mode)) {
+ closingControls.add(chg.leash)
+ }
+ }
+ startT.apply()
+
+ anim.addUpdateListener {
+ val t = Transaction()
+ closingControls.forEach { t.setAlpha(it, anim.animatedValue as Float) }
+ t.apply()
+ }
+ anim.addListener(
+ forEndCallback(
+ Runnable {
+ val t = Transaction()
+ closingControls.forEach { t.hide(it) }
+ try {
+ finishCB.onTransitionFinished(null, t)
+ } catch (e: RemoteException) {
+ // Ignore
+ }
+ }
+ )
+ )
+
+ Executors.MAIN_EXECUTOR.execute { anim.start() }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
deleted file mode 100644
index 10f2eaa..0000000
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ /dev/null
@@ -1,47 +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.util;
-
-import android.animation.AnimatorSet;
-import android.view.RemoteAnimationTarget;
-
-public abstract class RemoteAnimationProvider {
-
- public abstract AnimatorSet createWindowAnimation(RemoteAnimationTarget[] appTargets,
- RemoteAnimationTarget[] wallpaperTargets);
-
- /**
- * @return the target with the lowest opaque layer for a certain app animation, or null.
- */
- public static RemoteAnimationTarget findLowestOpaqueLayerTarget(
- RemoteAnimationTarget[] appTargets, int mode) {
- int lowestLayer = Integer.MAX_VALUE;
- int lowestLayerIndex = -1;
- for (int i = appTargets.length - 1; i >= 0; i--) {
- RemoteAnimationTarget target = appTargets[i];
- if (target.mode == mode && !target.isTranslucent) {
- int layer = target.prefixOrderIndex;
- if (layer < lowestLayer) {
- lowestLayer = layer;
- lowestLayerIndex = i;
- }
- }
- }
- return lowestLayerIndex != -1
- ? appTargets[lowestLayerIndex]
- : null;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
deleted file mode 100644
index 382cf79..0000000
--- a/quickstep/src/com/android/quickstep/util/RemoteFadeOutAnimationListener.java
+++ /dev/null
@@ -1,57 +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.util;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl.Transaction;
-
-import com.android.quickstep.RemoteAnimationTargets;
-
-/**
- * Animation listener which fades out the closing targets
- */
-public class RemoteFadeOutAnimationListener implements AnimatorUpdateListener {
-
- private final RemoteAnimationTargets mTarget;
- private boolean mFirstFrame = true;
-
- public RemoteFadeOutAnimationListener(RemoteAnimationTarget[] appTargets,
- RemoteAnimationTarget[] wallpaperTargets) {
- mTarget = new RemoteAnimationTargets(appTargets, wallpaperTargets,
- new RemoteAnimationTarget[0], MODE_CLOSING);
- }
-
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- Transaction t = new Transaction();
- if (mFirstFrame) {
- for (RemoteAnimationTarget target : mTarget.unfilteredApps) {
- t.show(target.leash);
- }
- mFirstFrame = false;
- }
-
- float alpha = 1 - valueAnimator.getAnimatedFraction();
- for (RemoteAnimationTarget app : mTarget.apps) {
- t.setAlpha(app.leash, alpha);
- }
- t.apply();
- }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 56d6857..c3774eb 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -60,8 +60,6 @@
)
}
- var splitInstructionsView: SplitInstructionsView? = null
-
/**
* Returns different elements to animate for the initial split selection animation
* depending on the state of the surface from which the split was initiated
@@ -235,7 +233,8 @@
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
splitSelectStateController.resetState()
- safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+ safeRemoveViewFromDragLayer(launcher,
+ splitSelectStateController.splitInstructionsView)
}
})
return animatorSet
@@ -246,8 +245,9 @@
* app for splitscreen
*/
fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>) : PendingAnimation {
- safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
- splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
+ safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
+ val splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
+ splitSelectStateController.splitInstructionsView = splitInstructionsView
val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
val anim = PendingAnimation(100 /*duration */)
anim.setViewAlpha(splitInstructionsView, 1f,
@@ -265,6 +265,11 @@
return anim
}
+ /** Removes the split instructions view from [launcher] drag layer. */
+ fun removeSplitInstructionsView(launcher: StatefulActivity<*>) {
+ safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
+ }
+
private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
if (view != null) {
launcher.dragLayer.removeView(view)
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 51211df..0c89766 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -17,8 +17,14 @@
package com.android.quickstep.util;
import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT;
import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK;
import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK;
@@ -29,6 +35,8 @@
import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT;
import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -36,11 +44,16 @@
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
@@ -55,7 +68,11 @@
import androidx.annotation.Nullable;
import com.android.internal.logging.InstanceId;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statehandlers.DepthController;
@@ -64,6 +81,11 @@
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SplitSelectionListener;
import com.android.quickstep.SystemUiProxy;
@@ -71,11 +93,16 @@
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.GroupedTaskView;
+import com.android.quickstep.views.SplitInstructionsView;
+import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.wm.shell.splitscreen.ISplitSelectListener;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -95,6 +122,7 @@
private final StatsLogManager mStatsLogManager;
private final SystemUiProxy mSystemUiProxy;
private final StateManager mStateManager;
+ private SplitFromDesktopController mSplitFromDesktopController;
@Nullable
private DepthController mDepthController;
private boolean mRecentsAnimationRunning;
@@ -110,6 +138,7 @@
private GroupedTaskView mLaunchingTaskView;
private FloatingTaskView mFirstFloatingTaskView;
+ private SplitInstructionsView mSplitInstructionsView;
private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>();
@@ -124,7 +153,7 @@
mDepthController = depthController;
mRecentTasksModel = recentsModel;
mSplitAnimationController = new SplitAnimationController(this);
- mAppPairsController = new AppPairsController(context, this);
+ mAppPairsController = new AppPairsController(context, this, statsLogManager);
mSplitSelectDataHolder = new SplitSelectDataHolder(mContext);
}
@@ -151,37 +180,46 @@
}
/**
- * Pulls the list of active Tasks from RecentsModel, and finds the most recently active Task
- * matching a given ComponentName. Then uses that Task (which could be null) with the given
- * callback.
+ * Maps a List<ComponentKey> to List<@Nullable Task>, searching through active Tasks in
+ * RecentsModel. If found, the Task will be the most recently-interacted-with instance of that
+ * Task. Then runs the given callback on that List.
* <p>
* Used in various task-switching or splitscreen operations when we need to check if there is a
* currently running Task of a certain type and use the most recent one.
*/
- public void findLastActiveTaskAndRunCallback(
- @Nullable ComponentKey componentKey, Consumer<Task> callback) {
+ public void findLastActiveTasksAndRunCallback(
+ @Nullable List<ComponentKey> componentKeys, Consumer<List<Task>> callback) {
mRecentTasksModel.getTasks(taskGroups -> {
- if (componentKey == null) {
- callback.accept(null);
+ if (componentKeys == null || componentKeys.isEmpty()) {
+ callback.accept(Collections.emptyList());
return;
}
- Task lastActiveTask = null;
- // Loop through tasks in reverse, since they are ordered with most-recent tasks last.
- for (int i = taskGroups.size() - 1; i >= 0; i--) {
- GroupTask groupTask = taskGroups.get(i);
- Task task1 = groupTask.task1;
- if (isInstanceOfComponent(task1, componentKey)) {
- lastActiveTask = task1;
- break;
+
+ List<Task> lastActiveTasks = new ArrayList<>();
+ // For each key we are looking for, add to lastActiveTasks with the corresponding Task
+ // (or null if not found).
+ for (ComponentKey key : componentKeys) {
+ Task lastActiveTask = null;
+ // Loop through tasks in reverse, since they are ordered with most-recent tasks last
+ for (int i = taskGroups.size() - 1; i >= 0; i--) {
+ GroupTask groupTask = taskGroups.get(i);
+ Task task1 = groupTask.task1;
+ // Don't add duplicate Tasks
+ if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) {
+ lastActiveTask = task1;
+ break;
+ }
+ Task task2 = groupTask.task2;
+ if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) {
+ lastActiveTask = task2;
+ break;
+ }
}
- Task task2 = groupTask.task2;
- if (isInstanceOfComponent(task2, componentKey)) {
- lastActiveTask = task2;
- break;
- }
+
+ lastActiveTasks.add(lastActiveTask);
}
- callback.accept(lastActiveTask);
+ callback.accept(lastActiveTasks);
});
}
@@ -224,7 +262,7 @@
* To be called when the both split tasks are ready to be launched. Call after launcher side
* animations are complete.
*/
- public void launchSplitTasks(Consumer<Boolean> callback) {
+ public void launchSplitTasks(@Nullable Consumer<Boolean> callback) {
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
launchTasks(callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
@@ -237,6 +275,14 @@
}
/**
+ * A version of {@link #launchTasks(Consumer, boolean, float, InstanceId)} with no success
+ * callback.
+ */
+ public void launchSplitTasks() {
+ launchSplitTasks(null);
+ }
+
+ /**
* To be called as soon as user selects the second task (even if animations aren't complete)
* @param task The second task that will be launched.
*/
@@ -269,8 +315,8 @@
* create a split instance, null for cases that bring existing instaces to the
* foreground (quickswitch, launching previous pairs from overview)
*/
- public void launchTasks(Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
- @Nullable InstanceId shellInstanceId) {
+ public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList,
+ float splitRatio, @Nullable InstanceId shellInstanceId) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
final ActivityOptions options1 = ActivityOptions.makeBasic();
@@ -293,7 +339,7 @@
if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
- secondTaskId, callback);
+ secondTaskId, callback, "LaunchSplitPair");
switch (launchData.getSplitLaunchType()) {
case SPLIT_TASK_TASK ->
mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
@@ -385,7 +431,7 @@
if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
- secondTaskId, callback);
+ secondTaskId, callback, "LaunchExistingPair");
mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
null /* options2 */, stagePosition, splitRatio,
remoteTransition, null /*shellInstanceId*/);
@@ -454,16 +500,24 @@
}
}
+ public void initSplitFromDesktopController(Launcher launcher) {
+ mSplitFromDesktopController = new SplitFromDesktopController(launcher);
+ }
+
+ public void enterSplitFromDesktop(ActivityManager.RunningTaskInfo taskInfo) {
+ mSplitFromDesktopController.enterSplitSelect(taskInfo);
+ }
+
private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
- Consumer<Boolean> callback) {
+ @Nullable Consumer<Boolean> callback, String transitionName) {
final RemoteSplitLaunchTransitionRunner animationRunner =
new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
return new RemoteTransition(animationRunner,
- ActivityThread.currentActivityThread().getApplicationThread(), "LaunchSplitPair");
+ ActivityThread.currentActivityThread().getApplicationThread(), transitionName);
}
private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
- Consumer<Boolean> callback) {
+ @Nullable Consumer<Boolean> callback) {
final RemoteSplitLaunchAnimationRunner animationRunner =
new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback);
return new RemoteAnimationAdapter(animationRunner, 300, 150,
@@ -512,7 +566,7 @@
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
- Consumer<Boolean> callback) {
+ @Nullable Consumer<Boolean> callback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = callback;
@@ -522,6 +576,7 @@
public void startAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishedCallback) {
+ testLogD(LAUNCH_SPLIT_PAIR, "Received split startAnimation");
final Runnable finishAdapter = () -> {
try {
finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
@@ -537,9 +592,8 @@
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
}
+ resetState();
});
- // After successful launch, call resetState
- resetState();
});
}
@@ -560,7 +614,7 @@
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
- Consumer<Boolean> successCallback) {
+ @Nullable Consumer<Boolean> successCallback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = successCallback;
@@ -609,6 +663,7 @@
mAnimateCurrentTaskDismissal = false;
mDismissingFromSplitPair = false;
mFirstFloatingTaskView = null;
+ mSplitInstructionsView = null;
}
/**
@@ -639,11 +694,20 @@
mFirstFloatingTaskView = floatingTaskView;
}
+ public void setSplitInstructionsView(SplitInstructionsView splitInstructionsView) {
+ mSplitInstructionsView = splitInstructionsView;
+ }
+
@Nullable
public FloatingTaskView getFirstFloatingTaskView() {
return mFirstFloatingTaskView;
}
+ @Nullable
+ public SplitInstructionsView getSplitInstructionsView() {
+ return mSplitInstructionsView;
+ }
+
public AppPairsController getAppPairsController() {
return mAppPairsController;
}
@@ -653,4 +717,114 @@
mSplitSelectDataHolder.dump(prefix, writer);
}
}
+
+ public class SplitFromDesktopController {
+ private static final String TAG = "SplitFromDesktopController";
+
+ private final Launcher mLauncher;
+ private final OverviewComponentObserver mOverviewComponentObserver;
+ private final int mSplitPlaceholderSize;
+ private final int mSplitPlaceholderInset;
+ private ActivityManager.RunningTaskInfo mTaskInfo;
+ private ISplitSelectListener mSplitSelectListener;
+ private Drawable mAppIcon;
+
+ public SplitFromDesktopController(Launcher launcher) {
+ mLauncher = launcher;
+ RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
+ launcher.getApplicationContext());
+ mOverviewComponentObserver =
+ new OverviewComponentObserver(launcher.getApplicationContext(), deviceState);
+ mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
+ R.dimen.split_placeholder_size);
+ mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
+ R.dimen.split_placeholder_inset);
+ mSplitSelectListener = new ISplitSelectListener.Stub() {
+ @Override
+ public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo) {
+ if (!ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE.get()) return false;
+ MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo));
+ return true;
+ }
+ };
+ SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener);
+ }
+
+ /**
+ * Enter split select from desktop mode.
+ * @param taskInfo the desktop task to move to split stage
+ */
+ public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo) {
+ mTaskInfo = taskInfo;
+ String packageName = mTaskInfo.realActivity.getPackageName();
+ PackageManager pm = mLauncher.getApplicationContext().getPackageManager();
+ IconProvider provider = new IconProvider(mLauncher.getApplicationContext());
+ try {
+ mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
+ PackageManager.ComponentInfoFlags.of(0)));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Package not found: " + packageName, e);
+ }
+ RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
+ SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()),
+ false /* allowMinimizeSplitScreen */);
+
+ DesktopSplitRecentsAnimationListener listener =
+ new DesktopSplitRecentsAnimationListener();
+
+ MAIN_EXECUTOR.execute(() -> {
+ callbacks.addListener(listener);
+ UI_HELPER_EXECUTOR.execute(
+ // Transition from app to enter stage split in launcher with
+ // recents animation.
+ () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+ mOverviewComponentObserver.getOverviewIntent(),
+ SystemClock.uptimeMillis(), callbacks, null, null));
+ });
+ }
+
+ private class DesktopSplitRecentsAnimationListener implements
+ RecentsAnimationCallbacks.RecentsAnimationListener {
+ private final Rect mTempRect = new Rect();
+
+ @Override
+ public void onRecentsAnimationStart(RecentsAnimationController controller,
+ RecentsAnimationTargets targets) {
+ setInitialTaskSelect(mTaskInfo, STAGE_POSITION_BOTTOM_OR_RIGHT,
+ null, LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM);
+
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
+ mSplitPlaceholderSize, mSplitPlaceholderInset,
+ mLauncher.getDeviceProfile(), getActiveSplitStagePosition(), mTempRect);
+
+ PendingAnimation anim = new PendingAnimation(
+ SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration());
+ RectF startingTaskRect = new RectF(mTaskInfo.configuration.windowConfiguration
+ .getBounds());
+ final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
+ mLauncher, mLauncher.getDragLayer(),
+ null /* thumbnail */,
+ mAppIcon, new RectF());
+ floatingTaskView.setAlpha(1);
+ floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
+ false /* fadeWithThumbnail */, true /* isStagedTask */);
+ setFirstFloatingTaskView(floatingTaskView);
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ controller.finish(true /* toRecents */, null /* onFinishComplete */,
+ false /* sendUserLeaveHint */);
+ }
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
+ .onDesktopSplitSelectAnimComplete(mTaskInfo);
+ }
+ });
+ anim.buildAnim().start();
+ }
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 148a45a..056f9aa 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -16,6 +16,7 @@
package com.android.quickstep.util;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE;
import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS;
import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE;
@@ -169,6 +170,7 @@
private void cleanUp() {
mLauncher.getDragLayer().removeView(firstFloatingTaskView);
mLauncher.getDragLayer().removeView(secondFloatingTaskView);
+ mController.getSplitAnimationController().removeSplitInstructionsView(mLauncher);
mController.resetState();
}
});
@@ -177,7 +179,8 @@
private boolean shouldIgnoreSecondSplitLaunch() {
return (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()
- && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get())
+ && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()
+ && !ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE.get())
|| !mController.isSplitSelectActive();
}
}
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 1112f4d..7cc2c46 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -26,7 +26,6 @@
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
-import android.os.SystemProperties;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -153,14 +152,9 @@
// Create a new overlay layer. We do not call detach on this instance, it's propagated
// to other classes like PipTaskOrganizer / RecentsAnimationController to complete
// the cleanup.
- if (SystemProperties.getBoolean(
- "persist.wm.debug.enable_pip_app_icon_overlay", true)) {
- mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
- mAppBounds, new IconProvider(context).getIcon(mActivityInfo),
- appIconSizePx);
- } else {
- mPipContentOverlay = new PipContentOverlay.PipColorOverlay(view.getContext());
- }
+ mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
+ mAppBounds, new IconProvider(context).getIcon(mActivityInfo),
+ appIconSizePx);
final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
mPipContentOverlay.attach(tx, mLeash);
} else {
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 5f7d694..c82cdb7 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -24,7 +24,6 @@
import android.view.WindowMetrics;
import com.android.internal.policy.SystemBarUtils;
-import com.android.launcher3.logging.FileLog;
import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.util.window.CachedDisplayInfo;
import com.android.launcher3.util.window.WindowManagerProxy;
@@ -61,7 +60,6 @@
WindowManager windowManager = displayInfoContext.getSystemService(WindowManager.class);
Set<WindowMetrics> possibleMaximumWindowMetrics =
windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
- FileLog.d("b/283944974", "possibleMaximumWindowMetrics: " + possibleMaximumWindowMetrics);
for (WindowMetrics windowMetrics : possibleMaximumWindowMetrics) {
CachedDisplayInfo info = getDisplayInfo(windowMetrics, Surface.ROTATION_0);
List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 1cfaf14..83e9945 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -51,6 +51,7 @@
import com.android.quickstep.util.RecentsOrientedState;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.QuickStepContract;
import java.util.ArrayList;
import java.util.Arrays;
@@ -79,7 +80,7 @@
private static final String TAG = DesktopTaskView.class.getSimpleName();
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
@NonNull
private List<Task> mTasks = new ArrayList<>();
@@ -91,6 +92,8 @@
private final ArrayList<CancellableTask<?>> mPendingThumbnailRequests = new ArrayList<>();
+ private final TaskView.FullscreenDrawParams mSnapshotDrawParams;
+
private View mBackgroundView;
public DesktopTaskView(Context context) {
@@ -103,6 +106,10 @@
public DesktopTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
+
+ mSnapshotDrawParams = new FullscreenDrawParams(
+ QuickStepContract.getWindowCornerRadius(context),
+ QuickStepContract.getWindowCornerRadius(context));
}
@Override
@@ -465,14 +472,20 @@
for (int i = 0; i < mSnapshotViewMap.size(); i++) {
TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i);
thumbnailView.getTaskOverlay().setFullscreenProgress(progress);
- updateSnapshotRadius();
}
+ updateSnapshotRadius();
}
@Override
protected void updateSnapshotRadius() {
+ super.updateSnapshotRadius();
for (int i = 0; i < mSnapshotViewMap.size(); i++) {
- mSnapshotViewMap.valueAt(i).setFullscreenParams(mCurrentFullscreenParams);
+ if (i == 0) {
+ // All snapshots share the same params. Only update it with the first snapshot.
+ updateFullscreenParams(mSnapshotDrawParams,
+ mSnapshotView.getPreviewPositionHelper());
+ }
+ mSnapshotViewMap.valueAt(i).setFullscreenParams(mSnapshotDrawParams);
}
}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 7bbe36a..e5a0e10 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -145,23 +145,29 @@
mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1;
mTask = task;
THREAD_POOL_EXECUTOR.execute(() -> {
- final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
- mTask.getTopComponent().getPackageName(),
- UserHandle.of(mTask.key.userId));
+ AppUsageLimit usageLimit = null;
+ try {
+ usageLimit = mLauncherApps.getAppUsageLimit(
+ mTask.getTopComponent().getPackageName(),
+ UserHandle.of(mTask.key.userId));
+ } catch (Exception e) {
+ Log.e(TAG, "Error initializing digital well being toast", e);
+ }
+ final long appUsageLimitTimeMs =
+ usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
+ final long appRemainingTimeMs =
+ usageLimit != null ? usageLimit.getUsageRemaining() : -1;
- final long appUsageLimitTimeMs =
- usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
- final long appRemainingTimeMs =
- usageLimit != null ? usageLimit.getUsageRemaining() : -1;
+ mTaskView.post(() -> {
+ if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+ setNoLimit();
+ } else {
+ setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
+ }
+ });
- mTaskView.post(() -> {
- if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
- setNoLimit();
- } else {
- setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
}
- });
- });
+ );
}
public void setSplitConfiguration(SplitBounds splitBounds) {
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 80e5a54..eb7598d 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -30,6 +30,7 @@
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
@@ -43,6 +44,7 @@
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.PendingSplitSelectInfo;
import com.android.launcher3.util.SplitConfigurationOptions;
@@ -165,6 +167,8 @@
@Override
public void setOverviewStateEnabled(boolean enabled) {
super.setOverviewStateEnabled(enabled);
+ Log.d(TestProtocol.OVERVIEW_OVER_HOME, "overview state enabled state has changed: "
+ + enabled);
if (enabled) {
LauncherState state = mActivity.getStateManager().getState();
boolean hasClearAllButton = (state.getVisibleElements(mActivity)
@@ -242,7 +246,7 @@
DesktopVisibilityController desktopVisibilityController =
mActivity.getDesktopVisibilityController();
if (desktopVisibilityController != null) {
- desktopVisibilityController.setGestureInProgress(true);
+ desktopVisibilityController.setRecentsGestureStart();
}
}
@@ -250,9 +254,11 @@
public void onGestureAnimationEnd() {
DesktopVisibilityController desktopVisibilityController = null;
boolean showDesktopApps = false;
+ GestureState.GestureEndTarget endTarget = null;
if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
desktopVisibilityController = mActivity.getDesktopVisibilityController();
- if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.LAST_TASK
+ endTarget = mCurrentGestureEndTarget;
+ if (endTarget == GestureState.GestureEndTarget.LAST_TASK
&& desktopVisibilityController.areFreeformTasksVisible()) {
// Recents gesture was cancelled and we are returning to the previous task.
// After super class has handled clean up, show desktop apps on top again
@@ -261,7 +267,7 @@
}
super.onGestureAnimationEnd();
if (desktopVisibilityController != null) {
- desktopVisibilityController.setGestureInProgress(false);
+ desktopVisibilityController.setRecentsGestureEnd(endTarget);
}
if (showDesktopApps) {
SystemUiProxy.INSTANCE.get(mActivity).showDesktopApps(mActivity.getDisplayId());
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index e47c089..b31791a 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -258,6 +258,13 @@
}
/**
+ * Returns the visibility of the overview actions buttons.
+ */
+ public @Visibility int getActionsButtonVisibility() {
+ return findViewById(R.id.action_buttons).getVisibility();
+ }
+
+ /**
* Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar.
*/
private void updatePadding() {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 21da7cf..4b8741d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -683,8 +683,6 @@
private final Toast mSplitUnsupportedToast = Toast.makeText(getContext(),
R.string.toast_split_app_unsupported, Toast.LENGTH_SHORT);
- private SplitInstructionsView mSplitInstructionsView;
-
@Nullable
private SplitSelectSource mSplitSelectSource;
@@ -1369,9 +1367,15 @@
return null;
}
+ // We're looking for a taskView that matches these ids, regardless of order
+ int[] taskIdsCopy = Arrays.copyOf(taskIds, taskIds.length);
+ Arrays.sort(taskIdsCopy);
+
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView taskView = requireTaskViewAt(i);
- if (Arrays.equals(taskIds, taskView.getTaskIds())) {
+ int[] taskViewIdsCopy = taskView.getTaskIds();
+ Arrays.sort(taskViewIdsCopy);
+ if (Arrays.equals(taskIdsCopy, taskViewIdsCopy)) {
return taskView;
}
}
@@ -3246,19 +3250,21 @@
firstFloatingTaskView.setOnClickListener(this::animateToFullscreen);
// SplitInstructionsView: animate in
- safeRemoveDragLayerView(mSplitInstructionsView);
- mSplitInstructionsView = SplitInstructionsView.getSplitInstructionsView(mActivity);
- mSplitInstructionsView.setAlpha(0);
- anim.setViewAlpha(mSplitInstructionsView, 1, clampToProgress(LINEAR,
+ safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
+ SplitInstructionsView splitInstructionsView =
+ SplitInstructionsView.getSplitInstructionsView(mActivity);
+ splitInstructionsView.setAlpha(0);
+ anim.setViewAlpha(splitInstructionsView, 1, clampToProgress(LINEAR,
timings.getInstructionsContainerFadeInStartOffset(),
timings.getInstructionsContainerFadeInEndOffset()));
- anim.setViewAlpha(mSplitInstructionsView.getTextView(), 1, clampToProgress(LINEAR,
+ anim.setViewAlpha(splitInstructionsView.getTextView(), 1, clampToProgress(LINEAR,
timings.getInstructionsTextFadeInStartOffset(),
timings.getInstructionsTextFadeInEndOffset()));
- anim.addFloat(mSplitInstructionsView, mSplitInstructionsView.UNFOLD, 0.1f, 1,
+ anim.addFloat(splitInstructionsView, splitInstructionsView.UNFOLD, 0.1f, 1,
clampToProgress(EMPHASIZED_DECELERATE,
timings.getInstructionsUnfoldStartOffset(),
timings.getInstructionsUnfoldEndOffset()));
+ mSplitSelectStateController.setSplitInstructionsView(splitInstructionsView);
InteractionJankMonitorWrapper.begin(this,
InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected");
@@ -4775,9 +4781,9 @@
mSecondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds,
secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */);
- pendingAnimation.setViewAlpha(mSplitInstructionsView, 0, clampToProgress(LINEAR,
- timings.getInstructionsFadeStartOffset(),
- timings.getInstructionsFadeEndOffset()));
+ pendingAnimation.setViewAlpha(mSplitSelectStateController.getSplitInstructionsView(), 0,
+ clampToProgress(LINEAR, timings.getInstructionsFadeStartOffset(),
+ timings.getInstructionsFadeEndOffset()));
pendingAnimation.addEndListener(aBoolean -> {
mSplitSelectStateController.launchSplitTasks(
@@ -4813,10 +4819,11 @@
FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
safeRemoveDragLayerView(mSecondFloatingTaskView);
- safeRemoveDragLayerView(mSplitInstructionsView);
+ safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
mSecondFloatingTaskView = null;
- mSplitInstructionsView = null;
mSplitSelectSource = null;
+ mSplitSelectStateController.getSplitAnimationController()
+ .removeSplitInstructionsView(mActivity);
}
if (mSecondSplitHiddenView != null) {
@@ -4913,8 +4920,8 @@
taskViewsFloat.first.set(this, getSplitSelectTranslation());
taskViewsFloat.second.set(this, 0f);
- if (mSplitInstructionsView != null) {
- mSplitInstructionsView.ensureProperRotation();
+ if (mSplitSelectStateController.getSplitInstructionsView() != null) {
+ mSplitSelectStateController.getSplitInstructionsView().ensureProperRotation();
}
}
@@ -6026,7 +6033,7 @@
@Nullable
public SplitInstructionsView getSplitInstructionsView() {
- return mSplitInstructionsView;
+ return mSplitSelectStateController.getSplitInstructionsView();
}
/** Update the current activity locus id to show the enabled state of Overview */
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 5301c7c..854c3c7 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -97,7 +97,6 @@
import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
import com.android.quickstep.TaskAnimationManager;
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;
@@ -413,7 +412,9 @@
private boolean mIsClickableAsLiveTile = true;
- @Nullable private final BorderAnimator mBorderAnimator;
+ @Nullable private final BorderAnimator mFocusBorderAnimator;
+
+ @Nullable private final BorderAnimator mHoverBorderAnimator;
public TaskView(Context context) {
this(context, null);
@@ -439,23 +440,40 @@
boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
|| DesktopTaskView.DESKTOP_MODE_SUPPORTED;
- setWillNotDraw(!keyboardFocusHighlightEnabled);
+ boolean willDrawBorder =
+ keyboardFocusHighlightEnabled || FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get();
+ setWillNotDraw(!willDrawBorder);
- TypedArray ta = context.obtainStyledAttributes(
- attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
+ if (willDrawBorder) {
+ TypedArray styledAttrs = context.obtainStyledAttributes(
+ attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
- mBorderAnimator = !keyboardFocusHighlightEnabled
- ? null
- : new BorderAnimator(
- /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
- /* borderColor= */ ta.getColor(
- R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR),
- /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
- /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
- R.dimen.keyboard_quick_switch_border_width),
- /* boundsBuilder= */ this::updateBorderBounds,
- /* targetView= */ this));
- ta.recycle();
+ mFocusBorderAnimator = keyboardFocusHighlightEnabled ? new BorderAnimator(
+ /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+ /* borderColor= */ styledAttrs.getColor(
+ R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR),
+ /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
+ /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+ R.dimen.keyboard_quick_switch_border_width),
+ /* boundsBuilder= */ this::updateBorderBounds,
+ /* targetView= */ this)) : null;
+
+ mHoverBorderAnimator =
+ FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get() ? new BorderAnimator(
+ /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+ /* borderColor= */ styledAttrs.getColor(
+ R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR),
+ /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
+ /* borderWidthPx= */ context.getResources()
+ .getDimensionPixelSize(R.dimen.task_hover_border_width),
+ /* boundsBuilder= */ this::updateBorderBounds,
+ /* targetView= */ this)) : null;
+
+ styledAttrs.recycle();
+ } else {
+ mFocusBorderAnimator = null;
+ mHoverBorderAnimator = null;
+ }
}
protected void updateBorderBounds(Rect bounds) {
@@ -509,16 +527,48 @@
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- if (mBorderAnimator != null) {
- mBorderAnimator.buildAnimator(gainFocus).start();
+ if (mFocusBorderAnimator != null) {
+ mFocusBorderAnimator.buildAnimator(gainFocus).start();
+ }
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ if (FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get()) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ mHoverBorderAnimator.buildAnimator(/* isAppearing= */ true).start();
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ mHoverBorderAnimator.buildAnimator(/* isAppearing= */ false).start();
+ break;
+ default:
+ break;
+ }
+ }
+ return super.onHoverEvent(event);
+ }
+
+ @Override
+ public boolean onInterceptHoverEvent(MotionEvent event) {
+ if (FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get()) {
+ // avoid triggering hover event on child elements which would cause HOVER_EXIT for this
+ // task view
+ return true;
+ } else {
+ return super.onInterceptHoverEvent(event);
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
- if (mBorderAnimator != null) {
- mBorderAnimator.drawBorder(canvas);
+ if (mFocusBorderAnimator != null) {
+ mFocusBorderAnimator.drawBorder(canvas);
+ }
+
+ if (mHoverBorderAnimator != null) {
+ mHoverBorderAnimator.drawBorder(canvas);
}
}
@@ -1657,10 +1707,15 @@
}
void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
+ updateFullscreenParams(mCurrentFullscreenParams, previewPositionHelper);
+ }
+
+ protected void updateFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams,
+ PreviewPositionHelper previewPositionHelper) {
if (getRecentsView() == null) {
return;
}
- mCurrentFullscreenParams.setProgress(mFullscreenProgress, getRecentsView().getScaleX(),
+ fullscreenParams.setProgress(mFullscreenProgress, getRecentsView().getScaleX(),
getScaleX(), getWidth(), mActivity.getDeviceProfile(), previewPositionHelper);
}
@@ -1810,9 +1865,12 @@
public float mCurrentDrawnCornerRadius;
public FullscreenDrawParams(Context context) {
- mCornerRadius = TaskCornerRadius.get(context);
- mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context);
+ this(TaskCornerRadius.get(context), QuickStepContract.getWindowCornerRadius(context));
+ }
+ FullscreenDrawParams(float cornerRadius, float windowCornerRadius) {
+ mCornerRadius = cornerRadius;
+ mWindowCornerRadius = windowCornerRadius;
mCurrentDrawnCornerRadius = mCornerRadius;
}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
index 82849be..6c0d44d 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -18,6 +18,7 @@
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
+import static com.android.launcher3.taskbar.TaskbarHoverToolTipController.HOVER_TOOL_TIP_REVEAL_START_DELAY;
import static com.google.common.truth.Truth.assertThat;
@@ -26,6 +27,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -56,7 +58,7 @@
*/
@SmallTest
@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase {
private TaskbarHoverToolTipController mTaskbarHoverToolTipController;
@@ -126,8 +128,10 @@
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
- waitForIdleSync();
+ // Verify fullscreen is not set until the delayed runnable to reveal the tooltip has run
+ verify(taskbarActivityContext, never()).setTaskbarWindowFullscreen(true);
+ waitForIdleSync();
assertThat(hoverHandled).isTrue();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
@@ -155,8 +159,10 @@
boolean hoverHandled =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
- waitForIdleSync();
+ // Verify fullscreen is not set until the delayed runnable to reveal the tooltip has run
+ verify(taskbarActivityContext, never()).setTaskbarWindowFullscreen(true);
+ waitForIdleSync();
assertThat(hoverHandled).isTrue();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
@@ -216,6 +222,7 @@
}
private void waitForIdleSync() {
+ mTestableLooper.moveTimeForward(HOVER_TOOL_TIP_REVEAL_START_DELAY + 1);
mTestableLooper.processAllMessages();
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
index 59f9d5f..3a5fb04 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
@@ -16,11 +16,17 @@
package com.android.quickstep;
+import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+
import static org.junit.Assert.assertTrue;
import android.os.SystemProperties;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Until;
+
import com.android.launcher3.Launcher;
+import com.android.launcher3.tapl.LaunchedAppState;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
import com.android.launcher3.ui.AbstractLauncherUiTest;
@@ -76,6 +82,21 @@
}
}
+ protected void assertTestActivityIsRunning(int activityNumber, String message) {
+ assertTrue(message, mDevice.wait(
+ Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity" + activityNumber)),
+ DEFAULT_UI_TIMEOUT));
+ }
+
+ protected LaunchedAppState getAndAssertLaunchedApp() {
+ final LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState();
+ executeOnLauncher(launcher -> assertTrue(
+ "Launcher activity is the top activity; expecting another activity to be the top "
+ + "one",
+ isInLaunchedApp(launcher)));
+ return launchedAppState;
+ }
+
private boolean isInLiveTileMode(Launcher launcher,
LauncherInstrumentation.ContainerType expectedContainerType) {
if (expectedContainerType != LauncherInstrumentation.ContainerType.OVERVIEW) {
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 5127190..20aa410 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -16,14 +16,17 @@
package com.android.quickstep;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,9 +47,9 @@
startTestActivity(2);
}
- @Ignore
@Test
@NavigationModeSwitch
+ @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/187761685
public void testStressPressHome() {
for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
// Destroy Launcher activity.
@@ -57,9 +60,9 @@
}
}
- @Ignore
@Test
@NavigationModeSwitch
+ @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/187761685
public void testStressSwipeToOverview() {
for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
// Destroy Launcher activity.
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 7350214..9d188ed 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -16,6 +16,7 @@
package com.android.quickstep;
+import static com.android.launcher3.testing.shared.TestProtocol.FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP;
import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
@@ -27,6 +28,7 @@
import android.content.Intent;
import android.platform.test.annotations.PlatinumTest;
+import android.util.Log;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -42,8 +44,10 @@
import com.android.launcher3.tapl.Overview;
import com.android.launcher3.tapl.OverviewActions;
import com.android.launcher3.tapl.OverviewTask;
+import com.android.launcher3.tapl.OverviewTaskMenu;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -144,7 +148,7 @@
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(
- By.pkg(getAppPackageName()).text("TestActivity2")),
+ By.pkg(getAppPackageName()).text("TestActivity2")),
DEFAULT_UI_TIMEOUT));
executeOnLauncher(launcher -> assertTrue(
"Launcher activity is the top activity; expecting another activity to be the top "
@@ -193,6 +197,19 @@
actionsView.clickAndDismissScreenshot();
}
+
+ @Test
+ public void testOverviewActionsMenu() throws Exception {
+ startTestAppsWithCheck();
+
+ OverviewTaskMenu menu = mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+
+ assertNotNull("Tapping App info menu item returned null", menu.tapAppInfoMenuItem());
+ executeOnLauncher(launcher -> assertTrue(
+ "Launcher activity is the top activity; expecting another activity to be the top",
+ isInLaunchedApp(launcher)));
+ }
+
private int getCurrentOverviewPage(Launcher launcher) {
return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
}
@@ -238,16 +255,6 @@
isInState(() -> LauncherState.OVERVIEW));
}
- private LaunchedAppState getAndAssertLaunchedApp() {
- final LaunchedAppState launchedAppState = mLauncher.getLaunchedAppState();
- assertNotNull("Launcher.getLaunchedApp() returned null", launchedAppState);
- executeOnLauncher(launcher -> assertTrue(
- "Launcher activity is the top activity; expecting another activity to be the top "
- + "one",
- isInLaunchedApp(launcher)));
- return launchedAppState;
- }
-
private void quickSwitchToPreviousAppAndAssert(boolean toRight) {
final LaunchedAppState launchedAppState = getAndAssertLaunchedApp();
if (toRight) {
@@ -289,22 +296,22 @@
startTestActivity(4);
quickSwitchToPreviousAppAndAssert(true /* toRight */);
- assertTrue("The first app we should have quick switched to is not running",
- isTestActivityRunning(3));
+ assertTestActivityIsRunning(3,
+ "The first app we should have quick switched to is not running");
quickSwitchToPreviousAppAndAssert(true /* toRight */);
if (mLauncher.getNavigationModel() == NavigationModel.THREE_BUTTON) {
// 3-button mode toggles between 2 apps, rather than going back further.
- assertTrue("Second quick switch should have returned to the first app.",
- isTestActivityRunning(4));
+ assertTestActivityIsRunning(4,
+ "Second quick switch should have returned to the first app.");
} else {
- assertTrue("The second app we should have quick switched to is not running",
- isTestActivityRunning(2));
+ assertTestActivityIsRunning(2,
+ "The second app we should have quick switched to is not running");
}
quickSwitchToPreviousAppAndAssert(false /* toRight */);
- assertTrue("The 2nd app we should have quick switched to is not running",
- isTestActivityRunning(3));
+ assertTestActivityIsRunning(3,
+ "The 2nd app we should have quick switched to is not running");
final LaunchedAppState launchedAppState = getAndAssertLaunchedApp();
launchedAppState.switchToOverview();
@@ -313,7 +320,6 @@
@Test
@ScreenRecord // b/242163205
@PlatinumTest(focusArea = "launcher")
- @TaskbarModeSwitch(mode = PERSISTENT)
public void testQuickSwitchToPreviousAppForTablet() throws Exception {
assumeTrue(mLauncher.isTablet());
startTestActivity(2);
@@ -329,17 +335,20 @@
// Quick-switch to the test app with swiping to right.
quickSwitchToPreviousAppAndAssert(true /* toRight */);
- assertTrue("The first app we should have quick switched to is not running",
- isTestActivityRunning(2));
+ assertTestActivityIsRunning(2,
+ "The first app we should have quick switched to is not running");
// Expect task bar visible when the launched app was the test activity.
launchedAppState = getAndAssertLaunchedApp();
- launchedAppState.assertTaskbarVisible();
- }
- private boolean isTestActivityRunning(int activityNumber) {
- return mDevice.wait(Until.hasObject(By.pkg(getAppPackageName())
- .text("TestActivity" + activityNumber)),
- DEFAULT_UI_TIMEOUT);
+ Log.e(FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP,
+ "is Taskbar Transient : " + DisplayController.isTransientTaskbar(mTargetContext));
+ // TODO(b/286084688): Remove this branching check after test corruption is resolved.
+ // Branching this check because of test corruption.
+ if (DisplayController.isTransientTaskbar(mTargetContext)) {
+ launchedAppState.assertTaskbarHidden();
+ } else {
+ launchedAppState.assertTaskbarVisible();
+ }
}
@Test
@@ -349,8 +358,8 @@
public void testQuickSwitchFromHome() throws Exception {
startTestActivity(2);
mLauncher.goHome().quickSwitchToPreviousApp();
- assertTrue("The most recent task is not running after quick switching from home",
- isTestActivityRunning(2));
+ assertTestActivityIsRunning(2,
+ "The most recent task is not running after quick switching from home");
getAndAssertLaunchedApp();
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index 3317ce1..dc7cb9d 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -22,6 +22,8 @@
import android.content.Intent;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.ui.TaplTestsLauncher3;
@@ -36,6 +38,9 @@
private static final String CALCULATOR_APP_PACKAGE =
resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+ private static final String READ_DEVICE_CONFIG_PERMISSION =
+ "android.permission.READ_DEVICE_CONFIG";
+
@Override
@Before
public void setUp() throws Exception {
@@ -46,6 +51,8 @@
mLauncher.enableBlockTimeout(true);
mLauncher.showTaskbarIfHidden();
}
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG_PERMISSION);
}
@After
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index 4c6874e..907dbcc 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -16,13 +16,22 @@
package com.android.quickstep;
+import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
+
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
+import android.app.Instrumentation;
+
import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
+import com.android.launcher3.tapl.Workspace;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -36,6 +45,9 @@
@RunWith(AndroidJUnit4.class)
public class TaplTestsTrackpad extends AbstractQuickStepTest {
+ private static final String READ_DEVICE_CONFIG_PERMISSION =
+ "android.permission.READ_DEVICE_CONFIG";
+
@Before
public void setUp() throws Exception {
super.setUp();
@@ -60,6 +72,29 @@
@Test
@PortraitLandscape
+ // TODO(b/291944684): Support back in 3-button mode. It requires triggering the logic to enable
+ // trackpad gesture back in SysUI. Normally it's triggered by the attachment of a trackpad. We
+ // need to figure out a way to emulate that in the test, or bypass the logic altogether.
+ @NavigationModeSwitch(mode = ZERO_BUTTON)
+ public void pressBack() throws Exception {
+ assumeTrue(mLauncher.isTablet());
+ assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+
+ try {
+ instrumentation.getUiAutomation().adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG_PERMISSION);
+ mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
+
+ startTestActivity(2);
+ mLauncher.pressBack();
+ } finally {
+ instrumentation.getUiAutomation().dropShellPermissionIdentity();
+ }
+ }
+
+ @Test
+ @PortraitLandscape
@NavigationModeSwitch
public void switchToOverview() throws Exception {
assumeTrue(mLauncher.isTablet());
@@ -79,4 +114,19 @@
assertNotNull("switchToAllApps() returned null",
mLauncher.getWorkspace().switchToAllApps());
}
+
+ @Test
+ @NavigationModeSwitch
+ @PortraitLandscape
+ public void testQuickSwitchFromHome() throws Exception {
+ assumeTrue(mLauncher.isTablet());
+
+ startTestActivity(2);
+ Workspace workspace = mLauncher.goHome();
+ mLauncher.setTrackpadGestureType(TrackpadGestureType.FOUR_FINGER);
+ workspace.quickSwitchToPreviousApp();
+ assertTestActivityIsRunning(2,
+ "The most recent task is not running after quick switching from home");
+ getAndAssertLaunchedApp();
+ }
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
index b58fe29..3869bf7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
@@ -18,7 +18,6 @@
import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
-
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
@@ -64,4 +63,15 @@
throw new RuntimeException(e);
}
}
+
+ @Test
+ @TaskbarModeSwitch(mode = TRANSIENT)
+ public void testClickHoveredTaskbarToGoHome() {
+ try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+ getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+ mLauncher.getLaunchedAppState().clickStashedTaskbarToGoHome();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 65542cf..69109c2 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -37,6 +37,7 @@
import com.android.quickstep.RecentsModel
import com.android.quickstep.SystemUiProxy
import com.android.systemui.shared.recents.model.Task
+import java.util.function.Consumer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@@ -48,7 +49,6 @@
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-import java.util.function.Consumer
@RunWith(AndroidJUnit4::class)
class SplitSelectStateControllerTest {
@@ -67,6 +67,9 @@
private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId)
private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
+ private var taskIdCounter = 0
+ private fun getUniqueId(): Int { return ++taskIdCounter }
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -100,15 +103,15 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
+ Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- nonMatchingComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonMatchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -139,27 +142,27 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> {
+ Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
- it.key.baseIntent.component.packageName,
+ it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
- it.key.baseIntent.component.className,
+ it[0].key.baseIntent.component?.className,
matchingClass
)
- assertEquals(it, groupTask1.task1)
+ assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- matchingComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -190,15 +193,15 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
+ Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- nonPrimaryUserComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonPrimaryUserComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -231,28 +234,28 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> {
+ Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
- it.key.baseIntent.component.packageName,
+ it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
- it.key.baseIntent.component.className,
+ it[0].key.baseIntent.component?.className,
matchingClass
)
- assertEquals("userId mismatched", it.key.userId, nonPrimaryUserHandle.identifier)
- assertEquals(it, groupTask1.task1)
+ assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
+ assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- nonPrimaryUserComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonPrimaryUserComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -283,27 +286,200 @@
tasks.add(groupTask1)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> {
+ Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
- it.key.baseIntent.component.packageName,
+ it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
- it.key.baseIntent.component.className,
+ it[0].key.baseIntent.component?.className,
matchingClass
)
- assertEquals(it, groupTask2.task2)
+ assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- matchingComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent),
+ taskConsumer
+ )
+ verify(recentsModel).getTasks(capture())
+ }
+
+ // Send our mocked tasks
+ consumer.accept(tasks)
+ }
+
+ @Test
+ fun activeTasks_multipleSearchShouldFindTask() {
+ val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle)
+ val matchingPackage = "hotdog"
+ val matchingClass = "juice"
+ val matchingComponent =
+ ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+ val groupTask1 =
+ generateGroupTask(
+ ComponentName("hotdog", "pie"),
+ ComponentName("pumpkin", "pie")
+ )
+ val groupTask2 =
+ generateGroupTask(
+ ComponentName("pomegranate", "juice"),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val tasks: ArrayList<GroupTask> = ArrayList()
+ tasks.add(groupTask2)
+ tasks.add(groupTask1)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTasksAndRunCallback
+ val taskConsumer =
+ Consumer<List<Task>> {
+ assertEquals("Expected array length 2", 2, it.size)
+ assertNull("No tasks should have matched", it[0] /*task*/)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[1].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[1].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[1], groupTask2.task2)
+ }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonMatchingComponent, matchingComponent),
+ taskConsumer
+ )
+ verify(recentsModel).getTasks(capture())
+ }
+
+ // Send our mocked tasks
+ consumer.accept(tasks)
+ }
+
+ @Test
+ fun activeTasks_multipleSearchShouldNotFindSameTaskTwice() {
+ val matchingPackage = "hotdog"
+ val matchingClass = "juice"
+ val matchingComponent =
+ ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+ val groupTask1 =
+ generateGroupTask(
+ ComponentName("hotdog", "pie"),
+ ComponentName("pumpkin", "pie")
+ )
+ val groupTask2 =
+ generateGroupTask(
+ ComponentName("pomegranate", "juice"),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val tasks: ArrayList<GroupTask> = ArrayList()
+ tasks.add(groupTask2)
+ tasks.add(groupTask1)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTasksAndRunCallback
+ val taskConsumer =
+ Consumer<List<Task>> {
+ assertEquals("Expected array length 2", 2, it.size)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[0].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[0].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[0], groupTask2.task2)
+ assertNull("No tasks should have matched", it[1] /*task*/)
+ }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent, matchingComponent),
+ taskConsumer
+ )
+ verify(recentsModel).getTasks(capture())
+ }
+
+ // Send our mocked tasks
+ consumer.accept(tasks)
+ }
+
+ @Test
+ fun activeTasks_multipleSearchShouldFindDifferentInstancesOfSameTask() {
+ val matchingPackage = "hotdog"
+ val matchingClass = "juice"
+ val matchingComponent =
+ ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+ val groupTask1 =
+ generateGroupTask(
+ ComponentName(matchingPackage, matchingClass),
+ ComponentName("pumpkin", "pie")
+ )
+ val groupTask2 =
+ generateGroupTask(
+ ComponentName("pomegranate", "juice"),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val tasks: ArrayList<GroupTask> = ArrayList()
+ tasks.add(groupTask2)
+ tasks.add(groupTask1)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTasksAndRunCallback
+ val taskConsumer =
+ Consumer<List<Task>> {
+ assertEquals("Expected array length 2", 2, it.size)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[0].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[0].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[0], groupTask1.task1)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[1].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[1].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[1], groupTask2.task2)
+ }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent, matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -366,6 +542,7 @@
): GroupTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
var intent = Intent()
intent.component = task1ComponentName
taskInfo.baseIntent = intent
@@ -373,6 +550,7 @@
val task2 = Task()
taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
intent = Intent()
intent.component = task2ComponentName
taskInfo.baseIntent = intent
@@ -393,6 +571,7 @@
): GroupTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
// Apply custom userHandle1
taskInfo.userId = userHandle1.identifier
var intent = Intent()
@@ -401,6 +580,7 @@
task1.key = Task.TaskKey(taskInfo)
val task2 = Task()
taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
// Apply custom userHandle2
taskInfo.userId = userHandle2.identifier
intent = Intent()
diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml
index 94ef06c..3d7ad2b 100644
--- a/res/anim-v33/shared_x_axis_activity_close_enter.xml
+++ b/res/anim-v33/shared_x_axis_activity_close_enter.xml
@@ -25,7 +25,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/standard_decelerate"
+ android:interpolator="@interpolator/standard_decelerate_interpolator"
android:startOffset="100"
android:duration="350" />
@@ -35,7 +35,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/fast_out_extra_slow_in"
+ android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml
index 19eb09e..fb63602 100644
--- a/res/anim-v33/shared_x_axis_activity_close_exit.xml
+++ b/res/anim-v33/shared_x_axis_activity_close_exit.xml
@@ -24,7 +24,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/standard_accelerate"
+ android:interpolator="@interpolator/standard_accelerate_interpolator"
android:startOffset="0"
android:duration="100" />
@@ -34,7 +34,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/fast_out_extra_slow_in"
+ android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml
index f699cec..cba74ba 100644
--- a/res/anim-v33/shared_x_axis_activity_open_enter.xml
+++ b/res/anim-v33/shared_x_axis_activity_open_enter.xml
@@ -25,7 +25,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/standard_decelerate"
+ android:interpolator="@interpolator/standard_decelerate_interpolator"
android:startOffset="100"
android:duration="350" />
@@ -35,7 +35,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/fast_out_extra_slow_in"
+ android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml
index 85988ec..22e878d 100644
--- a/res/anim-v33/shared_x_axis_activity_open_exit.xml
+++ b/res/anim-v33/shared_x_axis_activity_open_exit.xml
@@ -24,7 +24,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/standard_accelerate"
+ android:interpolator="@interpolator/standard_accelerate_interpolator"
android:startOffset="0"
android:duration="100" />
@@ -34,7 +34,7 @@
android:fillEnabled="true"
android:fillBefore="true"
android:fillAfter="true"
- android:interpolator="@interpolator/fast_out_extra_slow_in"
+ android:interpolator="@interpolator/emphasized_interpolator"
android:startOffset="0"
android:duration="450" />
diff --git a/res/drawable/ic_split_exit.xml b/res/drawable/ic_split_exit.xml
new file mode 100644
index 0000000..d7e8b03
--- /dev/null
+++ b/res/drawable/ic_split_exit.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="20dp"
+ android:tint="#000000"
+ android:viewportHeight="24"
+ android:viewportWidth="24"
+ android:width="20dp">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
\ No newline at end of file
diff --git a/res/interpolator/back_cancel.xml b/res/interpolator/back_cancel.xml
deleted file mode 100644
index 2165457..0000000
--- a/res/interpolator/back_cancel.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2022, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
- android:controlX1="0.2"
- android:controlY1="0"
- android:controlX2="0"
- android:controlY2="1"/>
\ No newline at end of file
diff --git a/res/interpolator/fast_out_extra_slow_in.xml b/res/interpolator/fast_out_extra_slow_in.xml
deleted file mode 100644
index f296a82..0000000
--- a/res/interpolator/fast_out_extra_slow_in.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
- -->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
\ No newline at end of file
diff --git a/res/interpolator/folder_interpolator.xml b/res/interpolator/folder_interpolator.xml
deleted file mode 100644
index b95d454..0000000
--- a/res/interpolator/folder_interpolator.xml
+++ /dev/null
@@ -1,24 +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.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
- android:controlX1="0.2"
- android:controlY1="0"
- android:controlX2="0"
- android:controlY2="1"/>
diff --git a/res/interpolator/large_folder_preview_item_close_interpolator.xml b/res/interpolator/large_folder_preview_item_close_interpolator.xml
deleted file mode 100644
index d28af63..0000000
--- a/res/interpolator/large_folder_preview_item_close_interpolator.xml
+++ /dev/null
@@ -1,24 +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.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
- android:controlX1="0.3"
- android:controlY1="0"
- android:controlX2="1"
- android:controlY2="1"/>
diff --git a/res/interpolator/standard_accelerate.xml b/res/interpolator/standard_accelerate.xml
deleted file mode 100644
index 394393d..0000000
--- a/res/interpolator/standard_accelerate.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
- android:controlX1="0.3"
- android:controlY1="0"
- android:controlX2="1"
- android:controlY2="1"/>
\ No newline at end of file
diff --git a/res/layout/app_pair_icon.xml b/res/layout/app_pair_icon.xml
new file mode 100644
index 0000000..2b9a98b
--- /dev/null
+++ b/res/layout/app_pair_icon.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.launcher3.apppairs.AppPairIcon
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:focusable="true" >
+ <com.android.launcher3.views.DoubleShadowBubbleTextView
+ style="@style/BaseIcon.Workspace"
+ android:id="@+id/app_pair_icon_name"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="false"
+ android:layout_gravity="top" />
+</com.android.launcher3.apppairs.AppPairIcon>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 39871f6..bcc626e 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -20,7 +20,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" msgid="649227358658669779">"Launcher3"</string>
- <string name="work_folder_name" msgid="3753320833950115786">"ስራ"</string>
+ <string name="work_folder_name" msgid="3753320833950115786">"ሥራ"</string>
<string name="activity_not_found" msgid="8071924732094499514">"መተግበሪያ አልተጫነም።"</string>
<string name="activity_not_available" msgid="7456344436509528827">"መተግበሪያ አይገኝም"</string>
<string name="safemode_shortcut_error" msgid="9160126848219158407">"የወረደው መተግበሪያ ደህንነቱ በተጠበቀ ሁኔታ ውስጥ ተሰናክሏል"</string>
@@ -48,7 +48,7 @@
<string name="no_widgets_available" msgid="4337693382501046170">"መግብሮች እና አቋራጮች አይገኙም"</string>
<string name="no_search_results" msgid="3787956167293097509">"ምንም መግብሮች ወይም አቋራጮች አልተገኙም"</string>
<string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"የግል"</string>
- <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ስራ"</string>
+ <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ሥራ"</string>
<string name="widget_category_conversations" msgid="8894438636213590446">"ውይይቶች"</string>
<string name="widget_category_note_taking" msgid="3469689394504266039">"የማስታወሻ አያያዝ"</string>
<string name="widget_education_header" msgid="4874760613775913787">"በጣቶችዎ ጫፎች ላይ ጠቃሚ መረጃ"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 45062fc..d3841a3 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -27,7 +27,7 @@
<string name="safemode_widget_error" msgid="4863470563535682004">"ନିରାପଦ ମୋଡରେ ୱିଜେଟ୍ ଅକ୍ଷମ କରାଗଲା"</string>
<string name="shortcut_not_available" msgid="2536503539825726397">"ଶର୍ଟକଟ୍ ଉପଲବ୍ଧ ନାହିଁ"</string>
<string name="home_screen" msgid="5629429142036709174">"ହୋମ"</string>
- <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ସ୍କ୍ରିନକୁ ସ୍ପ୍ଲିଟ୍ କରନ୍ତୁ"</string>
+ <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ସ୍କ୍ରିନକୁ ସ୍ପ୍ଲିଟ କରନ୍ତୁ"</string>
<string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ପାଇଁ ଆପ ସୂଚନା"</string>
<string name="save_app_pair" msgid="5647523853662686243">"ଆପ ପେୟାର ସେଭ କରନ୍ତୁ"</string>
<string name="long_press_widget_to_add" msgid="3587712543577675817">"ଏକ ୱିଜେଟକୁ ମୁଭ୍ କରିବା ପାଇଁ ସ୍ପର୍ଶ କରି ଧରି ରଖନ୍ତୁ।"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 733f527..05eaf88 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -202,16 +202,23 @@
<attr name="demoModeLayoutId" format="reference" />
<attr name="isScalable" format="boolean" />
<attr name="devicePaddingId" format="reference" />
- <!-- File that contains the specs for the workspace.
- Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
- <attr name="workspaceSpecsId" format="reference" />
- <!-- File that contains the specs for all apps.
- Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
- <attr name="allAppsSpecsId" format="reference" />
<!-- File that contains the specs for the workspace.
Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+ <attr name="workspaceSpecsId" format="reference" />
+ <attr name="workspaceSpecsTwoPanelId" format="reference" />
+ <!-- File that contains the specs for all apps.
+ Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+ <attr name="allAppsSpecsId" format="reference" />
+ <attr name="allAppsSpecsTwoPanelId" format="reference" />
+ <!-- File that contains the specs for the workspace.
+ Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
<attr name="folderSpecsId" format="reference" />
+ <attr name="folderSpecsTwoPanelId" format="reference" />
+ <!-- File that contains the specs for hotseat bar.
+ Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+ <attr name="hotseatSpecsId" format="reference" />
+ <attr name="hotseatSpecsTwoPanelId" format="reference" />
<!-- By default all categories are enabled -->
<attr name="deviceCategory" format="integer">
@@ -252,7 +259,7 @@
</declare-styleable>
<!-- Responsive grids attributes -->
- <declare-styleable name="WorkspaceSpec">
+ <declare-styleable name="ResponsiveSpec">
<attr name="specType" format="integer">
<enum name="height" value="0" />
<enum name="width" value="1" />
@@ -260,12 +267,9 @@
<attr name="maxAvailableSize" format="dimension" />
</declare-styleable>
- <declare-styleable name="SizeSpec">
- <attr name="fixedSize" format="dimension" />
- <attr name="ofAvailableSpace" format="float" />
- <attr name="ofRemainderSpace" format="float" />
- <attr name="matchWorkspace" format="boolean" />
- <attr name="maxSize" format="dimension" />
+ <declare-styleable name="WorkspaceSpec">
+ <attr name="specType" />
+ <attr name="maxAvailableSize" />
</declare-styleable>
<declare-styleable name="FolderSpec">
@@ -278,6 +282,19 @@
<attr name="maxAvailableSize" />
</declare-styleable>
+ <declare-styleable name="HotseatSpec">
+ <attr name="specType" />
+ <attr name="maxAvailableSize" />
+ </declare-styleable>
+
+ <declare-styleable name="SizeSpec">
+ <attr name="fixedSize" format="dimension" />
+ <attr name="ofAvailableSpace" format="float" />
+ <attr name="ofRemainderSpace" format="float" />
+ <attr name="matchWorkspace" format="boolean" />
+ <attr name="maxSize" format="dimension" />
+ </declare-styleable>
+
<declare-styleable name="ProfileDisplayOption">
<attr name="name" />
<attr name="minWidthDps" format="float" />
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 9a1ccd0..7131452 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -360,6 +360,37 @@
lp.y = sTmpRect.top;
}
+ // Handle invalid resize across CellLayouts in the two panel UI.
+ if (mCellLayout.getParent() instanceof Workspace) {
+ Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent();
+ CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout);
+ if (pairedCellLayout != null) {
+ Rect focusedCellLayoutBound = sTmpRect;
+ mDragLayerRelativeCoordinateHelper.viewToRect(mCellLayout, focusedCellLayoutBound);
+ Rect resizeFrameBound = sTmpRect2;
+ findViewById(R.id.widget_resize_frame).getGlobalVisibleRect(resizeFrameBound);
+ float progress = 1f;
+ if (workspace.indexOfChild(pairedCellLayout) < workspace.indexOfChild(mCellLayout)
+ && mDeltaX < 0
+ && resizeFrameBound.left < focusedCellLayoutBound.left) {
+ // Resize from right to left.
+ progress = (mDragAcrossTwoPanelOpacityMargin + mDeltaX)
+ / mDragAcrossTwoPanelOpacityMargin;
+ } else if (workspace.indexOfChild(pairedCellLayout)
+ > workspace.indexOfChild(mCellLayout)
+ && mDeltaX > 0
+ && resizeFrameBound.right > focusedCellLayoutBound.right) {
+ // Resize from left to right.
+ progress = (mDragAcrossTwoPanelOpacityMargin - mDeltaX)
+ / mDragAcrossTwoPanelOpacityMargin;
+ }
+ float alpha = Math.max(MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE, progress);
+ float springLoadedProgress = Math.min(1f, 1f - progress);
+ updateInvalidResizeEffect(mCellLayout, pairedCellLayout, alpha,
+ springLoadedProgress);
+ }
+ }
+
requestLayout();
}
@@ -516,6 +547,13 @@
}
final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+ final CellLayout pairedCellLayout;
+ if (mCellLayout.getParent() instanceof Workspace) {
+ Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent();
+ pairedCellLayout = workspace.getScreenPair(mCellLayout);
+ } else {
+ pairedCellLayout = null;
+ }
if (!animate) {
lp.width = newWidth;
lp.height = newHeight;
@@ -524,6 +562,10 @@
for (int i = 0; i < HANDLE_COUNT; i++) {
mDragHandles[i].setAlpha(1f);
}
+ if (pairedCellLayout != null) {
+ updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
+ /* springLoadedProgress= */ 0f);
+ }
requestLayout();
} else {
ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp,
@@ -539,6 +581,10 @@
set.play(mFirstFrameAnimatorHelper.addTo(
ObjectAnimator.ofFloat(mDragHandles[i], ALPHA, 1f)));
}
+ if (pairedCellLayout != null) {
+ updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
+ /* springLoadedProgress= */ 0f, /* animatorSet= */ set);
+ }
set.setDuration(SNAP_DURATION);
set.start();
}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 2356bcc..360e060 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -97,7 +97,7 @@
public static final int DISPLAY_ALL_APPS = 1;
private static final int DISPLAY_FOLDER = 2;
protected static final int DISPLAY_TASKBAR = 5;
- private static final int DISPLAY_SEARCH_RESULT = 6;
+ public static final int DISPLAY_SEARCH_RESULT = 6;
private static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
public static final int DISPLAY_PREDICTION_ROW = 8;
@@ -632,6 +632,11 @@
}
}
+ @VisibleForTesting
+ public boolean getForceHideDot() {
+ return mForceHideDot;
+ }
+
private boolean hasDot() {
return mDotInfo != null;
}
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 64ac841..4674401 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -2735,11 +2735,13 @@
}
public boolean isOccupied(int x, int y) {
- if (x < mCountX && y < mCountY) {
+ if (x >= 0 && x < mCountX && y >= 0 && y < mCountY) {
return mOccupied.cells[x][y];
- } else {
+ }
+ if (BuildConfig.IS_STUDIO_BUILD) {
throw new RuntimeException("Position exceeds the bound of this CellLayout");
}
+ return true;
}
@Override
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index f3b5155..7ece9a4 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -56,15 +56,17 @@
import com.android.launcher3.responsive.AllAppsSpecs;
import com.android.launcher3.responsive.CalculatedAllAppsSpec;
import com.android.launcher3.responsive.CalculatedFolderSpec;
+import com.android.launcher3.responsive.CalculatedHotseatSpec;
+import com.android.launcher3.responsive.CalculatedWorkspaceSpec;
import com.android.launcher3.responsive.FolderSpecs;
+import com.android.launcher3.responsive.HotseatSpecs;
+import com.android.launcher3.responsive.WorkspaceSpecs;
import com.android.launcher3.uioverrides.ApiWrapper;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.IconSizeSteps;
import com.android.launcher3.util.ResourceHelper;
import com.android.launcher3.util.WindowBounds;
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec;
-import com.android.launcher3.workspace.WorkspaceSpecs;
import java.io.PrintWriter;
import java.util.Locale;
@@ -115,15 +117,13 @@
// Responsive grid
private final boolean mIsResponsiveGrid;
- private WorkspaceSpecs mWorkspaceSpecs;
private CalculatedWorkspaceSpec mResponsiveWidthSpec;
private CalculatedWorkspaceSpec mResponsiveHeightSpec;
- private AllAppsSpecs mAllAppsSpecs;
private CalculatedAllAppsSpec mAllAppsResponsiveWidthSpec;
private CalculatedAllAppsSpec mAllAppsResponsiveHeightSpec;
- private FolderSpecs mFolderSpecs;
private CalculatedFolderSpec mResponsiveFolderWidthSpec;
private CalculatedFolderSpec mResponsiveFolderHeightSpec;
+ private CalculatedHotseatSpec mResponsiveHotseatSpec;
/**
* The maximum amount of left/right workspace padding as a percentage of the screen width.
@@ -319,7 +319,8 @@
// TODO(b/241386436): shouldn't change any launcher behaviour
mIsResponsiveGrid = inv.workspaceSpecsId != INVALID_RESOURCE_HANDLE
&& inv.allAppsSpecsId != INVALID_RESOURCE_HANDLE
- && inv.folderSpecsId != INVALID_RESOURCE_HANDLE;
+ && inv.folderSpecsId != INVALID_RESOURCE_HANDLE
+ && inv.hotseatSpecsId != INVALID_RESOURCE_HANDLE;
mIsScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
// Determine device posture.
@@ -498,7 +499,17 @@
int hotseatBarBottomSpace = pxFromDp(inv.hotseatBarBottomSpace[mTypeIndex], mMetrics);
int minQsbMargin = res.getDimensionPixelSize(R.dimen.min_qsb_margin);
- hotseatQsbSpace = pxFromDp(inv.hotseatQsbSpace[mTypeIndex], mMetrics);
+
+ if (mIsResponsiveGrid) {
+ HotseatSpecs hotseatSpecs =
+ HotseatSpecs.create(new ResourceHelper(context,
+ isTwoPanels ? inv.hotseatSpecsTwoPanelId : inv.hotseatSpecsId));
+ mResponsiveHotseatSpec = hotseatSpecs.getCalculatedHeightSpec(heightPx);
+ hotseatQsbSpace = mResponsiveHotseatSpec.getHotseatQsbSpace();
+ } else {
+ hotseatQsbSpace = pxFromDp(inv.hotseatQsbSpace[mTypeIndex], mMetrics);
+ }
+
// Have a little space between the inset and the QSB
if (mInsets.bottom + minQsbMargin > hotseatBarBottomSpace) {
int availableSpace = hotseatQsbSpace - (mInsets.bottom - hotseatBarBottomSpace);
@@ -545,29 +556,35 @@
// Needs to be calculated after hotseatBarSizePx is correct,
// for the available height to be correct
if (mIsResponsiveGrid) {
- mWorkspaceSpecs = new WorkspaceSpecs(new ResourceHelper(context, inv.workspaceSpecsId));
+ WorkspaceSpecs workspaceSpecs = WorkspaceSpecs.create(
+ new ResourceHelper(context,
+ isTwoPanels ? inv.workspaceSpecsTwoPanelId : inv.workspaceSpecsId));
int availableResponsiveWidth =
availableWidthPx - (isVerticalBarLayout() ? hotseatBarSizePx : 0);
+ int numColumns = getPanelCount() * inv.numColumns;
// don't use availableHeightPx because it subtracts bottom padding,
// but the workspace go behind it
int availableResponsiveHeight =
heightPx - mInsets.top - (isVerticalBarLayout() ? 0 : hotseatBarSizePx);
- mResponsiveWidthSpec = mWorkspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
+ mResponsiveWidthSpec = workspaceSpecs.getCalculatedWidthSpec(numColumns,
availableResponsiveWidth);
- mResponsiveHeightSpec = mWorkspaceSpecs.getCalculatedHeightSpec(inv.numRows,
+ mResponsiveHeightSpec = workspaceSpecs.getCalculatedHeightSpec(inv.numRows,
availableResponsiveHeight);
- mAllAppsSpecs = new AllAppsSpecs(new ResourceHelper(context, inv.allAppsSpecsId));
- mAllAppsResponsiveWidthSpec = mAllAppsSpecs.getCalculatedWidthSpec(inv.numColumns,
+ AllAppsSpecs allAppsSpecs = AllAppsSpecs.create(
+ new ResourceHelper(context,
+ isTwoPanels ? inv.allAppsSpecsTwoPanelId : inv.allAppsSpecsId));
+ mAllAppsResponsiveWidthSpec = allAppsSpecs.getCalculatedWidthSpec(numColumns,
mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
- mAllAppsResponsiveHeightSpec = mAllAppsSpecs.getCalculatedHeightSpec(inv.numRows,
- mResponsiveHeightSpec.getAvailableSpace(),
- mResponsiveHeightSpec);
+ mAllAppsResponsiveHeightSpec = allAppsSpecs.getCalculatedHeightSpec(inv.numRows,
+ mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
- mFolderSpecs = new FolderSpecs(new ResourceHelper(context, inv.folderSpecsId));
- mResponsiveFolderWidthSpec = mFolderSpecs.getWidthSpec(inv.numFolderColumns,
+ FolderSpecs folderSpecs = FolderSpecs.create(
+ new ResourceHelper(context,
+ isTwoPanels ? inv.folderSpecsTwoPanelId : inv.folderSpecsId));
+ mResponsiveFolderWidthSpec = folderSpecs.getCalculatedWidthSpec(inv.numFolderColumns,
mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
- mResponsiveFolderHeightSpec = mFolderSpecs.getHeightSpec(inv.numFolderRows,
+ mResponsiveFolderHeightSpec = folderSpecs.getCalculatedHeightSpec(inv.numFolderRows,
mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
}
@@ -615,6 +632,10 @@
// Hotseat and QSB width depends on updated cellSize and workspace padding
recalculateHotseatWidthAndBorderSpace();
+ if (mIsResponsiveGrid && isVerticalBarLayout()) {
+ hotseatBorderSpace = cellLayoutBorderSpacePx.y;
+ }
+
// AllApps height calculation depends on updated cellSize
if (isTablet) {
int collapseHandleHeight =
@@ -714,7 +735,7 @@
/** Updates hotseatCellHeightPx and hotseatBarSizePx */
private void updateHotseatSizes(int hotseatIconSizePx) {
// Ensure there is enough space for folder icons, which have a slightly larger radius.
- hotseatCellHeightPx = (int) Math.ceil(hotseatIconSizePx * ICON_OVERLAP_FACTOR);
+ hotseatCellHeightPx = getIconSizeWithOverlap(hotseatIconSizePx);
if (isVerticalBarLayout()) {
hotseatBarSizePx = hotseatIconSizePx + hotseatBarSidePaddingStartPx
@@ -780,7 +801,6 @@
hotseatBorderSpace = calculateHotseatBorderSpace(maxHotseatIconsWidthPx,
(isQsbInline ? 1 : 0) + /* border between nav buttons and first icon */ 1);
} while (hotseatBorderSpace < mMinHotseatIconSpacePx && numShownHotseatIcons > 1);
-
}
private Point getCellLayoutBorderSpace(InvariantDeviceProfile idp) {
@@ -863,11 +883,24 @@
float workspaceCellPaddingY = getCellSize().y - iconSizePx - iconDrawablePaddingPx
- iconTextHeight;
+ if (mIsResponsiveGrid) {
+ // Hide text only if doesn't fit inside the cell for responsive grid
+ if (workspaceCellPaddingY < 0) {
+ iconTextSizePx = 0;
+ iconDrawablePaddingPx = 0;
+ int iconSizeWithOverlap = getIconSizeWithOverlap(iconSizePx);
+ cellYPaddingPx = Math.max(0, getCellSize().y - iconSizeWithOverlap) / 2;
+ autoResizeAllAppsCells();
+ }
+
+ return;
+ }
+
// We want enough space so that the text is closer to its corresponding icon.
if (workspaceCellPaddingY < iconTextHeight) {
iconTextSizePx = 0;
iconDrawablePaddingPx = 0;
- cellHeightPx = (int) Math.ceil(iconSizePx * ICON_OVERLAP_FACTOR);
+ cellHeightPx = getIconSizeWithOverlap(iconSizePx);
autoResizeAllAppsCells();
}
}
@@ -885,6 +918,10 @@
updateIconSize(1f, res);
updateWorkspacePadding();
+ if (mIsResponsiveGrid) {
+ return 0;
+ }
+
// Check to see if the icons fit within the available height.
float usedHeight = getCellLayoutHeightSpecification();
final int maxHeight = getCellLayoutHeight();
@@ -943,6 +980,10 @@
return Math.max(0, drawablePadding - iconSizeDiff / 2);
}
+ private int getIconSizeWithOverlap(int iconSize) {
+ return (int) Math.ceil(iconSize * ICON_OVERLAP_FACTOR);
+ }
+
/**
* Updating the iconSize affects many aspects of the launcher layout, such as: iconSizePx,
* iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
@@ -1045,7 +1086,7 @@
} else {
iconDrawablePaddingPx = (int) (getNormalizedIconDrawablePadding() * iconScale);
cellWidthPx = iconSizePx + iconDrawablePaddingPx;
- cellHeightPx = (int) Math.ceil(iconSizePx * ICON_OVERLAP_FACTOR)
+ cellHeightPx = getIconSizeWithOverlap(iconSizePx)
+ iconDrawablePaddingPx
+ Utilities.calculateTextHeight(iconTextSizePx);
int cellPaddingY = (getCellSize().y - cellHeightPx) / 2;
@@ -1100,7 +1141,6 @@
return Math.min(hotseatBorderSpacePx, mMaxHotseatIconSpacePx);
}
-
/**
* Updates the iconSize for allApps* variants.
*/
@@ -1206,7 +1246,6 @@
allAppsStyle.recycle();
}
- // TODO(b/288075868): Resize the icon size to make sure it will fit inside the cell size
private void updateAvailableFolderCellDimensions(Resources res) {
updateFolderCellSize(1f, res);
@@ -1449,14 +1488,26 @@
private void updateWorkspacePadding() {
Rect padding = workspacePadding;
if (isVerticalBarLayout()) {
- padding.top = 0;
- padding.bottom = edgeMarginPx;
- if (isSeascape()) {
- padding.left = hotseatBarSizePx;
- padding.right = hotseatBarSidePaddingStartPx;
+ if (mIsResponsiveGrid) {
+ padding.top = mResponsiveHeightSpec.getStartPaddingPx();
+ padding.bottom = mResponsiveHeightSpec.getEndPaddingPx();
+ if (isSeascape()) {
+ padding.left = hotseatBarSizePx + mResponsiveWidthSpec.getEndPaddingPx();
+ padding.right = mResponsiveWidthSpec.getStartPaddingPx();
+ } else {
+ padding.left = mResponsiveWidthSpec.getStartPaddingPx();
+ padding.right = hotseatBarSizePx + mResponsiveWidthSpec.getEndPaddingPx();
+ }
} else {
- padding.left = hotseatBarSidePaddingStartPx;
- padding.right = hotseatBarSizePx;
+ padding.top = 0;
+ padding.bottom = edgeMarginPx;
+ if (isSeascape()) {
+ padding.left = hotseatBarSizePx;
+ padding.right = hotseatBarSidePaddingStartPx;
+ } else {
+ padding.left = hotseatBarSidePaddingStartPx;
+ padding.right = hotseatBarSizePx;
+ }
}
} else {
// Pad the bottom of the workspace with hotseat bar
@@ -1499,7 +1550,9 @@
// in vertical bar layout.
// Workspace icons are moved up by a small factor. The variable diffOverlapFactor
// is set to account for that difference.
- float diffOverlapFactor = iconSizePx * (ICON_OVERLAP_FACTOR - 1) / 2;
+ float diffOverlapFactor = mIsResponsiveGrid ? 0
+ : iconSizePx * (ICON_OVERLAP_FACTOR - 1) / 2;
+
int paddingTop = Math.max((int) (mInsets.top + cellLayoutPaddingPx.top
- diffOverlapFactor), 0);
int paddingBottom = Math.max((int) (mInsets.bottom + cellLayoutPaddingPx.bottom
@@ -1946,6 +1999,7 @@
+ mAllAppsResponsiveWidthSpec.toString());
writer.println(prefix + "\tmResponsiveFolderHeightSpec:" + mResponsiveFolderHeightSpec);
writer.println(prefix + "\tmResponsiveFolderWidthSpec:" + mResponsiveFolderWidthSpec);
+ writer.println(prefix + "\tmResponsiveHotseatSpec:" + mResponsiveHotseatSpec);
}
}
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index f94a3c5..3c90408 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -21,6 +21,7 @@
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.DragEvent;
import android.view.KeyEvent;
import android.view.inputmethod.InputMethodManager;
@@ -37,6 +38,8 @@
* Note: AppCompatEditText doesn't fully support #displayCompletions and #onCommitCompletion
*/
public class ExtendedEditText extends EditText {
+ private static final String TAG = "ExtendedEditText";
+
private final Set<OnFocusChangeListener> mOnFocusChangeListeners = new HashSet<>();
private boolean mForceDisableSuggestions = false;
@@ -89,9 +92,17 @@
return false;
}
- public void showKeyboard() {
+ /**
+ * Synchronously shows the soft input method.
+ *
+ * @param shouldFocus whether this EditText should also request focus.
+ * @return true if the keyboard is shown correctly and focus is given to this view (if
+ * applicable).
+ */
+ public boolean showKeyboard(boolean shouldFocus) {
onKeyboardShown();
- showSoftInput();
+ boolean focusResult = !shouldFocus || requestFocus();
+ return focusResult && showSoftInputInternal();
}
public void hideKeyboard() {
@@ -104,10 +115,15 @@
.keyboardStateManager().setKeyboardState(SHOW);
}
- private boolean showSoftInput() {
- return requestFocus() &&
- getContext().getSystemService(InputMethodManager.class)
- .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
+ private boolean showSoftInputInternal() {
+ boolean result = false;
+ InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
+ if (imm != null) {
+ result = imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
+ } else {
+ Log.w(TAG, "Failed to retrieve InputMethodManager from the system.");
+ }
+ return result;
}
public void dispatchBackKey() {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 4eaacdc..e3de79e 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.LauncherPrefs.GRID_NAME;
import static com.android.launcher3.Utilities.dpiFromPx;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME;
import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
@@ -49,6 +50,7 @@
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.DotRenderer;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.DeviceGridState;
import com.android.launcher3.provider.RestoreDbTask;
import com.android.launcher3.testing.shared.ResourceUtils;
@@ -180,9 +182,17 @@
@XmlRes
public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
@XmlRes
+ public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+ @XmlRes
public int allAppsSpecsId = INVALID_RESOURCE_HANDLE;
@XmlRes
+ public int allAppsSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+ @XmlRes
public int folderSpecsId = INVALID_RESOURCE_HANDLE;
+ @XmlRes
+ public int folderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+ public int hotseatSpecsId = INVALID_RESOURCE_HANDLE;
+ public int hotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
public String dbFile;
public int defaultLayoutId;
@@ -278,12 +288,13 @@
* Reinitialize the current grid after a restore, where some grids might now be disabled.
*/
public void reinitializeAfterRestore(Context context) {
+ FileLog.d(TAG, "Reinitializing grid after restore");
String currentGridName = getCurrentGridName(context);
String currentDbFile = dbFile;
String newGridName = initGrid(context, currentGridName);
String newDbFile = dbFile;
if (!newDbFile.equals(currentDbFile)) {
- Log.d(TAG, "Restored grid is disabled : " + currentGridName
+ FileLog.d(TAG, "Restored grid is disabled : " + currentGridName
+ ", migrating to: " + newGridName
+ ", removing all other grid db files");
for (String gridDbFile : LauncherFiles.GRID_DB_FILES) {
@@ -291,7 +302,7 @@
continue;
}
if (context.getDatabasePath(gridDbFile).delete()) {
- Log.d(TAG, "Removed old grid db file: " + gridDbFile);
+ FileLog.d(TAG, "Removed old grid db file: " + gridDbFile);
}
}
setCurrentGrid(context, newGridName);
@@ -305,7 +316,7 @@
int type = displayInfo.supportedBounds.stream()
.mapToInt(bounds -> displayInfo.isTablet(bounds) ? flagTablet : flagPhone)
.reduce(0, (a, b) -> a | b);
- if ((type == (flagPhone | flagTablet))) {
+ if ((type == (flagPhone | flagTablet)) && ENABLE_TWO_PANEL_HOME.get()) {
// device has profiles supporting both phone and table modes
return TYPE_MULTI_DISPLAY;
} else if (type == flagTablet) {
@@ -357,8 +368,13 @@
isScalable = closestProfile.isScalable;
devicePaddingId = closestProfile.devicePaddingId;
workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
+ workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
allAppsSpecsId = closestProfile.mAllAppsSpecsId;
+ allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
folderSpecsId = closestProfile.mFolderSpecsId;
+ folderSpecsTwoPanelId = closestProfile.mFolderSpecsTwoPanelId;
+ hotseatSpecsId = closestProfile.mHotseatSpecsId;
+ hotseatSpecsTwoPanelId = closestProfile.mHotseatSpecsTwoPanelId;
this.deviceType = deviceType;
inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
@@ -805,8 +821,13 @@
private final boolean isScalable;
private final int devicePaddingId;
private final int mWorkspaceSpecsId;
+ private final int mWorkspaceSpecsTwoPanelId;
private final int mAllAppsSpecsId;
+ private final int mAllAppsSpecsTwoPanelId;
private final int mFolderSpecsId;
+ private final int mFolderSpecsTwoPanelId;
+ private final int mHotseatSpecsId;
+ private final int mHotseatSpecsTwoPanelId;
public GridOption(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(
@@ -871,14 +892,33 @@
if (FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE.get()) {
mWorkspaceSpecsId = a.getResourceId(
R.styleable.GridDisplayOption_workspaceSpecsId, INVALID_RESOURCE_HANDLE);
+ mWorkspaceSpecsTwoPanelId = a.getResourceId(
+ R.styleable.GridDisplayOption_workspaceSpecsTwoPanelId,
+ INVALID_RESOURCE_HANDLE);
mAllAppsSpecsId = a.getResourceId(
R.styleable.GridDisplayOption_allAppsSpecsId, INVALID_RESOURCE_HANDLE);
+ mAllAppsSpecsTwoPanelId = a.getResourceId(
+ R.styleable.GridDisplayOption_allAppsSpecsTwoPanelId,
+ INVALID_RESOURCE_HANDLE);
mFolderSpecsId = a.getResourceId(
R.styleable.GridDisplayOption_folderSpecsId, INVALID_RESOURCE_HANDLE);
+ mFolderSpecsTwoPanelId = a.getResourceId(
+ R.styleable.GridDisplayOption_folderSpecsTwoPanelId,
+ INVALID_RESOURCE_HANDLE);
+ mHotseatSpecsId = a.getResourceId(
+ R.styleable.GridDisplayOption_hotseatSpecsId, INVALID_RESOURCE_HANDLE);
+ mHotseatSpecsTwoPanelId = a.getResourceId(
+ R.styleable.GridDisplayOption_hotseatSpecsTwoPanelId,
+ INVALID_RESOURCE_HANDLE);
} else {
mWorkspaceSpecsId = INVALID_RESOURCE_HANDLE;
+ mWorkspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
mAllAppsSpecsId = INVALID_RESOURCE_HANDLE;
+ mAllAppsSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
mFolderSpecsId = INVALID_RESOURCE_HANDLE;
+ mFolderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+ mHotseatSpecsId = INVALID_RESOURCE_HANDLE;
+ mHotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
}
int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index bfbd660..f166833 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -44,6 +44,7 @@
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.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
import static com.android.launcher3.config.FeatureFlags.SHOW_DOT_PAGINATION;
import static com.android.launcher3.logging.StatsLogManager.EventEnum;
@@ -99,7 +100,6 @@
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
-import android.os.CancellationSignal;
import android.os.Parcelable;
import android.os.StrictMode;
import android.os.SystemClock;
@@ -145,6 +145,7 @@
import com.android.launcher3.allapps.BaseSearchConfig;
import com.android.launcher3.allapps.DiscoveryBounce;
import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
@@ -153,7 +154,6 @@
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.dragndrop.LauncherDragController;
import com.android.launcher3.folder.Folder;
@@ -725,18 +725,23 @@
@Override
protected void onHandleConfigurationChanged() {
- if (!initDeviceProfile(mDeviceProfile.inv)) {
- return;
+ Trace.beginSection("Launcher#onHandleconfigurationChanged");
+ try {
+ if (!initDeviceProfile(mDeviceProfile.inv)) {
+ return;
+ }
+
+ dispatchDeviceProfileChanged();
+ reapplyUi();
+ mDragLayer.recreateControllers();
+
+ // Calling onSaveInstanceState ensures that static cache used by listWidgets is
+ // initialized properly.
+ onSaveInstanceState(new Bundle());
+ mModel.rebindCallbacks();
+ } finally {
+ Trace.endSection();
}
-
- dispatchDeviceProfileChanged();
- reapplyUi();
- mDragLayer.recreateControllers();
-
- // Calling onSaveInstanceState ensures that static cache used by listWidgets is
- // initialized properly.
- onSaveInstanceState(new Bundle());
- mModel.rebindCallbacks();
}
public void onAssistantVisibilityChanged(float visibility) {
@@ -760,7 +765,7 @@
}
onDeviceProfileInitiated();
- if (mDeviceProfile.isTwoPanels) {
+ if (FOLDABLE_SINGLE_PAGE.get() && mDeviceProfile.isTwoPanels) {
mCellPosMapper = new TwoPanelCellPosMapper(mDeviceProfile.inv.numColumns);
} else {
mCellPosMapper = CellPosMapper.DEFAULT;
@@ -1663,7 +1668,8 @@
if (isActionMain) {
if (!internalStateHandled) {
// In all these cases, only animate if we're already on home
- closeOpenViews(isStarted());
+ AbstractFloatingView.closeAllOpenViewsExcept(
+ this, isStarted(), AbstractFloatingView.TYPE_LISTENER);
if (!isInState(NORMAL)) {
// Only change state, if not already the same. This prevents cancelling any
@@ -2445,9 +2451,9 @@
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
- FolderInfo info = (FolderInfo) item;
- // TODO (jeremysim b/274189428): Create app pair icon
- view = null;
+ view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, this,
+ (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
+ (FolderInfo) item);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
@@ -3242,8 +3248,6 @@
return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
}
- public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { }
-
public void onDragLayerHierarchyChanged() {
updateDisallowBack();
}
@@ -3309,10 +3313,6 @@
return false;
}
- public DragOptions getDefaultWorkspaceDragOptions() {
- return new DragOptions();
- }
-
/**
* Animates Launcher elements during a transition to the All Apps page.
*
@@ -3389,4 +3389,12 @@
public View.OnLongClickListener getAllAppsItemLongClickListener() {
return ItemLongClickListener.INSTANCE_ALL_APPS;
}
+
+ /**
+ * Handles an app pair launch; overridden in
+ * {@link com.android.launcher3.uioverrides.QuickstepLauncher}
+ */
+ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ // Overridden
+ }
}
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
index 3d2700d..2617b93 100644
--- a/src/com/android/launcher3/LauncherBackupAgent.java
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -34,7 +34,7 @@
// Remove old files which might contain obsolete attributes like idp_grid_name in shared
// preference that will obstruct backup's attribute from writing to shared preferences.
if (destination.delete()) {
- FileLog.d("LauncherBackupAgent", "Removed obsolete file: " + destination);
+ FileLog.d(TAG, "onRestoreFile: Removed obsolete file " + destination);
}
super.onRestoreFile(data, size, destination, type, mode, mtime);
}
@@ -47,6 +47,7 @@
@Override
public void onRestoreFinished() {
+ FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
RestoreDbTask.setPending(this);
}
}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 5f3d27c..fd8f668 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -102,6 +102,9 @@
private LoaderTask mLoaderTask;
private boolean mIsLoaderTaskRunning;
+ // only allow this once per reboot to reload work apps
+ private boolean mShouldReloadWorkProfile = true;
+
// Indicates whether the current model data is valid or not.
// We start off with everything not loaded. After that, we assume that
// our monitoring of the package manager provides all updates and we never
@@ -308,17 +311,19 @@
*/
public void onUserEvent(UserHandle user, String action) {
if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
+ && mShouldReloadWorkProfile) {
+ mShouldReloadWorkProfile = false;
+ forceReload();
+ } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
|| Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
+ mShouldReloadWorkProfile = false;
enqueueModelUpdateTask(new PackageUpdatedTask(
PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
- }
-
- if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
+ } else if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
|| UserCache.ACTION_PROFILE_UNLOCKED.equals(action)) {
enqueueModelUpdateTask(new UserLockStateChangedTask(
user, UserCache.ACTION_PROFILE_UNLOCKED.equals(action)));
- }
- if (UserCache.ACTION_PROFILE_ADDED.equals(action)
+ } else if (UserCache.ACTION_PROFILE_ADDED.equals(action)
|| UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
forceReload();
}
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index bfbca65..d2ea7cc 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -386,8 +386,7 @@
* Gets the translation provider for workspace pages.
*/
public PageTranslationProvider getWorkspacePageTranslationProvider(Launcher launcher) {
- if (this != SPRING_LOADED
- || this != EDIT_MODE
+ if (!(this == SPRING_LOADED || this == EDIT_MODE)
|| !launcher.getDeviceProfile().isTwoPanels) {
return DEFAULT_PAGE_TRANSLATION_PROVIDER;
}
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index aaccb7d..d460ba8 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -32,6 +32,8 @@
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.util.Executors;
+import java.util.Locale;
+
/**
* BroadcastReceiver to handle session commit intent.
*/
@@ -63,9 +65,20 @@
}
InstallSessionHelper packageInstallerCompat = InstallSessionHelper.INSTANCE.get(context);
+ boolean alreadyAddedPromiseIcon =
+ packageInstallerCompat.promiseIconAddedForId(info.getSessionId());
if (TextUtils.isEmpty(info.getAppPackageName())
|| info.getInstallReason() != PackageManager.INSTALL_REASON_USER
- || packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) {
+ || alreadyAddedPromiseIcon) {
+ FileLog.d(LOG,
+ String.format(Locale.ENGLISH,
+ "Removing PromiseIcon for package: %s, install reason: %d,"
+ + " alreadyAddedPromiseIcon: %s",
+ info.getAppPackageName(),
+ info.getInstallReason(),
+ alreadyAddedPromiseIcon
+ )
+ );
packageInstallerCompat.removePromiseIconId(info.getSessionId());
return;
}
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index f921d1d..07b71b3 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -28,6 +28,7 @@
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.Trace;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -182,6 +183,7 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ Trace.beginSection("ShortcutAndWidgetConteiner#onLayout");
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
@@ -189,6 +191,7 @@
layoutChild(child);
}
}
+ Trace.endSection();
}
/**
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 4f5de05..e8c6ff9 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -334,14 +334,12 @@
public static void scaleRectAboutCenter(Rect r, float scale) {
if (scale != 1.0f) {
- int cx = r.centerX();
- int cy = r.centerY();
- r.offset(-cx, -cy);
- r.left = (int) (r.left * scale + 0.5f);
- r.top = (int) (r.top * scale + 0.5f);
- r.right = (int) (r.right * scale + 0.5f);
- r.bottom = (int) (r.bottom * scale + 0.5f);
- r.offset(cx, cy);
+ float cx = r.exactCenterX();
+ float cy = r.exactCenterY();
+ r.left = Math.round(cx + (r.left - cx) * scale);
+ r.top = Math.round(cy + (r.top - cy) * scale);
+ r.right = Math.round(cx + (r.right - cx) * scale);
+ r.bottom = Math.round(cy + (r.bottom - cy) * scale);
}
}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index b141e18..adaf20f 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -28,6 +28,7 @@
import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
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;
@@ -126,6 +127,7 @@
import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -501,14 +503,19 @@
.log(LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
}
- public void deferRemoveExtraEmptyScreen() {
- mDeferRemoveExtraEmptyScreen = true;
+ private boolean isTwoPanelEnabled() {
+ return !FOLDABLE_SINGLE_PAGE.get() && mLauncher.mDeviceProfile.isTwoPanels;
}
@Override
public int getPanelCount() {
- return super.getPanelCount();
+ return isTwoPanelEnabled() ? 2 : super.getPanelCount();
}
+
+ public void deferRemoveExtraEmptyScreen() {
+ mDeferRemoveExtraEmptyScreen = true;
+ }
+
@Override
public void onDragEnd() {
if (ENFORCE_DRAG_EVENT_ORDER) {
@@ -661,7 +668,7 @@
// created CellLayout.
DeviceProfile dp = mLauncher.getDeviceProfile();
CellLayout newScreen;
- if (dp.isTwoPanels) {
+ if (FOLDABLE_SINGLE_PAGE.get() && dp.isTwoPanels) {
newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
R.layout.workspace_screen_foldable, this, false /* attachToRoot */);
} else {
@@ -686,6 +693,15 @@
if (mDragSourceInternal != null) {
int dragSourceChildCount = mDragSourceInternal.getChildCount();
+
+ // If the icon was dragged from Hotseat, there is no page pair
+ if (isTwoPanelEnabled() && !(mDragSourceInternal.getParent() instanceof Hotseat)) {
+ int pagePairScreenId = getScreenPair(getCellPosMapper().mapModelToPresenter(
+ dragObject.dragInfo).screenId);
+ CellLayout pagePair = mWorkspaceScreens.get(pagePairScreenId);
+ dragSourceChildCount += pagePair.getShortcutsAndWidgets().getChildCount();
+ }
+
// When the drag view content is a LauncherAppWidgetHostView, we should increment the
// drag source child count by 1 because the widget in drag has been detached from its
// original parent, ShortcutAndWidgetContainer, and reattached to the DragView.
@@ -696,6 +712,11 @@
if (dragSourceChildCount == 1) {
lastChildOnScreen = true;
}
+ CellLayout cl = (CellLayout) mDragSourceInternal.getParent();
+ if (!FOLDABLE_SINGLE_PAGE.get() && getLeftmostVisiblePageForIndex(indexOfChild(cl))
+ == getLeftmostVisiblePageForIndex(getPageCount() - 1)) {
+ childOnFinalScreen = true;
+ }
}
// If this is the last item on the final screen
@@ -730,6 +751,9 @@
*/
private void forEachExtraEmptyPageId(Consumer<Integer> callback) {
callback.accept(EXTRA_EMPTY_SCREEN_ID);
+ if (isTwoPanelEnabled()) {
+ callback.accept(EXTRA_EMPTY_SCREEN_SECOND_ID);
+ }
}
/**
@@ -843,7 +867,9 @@
public boolean hasExtraEmptyScreens() {
return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)
- && getChildCount() > getPanelCount();
+ && getChildCount() > getPanelCount()
+ && (!isTwoPanelEnabled()
+ || mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_SECOND_ID));
}
/**
@@ -949,7 +975,14 @@
*/
@Nullable
public CellLayout getScreenPair(CellLayout cellLayout) {
- return null;
+ if (!isTwoPanelEnabled()) {
+ return null;
+ }
+ int screenId = getIdForScreen(cellLayout);
+ if (screenId == -1) {
+ return null;
+ }
+ return getScreenWithId(getScreenPair(screenId));
}
public void stripEmptyScreens() {
@@ -977,6 +1010,22 @@
}
}
+ // When two panel home is enabled we only remove an empty page if both visible pages are
+ // empty.
+ if (isTwoPanelEnabled()) {
+ // We go through all the pages that were marked as removable and check their page pair
+ Iterator<Integer> removeScreensIterator = removeScreens.iterator();
+ while (removeScreensIterator.hasNext()) {
+ int pageToRemove = removeScreensIterator.next();
+ int pagePair = getScreenPair(pageToRemove);
+ if (!removeScreens.contains(pagePair)) {
+ // The page pair isn't empty so we want to remove the current page from the
+ // removable pages' collection
+ removeScreensIterator.remove();
+ }
+ }
+ }
+
// We enforce at least one page (two pages on two panel home) to add new items to.
// In the case that we remove the last such screen(s), we convert the last screen(s)
// to the empty screen(s)
@@ -997,7 +1046,12 @@
removeView(cl);
} else {
// The last page(s) should be converted into extra empty page(s)
- int extraScreenId = EXTRA_EMPTY_SCREEN_ID;
+ int extraScreenId = isTwoPanelEnabled() && id % 2 == 1
+ // This is the right panel in a two panel scenario
+ ? EXTRA_EMPTY_SCREEN_SECOND_ID
+ // This is either the last screen in a one panel scenario, or the left panel
+ // in a two panel scenario when there are only two empty pages left
+ : EXTRA_EMPTY_SCREEN_ID;
mWorkspaceScreens.put(extraScreenId, cl);
mScreenOrder.add(extraScreenId);
}
@@ -2518,7 +2572,8 @@
// Go through the pages and check if the dragged item is inside one of them. This block
// is responsible for determining whether we need to snap to a different screen.
int nextPage = getNextPage();
- IntSet pageIndexesToVerify = IntSet.wrap(nextPage - 1, nextPage + 1);
+ IntSet pageIndexesToVerify = IntSet.wrap(nextPage - 1,
+ nextPage + (isTwoPanelEnabled() ? 2 : 1));
for (int pageIndex : pageIndexesToVerify) {
// When deciding whether to perform a page switch, we need to consider the most
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index eb4ecaf..40382b2 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -18,10 +18,12 @@
import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
+import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
import android.animation.Animator;
@@ -391,7 +393,10 @@
rebindAdapters(false);
mRebindAdaptersAfterSearchAnimation = false;
}
- if (!goingToSearch) {
+
+ if (goingToSearch) {
+ mSearchUiDelegate.onAnimateToSearchStateCompleted();
+ } else {
setSearchResults(null);
if (mViewPager != null) {
mViewPager.setCurrentPage(previousPage);
@@ -458,7 +463,7 @@
updateHeaderScroll(0);
if (exitSearch) {
// Reset the search bar after transitioning home.
- mSearchUiManager.resetSearch();
+ MAIN_EXECUTOR.getHandler().post(mSearchUiManager::resetSearch);
// Animate to A-Z with 0 time to reset the animation with proper state management.
animateToSearchState(false, 0);
}
@@ -548,17 +553,14 @@
mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
+ final AllAppsRecyclerView mainRecyclerView;
+ final AllAppsRecyclerView workRecyclerView;
if (mUsingTabs) {
- mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher);
- mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher());
- mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work);
- if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
- // Let main and work rv share same view pool.
- ((RecyclerView) mViewPager.getChildAt(0))
- .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
- ((RecyclerView) mViewPager.getChildAt(1))
- .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
- }
+ mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0);
+ workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1);
+ mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher);
+ mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher());
+ workRecyclerView.setId(R.id.apps_list_view_work);
if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener(
mWorkManager.newScrollListener());
@@ -583,9 +585,15 @@
onActivePageChanged(mViewPager.getNextPage());
}
} else {
- mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null);
+ mainRecyclerView = findViewById(R.id.apps_list_view);
+ workRecyclerView = null;
+ mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, null);
mAH.get(AdapterHolder.WORK).mRecyclerView = null;
}
+ setUpCustomRecyclerViewPool(
+ mainRecyclerView,
+ workRecyclerView,
+ mAllAppsStore.getRecyclerViewPool());
setupHeader();
if (isSearchBarFloating()) {
@@ -602,6 +610,30 @@
mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
}
+ /**
+ * If {@link ENABLE_ALL_APPS_RV_PREINFLATION} is enabled, wire custom
+ * {@link RecyclerView.RecycledViewPool} to main and work {@link AllAppsRecyclerView}.
+ *
+ * Then if {@link ALL_APPS_GONE_VISIBILITY} is enabled, update max pool size. This is because
+ * all apps rv's hidden visibility is changed to {@link View#GONE} from {@link View#INVISIBLE),
+ * thus we cannot rely on layout pass to update pool size.
+ */
+ private static void setUpCustomRecyclerViewPool(
+ @NonNull AllAppsRecyclerView mainRecyclerView,
+ @Nullable AllAppsRecyclerView workRecyclerView,
+ @NonNull RecyclerView.RecycledViewPool recycledViewPool) {
+ if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+ return;
+ }
+ mainRecyclerView.setRecycledViewPool(recycledViewPool);
+ if (workRecyclerView != null) {
+ workRecyclerView.setRecycledViewPool(recycledViewPool);
+ }
+ if (ALL_APPS_GONE_VISIBILITY.get()) {
+ mainRecyclerView.updatePoolSize();
+ }
+ }
+
private void replaceAppsRVContainer(boolean showTabs) {
for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) {
AdapterHolder adapterHolder = mAH.get(i);
@@ -902,6 +934,7 @@
public void onDeviceProfileChanged(DeviceProfile dp) {
for (AdapterHolder holder : mAH) {
holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns);
+ holder.mAppsList.setNumAppsPerRowAllApps(dp.numShownAllAppsColumns);
if (holder.mRecyclerView != null) {
// Remove all views and clear the pool, while keeping the data same. After this
// call, all the viewHolders will be recreated.
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 602d1a3..7edbeac 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
import static com.android.launcher3.logger.LauncherAtom.ContainerInfo;
import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN;
@@ -26,6 +27,8 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND;
+import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.EXTRA_ICONS_COUNT;
+import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.PREINFLATE_ICONS_ROW_COUNT;
import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING;
import android.content.Context;
@@ -96,8 +99,18 @@
int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
+
+ // If all apps' hidden visibility is INVISIBLE, we will need to preinflate one page of
+ // all apps icons for smooth scrolling.
+ int maxPoolSizeForAppIcons = (approxRows + 1) * grid.numShownAllAppsColumns;
+ if (ALL_APPS_GONE_VISIBILITY.get()) {
+ // If all apps' hidden visibility is GONE, we need to increase prefinated icons number
+ // by [PREINFLATE_ICONS_ROW_COUNT] rows + [EXTRA_ICONS_COUNT] for fast opening all apps.
+ maxPoolSizeForAppIcons +=
+ PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT;
+ }
pool.setMaxRecycledViews(
- AllAppsGridAdapter.VIEW_TYPE_ICON, (approxRows + 1) * grid.numShownAllAppsColumns);
+ AllAppsGridAdapter.VIEW_TYPE_ICON, maxPoolSizeForAppIcons);
}
@Override
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 0d7b736..c09a5b9 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -438,7 +438,8 @@
mAppsView = appsView;
mAppsView.setScrimView(scrimView);
- mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT);
+ mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT,
+ FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE);
mAppsViewAlpha.setUpdateVisibility(true);
mAppsViewTranslationY = new MultiPropertyFactory<>(
mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum);
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionListener.java b/src/com/android/launcher3/allapps/AllAppsTransitionListener.java
new file mode 100644
index 0000000..4a17e29
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionListener.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+/**
+ * An interface for listening to all-apps open-close transition
+ */
+public interface AllAppsTransitionListener {
+ /**
+ * Called when the transition starts
+ * @param toAllApps {@code true} if this transition is supposed to end in the AppApps UI
+ *
+ * @see ActivityAllAppsContainerView
+ */
+ void onAllAppsTransitionStart(boolean toAllApps);
+
+ /**
+ * Called when the transition ends
+ * @param toAllApps {@code true} if the final state is all-apps
+ *
+ * @see ActivityAllAppsContainerView
+ */
+ void onAllAppsTransitionEnd(boolean toAllApps);
+}
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 0657178..ee4b5bc 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -82,7 +82,7 @@
private final ArrayList<AdapterItem> mSearchResults = new ArrayList<>();
private BaseAllAppsAdapter<T> mAdapter;
private AppInfoComparator mAppNameComparator;
- private final int mNumAppsPerRowAllApps;
+ private int mNumAppsPerRowAllApps;
private int mNumAppRowsInAdapter;
private Predicate<ItemInfo> mItemFilter;
@@ -92,12 +92,17 @@
mActivityContext = ActivityContext.lookupContext(context);
mAppNameComparator = new AppInfoComparator(context);
mWorkProviderManager = workProfileManager;
- mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().inv.numAllAppsColumns;
+ mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
if (mAllAppsStore != null) {
mAllAppsStore.addUpdateListener(this);
}
}
+ /** Set the number of apps per row when device profile changes. */
+ public void setNumAppsPerRowAllApps(int numAppsPerRow) {
+ mNumAppsPerRowAllApps = numAppsPerRow;
+ }
+
public void updateItemFilter(Predicate<ItemInfo> itemFilter) {
this.mItemFilter = itemFilter;
onAppsUpdated();
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 4427a49..ecbc7a9 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -160,7 +160,7 @@
* Focuses the search field to handle key events.
*/
public void focusSearchField() {
- mInput.showKeyboard();
+ mInput.showKeyboard(true /* shouldFocus */);
}
/**
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
index 49cecca..2347bfd 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
@@ -49,6 +49,11 @@
// Do nothing.
}
+ /** Invoked when transition animations to go to search is completed . */
+ public void onAnimateToSearchStateCompleted() {
+ // Do nothing
+ }
+
/** Invoked when the search bar has been added to All Apps. */
public void onInitializeSearchBar() {
// Do nothing.
diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java
index 8dad1b4..4382174 100644
--- a/src/com/android/launcher3/anim/AlphaUpdateListener.java
+++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java
@@ -53,8 +53,18 @@
}
public static void updateVisibility(View view) {
- if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != View.INVISIBLE) {
- view.setVisibility(View.INVISIBLE);
+ updateVisibility(view, View.INVISIBLE);
+ }
+
+ /**
+ * Update view's visibility.
+ *
+ * @param view View that needs to update visibility.
+ * @param hiddenVisibility {@link View#GONE} or {@link View#INVISIBLE}
+ */
+ public static void updateVisibility(View view, int hiddenVisibility) {
+ if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != hiddenVisibility) {
+ view.setVisibility(hiddenVisibility);
} else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD
&& view.getVisibility() != View.VISIBLE) {
if (view instanceof ViewGroup) {
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
new file mode 100644
index 0000000..1dc4ad2
--- /dev/null
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.apppairs;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace.
+ */
+public class AppPairIcon extends FrameLayout implements DraggableView {
+
+ private ActivityContext mActivity;
+ private BubbleTextView mAppPairName;
+ private FolderInfo mInfo;
+
+ public AppPairIcon(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AppPairIcon(Context context) {
+ super(context);
+ }
+
+ /**
+ * Builds an AppPairIcon to be added to the Launcher
+ */
+ public static AppPairIcon inflateIcon(int resId, ActivityContext activity,
+ @Nullable ViewGroup group, FolderInfo appPairInfo) {
+
+ LayoutInflater inflater = (group != null)
+ ? LayoutInflater.from(group.getContext())
+ : activity.getLayoutInflater();
+ AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false);
+
+ // Sort contents, so that left-hand app comes first
+ Collections.sort(appPairInfo.contents, Comparator.comparingInt(a -> a.rank));
+
+ icon.setClipToPadding(false);
+ icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
+
+ // TODO (jeremysim b/274189428): Replace this placeholder icon
+ WorkspaceItemInfo placeholder = new WorkspaceItemInfo();
+ placeholder.newIcon(icon.getContext());
+ icon.mAppPairName.applyFromWorkspaceItem(placeholder);
+
+ icon.mAppPairName.setText(appPairInfo.title);
+
+ icon.setTag(appPairInfo);
+ icon.setOnClickListener(activity.getItemOnClickListener());
+ icon.mInfo = appPairInfo;
+ icon.mActivity = activity;
+
+ icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+
+ return icon;
+ }
+
+ @Override
+ public int getViewType() {
+ return DRAGGABLE_ICON;
+ }
+
+ @Override
+ public void getWorkspaceVisualDragBounds(Rect bounds) {
+ mAppPairName.getIconBounds(bounds);
+ }
+
+ public FolderInfo getInfo() {
+ return mInfo;
+ }
+}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index e3e1400..8bf4444 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -83,11 +83,11 @@
*/
// TODO(Block 1): Clean up flags
public static final BooleanFlag ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES = getReleaseFlag(
- 270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", DISABLED,
+ 270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", ENABLED,
"Enable option to replace decorator-based search result backgrounds with drawables");
public static final BooleanFlag ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION = getReleaseFlag(
- 270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", DISABLED,
+ 270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", ENABLED,
"Enable option to launch search results using the new view container transitions");
// TODO(Block 2): Clean up flags
@@ -129,7 +129,7 @@
// TODO(Block 5): Clean up flags
public static final BooleanFlag ENABLE_TWOLINE_DEVICESEARCH = getDebugFlag(201388851,
- "ENABLE_TWOLINE_DEVICESEARCH", TEAMFOOD,
+ "ENABLE_TWOLINE_DEVICESEARCH", ENABLED,
"Enable two line label for icons with labels on device search.");
public static final BooleanFlag ENABLE_ICON_IN_TEXT_HEADER = getDebugFlag(270395143,
@@ -183,6 +183,17 @@
"Enables predictive back animation from all apps and widgets to home");
// TODO(Block 11): Clean up flags
+ public static final BooleanFlag ENABLE_TWO_PANEL_HOME = getDebugFlag(270392643,
+ "ENABLE_TWO_PANEL_HOME", ENABLED,
+ "Uses two panel on home screen. Only applicable on large screen devices.");
+
+ public static final BooleanFlag FOLDABLE_WORKSPACE_REORDER = getDebugFlag(270395070,
+ "FOLDABLE_WORKSPACE_REORDER", DISABLED,
+ "In foldables, when reordering the icons and widgets, is now going to use both sides");
+
+ public static final BooleanFlag FOLDABLE_SINGLE_PAGE = getDebugFlag(270395274,
+ "FOLDABLE_SINGLE_PAGE", ENABLED, "Use a single page for the workspace");
+
public static final BooleanFlag ENABLE_PARAMETRIZE_REORDER = getDebugFlag(289420844,
"ENABLE_PARAMETRIZE_REORDER", DISABLED,
"Enables generating the reorder using a set of parameters");
@@ -227,7 +238,7 @@
"COLLECT_SEARCH_HISTORY", DISABLED, "Allow launcher to collect search history for log");
public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937,
- "ENABLE_TWOLINE_ALLAPPS", TEAMFOOD, "Enables two line label inside all apps.");
+ "ENABLE_TWOLINE_ALLAPPS", ENABLED, "Enables two line label inside all apps.");
public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(270391693,
"IME_STICKY_SNACKBAR_EDU", ENABLED, "Show sticky IME edu in AllApps");
@@ -245,7 +256,7 @@
"Inject fallback app corpus result when AiAi fails to return it.");
public static final BooleanFlag ENABLE_LONG_PRESS_NAV_HANDLE =
- getDebugFlag(282993230, "ENABLE_LONG_PRESS_NAV_HANDLE", DISABLED,
+ getReleaseFlag(282993230, "ENABLE_LONG_PRESS_NAV_HANDLE", TEAMFOOD,
"Enables long pressing on the bottom bar nav handle to trigger events.");
// TODO(Block 17): Clean up flags
@@ -277,6 +288,10 @@
"ENABLE_BACK_SWIPE_HOME_ANIMATION", ENABLED,
"Enables home animation to icon when user swipes back.");
+ public static final BooleanFlag ENABLE_DYNAMIC_TASKBAR_THRESHOLDS = getDebugFlag(294252473,
+ "ENABLE_DYNAMIC_TASKBAR_THRESHOLDS", TEAMFOOD,
+ "Enables taskbar thresholds that scale based on screen size.");
+
// TODO(Block 21): Clean up flags
public static final BooleanFlag ENABLE_APP_ICON_FOR_INLINE_SHORTCUTS = getDebugFlag(270395087,
"ENABLE_APP_ICON_IN_INLINE_SHORTCUTS", DISABLED, "Show app icon for inline shortcut");
@@ -366,6 +381,10 @@
270393453, "ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE", DISABLED,
"Enable initiating split screen from workspace to workspace.");
+ public static final BooleanFlag ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE = getDebugFlag(
+ 279586624, "ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE", DISABLED,
+ "Enable initiating split screen from desktop mode to workspace.");
+
public static final BooleanFlag ENABLE_TRACKPAD_GESTURE = getDebugFlag(271010401,
"ENABLE_TRACKPAD_GESTURE", ENABLED, "Enables trackpad gesture.");
@@ -398,7 +417,12 @@
"ENABLE_ALL_APPS_RV_PREINFLATION", DISABLED,
"Enables preinflating all apps icons to avoid scrolling jank.");
- // TODO(Block 34): Empty block
+ // TODO(Block 34): Clean up flags
+ public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514,
+ "ALL_APPS_GONE_VISIBILITY", DISABLED,
+ "Set all apps container view's hidden visibility to GONE instead of INVISIBLE.");
+
+ // TODO(Block 35): Empty block
public static class BooleanFlag {
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 00f4285..213c458 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -26,7 +26,6 @@
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import android.annotation.TargetApi;
-import android.app.ActivityOptions;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ClipData;
@@ -68,6 +67,7 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.pm.PinRequestHelper;
+import com.android.launcher3.uioverrides.ApiWrapper;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.views.AbstractSlideInView;
@@ -259,9 +259,7 @@
.setPackage(getPackageName())
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Launcher.ACTIVITY_TRACKER.registerCallback(listener);
- startActivity(homeIntent,
- ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out)
- .toBundle());
+ startActivity(homeIntent, ApiWrapper.createFadeOutAnimOptions(this).toBundle());
logCommand(LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED);
mFinishOnPause = true;
return false;
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index af43ae8..48b5646 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -31,7 +31,6 @@
import android.widget.RemoteViews;
import com.android.launcher3.DragSource;
-import com.android.launcher3.Launcher;
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -72,15 +71,6 @@
}
@Override
- public boolean init(Launcher launcher, boolean alreadyOnHome) {
- super.init(launcher, alreadyOnHome);
- if (!alreadyOnHome) {
- launcher.useFadeOutAnimationForLauncherStart(mCancelSignal);
- }
- return false;
- }
-
- @Override
protected PendingItemDragHelper createDragHelper() {
final PendingAddItemInfo item;
if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_SHORTCUT) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index f38cce1..55a539a 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -535,7 +535,7 @@
mFolderName.selectAll();
}
}
- mFolderName.showKeyboard();
+ mFolderName.showKeyboard(true /* shouldFocus */);
mFolderName.displayCompletions(
Stream.of(mInfo.suggestedFolderNames.getLabels())
.filter(Objects::nonNull)
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index b09985c..9e2e2bf 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -105,11 +105,11 @@
mDelay = res.getInteger(R.integer.config_folderDelay);
mFolderInterpolator = AnimationUtils.loadInterpolator(mContext,
- R.interpolator.folder_interpolator);
+ R.interpolator.standard_interpolator);
mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext,
R.interpolator.large_folder_preview_item_open_interpolator);
mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext,
- R.interpolator.large_folder_preview_item_close_interpolator);
+ R.interpolator.standard_accelerate_interpolator);
}
/**
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 7241b17..ae44f0a 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -45,12 +45,12 @@
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.ContextThemeWrapper;
+import android.view.Display;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.WindowInsets;
-import android.view.WindowManager;
+import android.widget.FrameLayout;
import android.widget.TextClock;
import androidx.annotation.NonNull;
@@ -70,6 +70,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.WorkspaceLayoutManager;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.config.FeatureFlags;
@@ -92,6 +93,7 @@
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
@@ -204,14 +206,7 @@
} else {
mDpOrig = mDp;
}
-
- WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class)
- .getCurrentWindowMetrics().getWindowInsets();
- mInsets = new Rect(
- currentWindowInsets.getSystemWindowInsetLeft(),
- currentWindowInsets.getSystemWindowInsetTop(),
- currentWindowInsets.getSystemWindowInsetRight(),
- mDp.isTaskbarPresent ? 0 : currentWindowInsets.getSystemWindowInsetBottom());
+ mInsets = getInsets(context);
mDp.updateInsets(mInsets);
mHomeElementInflater = LayoutInflater.from(
@@ -263,6 +258,26 @@
mAppWidgetHost = new LauncherPreviewAppWidgetHost(context);
}
+ /**
+ * Returns the insets of the screen closest to the display given by the context
+ */
+ private Rect getInsets(Context context) {
+ DisplayController.Info info = DisplayController.INSTANCE.get(context).getInfo();
+ float maxDiff = Float.MAX_VALUE;
+ Display display = context.getDisplay();
+ Rect insets = new Rect();
+ for (WindowBounds supportedBound : info.supportedBounds) {
+ double diff = Math.pow(display.getWidth() - supportedBound.availableSize.x, 2)
+ + Math.pow(display.getHeight() - supportedBound.availableSize.y, 2);
+ if (supportedBound.rotationHint == context.getDisplay().getRotation()
+ && diff < maxDiff) {
+ maxDiff = (float) diff;
+ insets = supportedBound.insets;
+ }
+ }
+ return new Rect(insets);
+ }
+
/** Populate preview and render it. */
public View getRenderedView(BgDataModel dataModel,
Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
@@ -358,12 +373,13 @@
addInScreenFromBind(icon, info);
}
- private void inflateAndAddFolder(FolderInfo info) {
+ private void inflateAndAddCollectionIcon(FolderInfo info) {
CellLayout screen = info.container == Favorites.CONTAINER_DESKTOP
? mWorkspaceScreens.get(info.screenId)
: mHotseat;
- FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, screen,
- info);
+ FrameLayout folderIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER
+ ? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, info)
+ : AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info);
addInScreenFromBind(folderIcon, info);
}
@@ -467,7 +483,8 @@
inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
break;
case Favorites.ITEM_TYPE_FOLDER:
- inflateAndAddFolder((FolderInfo) itemInfo);
+ case Favorites.ITEM_TYPE_APP_PAIR:
+ inflateAndAddCollectionIcon((FolderInfo) itemInfo);
break;
default:
break;
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index e89c0c5..aebcdd4 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -16,6 +16,8 @@
package com.android.launcher3.graphics;
+import static android.view.Display.DEFAULT_DISPLAY;
+
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -80,11 +82,12 @@
private static final String KEY_DISPLAY_ID = "display_id";
private static final String KEY_COLORS = "wallpaper_colors";
- private final Context mContext;
- private final InvariantDeviceProfile mIdp;
+ private Context mContext;
private final IBinder mHostToken;
private final int mWidth;
private final int mHeight;
+ private String mGridName;
+
private final Display mDisplay;
private final WallpaperColors mWallpaperColors;
private final RunnableList mOnDestroyCallbacks = new RunnableList();
@@ -97,15 +100,13 @@
public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
mContext = context;
-
- String gridName = bundle.getString("name");
+ mGridName = bundle.getString("name");
bundle.remove("name");
- if (gridName == null) {
- gridName = InvariantDeviceProfile.getCurrentGridName(context);
+ if (mGridName == null) {
+ mGridName = InvariantDeviceProfile.getCurrentGridName(context);
}
mWallpaperColors = bundle.getParcelable(KEY_COLORS);
mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
- mIdp = new InvariantDeviceProfile(context, gridName);
mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
mWidth = bundle.getInt(KEY_VIEW_WIDTH);
@@ -113,9 +114,9 @@
mDisplay = context.getSystemService(DisplayManager.class)
.getDisplay(bundle.getInt(KEY_DISPLAY_ID));
- mSurfaceControlViewHost = MAIN_EXECUTOR
- .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))
- .get(5, TimeUnit.SECONDS);
+ mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() -> new SurfaceControlViewHost(mContext,
+ context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
+ mHostToken)).get(5, TimeUnit.SECONDS);
mOnDestroyCallbacks.add(mSurfaceControlViewHost::release);
}
@@ -195,28 +196,33 @@
}
}
+ /***
+ * Generates a new context overriding the theme color and the display size without affecting the
+ * main application context
+ */
+ private Context getPreviewContext() {
+ Context context = mContext.createDisplayContext(mDisplay);
+ if (mWallpaperColors == null) {
+ return new ContextThemeWrapper(context,
+ Themes.getActivityThemeRes(context));
+ }
+ if (Utilities.ATLEAST_R) {
+ context = context.createWindowContext(
+ LayoutParams.TYPE_APPLICATION_OVERLAY, null);
+ }
+ LocalColorExtractor.newInstance(context)
+ .applyColorsOverride(context, mWallpaperColors);
+ return new ContextThemeWrapper(context,
+ Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
+ }
+
@WorkerThread
private void loadModelData() {
- final Context inflationContext;
- if (mWallpaperColors != null) {
- // Create a themed context, without affecting the main application context
- Context context = mContext.createDisplayContext(mDisplay);
- if (Utilities.ATLEAST_R) {
- context = context.createWindowContext(
- LayoutParams.TYPE_APPLICATION_OVERLAY, null);
- }
- LocalColorExtractor.newInstance(mContext)
- .applyColorsOverride(context, mWallpaperColors);
- inflationContext = new ContextThemeWrapper(context,
- Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
- } else {
- inflationContext = new ContextThemeWrapper(mContext,
- Themes.getActivityThemeRes(mContext));
- }
-
- if (GridSizeMigrationUtil.needsToMigrate(inflationContext, mIdp)) {
+ final Context inflationContext = getPreviewContext();
+ final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName);
+ if (GridSizeMigrationUtil.needsToMigrate(inflationContext, idp)) {
// Start the migration
- PreviewContext previewContext = new PreviewContext(inflationContext, mIdp);
+ PreviewContext previewContext = new PreviewContext(inflationContext, idp);
// Copy existing data to preview DB
LauncherDbUtils.copyTable(LauncherAppState.getInstance(mContext)
.getModel().getModelDbController().getDb(),
@@ -239,7 +245,7 @@
@Override
public void run() {
- DeviceProfile deviceProfile = mIdp.getDeviceProfile(previewContext);
+ DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
String query =
LauncherSettings.Favorites.SCREEN + " = " + Workspace.FIRST_SCREEN_ID
+ " or " + LauncherSettings.Favorites.CONTAINER + " = "
@@ -254,7 +260,8 @@
getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
MAIN_EXECUTOR.execute(() -> {
- renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo);
+ renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo,
+ idp);
mOnDestroyCallbacks.add(previewContext::onDestroy);
});
}
@@ -263,7 +270,7 @@
LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
if (dataModel != null) {
MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel, null,
- null));
+ null, idp));
} else {
Log.e(TAG, "Model loading failed");
}
@@ -274,11 +281,11 @@
@UiThread
private void renderView(Context inflationContext, BgDataModel dataModel,
Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap,
- @Nullable final SparseArray<Size> launcherWidgetSpanInfo) {
+ @Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp) {
if (mDestroyed) {
return;
}
- mRenderer = new LauncherPreviewRenderer(inflationContext, mIdp,
+ mRenderer = new LauncherPreviewRenderer(inflationContext, idp,
mWallpaperColors, launcherWidgetSpanInfo);
mRenderer.hideBottomRow(mHideQsb);
View view = mRenderer.getRenderedView(dataModel, widgetProviderInfoMap);
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 3e9731382..780cb5e 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -32,12 +32,9 @@
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.util.IntArray;
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.views.ActivityContext;
-import java.util.List;
-
/**
* Handles the user event logging in R+.
*
@@ -624,6 +621,9 @@
@UiEvent(doc = "User has invoked split to left half with a keyboard shortcut.")
LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP(1233),
+ @UiEvent(doc = "User has invoked split to right half with desktop mode app icon")
+ LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM(1412),
+
@UiEvent(doc = "User has collapsed the work FAB button by scrolling down in the all apps"
+ " work A-Z list.")
LAUNCHER_WORK_FAB_BUTTON_COLLAPSE(1276),
@@ -644,11 +644,17 @@
@UiEvent(doc = "User has swiped upwards from the gesture handle to show transient taskbar.")
LAUNCHER_TRANSIENT_TASKBAR_SHOW(1331),
+ @UiEvent(doc = "User has clicked an app pair and launched directly into split screen.")
+ LAUNCHER_APP_PAIR_LAUNCH(1374),
+
+ @UiEvent(doc = "User saved an app pair.")
+ LAUNCHER_APP_PAIR_SAVE(1456),
+
@UiEvent(doc = "App launched through pending intent")
- LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394),
- ;
+ LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394)
// ADD MORE
+ ;
private final int mId;
@@ -961,33 +967,32 @@
}
/**
- * Sets list of {@link com.android.app.search.ResultType} for the impression event.
+ * Sets {@link com.android.app.search.ResultType} for the impression event.
*/
- default StatsImpressionLogger withResultType(IntArray resultType) {
+ default StatsImpressionLogger withResultType(int resultType) {
return this;
}
/**
- * Sets list of count for each of {@link com.android.app.search.ResultType} for the
- * impression event.
- */
- default StatsImpressionLogger withResultCount(IntArray resultCount) {
- return this;
- }
-
- /**
- * Sets list of boolean for each of {@link com.android.app.search.ResultType} that indicates
+ * Sets boolean for each of {@link com.android.app.search.ResultType} that indicates
* if this result is above keyboard or not for the impression event.
*/
- default StatsImpressionLogger withAboveKeyboard(List<Boolean> aboveKeyboard) {
+ default StatsImpressionLogger withAboveKeyboard(boolean aboveKeyboard) {
return this;
}
/**
- * Sets list of uid for each of {@link com.android.app.search.ResultType} that indicates
+ * Sets uid for each of {@link com.android.app.search.ResultType} that indicates
* package name for the impression event.
*/
- default StatsImpressionLogger withUids(IntArray uid) {
+ default StatsImpressionLogger withUid(int uid) {
+ return this;
+ }
+
+ /**
+ * Sets result source that indicates the origin of the result for the impression event.
+ */
+ default StatsImpressionLogger withResultSource(int resultSource) {
return this;
}
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 556ac26..dbb29b8 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.os.Process;
+import android.os.Trace;
import android.util.Log;
import com.android.launcher3.InvariantDeviceProfile;
@@ -83,13 +84,18 @@
* Binds all loaded data to actual views on the main thread.
*/
public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
- if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
- DisjointWorkspaceBinder workspaceBinder =
+ Trace.beginSection("BaseLauncherBinder#bindWorkspace");
+ try {
+ if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
+ DisjointWorkspaceBinder workspaceBinder =
initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
- workspaceBinder.bindCurrentWorkspacePages(isBindSync);
- workspaceBinder.bindOtherWorkspacePages();
- } else {
- bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+ workspaceBinder.bindCurrentWorkspacePages(isBindSync);
+ workspaceBinder.bindOtherWorkspacePages();
+ } else {
+ bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+ }
+ } finally {
+ Trace.endSection();
}
}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 787ac38..0e68db2 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -374,6 +374,8 @@
final HashMap<PackageUserKey, SessionInfo> installingPkgs =
mSessionHelper.getActiveSessions();
installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
+ FileLog.d(TAG, "loadWorkspace: Packages with active install sessions: "
+ + installingPkgs.values());
final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
@@ -690,9 +692,11 @@
break;
case Favorites.ITEM_TYPE_FOLDER:
+ case Favorites.ITEM_TYPE_APP_PAIR:
FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
c.applyCommonProperties(folderInfo);
+ folderInfo.itemType = c.itemType;
// Do not trim the folder label, as is was set by the user.
folderInfo.title = c.getString(c.mTitleIndex);
folderInfo.spanX = 1;
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index a6b4d59..2358a9f 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -489,6 +489,7 @@
case Favorites.ITEM_TYPE_APPLICATION:
case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
case Favorites.ITEM_TYPE_FOLDER:
+ case Favorites.ITEM_TYPE_APP_PAIR:
if (!mBgDataModel.workspaceItems.contains(modelItem)) {
mBgDataModel.workspaceItems.add(modelItem);
}
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index e5a0eb1..9bf6d43 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -119,8 +119,8 @@
public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
FolderInfo newAppPair = new FolderInfo();
newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
- newAppPair.contents.add(app1);
- newAppPair.contents.add(app2);
+ newAppPair.add(app1, /* animate */ false);
+ newAppPair.add(app2, /* animate */ false);
return newAppPair;
}
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index ba1547f..9afa459 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -49,7 +49,6 @@
import com.android.launcher3.LauncherSettings.Animation;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.Workspace;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logger.LauncherAtom.AllAppsContainer;
import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
@@ -323,9 +322,7 @@
* Returns whether this item should use the background animation.
*/
public boolean shouldUseBackgroundAnimation() {
- return animationType == LauncherSettings.Animation.VIEW_BACKGROUND
- && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()
- && FeatureFlags.ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION.get();
+ return animationType == LauncherSettings.Animation.VIEW_BACKGROUND;
}
/**
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index c554def..4725dd1 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -82,11 +82,15 @@
public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
public static final String APPWIDGET_IDS = "appwidget_ids";
+ private static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen",
+ "container", "cellX", "cellY", "spanX", "spanY", "intent"};
+
/**
* Tries to restore the backup DB if needed
*/
public static void restoreIfNeeded(Context context, ModelDbController dbController) {
if (!isPending(context)) {
+ Log.d(TAG, "No restore task pending, exiting RestoreDbTask");
return;
}
if (!performRestore(context, dbController)) {
@@ -106,6 +110,7 @@
private static boolean performRestore(Context context, ModelDbController controller) {
SQLiteDatabase db = controller.getDb();
+ FileLog.d(TAG, "performRestore: starting restore from db");
try (SQLiteTransaction t = new SQLiteTransaction(db)) {
RestoreDbTask task = new RestoreDbTask();
task.sanitizeDB(context, controller, db, new BackupManager(context));
@@ -133,10 +138,11 @@
@VisibleForTesting
protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db,
BackupManager backupManager) throws Exception {
+ FileLog.d(TAG, "Old Launcher Database before sanitizing:");
// Primary user ids
long myProfileId = controller.getSerialNumberForUser(myUserHandle());
long oldProfileId = getDefaultProfileId(db);
- Log.d(TAG, "sanitizeDB: myProfileId=" + myProfileId + " oldProfileId=" + oldProfileId);
+ FileLog.d(TAG, "sanitizeDB: myProfileId=" + myProfileId + " oldProfileId=" + oldProfileId);
LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
+ 1);
@@ -149,8 +155,11 @@
if (user != null) {
long newManagedProfileId = controller.getSerialNumberForUser(user);
profileMapping.put(oldManagedProfileId, newManagedProfileId);
- Log.d(TAG, "sanitizeDB: managed profile id=" + oldManagedProfileId
+ FileLog.d(TAG, "sanitizeDB: managed profile id=" + oldManagedProfileId
+ " should be mapped to new id=" + newManagedProfileId);
+ } else {
+ FileLog.e(TAG, "sanitizeDB: No User found for old profileId, Ancestral Serial "
+ + "Number: " + oldManagedProfileId);
}
}
@@ -161,11 +170,13 @@
for (int i = numProfiles - 1; i >= 1; --i) {
profileIds[i] = Long.toString(profileMapping.keyAt(i));
}
+
final String[] args = new String[profileIds.length];
Arrays.fill(args, "?");
final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
- int itemsDeleted = db.delete(Favorites.TABLE_NAME, where, profileIds);
- FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
+ logUnrestoredItems(db, where, profileIds);
+ int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds);
+ FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted");
// Mark all items as restored.
boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS);
@@ -219,8 +230,48 @@
// Override shortcuts
maybeOverrideShortcuts(context, controller, db, myProfileId);
+ return itemsDeletedCount;
+ }
- return itemsDeleted;
+ /**
+ * Queries and logs the items we will delete from unrestored profiles in the launcher db.
+ * This is to understand why items might be missing during the restore process for Launcher.
+ * @param database the Launcher db to query from.
+ * @param where the SELECT statement to query items that will be deleted.
+ * @param profileIds the profile ID's the user will be migrating to.
+ */
+ private void logUnrestoredItems(SQLiteDatabase database, String where, String[] profileIds) {
+ try (Cursor itemsToDelete = database.query(
+ /* table */ Favorites.TABLE_NAME,
+ /* columns */ DB_COLUMNS_TO_LOG,
+ /* selection */ where,
+ /* selection args */ profileIds,
+ /* groupBy */ null,
+ /* having */ null,
+ /* orderBy */ null
+ )) {
+ if (itemsToDelete.moveToFirst()) {
+ String[] columnNames = itemsToDelete.getColumnNames();
+ StringBuilder stringBuilder = new StringBuilder(
+ "items to be deleted from the Favorites Table during restore:\n"
+ );
+ do {
+ for (String columnName : columnNames) {
+ stringBuilder.append(columnName)
+ .append("=")
+ .append(itemsToDelete.getString(
+ itemsToDelete.getColumnIndex(columnName)))
+ .append(" ");
+ }
+ stringBuilder.append("\n");
+ } while (itemsToDelete.moveToNext());
+ FileLog.d(TAG, stringBuilder.toString());
+ } else {
+ FileLog.d(TAG, "logDeletedItems: No items found to delete");
+ }
+ } catch (Exception e) {
+ FileLog.e(TAG, "logDeletedItems: Error reading from database", e);
+ }
}
/**
@@ -321,7 +372,7 @@
* Marks the DB state as pending restoration
*/
public static void setPending(Context context) {
- FileLog.d(TAG, "Restore data received through full backup ");
+ FileLog.d(TAG, "Restore data received through full backup");
LauncherPrefs.get(context)
.putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType()));
}
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 26dde29..3c59c1d 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -22,13 +22,14 @@
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.android.launcher3.BubbleTextView
import com.android.launcher3.allapps.BaseAllAppsAdapter
+import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
import com.android.launcher3.views.ActivityContext
import java.util.concurrent.Future
-private const val PREINFLATE_ICONS_ROW_COUNT = 4
-private const val EXTRA_ICONS_COUNT = 2
+const val PREINFLATE_ICONS_ROW_COUNT = 4
+const val EXTRA_ICONS_COUNT = 2
/**
* An [RecycledViewPool] that preinflates app icons ([ViewHolder] of [BubbleTextView]) of all apps
@@ -81,11 +82,21 @@
* After testing on phone, foldable and tablet, we found [PREINFLATE_ICONS_ROW_COUNT] rows of
* app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
* suffice fast scrolling.
+ *
+ * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
+ * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
+ * icons.
*/
fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
- val targetPreinflateCount =
+ var targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
+ if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) {
+ val grid = ActivityContext.lookupContext<T>(context).deviceProfile
+ val approxRows =
+ Math.ceil((grid.availableHeightPx / grid.allAppsIconSizePx).toDouble()).toInt()
+ targetPreinflateCount += (approxRows + 1) * grid.numShownAllAppsColumns
+ }
val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON)
return targetPreinflateCount - existingPreinflateCount
}
diff --git a/src/com/android/launcher3/responsive/AllAppsSpecs.kt b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
index 85e383e..8ed3ffc 100644
--- a/src/com/android/launcher3/responsive/AllAppsSpecs.kt
+++ b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
@@ -16,277 +16,89 @@
package com.android.launcher3.responsive
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-private const val LOG_TAG = "AllAppsSpecs"
+class AllAppsSpecs(widthSpecs: List<AllAppsSpec>, heightSpecs: List<AllAppsSpec>) :
+ ResponsiveSpecs<AllAppsSpec>(widthSpecs, heightSpecs) {
-class AllAppsSpecs(resourceHelper: ResourceHelper) {
- object XmlTags {
- const val ALL_APPS_SPECS = "allAppsSpecs"
-
- const val ALL_APPS_SPEC = "allAppsSpec"
- const val START_PADDING = "startPadding"
- const val END_PADDING = "endPadding"
- const val GUTTER = "gutter"
- const val CELL_SIZE = "cellSize"
- }
-
- val allAppsHeightSpecList = mutableListOf<AllAppsSpec>()
- val allAppsWidthSpecList = mutableListOf<AllAppsSpec>()
-
- // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
- init {
- var parser: XmlResourceParser? = null
- try {
- parser = resourceHelper.getXml()
- val depth = parser.depth
- var type: Int
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.ALL_APPS_SPECS == parser.name) {
- val displayDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (
- type == XmlPullParser.START_TAG && XmlTags.ALL_APPS_SPEC == parser.name
- ) {
- val attrs =
- resourceHelper.obtainStyledAttributes(
- Xml.asAttributeSet(parser),
- R.styleable.AllAppsSpec
- )
- val maxAvailableSize =
- attrs.getDimensionPixelSize(
- R.styleable.AllAppsSpec_maxAvailableSize,
- 0
- )
- val specType =
- AllAppsSpec.SpecType.values()[
- attrs.getInt(
- R.styleable.AllAppsSpec_specType,
- AllAppsSpec.SpecType.HEIGHT.ordinal
- )]
- attrs.recycle()
-
- var startPadding: SizeSpec? = null
- var endPadding: SizeSpec? = null
- var gutter: SizeSpec? = null
- var cellSize: SizeSpec? = null
-
- val limitDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- val attr: AttributeSet = Xml.asAttributeSet(parser)
- if (type == XmlPullParser.START_TAG) {
- when (parser.name) {
- XmlTags.START_PADDING -> {
- startPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.END_PADDING -> {
- endPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.GUTTER -> {
- gutter = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.CELL_SIZE -> {
- cellSize = SizeSpec.create(resourceHelper, attr)
- }
- }
- }
- }
-
- if (
- startPadding == null ||
- endPadding == null ||
- gutter == null ||
- cellSize == null
- ) {
- throw IllegalStateException(
- "All attributes in AllAppsSpec must be defined"
- )
- }
-
- val allAppsSpec =
- AllAppsSpec(
- maxAvailableSize,
- specType,
- startPadding,
- endPadding,
- gutter,
- cellSize
- )
- if (allAppsSpec.isValid()) {
- if (allAppsSpec.specType == AllAppsSpec.SpecType.HEIGHT)
- allAppsHeightSpecList.add(allAppsSpec)
- else allAppsWidthSpecList.add(allAppsSpec)
- } else {
- throw IllegalStateException("Invalid AllAppsSpec found.")
- }
- }
- }
-
- if (allAppsWidthSpecList.isEmpty() || allAppsHeightSpecList.isEmpty()) {
- throw IllegalStateException(
- "AllAppsSpecs is incomplete - " +
- "height list size = ${allAppsHeightSpecList.size}; " +
- "width list size = ${allAppsWidthSpecList.size}."
- )
- }
- }
- }
- } catch (e: Exception) {
- when (e) {
- is IOException,
- is XmlPullParserException -> {
- throw RuntimeException("Failure parsing all apps specs file.", e)
- }
- else -> throw e
- }
- } finally {
- parser?.close()
- }
- }
-
- /**
- * Returns the CalculatedAllAppsSpec for width, based on the available width, the AllAppsSpecs
- * and the CalculatedWorkspaceSpec.
- */
fun getCalculatedWidthSpec(
columns: Int,
availableWidth: Int,
calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedAllAppsSpec {
- val widthSpec = allAppsWidthSpecList.first { availableWidth <= it.maxAvailableSize }
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
+ "Invalid specType for CalculatedWorkspaceSpec. " +
+ "Expected: ${SpecType.WIDTH} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+ }
- return CalculatedAllAppsSpec(availableWidth, columns, widthSpec, calculatedWorkspaceSpec)
+ val spec = getWidthSpec(availableWidth)
+ return CalculatedAllAppsSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
}
- /**
- * Returns the CalculatedAllAppsSpec for height, based on the available height, the AllAppsSpecs
- * and the CalculatedWorkspaceSpec.
- */
fun getCalculatedHeightSpec(
rows: Int,
availableHeight: Int,
calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedAllAppsSpec {
- val heightSpec = allAppsHeightSpecList.first { availableHeight <= it.maxAvailableSize }
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
+ "Invalid specType for CalculatedWorkspaceSpec. " +
+ "Expected: ${SpecType.HEIGHT} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+ }
- return CalculatedAllAppsSpec(availableHeight, rows, heightSpec, calculatedWorkspaceSpec)
- }
-}
-
-class CalculatedAllAppsSpec(
- val availableSpace: Int,
- val cells: Int,
- private val allAppsSpec: AllAppsSpec,
- calculatedWorkspaceSpec: CalculatedWorkspaceSpec
-) {
- var startPaddingPx: Int = 0
- private set
- var endPaddingPx: Int = 0
- private set
- var gutterPx: Int = 0
- private set
- var cellSizePx: Int = 0
- private set
- init {
- // Copy values from workspace
- if (allAppsSpec.startPadding.matchWorkspace)
- startPaddingPx = calculatedWorkspaceSpec.startPaddingPx
- if (allAppsSpec.endPadding.matchWorkspace)
- endPaddingPx = calculatedWorkspaceSpec.endPaddingPx
- if (allAppsSpec.gutter.matchWorkspace) gutterPx = calculatedWorkspaceSpec.gutterPx
- if (allAppsSpec.cellSize.matchWorkspace) cellSizePx = calculatedWorkspaceSpec.cellSizePx
-
- // Calculate all fixed size first
- if (allAppsSpec.startPadding.fixedSize > 0)
- startPaddingPx = allAppsSpec.startPadding.fixedSize.roundToInt()
- if (allAppsSpec.endPadding.fixedSize > 0)
- endPaddingPx = allAppsSpec.endPadding.fixedSize.roundToInt()
- if (allAppsSpec.gutter.fixedSize > 0) gutterPx = allAppsSpec.gutter.fixedSize.roundToInt()
- if (allAppsSpec.cellSize.fixedSize > 0)
- cellSizePx = allAppsSpec.cellSize.fixedSize.roundToInt()
-
- // Calculate all available space next
- if (allAppsSpec.startPadding.ofAvailableSpace > 0)
- startPaddingPx =
- (allAppsSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (allAppsSpec.endPadding.ofAvailableSpace > 0)
- endPaddingPx = (allAppsSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (allAppsSpec.gutter.ofAvailableSpace > 0)
- gutterPx = (allAppsSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
- if (allAppsSpec.cellSize.ofAvailableSpace > 0)
- cellSizePx = (allAppsSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
- // Calculate remainder space last
- val gutters = cells - 1
- val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
- val remainderSpace = availableSpace - usedSpace
- if (allAppsSpec.startPadding.ofRemainderSpace > 0)
- startPaddingPx =
- (allAppsSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (allAppsSpec.endPadding.ofRemainderSpace > 0)
- endPaddingPx = (allAppsSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (allAppsSpec.gutter.ofRemainderSpace > 0)
- gutterPx = (allAppsSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
- if (allAppsSpec.cellSize.ofRemainderSpace > 0)
- cellSizePx = (allAppsSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
+ val spec = getHeightSpec(availableHeight)
+ return CalculatedAllAppsSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
}
- override fun toString(): String {
- return "CalculatedAllAppsSpec(availableSpace=$availableSpace, " +
- "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
- "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
- "AllAppsSpec.maxAvailableSize=${allAppsSpec.maxAvailableSize})"
+ companion object {
+ private const val XML_ALL_APPS_SPEC = "allAppsSpec"
+
+ @JvmStatic
+ fun create(resourceHelper: ResourceHelper): AllAppsSpecs {
+ val parser = ResponsiveSpecsParser(resourceHelper)
+ val specs = parser.parseXML(XML_ALL_APPS_SPEC, ::AllAppsSpec)
+ val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+ return AllAppsSpecs(widthSpecs, heightSpecs)
+ }
}
}
data class AllAppsSpec(
- val maxAvailableSize: Int,
- val specType: SpecType,
- val startPadding: SizeSpec,
- val endPadding: SizeSpec,
- val gutter: SizeSpec,
- val cellSize: SizeSpec
-) {
+ override val maxAvailableSize: Int,
+ override val specType: SpecType,
+ override val startPadding: SizeSpec,
+ override val endPadding: SizeSpec,
+ override val gutter: SizeSpec,
+ override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
- enum class SpecType {
- HEIGHT,
- WIDTH
+ init {
+ check(isValid()) { "Invalid AllAppsSpec found." }
}
- fun isValid(): Boolean {
- if (maxAvailableSize <= 0) {
- Log.e(LOG_TAG, "AllAppsSpec#isValid - maxAvailableSize <= 0")
- return false
- }
-
- // All specs need to be individually valid
- if (!allSpecsAreValid()) {
- Log.e(LOG_TAG, "AllAppsSpec#isValid - !allSpecsAreValid()")
- return false
- }
-
- return true
- }
-
- private fun allSpecsAreValid(): Boolean =
- startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid()
+ constructor(
+ attrs: TypedArray,
+ specs: Map<String, SizeSpec>
+ ) : this(
+ maxAvailableSize =
+ attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+ specType =
+ SpecType.values()[
+ attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+ startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+ endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+ gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+ cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+ )
}
+
+class CalculatedAllAppsSpec(
+ availableSpace: Int,
+ cells: Int,
+ spec: AllAppsSpec,
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/FolderSpecs.kt b/src/com/android/launcher3/responsive/FolderSpecs.kt
index f4446bc..bc2db28 100644
--- a/src/com/android/launcher3/responsive/FolderSpecs.kt
+++ b/src/com/android/launcher3/responsive/FolderSpecs.kt
@@ -1,280 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.launcher3.responsive
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
import com.android.launcher3.R
-import com.android.launcher3.responsive.FolderSpec.*
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import com.android.launcher3.workspace.WorkspaceSpec
-import java.io.IOException
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-private const val LOG_TAG = "FolderSpecs"
+class FolderSpecs(widthSpecs: List<FolderSpec>, heightSpecs: List<FolderSpec>) :
+ ResponsiveSpecs<FolderSpec>(widthSpecs, heightSpecs) {
-class FolderSpecs(resourceHelper: ResourceHelper) {
-
- object XmlTags {
- const val FOLDER_SPECS = "folderSpecs"
-
- const val FOLDER_SPEC = "folderSpec"
- const val START_PADDING = "startPadding"
- const val END_PADDING = "endPadding"
- const val GUTTER = "gutter"
- const val CELL_SIZE = "cellSize"
- }
-
- private val _heightSpecs = mutableListOf<FolderSpec>()
- val heightSpecs: List<FolderSpec>
- get() = _heightSpecs
-
- private val _widthSpecs = mutableListOf<FolderSpec>()
- val widthSpecs: List<FolderSpec>
- get() = _widthSpecs
-
- // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
- init {
- var parser: XmlResourceParser? = null
- try {
- parser = resourceHelper.getXml()
- val depth = parser.depth
- var type: Int
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.FOLDER_SPECS == parser.name) {
- val displayDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.FOLDER_SPEC == parser.name) {
- val attrs =
- resourceHelper.obtainStyledAttributes(
- Xml.asAttributeSet(parser),
- R.styleable.FolderSpec
- )
- val maxAvailableSize =
- attrs.getDimensionPixelSize(
- R.styleable.FolderSpec_maxAvailableSize,
- 0
- )
- val specType =
- SpecType.values()[
- attrs.getInt(
- R.styleable.FolderSpec_specType,
- SpecType.HEIGHT.ordinal
- )]
- attrs.recycle()
-
- var startPadding: SizeSpec? = null
- var endPadding: SizeSpec? = null
- var gutter: SizeSpec? = null
- var cellSize: SizeSpec? = null
-
- val limitDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- val attr: AttributeSet = Xml.asAttributeSet(parser)
- if (type == XmlPullParser.START_TAG) {
- val sizeSpec = SizeSpec.create(resourceHelper, attr)
- when (parser.name) {
- XmlTags.START_PADDING -> startPadding = sizeSpec
- XmlTags.END_PADDING -> endPadding = sizeSpec
- XmlTags.GUTTER -> gutter = sizeSpec
- XmlTags.CELL_SIZE -> cellSize = sizeSpec
- }
- }
- }
-
- checkNotNull(startPadding) {
- "Attr 'startPadding' in FolderSpec must be defined."
- }
- checkNotNull(endPadding) {
- "Attr 'endPadding' in FolderSpec must be defined."
- }
- checkNotNull(gutter) { "Attr 'gutter' in FolderSpec must be defined." }
- checkNotNull(cellSize) {
- "Attr 'cellSize' in FolderSpec must be defined."
- }
-
- val folderSpec =
- FolderSpec(
- maxAvailableSize,
- specType,
- startPadding,
- endPadding,
- gutter,
- cellSize
- )
-
- check(folderSpec.isValid()) { "Invalid FolderSpec found." }
-
- if (folderSpec.specType == SpecType.HEIGHT) {
- _heightSpecs += folderSpec
- } else {
- _widthSpecs += folderSpec
- }
- }
- }
-
- check(_widthSpecs.isNotEmpty() && _heightSpecs.isNotEmpty()) {
- "FolderSpecs is incomplete - " +
- "height list size = ${_heightSpecs.size}; " +
- "width list size = ${_widthSpecs.size}."
- }
- }
- }
- } catch (e: Exception) {
- when (e) {
- is IOException,
- is XmlPullParserException -> {
- throw RuntimeException("Failure parsing folder specs file.", e)
- }
- else -> throw e
- }
- } finally {
- parser?.close()
- }
- }
-
- /**
- * Returns the [CalculatedFolderSpec] for width, based on the available width, FolderSpecs and
- * WorkspaceSpecs.
- */
- fun getWidthSpec(
+ fun getCalculatedWidthSpec(
columns: Int,
availableWidth: Int,
- workspaceSpec: CalculatedWorkspaceSpec
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedFolderSpec {
- check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.WIDTH) {
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
"Invalid specType for CalculatedWorkspaceSpec. " +
- "Expected: ${WorkspaceSpec.SpecType.WIDTH} - " +
- "Found: ${workspaceSpec.workspaceSpec.specType}}"
+ "Expected: ${SpecType.WIDTH} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
}
- val widthSpec = _widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
- check(widthSpec != null) { "No FolderSpec for width spec found with $availableWidth." }
-
- return convertToCalculatedFolderSpec(widthSpec, availableWidth, columns, workspaceSpec)
+ val spec = getWidthSpec(availableWidth)
+ return CalculatedFolderSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
}
- /**
- * Returns the [CalculatedFolderSpec] for height, based on the available height, FolderSpecs and
- * WorkspaceSpecs.
- */
- fun getHeightSpec(
+ fun getCalculatedHeightSpec(
rows: Int,
availableHeight: Int,
- workspaceSpec: CalculatedWorkspaceSpec
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
): CalculatedFolderSpec {
- check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) {
+ check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
"Invalid specType for CalculatedWorkspaceSpec. " +
- "Expected: ${WorkspaceSpec.SpecType.HEIGHT} - " +
- "Found: ${workspaceSpec.workspaceSpec.specType}}"
+ "Expected: ${SpecType.HEIGHT} - " +
+ "Found: ${calculatedWorkspaceSpec.spec.specType}}"
}
- val heightSpec = _heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
- check(heightSpec != null) { "No FolderSpec for height spec found with $availableHeight." }
+ val spec = getHeightSpec(availableHeight)
+ return CalculatedFolderSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
+ }
- return convertToCalculatedFolderSpec(heightSpec, availableHeight, rows, workspaceSpec)
+ companion object {
+
+ private const val XML_FOLDER_SPEC = "folderSpec"
+
+ @JvmStatic
+ fun create(resourceHelper: ResourceHelper): FolderSpecs {
+ val parser = ResponsiveSpecsParser(resourceHelper)
+ val specs = parser.parseXML(XML_FOLDER_SPEC, ::FolderSpec)
+ val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+ return FolderSpecs(widthSpecs, heightSpecs)
+ }
}
}
-data class CalculatedFolderSpec(
- val availableSpace: Int,
- val cells: Int,
- val startPaddingPx: Int,
- val endPaddingPx: Int,
- val gutterPx: Int,
- val cellSizePx: Int
-)
-
-/**
- * Responsive folder specs to be used to calculate the paddings, gutter and cell size for folders in
- * the workspace.
- *
- * @param maxAvailableSize indicates the breakpoint to use this specification.
- * @param specType indicates whether the paddings and gutters will be applied vertically or
- * horizontally.
- * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
- * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
- * @param gutter the space between the cells vertically or horizontally depending on the [specType].
- * @param cellSize height or width of the cell depending on the [specType].
- */
data class FolderSpec(
- val maxAvailableSize: Int,
- val specType: SpecType,
- val startPadding: SizeSpec,
- val endPadding: SizeSpec,
- val gutter: SizeSpec,
- val cellSize: SizeSpec
-) {
+ override val maxAvailableSize: Int,
+ override val specType: SpecType,
+ override val startPadding: SizeSpec,
+ override val endPadding: SizeSpec,
+ override val gutter: SizeSpec,
+ override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
- enum class SpecType {
- HEIGHT,
- WIDTH
+ init {
+ check(isValid()) { "Invalid FolderSpec found." }
}
- fun isValid(): Boolean {
- if (maxAvailableSize <= 0) {
- Log.e(LOG_TAG, "FolderSpec#isValid - maxAvailableSize <= 0")
- return false
- }
-
- // All specs are valid
- if (
- !(startPadding.isValid() &&
- endPadding.isValid() &&
- gutter.isValid() &&
- cellSize.isValid())
- ) {
- Log.e(LOG_TAG, "FolderSpec#isValid - !allSpecsAreValid()")
- return false
- }
-
- return true
- }
-}
-
-/** Helper function to convert [FolderSpec] to [CalculatedFolderSpec] */
-private fun convertToCalculatedFolderSpec(
- folderSpec: FolderSpec,
- availableSpace: Int,
- cells: Int,
- workspaceSpec: CalculatedWorkspaceSpec
-): CalculatedFolderSpec {
- // Map if is fixedSize, ofAvailableSpace or matchWorkspace
- var startPaddingPx =
- folderSpec.startPadding.getCalculatedValue(availableSpace, workspaceSpec.startPaddingPx)
- var endPaddingPx =
- folderSpec.endPadding.getCalculatedValue(availableSpace, workspaceSpec.endPaddingPx)
- var gutterPx = folderSpec.gutter.getCalculatedValue(availableSpace, workspaceSpec.gutterPx)
- var cellSizePx =
- folderSpec.cellSize.getCalculatedValue(availableSpace, workspaceSpec.cellSizePx)
-
- // Remainder space
- val gutters = cells - 1
- val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
- val remainderSpace = availableSpace - usedSpace
-
- startPaddingPx = folderSpec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
- endPaddingPx = folderSpec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
- gutterPx = folderSpec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
- cellSizePx = folderSpec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
-
- return CalculatedFolderSpec(
- availableSpace = availableSpace,
- cells = cells,
- startPaddingPx = startPaddingPx,
- endPaddingPx = endPaddingPx,
- gutterPx = gutterPx,
- cellSizePx = cellSizePx
+ constructor(
+ attrs: TypedArray,
+ specs: Map<String, SizeSpec>
+ ) : this(
+ maxAvailableSize =
+ attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+ specType =
+ SpecType.values()[
+ attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+ startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+ endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+ gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+ cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
)
}
+
+class CalculatedFolderSpec(
+ availableSpace: Int,
+ cells: Int,
+ spec: FolderSpec,
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/HotseatSpecs.kt b/src/com/android/launcher3/responsive/HotseatSpecs.kt
new file mode 100644
index 0000000..482508d
--- /dev/null
+++ b/src/com/android/launcher3/responsive/HotseatSpecs.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.util.Log
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+
+class HotseatSpecs(val specs: List<HotseatSpec>) {
+
+ fun getCalculatedHeightSpec(availableHeight: Int): CalculatedHotseatSpec {
+ val spec = specs.firstOrNull { availableHeight <= it.maxAvailableSize }
+ check(spec != null) { "No available height spec found within $availableHeight." }
+ return CalculatedHotseatSpec(availableHeight, spec)
+ }
+
+ companion object {
+ private const val XML_HOTSEAT_SPEC = "hotseatSpec"
+
+ @JvmStatic
+ fun create(resourceHelper: ResourceHelper): HotseatSpecs {
+ val parser = ResponsiveSpecsParser(resourceHelper)
+ val specs = parser.parseXML(XML_HOTSEAT_SPEC, ::HotseatSpec)
+ return HotseatSpecs(specs.filter { it.specType == ResponsiveSpec.SpecType.HEIGHT })
+ }
+ }
+}
+
+data class HotseatSpec(
+ val maxAvailableSize: Int,
+ val specType: ResponsiveSpec.SpecType,
+ val hotseatQsbSpace: SizeSpec
+) {
+
+ init {
+ check(isValid()) { "Invalid HotseatSpec found." }
+ }
+
+ constructor(
+ attrs: TypedArray,
+ specs: Map<String, SizeSpec>
+ ) : this(
+ maxAvailableSize =
+ attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+ specType =
+ ResponsiveSpec.SpecType.values()[
+ attrs.getInt(
+ R.styleable.ResponsiveSpec_specType,
+ ResponsiveSpec.SpecType.HEIGHT.ordinal
+ )],
+ hotseatQsbSpace = specs.getOrError(SizeSpec.XmlTags.HOTSEAT_QSB_SPACE)
+ )
+
+ fun isValid(): Boolean {
+ if (maxAvailableSize <= 0) {
+ Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
+ return false
+ }
+
+ // All specs need to be individually valid
+ if (!allSpecsAreValid()) {
+ Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
+ return false
+ }
+
+ return true
+ }
+
+ private fun allSpecsAreValid(): Boolean {
+ return hotseatQsbSpace.isValid() && hotseatQsbSpace.onlyFixedSize()
+ }
+
+ companion object {
+ private const val LOG_TAG = "HotseatSpec"
+ }
+}
+
+class CalculatedHotseatSpec(val availableSpace: Int, val spec: HotseatSpec) {
+
+ var hotseatQsbSpace: Int = 0
+ private set
+
+ init {
+ hotseatQsbSpace = spec.hotseatQsbSpace.getCalculatedValue(availableSpace)
+ }
+
+ override fun hashCode(): Int {
+ var result = availableSpace.hashCode()
+ result = 31 * result + hotseatQsbSpace.hashCode()
+ result = 31 * result + spec.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is CalculatedHotseatSpec &&
+ availableSpace == other.availableSpace &&
+ hotseatQsbSpace == other.hotseatQsbSpace &&
+ spec == other.spec
+ }
+
+ override fun toString(): String {
+ return "${this::class.simpleName}(" +
+ "availableSpace=$availableSpace, hotseatQsbSpace=$hotseatQsbSpace, " +
+ "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+ ")"
+ }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecs.kt b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
new file mode 100644
index 0000000..72a0ea4
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.util.Log
+
+/**
+ * Base class for responsive specs that holds a list of width and height specs.
+ *
+ * @param widthSpecs List of width responsive specifications
+ * @param heightSpecs List of height responsive specifications
+ */
+abstract class ResponsiveSpecs<T : ResponsiveSpec>(
+ val widthSpecs: List<T>,
+ val heightSpecs: List<T>
+) {
+
+ init {
+ check(widthSpecs.isNotEmpty() && heightSpecs.isNotEmpty()) {
+ "${this::class.simpleName} is incomplete - " +
+ "width list size = ${widthSpecs.size}; " +
+ "height list size = ${heightSpecs.size}."
+ }
+ }
+
+ /**
+ * Get a [ResponsiveSpec] for width within the breakpoint.
+ *
+ * @param availableWidth The width breakpoint for the spec
+ * @return A [ResponsiveSpec] for width.
+ */
+ fun getWidthSpec(availableWidth: Int): T {
+ val spec = widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
+ check(spec != null) { "No available width spec found within $availableWidth." }
+ return spec
+ }
+
+ /**
+ * Get a [ResponsiveSpec] for height within the breakpoint.
+ *
+ * @param availableHeight The height breakpoint for the spec
+ * @return A [ResponsiveSpec] for height.
+ */
+ fun getHeightSpec(availableHeight: Int): T {
+ val spec = heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
+ check(spec != null) { "No available height spec found within $availableHeight." }
+ return spec
+ }
+}
+
+/**
+ * Base class for a responsive specification that is used to calculate the paddings, gutter and cell
+ * size.
+ *
+ * @param maxAvailableSize indicates the breakpoint to use this specification.
+ * @param specType indicates whether the paddings and gutters will be applied vertically or
+ * horizontally.
+ * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
+ * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
+ * @param gutter the space between the cells vertically or horizontally depending on the [specType].
+ * @param cellSize height or width of the cell depending on the [specType].
+ */
+abstract class ResponsiveSpec(
+ open val maxAvailableSize: Int,
+ open val specType: SpecType,
+ open val startPadding: SizeSpec,
+ open val endPadding: SizeSpec,
+ open val gutter: SizeSpec,
+ open val cellSize: SizeSpec
+) {
+ open fun isValid(): Boolean {
+ if (maxAvailableSize <= 0) {
+ Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
+ return false
+ }
+
+ // All specs need to be individually valid
+ if (!allSpecsAreValid()) {
+ Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
+ return false
+ }
+
+ return true
+ }
+
+ private fun allSpecsAreValid(): Boolean {
+ return startPadding.isValid() &&
+ endPadding.isValid() &&
+ gutter.isValid() &&
+ cellSize.isValid()
+ }
+
+ enum class SpecType {
+ HEIGHT,
+ WIDTH
+ }
+
+ companion object {
+ private const val LOG_TAG = "ResponsiveSpec"
+ }
+}
+
+/**
+ * Calculated responsive specs contains the final paddings, gutter and cell size in pixels after
+ * they are calculated from the available space, cells and workspace specs.
+ */
+sealed class CalculatedResponsiveSpec {
+ var availableSpace: Int = 0
+ private set
+
+ var cells: Int = 0
+ private set
+
+ var startPaddingPx: Int = 0
+ private set
+
+ var endPaddingPx: Int = 0
+ private set
+
+ var gutterPx: Int = 0
+ private set
+
+ var cellSizePx: Int = 0
+ private set
+
+ var spec: ResponsiveSpec
+ private set
+
+ constructor(
+ availableSpace: Int,
+ cells: Int,
+ spec: ResponsiveSpec,
+ calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+ ) {
+ this.availableSpace = availableSpace
+ this.cells = cells
+ this.spec = spec
+
+ // Map if is fixedSize, ofAvailableSpace or matchWorkspace
+ startPaddingPx =
+ spec.startPadding.getCalculatedValue(
+ availableSpace,
+ calculatedWorkspaceSpec.startPaddingPx
+ )
+ endPaddingPx =
+ spec.endPadding.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.endPaddingPx)
+ gutterPx = spec.gutter.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.gutterPx)
+ cellSizePx =
+ spec.cellSize.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.cellSizePx)
+
+ updateRemainderSpaces(availableSpace, cells, spec)
+ }
+
+ constructor(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+ this.availableSpace = availableSpace
+ this.cells = cells
+ this.spec = spec
+
+ // Map if is fixedSize or ofAvailableSpace
+ startPaddingPx = spec.startPadding.getCalculatedValue(availableSpace)
+ endPaddingPx = spec.endPadding.getCalculatedValue(availableSpace)
+ gutterPx = spec.gutter.getCalculatedValue(availableSpace)
+ cellSizePx = spec.cellSize.getCalculatedValue(availableSpace)
+
+ updateRemainderSpaces(availableSpace, cells, spec)
+ }
+
+ private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+ val gutters = cells - 1
+ val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
+ val remainderSpace = availableSpace - usedSpace
+
+ startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
+ endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
+ gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
+ cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
+ }
+
+ override fun hashCode(): Int {
+ var result = availableSpace.hashCode()
+ result = 31 * result + cells.hashCode()
+ result = 31 * result + startPaddingPx.hashCode()
+ result = 31 * result + endPaddingPx.hashCode()
+ result = 31 * result + gutterPx.hashCode()
+ result = 31 * result + cellSizePx.hashCode()
+ result = 31 * result + spec.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is CalculatedResponsiveSpec &&
+ availableSpace == other.availableSpace &&
+ cells == other.cells &&
+ startPaddingPx == other.startPaddingPx &&
+ endPaddingPx == other.endPaddingPx &&
+ gutterPx == other.gutterPx &&
+ cellSizePx == other.cellSizePx &&
+ spec == other.spec
+ }
+
+ override fun toString(): String {
+ return "${this::class.simpleName}(" +
+ "availableSpace=$availableSpace, cells=$cells, startPaddingPx=$startPaddingPx, " +
+ "endPaddingPx=$endPaddingPx, gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
+ "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+ ")"
+ }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
new file mode 100644
index 0000000..a89b619
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.content.res.XmlResourceParser
+import android.util.Xml
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+import java.io.IOException
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+class ResponsiveSpecsParser(private val resourceHelper: ResourceHelper) {
+
+ private fun parseSizeSpecs(parser: XmlResourceParser): Map<String, SizeSpec> {
+ val parentName = parser.name
+ parser.next()
+
+ val result = mutableMapOf<String, SizeSpec>()
+ while (parser.eventType != XmlPullParser.END_DOCUMENT && parser.name != parentName) {
+ if (parser.eventType == XmlResourceParser.START_TAG) {
+ result[parser.name] = SizeSpec.create(resourceHelper, Xml.asAttributeSet(parser))
+ }
+ parser.next()
+ }
+
+ return result
+ }
+
+ fun <T> parseXML(
+ tagName: String,
+ map: (attributes: TypedArray, sizeSpecs: Map<String, SizeSpec>) -> T
+ ): List<T> {
+ val parser: XmlResourceParser = resourceHelper.getXml()
+
+ try {
+ val list = mutableListOf<T>()
+
+ var eventType = parser.eventType
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlResourceParser.START_TAG && parser.name == tagName) {
+ val attrs =
+ resourceHelper.obtainStyledAttributes(
+ Xml.asAttributeSet(parser),
+ R.styleable.ResponsiveSpec
+ )
+
+ val sizeSpecs = parseSizeSpecs(parser)
+ list += map(attrs, sizeSpecs)
+ attrs.recycle()
+ }
+
+ eventType = parser.next()
+ }
+
+ parser.close()
+
+ return list
+ } catch (e: Exception) {
+ when (e) {
+ is NoSuchFieldException,
+ is IOException,
+ is XmlPullParserException ->
+ throw RuntimeException("Failure parsing specs file.", e)
+ else -> throw e
+ }
+ } finally {
+ parser.close()
+ }
+ }
+}
+
+fun Map<String, SizeSpec>.getOrError(key: String): SizeSpec {
+ return this.getOrElse(key) { error("Attr '$key' must be defined.") }
+}
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index 3d618f9..c868c9f 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.launcher3.responsive
import android.content.res.TypedArray
@@ -26,7 +42,7 @@
) {
/** Retrieves the correct value for [SizeSpec]. */
- fun getCalculatedValue(availableSpace: Int, workspaceValue: Int): Int {
+ fun getCalculatedValue(availableSpace: Int, workspaceValue: Int = 0): Int {
val calculatedValue =
when {
fixedSize > 0 -> fixedSize.roundToInt()
@@ -91,6 +107,22 @@
return true
}
+ fun onlyFixedSize(): Boolean {
+ if (ofAvailableSpace > 0 || ofRemainderSpace > 0 || matchWorkspace) {
+ Log.e(TAG, "SizeSpec#onlyFixedSize - only fixed size allowed for this tag")
+ return false
+ }
+ return true
+ }
+
+ object XmlTags {
+ const val START_PADDING = "startPadding"
+ const val END_PADDING = "endPadding"
+ const val GUTTER = "gutter"
+ const val CELL_SIZE = "cellSize"
+ const val HOTSEAT_QSB_SPACE = "hotseatQsbSpace"
+ }
+
companion object {
private const val TAG = "SizeSpec"
diff --git a/src/com/android/launcher3/responsive/WorkspaceSpecs.kt b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
new file mode 100644
index 0000000..0da7026
--- /dev/null
+++ b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.util.Log
+import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.util.ResourceHelper
+
+private const val TAG = "WorkspaceSpecs"
+
+class WorkspaceSpecs(widthSpecs: List<WorkspaceSpec>, heightSpecs: List<WorkspaceSpec>) :
+ ResponsiveSpecs<WorkspaceSpec>(widthSpecs, heightSpecs) {
+
+ fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
+ val spec = getWidthSpec(availableWidth)
+ return CalculatedWorkspaceSpec(availableWidth, columns, spec)
+ }
+
+ fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
+ val spec = getHeightSpec(availableHeight)
+ return CalculatedWorkspaceSpec(availableHeight, rows, spec)
+ }
+
+ companion object {
+ private const val XML_WORKSPACE_SPEC = "workspaceSpec"
+
+ @JvmStatic
+ fun create(resourceHelper: ResourceHelper): WorkspaceSpecs {
+ val parser = ResponsiveSpecsParser(resourceHelper)
+ val specs = parser.parseXML(XML_WORKSPACE_SPEC, ::WorkspaceSpec)
+ val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+ return WorkspaceSpecs(widthSpecs, heightSpecs)
+ }
+ }
+}
+
+data class WorkspaceSpec(
+ override val maxAvailableSize: Int,
+ override val specType: SpecType,
+ override val startPadding: SizeSpec,
+ override val endPadding: SizeSpec,
+ override val gutter: SizeSpec,
+ override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
+
+ init {
+ check(isValid()) { "Invalid WorkspaceSpec found." }
+ }
+
+ constructor(
+ attrs: TypedArray,
+ specs: Map<String, SizeSpec>
+ ) : this(
+ maxAvailableSize =
+ attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+ specType =
+ SpecType.values()[
+ attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+ startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+ endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+ gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+ cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+ )
+
+ override fun isValid(): Boolean {
+ // Workspace spec should not match workspace
+ if (
+ startPadding.matchWorkspace ||
+ endPadding.matchWorkspace ||
+ gutter.matchWorkspace ||
+ cellSize.matchWorkspace
+ ) {
+ Log.e(TAG, "WorkspaceSpec#isValid - workspace shouldn't contain matchWorkspace!")
+ return false
+ }
+
+ return super.isValid()
+ }
+}
+
+class CalculatedWorkspaceSpec(availableSpace: Int, cells: Int, spec: WorkspaceSpec) :
+ CalculatedResponsiveSpec(availableSpace, cells, spec)
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index b1586dc..360ff7e 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -27,6 +27,7 @@
import android.animation.AnimatorSet;
import android.os.Handler;
import android.os.Looper;
+import android.util.Log;
import androidx.annotation.FloatRange;
@@ -35,6 +36,7 @@
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
+import com.android.launcher3.testing.shared.TestProtocol;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -225,6 +227,8 @@
private void goToState(
STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
+ Log.d(TestProtocol.OVERVIEW_OVER_HOME, "go to state " + state);
+
animated &= areAnimatorsEnabled();
if (mActivity.isInState(state)) {
if (mConfig.currentAnimation == null) {
@@ -379,6 +383,8 @@
mState = state;
mActivity.onStateSetStart(mState);
+ Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying listeners for state transition start"
+ + " to state: " + state.toString());
for (int i = mListeners.size() - 1; i >= 0; i--) {
mListeners.get(i).onStateTransitionStart(state);
}
@@ -396,6 +402,8 @@
setRestState(null);
}
+ Log.d(TestProtocol.OVERVIEW_OVER_HOME, "Notifying " + mListeners.size() + " listeners "
+ + "for end transition for state: " + state.toString());
for (int i = mListeners.size() - 1; i >= 0; i--) {
mListeners.get(i).onStateTransitionComplete(state);
}
@@ -433,6 +441,7 @@
* Cancels the current animation.
*/
public void cancelAnimation() {
+ Log.d(TestProtocol.OVERVIEW_OVER_HOME, "current animation cancelled");
mConfig.reset();
// It could happen that a new animation is set as a result of an endListener on the
// existing animation.
@@ -456,6 +465,7 @@
* @param toState The state we are animating towards.
*/
public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
+ Log.d(TestProtocol.OVERVIEW_OVER_HOME, "setting animation to " + toState.toString());
cancelAnimation();
setCurrentAnimation(anim);
anim.addListener(createStateAnimationListener(toState));
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index b054f51..5306932 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -17,6 +17,7 @@
import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
+import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.annotation.TargetApi;
@@ -156,7 +157,8 @@
return response;
case TestProtocol.REQUEST_IS_TWO_PANELS:
- response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, false);
+ response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+ FOLDABLE_SINGLE_PAGE.get() ? false : mDeviceProfile.isTwoPanels);
return response;
case TestProtocol.REQUEST_GET_HAD_NONTEST_EVENTS:
@@ -202,10 +204,11 @@
}
case TestProtocol.REQUEST_WORKSPACE_COLUMNS_ROWS: {
+ InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
return getLauncherUIProperty(Bundle::putParcelable, launcher -> new Point(
- InvariantDeviceProfile.INSTANCE.get(mContext).numColumns,
- InvariantDeviceProfile.INSTANCE.get(mContext).numRows)
- );
+ idp.getDeviceProfile(mContext).getPanelCount() * idp.numColumns,
+ idp.numRows
+ ));
}
case TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX: {
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 790c226..8c12547 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -42,6 +42,7 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.logging.InstanceId;
@@ -95,6 +96,8 @@
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
onClickFolderIcon(v);
+ } else if (v instanceof AppPairIcon) {
+ onClickAppPairIcon(v);
}
} else if (tag instanceof AppInfo) {
startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
@@ -123,6 +126,17 @@
}
/**
+ * Event handler for an app pair icon click.
+ *
+ * @param v The view that was clicked. Must be an instance of {@link AppPairIcon}.
+ */
+ private static void onClickAppPairIcon(View v) {
+ Launcher launcher = Launcher.getLauncher(v.getContext());
+ FolderInfo folderInfo = ((AppPairIcon) v).getInfo();
+ launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1));
+ }
+
+ /**
* Event handler for the app widget view which has not fully restored.
*/
private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) {
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 9cba19d..122b1e0 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -67,7 +67,7 @@
if (!(v.getTag() instanceof ItemInfo)) return false;
launcher.setWaitingForResult(null);
- beginDrag(v, launcher, (ItemInfo) v.getTag(), launcher.getDefaultWorkspaceDragOptions());
+ beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
return true;
}
diff --git a/src/com/android/launcher3/util/EventLogArray.kt b/src/com/android/launcher3/util/EventLogArray.kt
new file mode 100644
index 0000000..a17d650
--- /dev/null
+++ b/src/com/android/launcher3/util/EventLogArray.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.launcher3.util
+
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * A utility class to record and log events. Events are stored in a fixed size array and old logs
+ * are purged as new events come.
+ */
+class EventLogArray(private val name: String, size: Int) {
+
+ companion object {
+ private const val TYPE_ONE_OFF = 0
+ private const val TYPE_FLOAT = 1
+ private const val TYPE_INTEGER = 2
+ private const val TYPE_BOOL_TRUE = 3
+ private const val TYPE_BOOL_FALSE = 4
+ private fun isEntrySame(entry: EventEntry?, type: Int, event: String): Boolean {
+ return entry != null && entry.type == type && entry.event == event
+ }
+ }
+
+ private val logs: Array<EventEntry?>
+ private var nextIndex = 0
+
+ init {
+ logs = arrayOfNulls(size)
+ }
+
+ fun addLog(event: String) {
+ addLog(TYPE_ONE_OFF, event, 0f)
+ }
+
+ fun addLog(event: String, extras: Int) {
+ addLog(TYPE_INTEGER, event, extras.toFloat())
+ }
+
+ fun addLog(event: String, extras: Float) {
+ addLog(TYPE_FLOAT, event, extras)
+ }
+
+ fun addLog(event: String, extras: Boolean) {
+ addLog(if (extras) TYPE_BOOL_TRUE else TYPE_BOOL_FALSE, event, 0f)
+ }
+
+ private fun addLog(type: Int, event: String, extras: Float) {
+ // Merge the logs if it's a duplicate
+ val last = (nextIndex + logs.size - 1) % logs.size
+ val secondLast = (nextIndex + logs.size - 2) % logs.size
+ if (isEntrySame(logs[last], type, event) && isEntrySame(logs[secondLast], type, event)) {
+ logs[last]!!.update(type, event, extras)
+ logs[secondLast]!!.duplicateCount++
+ return
+ }
+ if (logs[nextIndex] == null) {
+ logs[nextIndex] = EventEntry()
+ }
+ logs[nextIndex]!!.update(type, event, extras)
+ nextIndex = (nextIndex + 1) % logs.size
+ }
+
+ fun dump(prefix: String, writer: PrintWriter) {
+ writer.println("$prefix$name event history:")
+ val sdf = SimpleDateFormat(" HH:mm:ss.SSSZ ", Locale.US)
+ val date = Date()
+ for (i in logs.indices) {
+ val log = logs[(nextIndex + logs.size - i - 1) % logs.size] ?: continue
+ date.time = log.time
+ val msg = StringBuilder(prefix).append(sdf.format(date)).append(log.event)
+ when (log.type) {
+ TYPE_BOOL_FALSE -> msg.append(": false")
+ TYPE_BOOL_TRUE -> msg.append(": true")
+ TYPE_FLOAT -> msg.append(": ").append(log.extras)
+ TYPE_INTEGER -> msg.append(": ").append(log.extras.toInt())
+ else -> {}
+ }
+ if (log.duplicateCount > 0) {
+ msg.append(" & ").append(log.duplicateCount).append(" similar events")
+ }
+ writer.println(msg)
+ }
+ }
+
+ /** A single event entry. */
+ private class EventEntry {
+ var type = 0
+ var event: String? = null
+ var extras = 0f
+ var time: Long = 0
+ var duplicateCount = 0
+ fun update(type: Int, event: String, extras: Float) {
+ this.type = type
+ this.event = event
+ this.extras = extras
+ time = System.currentTimeMillis()
+ duplicateCount = 0
+ }
+ }
+}
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index ac016a8..a66a9d2 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -32,8 +32,15 @@
// Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
private boolean mUpdateVisibility;
+ private final int mHiddenVisibility;
+
public MultiValueAlpha(View view, int size) {
+ this(view, size, View.INVISIBLE);
+ }
+
+ public MultiValueAlpha(View view, int size, int hiddenVisibility) {
super(view, VIEW_ALPHA, size, ALPHA_AGGREGATOR, 1f);
+ this.mHiddenVisibility = hiddenVisibility;
}
/** Sets whether we should update between INVISIBLE and VISIBLE based on alpha. */
@@ -45,7 +52,7 @@
protected void apply(float value) {
super.apply(value);
if (mUpdateVisibility) {
- AlphaUpdateListener.updateVisibility(mTarget);
+ AlphaUpdateListener.updateVisibility(mTarget, mHiddenVisibility);
}
}
}
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index 348c8d8..f8f4b5f 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -38,8 +38,6 @@
public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_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_KEYBOARD_EDU_SEEN = "launcher.search_edu_seen";
- public static final String SEARCH_SNACKBAR_COUNT = "launcher.keyboard_snackbar_count";
public static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
public static final String TASKBAR_EDU_TOOLTIP_STEP = "launcher.taskbar_edu_tooltip_step";
// When adding a new key, add it here as well, to be able to reset it from Developer Options.
@@ -47,7 +45,6 @@
"All Apps Bounce", new String[] { HOME_BOUNCE_SEEN, HOME_BOUNCE_COUNT },
"Hybrid Hotseat Education", new String[] { HOTSEAT_DISCOVERY_TIP_COUNT,
HOTSEAT_LONGPRESS_TIP_SEEN },
- "Search Education", new String[] { SEARCH_KEYBOARD_EDU_SEEN, SEARCH_SNACKBAR_COUNT},
"Taskbar Education", new String[] { TASKBAR_EDU_TOOLTIP_STEP },
"All Apps Visited Count", new String[] {ALL_APPS_VISITED_COUNT}
);
@@ -58,7 +55,6 @@
@StringDef(value = {
HOME_BOUNCE_SEEN,
HOTSEAT_LONGPRESS_TIP_SEEN,
- SEARCH_KEYBOARD_EDU_SEEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventBoolKey {}
@@ -69,7 +65,6 @@
@StringDef(value = {
HOME_BOUNCE_COUNT,
HOTSEAT_DISCOVERY_TIP_COUNT,
- SEARCH_SNACKBAR_COUNT,
ALL_APPS_VISITED_COUNT,
TASKBAR_EDU_TOOLTIP_STEP,
})
@@ -82,7 +77,6 @@
Map<String, Integer> maxCounts = new ArrayMap<>(5);
maxCounts.put(HOME_BOUNCE_COUNT, 3);
maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
- maxCounts.put(SEARCH_SNACKBAR_COUNT, 3);
maxCounts.put(ALL_APPS_VISITED_COUNT, 20);
maxCounts.put(TASKBAR_EDU_TOOLTIP_STEP, 2);
MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 91eb109..30e0971 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -17,6 +17,7 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
@@ -24,17 +25,14 @@
import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS;
import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
-import android.util.Property;
+import android.util.FloatProperty;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -51,6 +49,9 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.touch.BaseSwipeDetector;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
@@ -66,8 +67,8 @@
public abstract class AbstractSlideInView<T extends Context & ActivityContext>
extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {
- protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
- new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
+ protected static final FloatProperty<AbstractSlideInView<?>> TRANSLATION_SHIFT =
+ new FloatProperty<>("translationShift") {
@Override
public Float get(AbstractSlideInView view) {
@@ -75,25 +76,54 @@
}
@Override
- public void set(AbstractSlideInView view, Float value) {
+ public void setValue(AbstractSlideInView view, float value) {
view.setTranslationShift(value);
}
};
protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
protected static final float TRANSLATION_SHIFT_OPENED = 0f;
private static final float VIEW_NO_SCALE = 1f;
+ private static final int DEFAULT_DURATION = 300;
protected final T mActivityContext;
protected final SingleAxisSwipeDetector mSwipeDetector;
- protected final ObjectAnimator mOpenCloseAnimator;
+ protected @NonNull AnimatorPlaybackController mOpenCloseAnimation;
protected ViewGroup mContent;
protected final View mColorScrim;
- protected Interpolator mScrollInterpolator;
+
+ /**
+ * Interpolator for {@link #mOpenCloseAnimation} when we are closing due to dragging downwards.
+ */
+ private Interpolator mScrollInterpolator;
+ private long mScrollDuration;
+ /**
+ * End progress for {@link #mOpenCloseAnimation} when we are closing due to dragging downloads.
+ * <p>
+ * There are two cases that determine this value:
+ * <ol>
+ * <li>
+ * If the drag interrupts the opening transition (i.e. {@link #mToTranslationShift}
+ * is {@link #TRANSLATION_SHIFT_OPENED}), we need to animate back to {@code 0} to
+ * reverse the animation that was paused at {@link #onDragStart(boolean, float)}.
+ * </li>
+ * <li>
+ * If the drag started after the view is fully opened (i.e.
+ * {@link #mToTranslationShift} is {@link #TRANSLATION_SHIFT_CLOSED}), the animation
+ * that was set up at {@link #onDragStart(boolean, float)} for closing the view
+ * should go forward to {@code 1}.
+ * </li>
+ * </ol>
+ */
+ private float mScrollEndProgress;
// range [0, 1], 0=> completely open, 1=> completely closed
protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
+ protected float mFromTranslationShift;
+ protected float mToTranslationShift;
+ /** {@link #mOpenCloseAnimation} progress at {@link #onDragStart(boolean, float)}. */
+ private float mDragStartProgress;
protected boolean mNoIntercept;
protected @Nullable OnCloseListener mOnCloseBeginListener;
@@ -102,8 +132,8 @@
protected final AnimatedFloat mSlideInViewScale =
new AnimatedFloat(this::onScaleProgressChanged, VIEW_NO_SCALE);
protected boolean mIsBackProgressing;
- @Nullable private Drawable mContentBackground;
- @Nullable private View mContentBackgroundParentView;
+ private @Nullable Drawable mContentBackground;
+ private @Nullable View mContentBackgroundParentView;
protected final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
@Override
@@ -122,21 +152,78 @@
mActivityContext = ActivityContext.lookupContext(context);
mScrollInterpolator = Interpolators.SCROLL_CUBIC;
+ mScrollDuration = DEFAULT_DURATION;
mSwipeDetector = new SingleAxisSwipeDetector(context, this,
SingleAxisSwipeDetector.VERTICAL);
- mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
- mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mSwipeDetector.finishedScrolling();
- announceAccessibilityChanges();
- }
- });
+ mOpenCloseAnimation = new PendingAnimation(0).createPlaybackController();
+
int scrimColor = getScrimColor(context);
mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
}
+ /**
+ * Sets up a {@link #mOpenCloseAnimation} for opening with default parameters.
+ *
+ * @see #setUpOpenCloseAnimation(float, float, long)
+ */
+ protected final AnimatorPlaybackController setUpDefaultOpenAnimation() {
+ AnimatorPlaybackController animation = setUpOpenCloseAnimation(
+ TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, DEFAULT_DURATION);
+ animation.getAnimationPlayer().setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ return animation;
+ }
+
+ /**
+ * Sets up a {@link #mOpenCloseAnimation} for opening with a given duration.
+ *
+ * @see #setUpOpenCloseAnimation(float, float, long)
+ */
+ protected final AnimatorPlaybackController setUpOpenAnimation(long duration) {
+ return setUpOpenCloseAnimation(
+ TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, duration);
+ }
+
+ private AnimatorPlaybackController setUpCloseAnimation(long duration) {
+ return setUpOpenCloseAnimation(
+ TRANSLATION_SHIFT_OPENED, TRANSLATION_SHIFT_CLOSED, duration);
+ }
+
+ /**
+ * Initializes a new {@link #mOpenCloseAnimation}.
+ *
+ * @param fromTranslationShift translation shift to animate from.
+ * @param toTranslationShift translation shift to animate to.
+ * @param duration animation duration.
+ * @return {@link #mOpenCloseAnimation}
+ */
+ private AnimatorPlaybackController setUpOpenCloseAnimation(
+ float fromTranslationShift, float toTranslationShift, long duration) {
+ mFromTranslationShift = fromTranslationShift;
+ mToTranslationShift = toTranslationShift;
+
+ PendingAnimation animation = new PendingAnimation(duration);
+ animation.addEndListener(b -> {
+ mSwipeDetector.finishedScrolling();
+ announceAccessibilityChanges();
+ });
+
+ animation.addFloat(
+ this, TRANSLATION_SHIFT, fromTranslationShift, toTranslationShift, LINEAR);
+ onOpenCloseAnimationPending(animation);
+
+ mOpenCloseAnimation = animation.createPlaybackController();
+ return mOpenCloseAnimation;
+ }
+
+ /**
+ * Invoked when a {@link #mOpenCloseAnimation} is being set up.
+ * <p>
+ * Subclasses can override this method to modify the animation before it's used to create a
+ * {@link AnimatorPlaybackController}.
+ */
+ protected void onOpenCloseAnimationPending(PendingAnimation animation) {}
+
protected void attachToContainer() {
if (mColorScrim != null) {
getPopupContainer().addView(mColorScrim);
@@ -279,19 +366,28 @@
}
private boolean isOpeningAnimationRunning() {
- return mIsOpen && mOpenCloseAnimator.isRunning();
+ return mIsOpen && mOpenCloseAnimation.getAnimationPlayer().isRunning();
}
/* SingleAxisSwipeDetector.Listener */
@Override
- public void onDragStart(boolean start, float startDisplacement) { }
+ public void onDragStart(boolean start, float startDisplacement) {
+ if (mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
+ mOpenCloseAnimation.pause();
+ mDragStartProgress = mOpenCloseAnimation.getProgressFraction();
+ } else {
+ setUpCloseAnimation(DEFAULT_DURATION);
+ mDragStartProgress = 0;
+ }
+ }
@Override
public boolean onDrag(float displacement) {
- float range = getShiftRange();
- displacement = Utilities.boundToRange(displacement, 0, range);
- setTranslationShift(displacement / range);
+ float progress = mDragStartProgress
+ + Math.signum(mToTranslationShift - mFromTranslationShift)
+ * (displacement / getShiftRange());
+ mOpenCloseAnimation.setPlayFraction(Utilities.boundToRange(progress, 0, 1));
return true;
}
@@ -302,16 +398,18 @@
if ((mSwipeDetector.isFling(velocity) && velocity > 0)
|| mTranslationShift > successfulShiftThreshold) {
mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
- mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
- velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
+ mScrollDuration = BaseSwipeDetector.calculateDuration(
+ velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift);
+ mScrollEndProgress = mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 0 : 1;
close(true);
} else {
- mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
- TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setDuration(
- BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
- .setInterpolator(Interpolators.DECELERATE);
- mOpenCloseAnimator.start();
+ ValueAnimator animator = mOpenCloseAnimation.getAnimationPlayer();
+ animator.setInterpolator(Interpolators.DECELERATE);
+ animator.setFloatValues(
+ mOpenCloseAnimation.getProgressFraction(),
+ mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 1 : 0);
+ animator.setDuration(BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
+ .start();
}
}
@@ -332,28 +430,27 @@
Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed);
if (!animate) {
- mOpenCloseAnimator.cancel();
+ mOpenCloseAnimation.pause();
setTranslationShift(TRANSLATION_SHIFT_CLOSED);
onCloseComplete();
return;
}
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
- mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mOpenCloseAnimator.removeListener(this);
- onCloseComplete();
- }
- });
+
+ final ValueAnimator animator;
if (mSwipeDetector.isIdleState()) {
- mOpenCloseAnimator
- .setDuration(defaultDuration)
- .setInterpolator(getIdleInterpolator());
+ setUpCloseAnimation(defaultDuration);
+ animator = mOpenCloseAnimation.getAnimationPlayer();
+ animator.setInterpolator(getIdleInterpolator());
} else {
- mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
+ animator = mOpenCloseAnimation.getAnimationPlayer();
+ animator.setInterpolator(mScrollInterpolator);
+ animator.setDuration(mScrollDuration);
+ mOpenCloseAnimation.getAnimationPlayer().setFloatValues(
+ mOpenCloseAnimation.getProgressFraction(), mScrollEndProgress);
}
- mOpenCloseAnimator.start();
+
+ animator.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
+ animator.start();
}
protected Interpolator getIdleInterpolator() {
diff --git a/src/com/android/launcher3/views/WidgetsEduView.java b/src/com/android/launcher3/views/WidgetsEduView.java
index 9180781..e70b1cb 100644
--- a/src/com/android/launcher3/views/WidgetsEduView.java
+++ b/src/com/android/launcher3/views/WidgetsEduView.java
@@ -15,9 +15,6 @@
*/
package com.android.launcher3.views;
-import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
-
-import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -119,14 +116,11 @@
}
private void animateOpen() {
- if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
return;
}
mIsOpen = true;
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
- mOpenCloseAnimator.start();
+ setUpDefaultOpenAnimation().start();
}
/** Shows widget education dialog. */
diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
index 473abf1..80b1cdd 100644
--- a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
@@ -16,10 +16,8 @@
package com.android.launcher3.widget;
-import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.Utilities.ATLEAST_R;
-import android.animation.PropertyValuesHolder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Insets;
@@ -130,14 +128,11 @@
}
private void animateOpen() {
- if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
return;
}
mIsOpen = true;
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
- mOpenCloseAnimator.start();
+ setUpDefaultOpenAnimation().start();
}
@Override
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 93f7cb3..c347939 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -16,10 +16,8 @@
package com.android.launcher3.widget;
-import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
-import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -226,15 +224,12 @@
}
private void animateOpen() {
- if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) {
return;
}
mIsOpen = true;
setupNavBarColor();
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
- mOpenCloseAnimator.start();
+ setUpDefaultOpenAnimation().start();
}
@Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index c9cd4b6..438d4a0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -23,8 +23,6 @@
import static com.android.launcher3.testing.shared.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;
@@ -627,20 +625,13 @@
mContent.setAlpha(0);
setTranslationShift(VERTICAL_START_POSITION);
}
- mOpenCloseAnimator.setValues(
- PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
- mOpenCloseAnimator
- .setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
- .setInterpolator(AnimationUtils.loadInterpolator(
- getContext(), android.R.interpolator.linear_out_slow_in));
- mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mOpenCloseAnimator.removeListener(this);
- }
- });
+ setUpOpenAnimation(mActivityContext.getDeviceProfile().bottomSheetOpenDuration);
+ Animator animator = mOpenCloseAnimation.getAnimationPlayer();
+ animator.setInterpolator(AnimationUtils.loadInterpolator(
+ getContext(), android.R.interpolator.linear_out_slow_in));
post(() -> {
- mOpenCloseAnimator.start();
+ animator.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
+ .start();
mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
});
} else {
@@ -897,6 +888,18 @@
return mContent;
}
+ /** Gets the search bar, which is used for testing */ // b/294050472
+ @VisibleForTesting
+ public View getSearchBar() {
+ return (View) mSearchBar;
+ }
+
+ /** Gets the search bar container, which is used for testing */ // b/294050472
+ @VisibleForTesting
+ public View getSearchBarContainer() {
+ return (View) mSearchBarContainer;
+ }
+
/** Opens the first header in widget picker and scrolls to the top of the RecyclerView. */
@VisibleForTesting
public void openFirstHeader() {
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
deleted file mode 100644
index 8cc0c59..0000000
--- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.workspace
-
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
-import com.android.launcher3.R
-import com.android.launcher3.responsive.SizeSpec
-import com.android.launcher3.util.ResourceHelper
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-
-private const val TAG = "WorkspaceSpecs"
-
-class WorkspaceSpecs(resourceHelper: ResourceHelper) {
- object XmlTags {
- const val WORKSPACE_SPECS = "workspaceSpecs"
-
- const val WORKSPACE_SPEC = "workspaceSpec"
- const val START_PADDING = "startPadding"
- const val END_PADDING = "endPadding"
- const val GUTTER = "gutter"
- const val CELL_SIZE = "cellSize"
- }
-
- val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>()
- val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>()
-
- // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
- init {
- try {
- val parser: XmlResourceParser = resourceHelper.getXml()
- val depth = parser.depth
- var type: Int
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPECS == parser.name) {
- val displayDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- if (
- type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPEC == parser.name
- ) {
- val attrs =
- resourceHelper.obtainStyledAttributes(
- Xml.asAttributeSet(parser),
- R.styleable.WorkspaceSpec
- )
- val maxAvailableSize =
- attrs.getDimensionPixelSize(
- R.styleable.WorkspaceSpec_maxAvailableSize,
- 0
- )
- val specType =
- WorkspaceSpec.SpecType.values()[
- attrs.getInt(
- R.styleable.WorkspaceSpec_specType,
- WorkspaceSpec.SpecType.HEIGHT.ordinal
- )]
- attrs.recycle()
-
- var startPadding: SizeSpec? = null
- var endPadding: SizeSpec? = null
- var gutter: SizeSpec? = null
- var cellSize: SizeSpec? = null
-
- val limitDepth = parser.depth
- while (
- (parser.next().also { type = it } != XmlPullParser.END_TAG ||
- parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
- ) {
- val attr: AttributeSet = Xml.asAttributeSet(parser)
- if (type == XmlPullParser.START_TAG) {
- when (parser.name) {
- XmlTags.START_PADDING -> {
- startPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.END_PADDING -> {
- endPadding = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.GUTTER -> {
- gutter = SizeSpec.create(resourceHelper, attr)
- }
- XmlTags.CELL_SIZE -> {
- cellSize = SizeSpec.create(resourceHelper, attr)
- }
- }
- }
- }
-
- if (
- startPadding == null ||
- endPadding == null ||
- gutter == null ||
- cellSize == null
- ) {
- throw IllegalStateException(
- "All attributes in workspaceSpec must be defined"
- )
- }
-
- val workspaceSpec =
- WorkspaceSpec(
- maxAvailableSize,
- specType,
- startPadding,
- endPadding,
- gutter,
- cellSize
- )
- if (workspaceSpec.isValid()) {
- if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT)
- workspaceHeightSpecList.add(workspaceSpec)
- else workspaceWidthSpecList.add(workspaceSpec)
- } else {
- throw IllegalStateException("Invalid workspaceSpec found.")
- }
- }
- }
-
- if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) {
- throw IllegalStateException(
- "WorkspaceSpecs is incomplete - " +
- "height list size = ${workspaceHeightSpecList.size}; " +
- "width list size = ${workspaceWidthSpecList.size}."
- )
- }
- }
- }
- parser.close()
- } catch (e: Exception) {
- when (e) {
- is IOException,
- is XmlPullParserException -> {
- throw RuntimeException("Failure parsing workspaces specs file.", e)
- }
- else -> throw e
- }
- }
- }
-
- /**
- * Returns the CalculatedWorkspaceSpec for width, based on the available width and the
- * WorkspaceSpecs.
- */
- fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
- val widthSpec = workspaceWidthSpecList.first { availableWidth <= it.maxAvailableSize }
-
- return CalculatedWorkspaceSpec(availableWidth, columns, widthSpec)
- }
-
- /**
- * Returns the CalculatedWorkspaceSpec for height, based on the available height and the
- * WorkspaceSpecs.
- */
- fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
- val heightSpec = workspaceHeightSpecList.first { availableHeight <= it.maxAvailableSize }
-
- return CalculatedWorkspaceSpec(availableHeight, rows, heightSpec)
- }
-}
-
-class CalculatedWorkspaceSpec(
- val availableSpace: Int,
- val cells: Int,
- val workspaceSpec: WorkspaceSpec
-) {
- var startPaddingPx: Int = 0
- private set
- var endPaddingPx: Int = 0
- private set
- var gutterPx: Int = 0
- private set
- var cellSizePx: Int = 0
- private set
- init {
- // Calculate all fixed size first
- if (workspaceSpec.startPadding.fixedSize > 0)
- startPaddingPx = workspaceSpec.startPadding.fixedSize.roundToInt()
- if (workspaceSpec.endPadding.fixedSize > 0)
- endPaddingPx = workspaceSpec.endPadding.fixedSize.roundToInt()
- if (workspaceSpec.gutter.fixedSize > 0)
- gutterPx = workspaceSpec.gutter.fixedSize.roundToInt()
- if (workspaceSpec.cellSize.fixedSize > 0)
- cellSizePx = workspaceSpec.cellSize.fixedSize.roundToInt()
-
- // Calculate all available space next
- if (workspaceSpec.startPadding.ofAvailableSpace > 0)
- startPaddingPx =
- (workspaceSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (workspaceSpec.endPadding.ofAvailableSpace > 0)
- endPaddingPx = (workspaceSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
- if (workspaceSpec.gutter.ofAvailableSpace > 0)
- gutterPx = (workspaceSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
- if (workspaceSpec.cellSize.ofAvailableSpace > 0)
- cellSizePx = (workspaceSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
- // Calculate remainder space last
- val gutters = cells - 1
- val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
- val remainderSpace = availableSpace - usedSpace
- if (workspaceSpec.startPadding.ofRemainderSpace > 0)
- startPaddingPx =
- (workspaceSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (workspaceSpec.endPadding.ofRemainderSpace > 0)
- endPaddingPx = (workspaceSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
- if (workspaceSpec.gutter.ofRemainderSpace > 0)
- gutterPx = (workspaceSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
- if (workspaceSpec.cellSize.ofRemainderSpace > 0)
- cellSizePx = (workspaceSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
- }
-
- override fun toString(): String {
- return "CalculatedWorkspaceSpec(availableSpace=$availableSpace, " +
- "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
- "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
- "workspaceSpec.maxAvailableSize=${workspaceSpec.maxAvailableSize})"
- }
-}
-
-data class WorkspaceSpec(
- val maxAvailableSize: Int,
- val specType: SpecType,
- val startPadding: SizeSpec,
- val endPadding: SizeSpec,
- val gutter: SizeSpec,
- val cellSize: SizeSpec
-) {
-
- enum class SpecType {
- HEIGHT,
- WIDTH
- }
-
- fun isValid(): Boolean {
- if (maxAvailableSize <= 0) {
- Log.e(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0")
- return false
- }
-
- // All specs need to be individually valid
- if (!allSpecsAreValid()) {
- Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()")
- return false
- }
-
- return true
- }
-
- private fun allSpecsAreValid(): Boolean =
- startPadding.isValid() &&
- endPadding.isValid() &&
- gutter.isValid() &&
- cellSize.isValid() &&
- !startPadding.matchWorkspace &&
- !endPadding.matchWorkspace &&
- !gutter.matchWorkspace &&
- !cellSize.matchWorkspace
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
index d6b41c9..599a591 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -16,6 +16,7 @@
package com.android.launcher3.uioverrides;
+import android.app.ActivityOptions;
import android.app.Person;
import android.content.Context;
import android.content.pm.LauncherActivityInfo;
@@ -40,4 +41,11 @@
public static Map<String, LauncherActivityInfo> getActivityOverrides(Context context) {
return Collections.emptyMap();
}
+
+ /**
+ * Creates an ActivityOptions to play fade-out animation on closing targets
+ */
+ public static ActivityOptions createFadeOutAnimOptions(Context context) {
+ return ActivityOptions.makeCustomAnimation(context, 0, android.R.anim.fade_out);
+ }
}
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index c8b5a20..bb61fbe 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -32,6 +32,7 @@
<receiver
android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
android:exported="true"
+ android:icon="@drawable/test_widget_no_config_icon"
android:label="No Config">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -65,6 +66,7 @@
<receiver
android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
android:exported="true"
+ android:icon="@drawable/test_widget_with_config_icon"
android:label="With Config">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -76,6 +78,7 @@
<receiver
android:name="com.android.launcher3.testcomponent.AppWidgetWithDialog"
android:exported="true"
+ android:icon="@drawable/test_widget_with_dialog_icon"
android:label="With Dialog">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -87,6 +90,7 @@
<receiver
android:name="com.android.launcher3.testcomponent.AppWidgetDynamicColors"
android:exported="true"
+ android:icon="@drawable/test_widget_dynamic_colors_icon"
android:label="Dynamic Colors">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -299,6 +303,28 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
+ <activity-alias android:name="MaxShortcutsActivity"
+ android:label="TestActivityMaxShortcuts"
+ android:exported="true"
+ android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ <meta-data android:name="android.app.shortcuts"
+ android:resource="@xml/max_shortcuts"/>
+ </activity-alias>
+ <activity-alias android:name="SingleShortcutActivity"
+ android:label="TestActivitySingleShortcut"
+ android:exported="true"
+ android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ <meta-data android:name="android.app.shortcuts"
+ android:resource="@xml/single_shortcut"/>
+ </activity-alias>
<activity
android:name="com.android.launcher3.testcomponent.DialogTestActivity"
android:label="Dialog Activity"
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder b/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder
new file mode 100644
index 0000000..c6d65f8
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+# * # (coments): Lines starting with this character would be ignored.
+# * arguments: is set of words separated by spaces that can later be parsed
+# * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+# * x: The x character represents spaces that would be ignored, for example it can be used in
+# the first row if we don't know how wide the smartspace is.
+# * i: Represents an icon on the workspace, none in particular just an icon
+# * [a-z]: Represents a widget and it can be any number or character
+# except any other already in use. The whole continuos are of the same character is the
+# area of the widget.
+# * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+# letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|aeeee
+--mm-|acccc
+--mm-|acccc
+ggggg|acccc
+ggggg|adddd
+arguments: 7 1
+board: 10x5
+xxxxx|aeeee
+---mm|acccc
+---mm|acccc
+ggggg|acccc
+ggggg|adddd
+# 4x4 Test
+board: 4x4
+xxxx|aeee
+--mm|accc
+--mm|accc
+gggg|accc
+arguments: 5 1
+board: 8x4
+xxxx|aeee
+--mm|accc
+--mm|accc
+gggg|accc
+# 6x5 Test
+board: 6x5
+xxxxxx|aeeeee
+--mm--|accccc
+--mm--|accccc
+gggggg|accccc
+gggggg|addddd
+arguments: 8 1
+board: 12x5
+xxxxxx|aeeeee
+----mm|accccc
+----mm|accccc
+gggggg|accccc
+gggggg|addddd
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side b/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side
new file mode 100644
index 0000000..376638e
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+# * # (coments): Lines starting with this character would be ignored.
+# * arguments: is set of words separated by spaces that can later be parsed
+# * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+# * x: The x character represents spaces that would be ignored, for example it can be used in
+# the first row if we don't know how wide the smartspace is.
+# * i: Represents an icon on the workspace, none in particular just an icon
+# * [a-z]: Represents a widget and it can be any number or character
+# except any other already in use. The whole continuos are of the same character is the
+# area of the widget.
+# * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+# letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|aaaaa
+--mm-|plllh
+--mm-|piiih
+ggggg|piiih
+ggggg|fffff
+arguments: 7 1
+board: 10x5
+xxxxx|aaaaa
+--lll|p-mmh
+---ii|pimmh
+ggggg|piiih
+ggggg|fffff
+# 4x4 Test
+board: 4x4
+xxxx|aaaa
+--mm|pllh
+--mm|piih
+gggg|ffff
+arguments: 5 1
+board: 8x4
+xxxx|aaaa
+--ll|pmmh
+--ii|pmmh
+gggg|ffff
+# 6x5 Test
+board: 6x5
+xxxxxx|aaaaaa
+--mmm-|pllllh
+--mmm-|piiiih
+--mmm-|piiiih
+gggggg|ffffff
+arguments: 8 1
+board: 12x5
+xxxxxx|aaaaaa
+--llll|p-mmmh
+---iii|pimmmh
+---iii|pimmmh
+gggggg|ffffff
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder b/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder
new file mode 100644
index 0000000..44301d2
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+# * # (coments): Lines starting with this character would be ignored.
+# * arguments: is set of words separated by spaces that can later be parsed
+# * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+# * x: The x character represents spaces that would be ignored, for example it can be used in
+# the first row if we don't know how wide the smartspace is.
+# * i: Represents an icon on the workspace, none in particular just an icon
+# * [a-z]: Represents a widget and it can be any number or character
+# except any other already in use. The whole continuos are of the same character is the
+# area of the widget.
+# * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+# letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|-----
+--mm-|-----
+--mm-|-----
+-----|-----
+-----|-----
+arguments: 8 3
+board: 10x5
+xxxxx|-----
+-----|-----
+-----|-----
+-----|---mm
+-----|---mm
+# 4x4 Test
+board: 4x4
+xxxx|----
+--mm|----
+--mm|----
+----|----
+arguments: 5 3
+board: 8x4
+xxxx|----
+----|----
+----|-mm-
+----|-mm-
+# 6x5 Test
+board: 6x5
+xxxxxx|------
+--m---|------
+------|------
+------|------
+------|------
+arguments: 10 4
+board: 12x5
+xxxxxx|------
+------|------
+------|------
+------|------
+------|----m-
\ No newline at end of file
diff --git a/tests/res/drawable/test_widget_dynamic_colors_icon.xml b/tests/res/drawable/test_widget_dynamic_colors_icon.xml
new file mode 100644
index 0000000..69f6675
--- /dev/null
+++ b/tests/res/drawable/test_widget_dynamic_colors_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@android:color/white"/>
+ <foreground>
+ <color android:color="#964B00"/>
+ </foreground>
+ <monochrome>
+ <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+ </vector>
+ </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_no_config_icon.xml b/tests/res/drawable/test_widget_no_config_icon.xml
new file mode 100644
index 0000000..e3d0125
--- /dev/null
+++ b/tests/res/drawable/test_widget_no_config_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@android:color/white"/>
+ <foreground>
+ <color android:color="#00FFFF"/>
+ </foreground>
+ <monochrome>
+ <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+ </vector>
+ </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_with_config_icon.xml b/tests/res/drawable/test_widget_with_config_icon.xml
new file mode 100644
index 0000000..98b797b
--- /dev/null
+++ b/tests/res/drawable/test_widget_with_config_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@android:color/white"/>
+ <foreground>
+ <color android:color="#008000" />
+ </foreground>
+ <monochrome>
+ <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+ </vector>
+ </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_with_dialog_icon.xml b/tests/res/drawable/test_widget_with_dialog_icon.xml
new file mode 100644
index 0000000..d2879d2
--- /dev/null
+++ b/tests/res/drawable/test_widget_with_dialog_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@android:color/white"/>
+ <foreground>
+ <color android:color="#800080"/>
+ </foreground>
+ <monochrome>
+ <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+ </vector>
+ </monochrome>
+</adaptive-icon>
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index 0d586c2..e5ee064 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -18,7 +18,7 @@
<!-- Attributes have to be copied to test for correct parsing of files -->
<resources>
<!-- Responsive grids attributes -->
- <declare-styleable name="WorkspaceSpec">
+ <declare-styleable name="ResponsiveSpec">
<attr name="specType" format="integer">
<enum name="height" value="0" />
<enum name="width" value="1" />
@@ -26,12 +26,9 @@
<attr name="maxAvailableSize" format="dimension" />
</declare-styleable>
- <declare-styleable name="SizeSpec">
- <attr name="fixedSize" format="dimension" />
- <attr name="ofAvailableSpace" format="float" />
- <attr name="ofRemainderSpace" format="float" />
- <attr name="matchWorkspace" format="boolean" />
- <attr name="maxSize" format="dimension" />
+ <declare-styleable name="WorkspaceSpec">
+ <attr name="specType" />
+ <attr name="maxAvailableSize" />
</declare-styleable>
<declare-styleable name="FolderSpec">
@@ -43,4 +40,12 @@
<attr name="specType" />
<attr name="maxAvailableSize" />
</declare-styleable>
+
+ <declare-styleable name="SizeSpec">
+ <attr name="fixedSize" format="dimension" />
+ <attr name="ofAvailableSpace" format="float" />
+ <attr name="ofRemainderSpace" format="float" />
+ <attr name="matchWorkspace" format="boolean" />
+ <attr name="maxSize" format="dimension" />
+ </declare-styleable>
</resources>
diff --git a/tests/res/values/strings.xml b/tests/res/values/strings.xml
index 0ad87fb..54ade56 100644
--- a/tests/res/values/strings.xml
+++ b/tests/res/values/strings.xml
@@ -3,4 +3,5 @@
<string name="shortcut1" translatable="false">Shortcut 1</string>
<string name="shortcut2" translatable="false">Shortcut 2</string>
<string name="shortcut3" translatable="false">Shortcut 3</string>
+ <string name="shortcut4" translatable="false">Shortcut 4</string>
</resources>
diff --git a/tests/res/xml/invalid_hotseat_file_case_1.xml b/tests/res/xml/invalid_hotseat_file_case_1.xml
new file mode 100644
index 0000000..fcbc5ea
--- /dev/null
+++ b/tests/res/xml/invalid_hotseat_file_case_1.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<hotseatSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+ <hotseatSpec
+ launcher:specType="height"
+ launcher:maxAvailableSize="847dp">
+ <hotseatQsbSpace launcher:ofAvailableSpace="1" />
+ </hotseatSpec>
+
+ <hotseatSpec
+ launcher:specType="height"
+ launcher:maxAvailableSize="9999dp">
+ <hotseatQsbSpace launcher:fixedSize="36dp" />
+ </hotseatSpec>
+
+</hotseatSpecs>
\ No newline at end of file
diff --git a/tests/res/xml/max_shortcuts.xml b/tests/res/xml/max_shortcuts.xml
new file mode 100644
index 0000000..312a24c
--- /dev/null
+++ b/tests/res/xml/max_shortcuts.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="max_shortcut1"
+ android:icon="@drawable/test_theme_icon"
+ android:shortcutShortLabel="@string/shortcut1">
+ <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+ </shortcut>
+ <shortcut
+ android:shortcutId="max_shortcut2"
+ android:shortcutShortLabel="@string/shortcut2">
+ <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+ </shortcut>
+ <shortcut
+ android:shortcutId="max_shortcut3"
+ android:shortcutShortLabel="@string/shortcut3">
+ <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+ </shortcut>
+ <shortcut
+ android:shortcutId="max_shortcut4"
+ android:shortcutShortLabel="@string/shortcut4">
+ <intent android:action="com.android.launcher3.intent.action.test_shortcut_max"/>
+ </shortcut>
+</shortcuts>
diff --git a/tests/res/xml/shortcuts.xml b/tests/res/xml/shortcuts.xml
index 94e8edd..2ba9d7f 100644
--- a/tests/res/xml/shortcuts.xml
+++ b/tests/res/xml/shortcuts.xml
@@ -1,4 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="shortcut1_themed"
diff --git a/tests/res/xml/single_shortcut.xml b/tests/res/xml/single_shortcut.xml
new file mode 100644
index 0000000..e8d938f
--- /dev/null
+++ b/tests/res/xml/single_shortcut.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
+ <shortcut
+ android:shortcutId="single_shortcut_themed"
+ android:icon="@drawable/test_theme_icon"
+ android:shortcutShortLabel="@string/shortcut1">
+ <intent android:action="com.android.launcher3.intent.action.test_shortcut_single"/>
+ </shortcut>
+</shortcuts>
diff --git a/tests/res/xml/valid_hotseat_file.xml b/tests/res/xml/valid_hotseat_file.xml
new file mode 100644
index 0000000..c7f52e8
--- /dev/null
+++ b/tests/res/xml/valid_hotseat_file.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<hotseatSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+ <hotseatSpec
+ launcher:specType="height"
+ launcher:maxAvailableSize="847dp">
+ <hotseatQsbSpace launcher:fixedSize="24dp" />
+ </hotseatSpec>
+
+ <hotseatSpec
+ launcher:specType="height"
+ launcher:maxAvailableSize="9999dp">
+ <hotseatQsbSpace launcher:fixedSize="36dp" />
+ </hotseatSpec>
+
+</hotseatSpecs>
\ No newline at end of file
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 4073517..d86a2f8 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -156,7 +156,11 @@
public static final String PERMANENT_DIAG_TAG = "TaplTarget";
public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
public static final String FLAKY_ACTIVITY_COUNT = "b/260260325";
+ public static final String FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP = "b/286084688";
public static final String ICON_MISSING = "b/282963545";
+ public static final String LAUNCH_SPLIT_PAIR = "b/288939273";
+
+ public static final String OVERVIEW_OVER_HOME = "b/279059025";
public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index f0cedd3..dd79ca8 100644
--- a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -22,6 +22,7 @@
import android.util.DisplayMetrics
import android.view.Surface
import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.NavigationMode
import com.android.launcher3.util.WindowBounds
@@ -158,41 +159,55 @@
}
protected fun initializeVarsForTwoPanel(
- deviceTabletSpec: DeviceSpec,
- deviceSpec: DeviceSpec,
+ deviceSpecUnfolded: DeviceSpec,
+ deviceSpecFolded: DeviceSpec,
isLandscape: Boolean = false,
- isGestureMode: Boolean = true
+ isGestureMode: Boolean = true,
+ isFolded: Boolean = false
) {
- val (tabletNaturalX, tabletNaturalY) = deviceTabletSpec.naturalSize
- val tabletWindowsBounds =
- tabletWindowsBounds(deviceTabletSpec, tabletNaturalX, tabletNaturalY)
- val tabletDisplayInfo =
+ val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
+ val unfoldedWindowsBounds =
+ tabletWindowsBounds(deviceSpecUnfolded, unfoldedNaturalX, unfoldedNaturalY)
+ val unfoldedDisplayInfo =
CachedDisplayInfo(
- Point(tabletNaturalX, tabletNaturalY),
+ Point(unfoldedNaturalX, unfoldedNaturalY),
Surface.ROTATION_0,
Rect(0, 0, 0, 0)
)
- val (phoneNaturalX, phoneNaturalY) = deviceSpec.naturalSize
- val phoneWindowsBounds =
- phoneWindowsBounds(deviceSpec, isGestureMode, phoneNaturalX, phoneNaturalY)
- val phoneDisplayInfo =
+ val (foldedNaturalX, foldedNaturalY) = deviceSpecFolded.naturalSize
+ val foldedWindowsBounds =
+ phoneWindowsBounds(deviceSpecFolded, isGestureMode, foldedNaturalX, foldedNaturalY)
+ val foldedDisplayInfo =
CachedDisplayInfo(
- Point(phoneNaturalX, phoneNaturalY),
+ Point(foldedNaturalX, foldedNaturalY),
Surface.ROTATION_0,
Rect(0, 0, 0, 0)
)
val perDisplayBoundsCache =
- mapOf(tabletDisplayInfo to tabletWindowsBounds, phoneDisplayInfo to phoneWindowsBounds)
+ mapOf(
+ unfoldedDisplayInfo to unfoldedWindowsBounds,
+ foldedDisplayInfo to foldedWindowsBounds
+ )
- initializeCommonVars(
- perDisplayBoundsCache,
- tabletDisplayInfo,
- rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
- isGestureMode,
- densityDpi = deviceTabletSpec.densityDpi
- )
+ if (isFolded) {
+ initializeCommonVars(
+ perDisplayBoundsCache = perDisplayBoundsCache,
+ displayInfo = foldedDisplayInfo,
+ rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
+ isGestureMode = isGestureMode,
+ densityDpi = deviceSpecFolded.densityDpi
+ )
+ } else {
+ initializeCommonVars(
+ perDisplayBoundsCache = perDisplayBoundsCache,
+ displayInfo = unfoldedDisplayInfo,
+ rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
+ isGestureMode = isGestureMode,
+ densityDpi = deviceSpecUnfolded.densityDpi
+ )
+ }
}
private fun phoneWindowsBounds(
@@ -306,4 +321,12 @@
private fun writeToDevice(context: Context, fileName: String, content: String) {
File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content)
}
+
+ protected fun Float.dpToPx(): Float {
+ return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat()
+ }
+
+ protected fun Int.dpToPx(): Int {
+ return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
+ }
}
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
index 28899d9..ff667e6 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
@@ -141,9 +141,14 @@
return this.mType == CellType.IGNORE;
}
+ boolean contains(int x, int y) {
+ return mBounds.contains(x, y);
+ }
+
@Override
public String toString() {
- return "WidgetRect type = " + mType + " bounds = " + mBounds.toString();
+ return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY()
+ + " xs = " + getSpanX() + " ys = " + getSpanY();
}
}
@@ -227,6 +232,17 @@
}
}
+ public boolean pointInsideRect(int x, int y, WidgetRect rect) {
+ Boolean isXInRect = x >= rect.getCellX() && x < rect.getCellX() + rect.getSpanX();
+ Boolean isYInRect = y >= rect.getCellY() && y < rect.getCellY() + rect.getSpanY();
+ return isXInRect && isYInRect;
+ }
+
+ public WidgetRect getWidgetAt(int x, int y) {
+ return mWidgetsRects.stream()
+ .filter(widgetRect -> pointInsideRect(x, y, widgetRect)).findFirst().orElse(null);
+ }
+
public List<WidgetRect> getWidgets() {
return mWidgetsRects;
}
@@ -443,6 +459,17 @@
return null;
}
+ public static WidgetRect getWidgetIn(List<CellLayoutBoard> boards, int x, int y) {
+ for (CellLayoutBoard board : boards) {
+ WidgetRect main = board.getWidgetAt(x, y);
+ if (main != null) {
+ return main;
+ }
+ x -= board.mWidth;
+ }
+ return null;
+ }
+
public static CellLayoutBoard boardFromString(String boardStr) {
String[] lines = boardStr.split("\n");
CellLayoutBoard board = new CellLayoutBoard();
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
index 0d2f252..b6c55af 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
@@ -42,8 +42,8 @@
params.getCellX(), params.getCellY(),
launcher.getWorkspace().getIdForScreen(cellLayout), CONTAINER_DESKTOP);
int screenId = pos.screenId;
- if (screenId >= boards.size() - 1) {
- boards.add(new CellLayoutBoard());
+ if (screenId > boards.size() - 1) {
+ boards.add(new CellLayoutBoard(cellLayout.getCountX(), cellLayout.getCountY()));
}
CellLayoutBoard board = boards.get(screenId);
// is icon
@@ -51,7 +51,7 @@
board.addIcon(pos.cellX, pos.cellY);
} else {
// is widget
- board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
+ board.addWidget(pos.cellX, pos.cellY, params.cellHSpan,
params.cellVSpan);
}
}
diff --git a/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java b/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java
deleted file mode 100644
index 706c1a7..0000000
--- a/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.celllayout.testcases;
-
-import android.graphics.Point;
-
-import com.android.launcher3.celllayout.ReorderTestCase;
-
-import java.util.Map;
-
-/**
- * The grids represent the workspace to be build by TestWorkspaceBuilder, to see what each character
- * in the board mean refer to {@code CellType}
- */
-public class MultipleCellLayoutsSimpleReorder {
-
- /** 5x5 Test
- **/
- private static final String START_BOARD_STR_5x5 = ""
- + "xxxxx|-----\n"
- + "--mm-|-----\n"
- + "--mm-|-----\n"
- + "-----|-----\n"
- + "-----|-----";
- private static final Point MOVE_TO_5x5 = new Point(8, 3);
- private static final String END_BOARD_STR_5x5 = ""
- + "xxxxx|-----\n"
- + "-----|-----\n"
- + "-----|-----\n"
- + "-----|---mm\n"
- + "-----|---mm";
- private static final ReorderTestCase TEST_CASE_5x5 = new ReorderTestCase(START_BOARD_STR_5x5,
- MOVE_TO_5x5,
- END_BOARD_STR_5x5);
-
- /** 4x4 Test
- **/
- private static final String START_BOARD_STR_4x4 = ""
- + "xxxx|----\n"
- + "--mm|----\n"
- + "--mm|----\n"
- + "----|----";
- private static final Point MOVE_TO_4x4 = new Point(5, 3);
- private static final String END_BOARD_STR_4x4 = ""
- + "xxxx|----\n"
- + "----|----\n"
- + "----|-mm-\n"
- + "----|-mm-";
- private static final ReorderTestCase TEST_CASE_4x4 = new ReorderTestCase(START_BOARD_STR_4x4,
- MOVE_TO_4x4,
- END_BOARD_STR_4x4);
-
-
- /** 6x5 Test
- **/
- private static final String START_BOARD_STR_6x5 = ""
- + "xxxxxx|------\n"
- + "--m---|------\n"
- + "------|------\n"
- + "------|------\n"
- + "------|------";
- private static final Point MOVE_TO_6x5 = new Point(10, 4);
- private static final String END_BOARD_STR_6x5 = ""
- + "xxxxxx|------\n"
- + "------|------\n"
- + "------|------\n"
- + "------|------\n"
- + "------|----m-";
- private static final ReorderTestCase TEST_CASE_6x5 = new ReorderTestCase(START_BOARD_STR_6x5,
- MOVE_TO_6x5,
- END_BOARD_STR_6x5);
-
- public static final Map<Point, ReorderTestCase> TEST_BY_GRID_SIZE =
- Map.of(new Point(5, 5), TEST_CASE_5x5,
- new Point(4, 4), TEST_CASE_4x4,
- new Point(6, 5), TEST_CASE_6x5);
-}
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
index 7ec78bb..00d7ce6 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
@@ -18,20 +18,24 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import android.content.ContentResolver;
+import android.content.ContentValues;
import android.graphics.Point;
+import android.net.Uri;
import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.celllayout.testcases.MultipleCellLayoutsSimpleReorder;
+import com.android.launcher3.MultipageCellLayout;
import com.android.launcher3.tapl.Widget;
import com.android.launcher3.tapl.WidgetResizeFrame;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.launcher3.util.rule.ShellCommandRule;
+import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
@@ -44,7 +48,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ExecutionException;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -55,6 +58,8 @@
private static final String TAG = ReorderWidgets.class.getSimpleName();
+ private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
+
TestWorkspaceBuilder mWorkspaceBuilder;
@Before
@@ -101,8 +106,48 @@
return getFromLauncher(CellLayoutTestUtils::workspaceToBoards);
}
- private void runTestCase(ReorderTestCase testCase)
- throws ExecutionException, InterruptedException {
+ private CellLayoutBoard.WidgetRect getWidgetClosestTo(Point point) {
+ ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
+ int maxDistance = 9999;
+ CellLayoutBoard.WidgetRect bestRect = null;
+ for (int i = 0; i < workspaceBoards.get(0).getWidgets().size(); i++) {
+ CellLayoutBoard.WidgetRect widget = workspaceBoards.get(0).getWidgets().get(i);
+ if (widget.getCellX() == 0 && widget.getCellY() == 0) {
+ continue;
+ }
+ int distance = Math.abs(point.x - widget.getCellX())
+ + Math.abs(point.y - widget.getCellY());
+ if (distance == 0) {
+ break;
+ }
+ if (distance < maxDistance) {
+ maxDistance = distance;
+ bestRect = widget;
+ }
+ }
+ return bestRect;
+ }
+
+ /**
+ * This function might be odd, its function is to select a widget and leave it in its place.
+ * The idea is to make the test broader and also test after a widgets resized because the
+ * underlying code does different things in that case
+ */
+ private void triggerWidgetResize(ReorderTestCase testCase) {
+ CellLayoutBoard.WidgetRect widgetRect = getWidgetClosestTo(testCase.moveMainTo);
+ if (widgetRect == null) {
+ // Some test doesn't have a widget in the final position, in those cases we will ignore
+ // them
+ return;
+ }
+ Widget widget = mLauncher.getWorkspace().getWidgetAtCell(widgetRect.getCellX(),
+ widgetRect.getCellY());
+ WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(widgetRect.getCellX(),
+ widgetRect.getCellY(), widgetRect.getSpanX(), widgetRect.getSpanY());
+ resizeFrame.dismiss();
+ }
+
+ private void runTestCase(ReorderTestCase testCase) {
CellLayoutBoard.WidgetRect mainWidgetCellPos = CellLayoutBoard.getMainFromList(
testCase.mStart);
@@ -115,6 +160,9 @@
// waitForLauncherCondition to wait for that condition, otherwise the condition would
// always be true and it wouldn't wait for the changes to be applied.
waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
+
+ triggerWidgetResize(testCase);
+
Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(),
mainWidgetCellPos.getCellY());
assertNotNull(widget);
@@ -136,41 +184,91 @@
*
* @param testCaseMap map containing all the tests per grid size (Point)
*/
- private void runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName)
- throws ExecutionException, InterruptedException {
+ private boolean runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName) {
Point iconGridDimensions = mLauncher.getWorkspace().getIconGridDimensions();
Log.d(TAG, "Running test " + testName + " for grid " + iconGridDimensions);
- Assume.assumeTrue(
- "The test " + testName + " doesn't support " + iconGridDimensions + " grid layout",
- testCaseMap.containsKey(iconGridDimensions));
+ if (!testCaseMap.containsKey(iconGridDimensions)) {
+ Log.d(TAG, "The test " + testName + " doesn't support " + iconGridDimensions
+ + " grid layout");
+ return false;
+ }
runTestCase(testCaseMap.get(iconGridDimensions));
+
+ return true;
+ }
+
+ private void runTestCaseMapForAllGrids(Map<Point, ReorderTestCase> testCaseMap,
+ String testName) {
+ boolean runAtLeastOnce = false;
+ for (String grid : FOLDABLE_GRIDS) {
+ applyGridOption(grid);
+ mLauncher.waitForLauncherInitialized();
+ runAtLeastOnce |= runTestCaseMap(testCaseMap, testName);
+ }
+ Assume.assumeTrue("None of the grids are supported", runAtLeastOnce);
+ }
+
+ private void applyGridOption(Object argValue) {
+ String testProviderAuthority = mTargetContext.getPackageName() + ".grid_control";
+ Uri gridUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(testProviderAuthority)
+ .appendPath("default_grid")
+ .build();
+ ContentValues values = new ContentValues();
+ values.putObject("name", argValue);
+ Assert.assertEquals(1,
+ mTargetContext.getContentResolver().update(gridUri, values, null, null));
}
@Test
public void simpleReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"), "push_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"),
+ "push_reorder_case");
}
@Test
public void pushTest() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"), "push_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"),
+ "push_reorder_case");
}
@Test
public void fullReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"), "full_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"),
+ "full_reorder_case");
}
@Test
public void moveOutReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"), "move_out_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"),
+ "move_out_reorder_case");
}
@Test
- public void multipleCellLayoutsSimpleReorder() throws ExecutionException, InterruptedException {
- Assume.assumeTrue("Test doesn't support foldables", !mLauncher.isTwoPanels());
- runTestCaseMap(MultipleCellLayoutsSimpleReorder.TEST_BY_GRID_SIZE,
- MultipleCellLayoutsSimpleReorder.class.getSimpleName());
+ public void multipleCellLayoutsSimpleReorder() throws Exception {
+ Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+ l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+ runTestCaseMapForAllGrids(getTestMap("ReorderWidgets/multiple_cell_layouts_simple_reorder"),
+ "multiple_cell_layouts_simple_reorder");
+ }
+
+ @Test
+ public void multipleCellLayoutsNoSpaceReorder() throws Exception {
+ Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+ l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+ runTestCaseMapForAllGrids(
+ getTestMap("ReorderWidgets/multiple_cell_layouts_no_space_reorder"),
+ "multiple_cell_layouts_no_space_reorder");
+ }
+
+ @Test
+ public void multipleCellLayoutsReorderToOtherSide() throws Exception {
+ Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+ l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+ runTestCaseMapForAllGrids(
+ getTestMap("ReorderWidgets/multiple_cell_layouts_reorder_other_side"),
+ "multiple_cell_layouts_reorder_other_side");
}
private void addTestCase(Iterator<CellLayoutTestCaseReader.TestSection> sections,
@@ -183,7 +281,7 @@
((CellLayoutTestCaseReader.Board) sections.next());
Point moveTo = new Point(Integer.parseInt(point.arguments[0]),
Integer.parseInt(point.arguments[1]));
- testCaseMap.put(startBoard.gridSize,
+ testCaseMap.put(endBoard.gridSize,
new ReorderTestCase(startBoard.board, moveTo, endBoard.board));
}
diff --git a/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java b/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java
index 398bd82..6489bc1 100644
--- a/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java
+++ b/tests/src/com/android/launcher3/celllayout/TestWorkspaceBuilder.java
@@ -16,6 +16,7 @@
package com.android.launcher3.celllayout;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.ui.TestViewHelpers.findWidgetProvider;
import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
@@ -43,9 +44,9 @@
public class TestWorkspaceBuilder {
private static final String TAG = "CellLayoutBoardBuilder";
- private static final ComponentName APP_COMPONENT_NAME = new ComponentName(
+ private static final String TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests.";
+ private ComponentName mAppComponentName = new ComponentName(
"com.google.android.calculator", "com.android.calculator2.Calculator");
-
private UserHandle mMyUser;
private Context mContext;
@@ -80,8 +81,20 @@
}
private AppInfo getApp() {
- return new AppInfo(APP_COMPONENT_NAME, "test icon", mMyUser,
- AppInfo.makeLaunchIntent(APP_COMPONENT_NAME));
+ return new AppInfo(mAppComponentName, "test icon", mMyUser,
+ AppInfo.makeLaunchIntent(mAppComponentName));
+ }
+
+ /**
+ * Helper to set the app to use for the test workspace,
+ * using activity-alias from AndroidManifest-common.
+ * @param testAppName the android:name field of the test app activity-alias to use
+ */
+ public void setTestAppActivityAlias(String testAppName) {
+ this.mAppComponentName = new ComponentName(
+ getInstrumentation().getContext().getPackageName(),
+ TEST_ACTIVITY_PACKAGE_PREFIX + testAppName
+ );
}
private void addCorrespondingWidgetRect(CellLayoutBoard.WidgetRect widgetRect,
diff --git a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
index 77ea5ba..cd95e99 100644
--- a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
@@ -41,9 +41,9 @@
@Test
fun parseValidFile() {
val allAppsSpecs =
- AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
- assertThat(allAppsSpecs.allAppsHeightSpecList.size).isEqualTo(1)
- assertThat(allAppsSpecs.allAppsHeightSpecList[0].toString())
+ AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+ assertThat(allAppsSpecs.heightSpecs.size).isEqualTo(1)
+ assertThat(allAppsSpecs.heightSpecs[0].toString())
.isEqualTo(
"AllAppsSpec(" +
"maxAvailableSize=26247, " +
@@ -71,8 +71,8 @@
")"
)
- assertThat(allAppsSpecs.allAppsWidthSpecList.size).isEqualTo(1)
- assertThat(allAppsSpecs.allAppsWidthSpecList[0].toString())
+ assertThat(allAppsSpecs.widthSpecs.size).isEqualTo(1)
+ assertThat(allAppsSpecs.widthSpecs[0].toString())
.isEqualTo(
"AllAppsSpec(" +
"maxAvailableSize=26247, " +
@@ -103,16 +103,16 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
- AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_1))
+ AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_1))
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
- AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_2))
+ AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_2))
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
- AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_3))
+ AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_3))
}
}
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
index 9f981fa..0f12e58 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
@@ -23,7 +23,6 @@
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.WorkspaceSpecs
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,12 +48,12 @@
val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
val workspaceSpecs =
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+ WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
val allAppsSpecs =
- AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+ AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
with(allAppsSpecs.getCalculatedWidthSpec(4, availableWidth, widthSpec)) {
assertThat(availableSpace).isEqualTo(availableWidth)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
index c14722c..f2a269a 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
@@ -21,10 +21,8 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
-import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.WorkspaceSpecs
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -48,11 +46,11 @@
// Loading workspace specs
val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
- val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+ val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
// Loading folders specs
val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
- val folderSpecs = FolderSpecs(resourceHelperFolder)
+ val folderSpecs = FolderSpecs.create(resourceHelperFolder)
assertThat(folderSpecs.widthSpecs.size).isEqualTo(2)
assertThat(folderSpecs.widthSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
@@ -62,7 +60,7 @@
var availableWidth = deviceSpec.naturalSize.first
var calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
var calculatedWidthFolderSpec =
- folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+ folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
with(calculatedWidthFolderSpec) {
assertThat(availableSpace).isEqualTo(availableWidth)
assertThat(cells).isEqualTo(columns)
@@ -76,7 +74,7 @@
availableWidth = 2000.dpToPx()
calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
calculatedWidthFolderSpec =
- folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+ folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
with(calculatedWidthFolderSpec) {
assertThat(availableSpace).isEqualTo(availableWidth)
assertThat(cells).isEqualTo(columns)
@@ -97,11 +95,11 @@
// Loading workspace specs
val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
- val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+ val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
// Loading folders specs
val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
- val folderSpecs = FolderSpecs(resourceHelperFolder)
+ val folderSpecs = FolderSpecs.create(resourceHelperFolder)
assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
assertThat(folderSpecs.heightSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
@@ -109,7 +107,7 @@
// Validate height spec
val calculatedWorkspace = workspaceSpecs.getCalculatedHeightSpec(rows, availableHeight)
val calculatedFolderSpec =
- folderSpecs.getHeightSpec(rows, availableHeight, calculatedWorkspace)
+ folderSpecs.getCalculatedHeightSpec(rows, availableHeight, calculatedWorkspace)
with(calculatedFolderSpec) {
assertThat(availableSpace).isEqualTo(availableHeight)
assertThat(cells).isEqualTo(rows)
@@ -119,8 +117,4 @@
assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
}
}
-
- private fun Int.dpToPx(): Int {
- return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
- }
}
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
new file mode 100644
index 0000000..0ecf7ba
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CalculatedHotseatSpecTest : AbstractDeviceProfileTest() {
+ override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+ /**
+ * This test tests:
+ * - (height spec) gets the correct breakpoint from the XML - skips the first breakpoint
+ */
+ @Test
+ fun normalPhone_returnsSecondBreakpointSpec() {
+ val deviceSpec = deviceSpecs["phone"]!!
+ initializeVarsForPhone(deviceSpec)
+
+ // Hotseat uses the whole device height
+ val availableHeight = deviceSpec.naturalSize.second
+
+ val hotseatSpecs =
+ HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
+ val heightSpec = hotseatSpecs.getCalculatedHeightSpec(availableHeight)
+
+ assertThat(heightSpec.availableSpace).isEqualTo(availableHeight)
+ assertThat(heightSpec.hotseatQsbSpace).isEqualTo(95)
+ }
+
+ /**
+ * This test tests:
+ * - (height spec) gets the correct breakpoint from the XML - use the first breakpoint
+ */
+ @Test
+ fun smallPhone_returnsFirstBreakpointSpec() {
+ val deviceSpec = deviceSpecs["phone"]!!
+ deviceSpec.densityDpi = 540 // larger display size
+ initializeVarsForPhone(deviceSpec)
+
+ // Hotseat uses the whole device height
+ val availableHeight = deviceSpec.naturalSize.second
+
+ val hotseatSpecs =
+ HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
+ val heightSpec = hotseatSpecs.getCalculatedHeightSpec(availableHeight)
+
+ assertThat(heightSpec.availableSpace).isEqualTo(availableHeight)
+ assertThat(heightSpec.hotseatQsbSpace).isEqualTo(81)
+ }
+}
diff --git a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
rename to tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
index 7f03ba2..0af694e 100644
--- a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.launcher3.workspace
+package com.android.launcher3.responsive
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -49,7 +49,7 @@
val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
val workspaceSpecs =
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+ WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
@@ -86,7 +86,7 @@
val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 640
val workspaceSpecs =
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+ WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
index 796bf9a..4b05949 100644
--- a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
@@ -21,11 +21,9 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
-import com.android.launcher3.testing.shared.ResourceUtils
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import com.android.launcher3.workspace.WorkspaceSpec
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -44,14 +42,14 @@
@Test
fun parseValidFile() {
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
- val folderSpecs = FolderSpecs(resourceHelper)
+ val folderSpecs = FolderSpecs.create(resourceHelper)
val sizeSpec16 = SizeSpec(16f.dpToPx())
val widthSpecsExpected =
listOf(
FolderSpec(
maxAvailableSize = 800.dpToPx(),
- specType = FolderSpec.SpecType.WIDTH,
+ specType = SpecType.WIDTH,
startPadding = sizeSpec16,
endPadding = sizeSpec16,
gutter = sizeSpec16,
@@ -59,7 +57,7 @@
),
FolderSpec(
maxAvailableSize = 9999.dpToPx(),
- specType = FolderSpec.SpecType.WIDTH,
+ specType = SpecType.WIDTH,
startPadding = sizeSpec16,
endPadding = sizeSpec16,
gutter = sizeSpec16,
@@ -70,7 +68,7 @@
val heightSpecsExpected =
FolderSpec(
maxAvailableSize = 9999.dpToPx(),
- specType = FolderSpec.SpecType.HEIGHT,
+ specType = SpecType.HEIGHT,
startPadding = SizeSpec(24f.dpToPx()),
endPadding = SizeSpec(64f.dpToPx()),
gutter = sizeSpec16,
@@ -88,25 +86,25 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_1)
- FolderSpecs(resourceHelper)
+ FolderSpecs.create(resourceHelper)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_2)
- FolderSpecs(resourceHelper)
+ FolderSpecs.create(resourceHelper)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_3)
- FolderSpecs(resourceHelper)
+ FolderSpecs.create(resourceHelper)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingSpecs_throwsError() {
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_4)
- FolderSpecs(resourceHelper)
+ FolderSpecs.create(resourceHelper)
}
@Test(expected = IllegalStateException::class)
@@ -117,7 +115,7 @@
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
- specType = WorkspaceSpec.SpecType.WIDTH,
+ specType = SpecType.WIDTH,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
@@ -126,8 +124,8 @@
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
- val folderSpecs = FolderSpecs(resourceHelper)
- folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+ val folderSpecs = FolderSpecs.create(resourceHelper)
+ folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
@Test(expected = IllegalStateException::class)
@@ -138,7 +136,7 @@
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
- specType = WorkspaceSpec.SpecType.HEIGHT,
+ specType = SpecType.HEIGHT,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
@@ -147,8 +145,8 @@
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
- val folderSpecs = FolderSpecs(resourceHelper)
- folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+ val folderSpecs = FolderSpecs.create(resourceHelper)
+ folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
@Test
@@ -159,7 +157,7 @@
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
- specType = WorkspaceSpec.SpecType.WIDTH,
+ specType = SpecType.WIDTH,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
@@ -167,21 +165,17 @@
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
- val expectedResult =
- CalculatedFolderSpec(
- startPaddingPx = 16.dpToPx(),
- endPaddingPx = 16.dpToPx(),
- gutterPx = 16.dpToPx(),
- cellSizePx = calculatedWorkspaceSpec.cellSizePx,
- availableSpace = availableSpace,
- cells = cells
- )
-
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
- val folderSpecs = FolderSpecs(resourceHelper)
+ val folderSpecs = FolderSpecs.create(resourceHelper)
val calculatedWidthSpec =
- folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
- assertThat(calculatedWidthSpec).isEqualTo(expectedResult)
+ folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+ assertThat(calculatedWidthSpec.cells).isEqualTo(cells)
+ assertThat(calculatedWidthSpec.availableSpace).isEqualTo(availableSpace)
+ assertThat(calculatedWidthSpec.startPaddingPx).isEqualTo(16.dpToPx())
+ assertThat(calculatedWidthSpec.endPaddingPx).isEqualTo(16.dpToPx())
+ assertThat(calculatedWidthSpec.gutterPx).isEqualTo(16.dpToPx())
+ assertThat(calculatedWidthSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
}
@Test(expected = IllegalStateException::class)
@@ -192,7 +186,7 @@
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
- specType = WorkspaceSpec.SpecType.HEIGHT,
+ specType = SpecType.HEIGHT,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
@@ -201,8 +195,8 @@
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
- val folderSpecs = FolderSpecs(resourceHelper)
- folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+ val folderSpecs = FolderSpecs.create(resourceHelper)
+ folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
@Test
@@ -213,7 +207,7 @@
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
- specType = WorkspaceSpec.SpecType.HEIGHT,
+ specType = SpecType.HEIGHT,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
@@ -221,21 +215,17 @@
)
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
- val expectedResult =
- CalculatedFolderSpec(
- startPaddingPx = 24.dpToPx(),
- endPaddingPx = 64.dpToPx(),
- gutterPx = 16.dpToPx(),
- cellSizePx = calculatedWorkspaceSpec.cellSizePx,
- availableSpace = availableSpace,
- cells = cells
- )
-
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
- val folderSpecs = FolderSpecs(resourceHelper)
+ val folderSpecs = FolderSpecs.create(resourceHelper)
val calculatedHeightSpec =
- folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
- assertThat(calculatedHeightSpec).isEqualTo(expectedResult)
+ folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+ assertThat(calculatedHeightSpec.cells).isEqualTo(cells)
+ assertThat(calculatedHeightSpec.availableSpace).isEqualTo(availableSpace)
+ assertThat(calculatedHeightSpec.startPaddingPx).isEqualTo(24.dpToPx())
+ assertThat(calculatedHeightSpec.endPaddingPx).isEqualTo(64.dpToPx())
+ assertThat(calculatedHeightSpec.gutterPx).isEqualTo(16.dpToPx())
+ assertThat(calculatedHeightSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
}
@Test(expected = IllegalStateException::class)
@@ -246,7 +236,7 @@
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize = availableSpace,
- specType = WorkspaceSpec.SpecType.WIDTH,
+ specType = SpecType.WIDTH,
startPadding = SizeSpec(fixedSize = 10f),
endPadding = SizeSpec(fixedSize = 10f),
gutter = SizeSpec(fixedSize = 10f),
@@ -255,15 +245,7 @@
val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
- val folderSpecs = FolderSpecs(resourceHelper)
- folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
- }
-
- private fun Float.dpToPx(): Float {
- return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat()
- }
-
- private fun Int.dpToPx(): Int {
- return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
+ val folderSpecs = FolderSpecs.create(resourceHelper)
+ folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
}
}
diff --git a/tests/src/com/android/launcher3/responsive/HotseatSpecsTest.kt b/tests/src/com/android/launcher3/responsive/HotseatSpecsTest.kt
new file mode 100644
index 0000000..c764e47
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/HotseatSpecsTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.android.systemui.util.dpToPx
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HotseatSpecsTest : AbstractDeviceProfileTest() {
+ override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+ @Before
+ fun setup() {
+ initializeVarsForPhone(deviceSpecs["phone"]!!)
+ }
+
+ @Test
+ fun parseValidFile() {
+ val hotseatSpecs =
+ HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
+ assertThat(hotseatSpecs.specs.size).isEqualTo(2)
+
+ val expectedSpecs =
+ listOf(
+ HotseatSpec(
+ maxAvailableSize = 847.dpToPx(),
+ specType = ResponsiveSpec.SpecType.HEIGHT,
+ hotseatQsbSpace = SizeSpec(24f.dpToPx())
+ ),
+ HotseatSpec(
+ maxAvailableSize = 9999.dpToPx(),
+ specType = ResponsiveSpec.SpecType.HEIGHT,
+ hotseatQsbSpace = SizeSpec(36f.dpToPx())
+ ),
+ )
+
+ assertThat(hotseatSpecs.specs.size).isEqualTo(expectedSpecs.size)
+ assertThat(hotseatSpecs.specs[0]).isEqualTo(expectedSpecs[0])
+ assertThat(hotseatSpecs.specs[1]).isEqualTo(expectedSpecs[1])
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun parseInvalidFile_spaceIsNotFixedSize_throwsError() {
+ HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_hotseat_file_case_1))
+ }
+}
diff --git a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
index 088cae1..8ca07c6 100644
--- a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
@@ -139,4 +139,20 @@
assertThat(instance.isValid()).isEqualTo(false)
}
}
+
+ @Test
+ fun onlyFixedSize() {
+ assertThat(SizeSpec(fixedSize = 16f).onlyFixedSize()).isEqualTo(true)
+
+ val combinations =
+ listOf(
+ SizeSpec(0f, 1.1f, 0f, false),
+ SizeSpec(0f, 0f, 1.1f, false),
+ SizeSpec(0f, 0f, 0f, true)
+ )
+
+ for (instance in combinations) {
+ assertThat(instance.onlyFixedSize()).isEqualTo(false)
+ }
+ }
}
diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
similarity index 86%
rename from tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
rename to tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
index 8b99a3a..0364069 100644
--- a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.launcher3.workspace
+package com.android.launcher3.responsive
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -41,9 +41,9 @@
@Test
fun parseValidFile() {
val workspaceSpecs =
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
- assertThat(workspaceSpecs.workspaceHeightSpecList.size).isEqualTo(3)
- assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString())
+ WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+ assertThat(workspaceSpecs.heightSpecs.size).isEqualTo(3)
+ assertThat(workspaceSpecs.heightSpecs[0].toString())
.isEqualTo(
"WorkspaceSpec(" +
"maxAvailableSize=1533, " +
@@ -70,7 +70,7 @@
"maxSize=2147483647)" +
")"
)
- assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
+ assertThat(workspaceSpecs.heightSpecs[1].toString())
.isEqualTo(
"WorkspaceSpec(" +
"maxAvailableSize=1607, " +
@@ -97,7 +97,7 @@
"maxSize=2147483647)" +
")"
)
- assertThat(workspaceSpecs.workspaceHeightSpecList[2].toString())
+ assertThat(workspaceSpecs.heightSpecs[2].toString())
.isEqualTo(
"WorkspaceSpec(" +
"maxAvailableSize=26247, " +
@@ -124,8 +124,8 @@
"maxSize=2147483647)" +
")"
)
- assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
- assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString())
+ assertThat(workspaceSpecs.widthSpecs.size).isEqualTo(1)
+ assertThat(workspaceSpecs.widthSpecs[0].toString())
.isEqualTo(
"WorkspaceSpec(" +
"maxAvailableSize=26247, " +
@@ -156,21 +156,29 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1))
+ WorkspaceSpecs.create(
+ TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1)
+ )
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2))
+ WorkspaceSpecs.create(
+ TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2)
+ )
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3))
+ WorkspaceSpecs.create(
+ TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3)
+ )
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_matchWorkspace_true_throwsError() {
- WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4))
+ WorkspaceSpecs.create(
+ TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4)
+ )
}
}
diff --git a/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java b/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
new file mode 100644
index 0000000..15db1d8
--- /dev/null
+++ b/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class TaplUtilityTests {
+
+ @Test
+ public void testNewStringWithRegex() {
+ assertTrue(AppIcon.makeMultilinePattern("Play Store")
+ .matcher("Play Store has 7 notifications").matches());
+ assertTrue(AppIcon.makeMultilinePattern("Play Store")
+ .matcher("Play Store!").matches());
+ assertFalse(AppIcon.makeMultilinePattern("Play Store")
+ .matcher("play store").matches());
+ assertFalse(AppIcon.makeMultilinePattern("Play Store")
+ .matcher("").matches());
+ assertTrue(AppIcon.makeMultilinePattern("Play Store")
+ .matcher("Play \n Store").matches());
+ }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
index d3ce67c..81a59b9 100644
--- a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
+++ b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
@@ -69,7 +69,10 @@
mView.setBackgroundColor(Color.BLUE);
setContentView(mView);
- registerReceiver(mCommandReceiver, new IntentFilter(mAction + SUFFIX_COMMAND));
+ registerReceiver(
+ mCommandReceiver,
+ new IntentFilter(mAction + SUFFIX_COMMAND),
+ RECEIVER_EXPORTED);
}
protected void addButton(String title, String method) {
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index d22a353..5240e6a 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -100,7 +100,7 @@
private static boolean sDumpWasGenerated = false;
private static boolean sActivityLeakReported = false;
- private static boolean sSeenKeygard = false;
+ private static boolean sSeenKeyguard = false;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
@@ -261,9 +261,9 @@
}
private static void verifyKeyguardInvisible() {
- final boolean keyguardAlreadyVisible = sSeenKeygard;
+ final boolean keyguardAlreadyVisible = sSeenKeyguard;
- sSeenKeygard = sSeenKeygard
+ sSeenKeyguard = sSeenKeyguard
|| !TestHelpers.wait(
Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
@@ -271,7 +271,7 @@
"Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
+ " for the first time = "
+ !keyguardAlreadyVisible,
- sSeenKeygard);
+ sSeenKeyguard);
}
@After
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 0d63a68..753d89d 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -318,6 +318,7 @@
}
@Test
+ @Ignore // b/293191790
@PortraitLandscape
public void testWidgets() throws Exception {
// Test opening widgets.
@@ -683,8 +684,8 @@
HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
allApps.freeze();
try {
- HomeAppIcon icon = allApps.getAppIcon(APP_NAME);
- assertEquals("Wrong app icon name.", icon.getIconName(), APP_NAME);
+ // getAppIcon() already verifies that the icon is not null and is the correct icon name.
+ allApps.getAppIcon(APP_NAME);
} finally {
allApps.unfreeze();
}
diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
index 7237387..5b9adcd 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -85,7 +85,7 @@
waitForStateTransitionToEnd("Launcher internal state didn't switch to Normal",
() -> NORMAL);
waitForResumed("Launcher internal state is still Background");
- executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+ mLauncher.getWorkspace().switchToAllApps();
waitForStateTransitionToEnd("Launcher internal state didn't switch to All Apps",
() -> ALL_APPS);
}
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index 7ba0b53..8e5e9cc 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -147,9 +147,8 @@
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
viewQueue.add(parent.getChildAt(i));
}
- } else if (view instanceof BubbleTextView) {
- BubbleTextView btv = (BubbleTextView) view;
- if (title.equals(btv.getText())) {
+ } else if (view instanceof BubbleTextView btv) {
+ if (title.equals(btv.getContentDescription().toString())) {
icon = btv;
break;
}
diff --git a/tests/src/com/android/launcher3/util/rule/LazyActivityRule.java b/tests/src/com/android/launcher3/util/rule/LazyActivityRule.java
deleted file mode 100644
index 6c300bb..0000000
--- a/tests/src/com/android/launcher3/util/rule/LazyActivityRule.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.util.rule;
-
-import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-
-import androidx.annotation.Nullable;
-import androidx.test.core.app.ActivityScenario;
-
-import com.android.launcher3.Launcher;
-
-import org.junit.rules.ExternalResource;
-
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-/**
- * Similar to {@code ActivityScenarioRule} but it creates the activity lazily when needed
- */
-public class LazyActivityRule<A extends Activity> extends ExternalResource {
-
- private final Supplier<ActivityScenario<A>> mScenarioSupplier;
-
- @Nullable private ActivityScenario<A> mScenario;
-
- /**
- * Constructs LazyActivityScenarioRule for a given scenario provider.
- */
- public LazyActivityRule(Supplier<ActivityScenario<A>> supplier) {
- mScenarioSupplier = supplier;
- }
-
- /**
- * Resets the rule, such that the activity is in closed state
- */
- public synchronized void reset() {
- if (mScenario != null) {
- try {
- mScenario.close();
- } catch (AssertionError e) {
- // Ignore errors during close
- }
- }
- mScenario = null;
- }
-
- @Override
- protected synchronized void after() {
- reset();
- }
-
- /**
- * Returns the scenario, creating one if it doesn't exist
- */
- public synchronized ActivityScenario<A> getScenario() {
- if (mScenario == null) {
- mScenario = mScenarioSupplier.get();
- }
- return mScenario;
- }
-
- /**
- * Executes the function {@code f} on the activities main thread and returns the result
- */
- public <T> T getFromActivity(Function<A, T> f) {
- AtomicReference<T> result = new AtomicReference<>();
- getScenario().onActivity(a -> result.set(f.apply(a)));
- return result.get();
- }
-
- /**
- * Runs the provided function {@code f} on the activity if the scenario is already created
- */
- public synchronized void runOnActivity(Consumer<A> f) {
- if (mScenario != null) {
- mScenario.onActivity(f::accept);
- }
- }
-
- /**
- * Returns a {@link LazyActivityRule} for the Launcher activity
- */
- public static <T extends Launcher> LazyActivityRule<T> forLauncher() {
- Context context = getInstrumentation().getTargetContext();
- // Create the activity after the model setup is done.
- Intent homeIntent = new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK
- | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | FLAG_ACTIVITY_NO_ANIMATION);
- ResolveInfo ri = context.getPackageManager().resolveActivity(
- new Intent(homeIntent).setPackage(context.getPackageName()), 0);
- homeIntent.setComponent(ri.getComponentInfo().getComponentName());
- return new LazyActivityRule<>(() -> ActivityScenario.launch(homeIntent));
- }
-}
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index f33a50a..38de071 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -17,6 +17,8 @@
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static org.junit.Assume.assumeTrue;
+
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
@@ -69,12 +71,9 @@
return new Statement() {
@Override
public void evaluate() throws Throwable {
- if ((stability.flavors() & getRunFlavor()) != 0) {
- Log.d(TAG, "Running " + description.getDisplayName());
- base.evaluate();
- } else {
- Log.d(TAG, "Skipping " + description.getDisplayName());
- }
+ assumeTrue("Ignoring the test due to @Stability annotation",
+ (stability.flavors() & getRunFlavor()) != 0);
+ base.evaluate();
}
};
} else {
diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
index b2a2f7f..0f08eef 100644
--- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
+++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
@@ -23,9 +23,17 @@
import com.android.app.viewcapture.SimpleViewCapture
import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR
import com.android.app.viewcapture.data.ExportedData
+import com.android.launcher3.tapl.TestHelpers
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
+import com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStreamWriter
import java.util.function.Supplier
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@@ -47,20 +55,15 @@
viewCaptureData = null
val windowListenerCloseables = mutableListOf<SafeCloseable>()
- val alreadyOpenActivity = alreadyOpenActivitySupplier.get()
- if (alreadyOpenActivity != null) {
- startCapture(windowListenerCloseables, alreadyOpenActivity)
- }
+ startCapturingExistingActivity(windowListenerCloseables)
val lifecycleCallbacks =
object : ActivityLifecycleCallbacksAdapter {
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
- super.onActivityCreated(activity, bundle)
startCapture(windowListenerCloseables, activity)
}
override fun onActivityDestroyed(activity: Activity) {
- super.onActivityDestroyed(activity)
viewCapture.stopCapture(activity.window.decorView)
}
}
@@ -84,7 +87,16 @@
MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
}
- ViewCaptureAnalyzer.assertNoAnomalies(viewCaptureData)
+ analyzeViewCapture(description)
+ }
+
+ private fun startCapturingExistingActivity(
+ windowListenerCloseables: MutableCollection<SafeCloseable>
+ ) {
+ val alreadyOpenActivity = alreadyOpenActivitySupplier.get()
+ if (alreadyOpenActivity != null) {
+ startCapture(windowListenerCloseables, alreadyOpenActivity)
+ }
}
private fun startCapture(
@@ -100,4 +112,43 @@
}
}
}
+
+ private fun analyzeViewCapture(description: Description) {
+ // OOP tests don't produce ViewCapture data
+ if (!TestHelpers.isInLauncherProcess()) return
+
+ // Due to flakiness of ViewCapture verifier, don't run the check in presubmit
+ if (TestStabilityRule.getRunFlavor() != PLATFORM_POSTSUBMIT) return
+
+ var frameCount = 0
+ for (i in 0 until viewCaptureData!!.windowDataCount) {
+ frameCount += viewCaptureData!!.getWindowData(i).frameDataCount
+ }
+ assertTrue("Empty ViewCapture data", frameCount > 0)
+
+ val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData)
+ if (!anomalies.isEmpty()) {
+ val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt")
+ try {
+ OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer ->
+ writer.write("View animation anomalies detected.\r\n")
+ writer.write(
+ "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n"
+ )
+ writer.write("List of views with animation anomalies:\r\n")
+
+ for ((viewPath, message) in anomalies) {
+ writer.write("View: $viewPath\r\n $message\r\n")
+ }
+ }
+ } catch (ex: IOException) {
+ throw RuntimeException(ex)
+ }
+
+ val (viewPath, message) = anomalies.entries.first()
+ fail(
+ "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details."
+ )
+ }
+ }
}
diff --git a/tests/src/com/android/launcher3/util/rule/WrapperRule.kt b/tests/src/com/android/launcher3/util/rule/WrapperRule.kt
deleted file mode 100644
index 290cc45..0000000
--- a/tests/src/com/android/launcher3/util/rule/WrapperRule.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.launcher3.util.rule
-
-import com.android.launcher3.config.FeatureFlags.BooleanFlag
-import com.android.launcher3.config.FeatureFlags.IntFlag
-import com.android.launcher3.util.SafeCloseable
-import com.android.launcher3.util.TestUtil
-import java.util.function.Supplier
-import org.junit.rules.ExternalResource
-
-/** Simple rule which wraps any SafeCloseable object */
-class WrapperRule(private val overrideProvider: Supplier<SafeCloseable>) : ExternalResource() {
-
- private lateinit var overrideClosable: SafeCloseable
-
- override fun before() {
- overrideClosable = overrideProvider.get()
- }
-
- override fun after() {
- overrideClosable.close()
- }
-
- companion object {
-
- fun BooleanFlag.overrideFlag(value: Boolean) = WrapperRule {
- TestUtil.overrideFlag(this, value)
- }
-
- fun IntFlag.overrideFlag(value: Int) = WrapperRule { TestUtil.overrideFlag(this, value) }
- }
-}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
index 2501801..49abad4 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
@@ -15,13 +15,9 @@
*/
package com.android.launcher3.util.viewcapture_analysis;
-import static com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.diagPathFromRoot;
-
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
-import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnomalyDetector;
-import java.util.Collection;
-import java.util.Set;
+import java.util.List;
/**
* Anomaly detector that triggers an error when alpha of a view changes too rapidly.
@@ -32,9 +28,10 @@
private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
private static final String DRAG_LAYER =
CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
+ private static final String RECENTS_DRAG_LAYER =
+ CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
- // Paths of nodes that are excluded from analysis.
- private static final Collection<String> PATHS_TO_IGNORE = Set.of(
+ private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
CONTENT
+ "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
@@ -107,53 +104,101 @@
DRAG_LAYER + "Snackbar|TextView:id/label",
DRAG_LAYER + "SplitInstructionsView|AppCompatTextView:id/split_instructions_text",
DRAG_LAYER + "TaskMenuView|LinearLayout:id/menu_option_layout",
+ DRAG_LAYER + "TaskMenuViewWithArrow|LinearLayout:id/menu_option_layout",
DRAG_LAYER + "TaskMenuView|TextView:id/task_name",
DRAG_LAYER + "View",
DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
- CONTENT
- + "LauncherRootView|RecentsDragLayer:id/drag_layer|FallbackRecentsView:id"
- + "/overview_panel",
- CONTENT
- + "LauncherRootView|RecentsDragLayer:id/drag_layer|NexusOverviewActionsView"
- + ":id/overview_actions_view|LinearLayout:id/action_buttons|Button:id"
- + "/action_screenshot",
- CONTENT
- + "LauncherRootView|RecentsDragLayer:id/drag_layer|NexusOverviewActionsView"
- + ":id/overview_actions_view|LinearLayout:id/action_buttons|Button:id"
- + "/action_select"
- );
+ RECENTS_DRAG_LAYER + "ArrowTipView",
+ DRAG_LAYER + "ArrowTipView",
+ DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
+ RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
+ DRAG_LAYER
+ + "NexusOverviewActionsView:id/overview_actions_view"
+ + "|LinearLayout:id/action_buttons|Button:id/action_screenshot",
+ RECENTS_DRAG_LAYER
+ + "NexusOverviewActionsView:id/overview_actions_view"
+ + "|LinearLayout:id/action_buttons|Button:id/action_screenshot",
+ DRAG_LAYER
+ + "NexusOverviewActionsView:id/overview_actions_view"
+ + "|LinearLayout:id/action_buttons|Button:id/action_select",
+ RECENTS_DRAG_LAYER
+ + "NexusOverviewActionsView:id/overview_actions_view"
+ + "|LinearLayout:id/action_buttons|Button:id/action_select",
+ DRAG_LAYER
+ + "NexusOverviewActionsView:id/overview_actions_view"
+ + "|LinearLayout:id/action_buttons|Button:id/action_split",
+ RECENTS_DRAG_LAYER
+ + "NexusOverviewActionsView:id/overview_actions_view"
+ + "|LinearLayout:id/action_buttons|Button:id/action_split",
+ DRAG_LAYER + "IconView"
+ ));
+
// Minimal increase or decrease of view's alpha between frames that triggers the error.
private static final float ALPHA_JUMP_THRESHOLD = 1f;
- @Override
- void initializeNode(AnalysisNode info) {
- // If the parent view ignores alpha jumps, its descendants will too.
- final boolean parentIgnoreAlphaJumps = info.parent != null && info.parent.ignoreAlphaJumps;
- info.ignoreAlphaJumps = parentIgnoreAlphaJumps
- || PATHS_TO_IGNORE.contains(diagPathFromRoot(info));
+ // Per-AnalysisNode data that's specific to this detector.
+ private static class NodeData {
+ public boolean ignoreAlphaJumps;
+
+ // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
+ // ignored.
+ // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
+ // children.
+ public IgnoreNode ignoreNode;
+ }
+
+ private NodeData getNodeData(AnalysisNode info) {
+ return (NodeData) info.detectorsData[detectorOrdinal];
}
@Override
- void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) {
+ void initializeNode(AnalysisNode info) {
+ final NodeData nodeData = new NodeData();
+ info.detectorsData[detectorOrdinal] = nodeData;
+
+ // If the parent view ignores alpha jumps, its descendants will too.
+ final boolean parentIgnoresAlphaJumps = info.parent != null && getNodeData(
+ info.parent).ignoreAlphaJumps;
+ if (parentIgnoresAlphaJumps) {
+ nodeData.ignoreAlphaJumps = true;
+ return;
+ }
+
+ // Parent view doesn't ignore alpha jumps.
+ // Initialize this AnalysisNode's ignore-node with the corresponding child of the
+ // ignore-node of the parent, if present.
+ final IgnoreNode parentIgnoreNode = info.parent != null
+ ? getNodeData(info.parent).ignoreNode
+ : IGNORED_NODES_ROOT;
+ nodeData.ignoreNode = parentIgnoreNode != null
+ ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
+ // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
+ nodeData.ignoreAlphaJumps =
+ nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
+ }
+
+ @Override
+ String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long timestamp) {
// If the view was previously seen, proceed with analysis only if it was present in the
// view hierarchy in the previous frame.
- if (oldInfo != null && oldInfo.frameN != frameN) return;
+ if (oldInfo != null && oldInfo.frameN != frameN) return null;
final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
- if (latestInfo.ignoreAlphaJumps) return;
+ final NodeData nodeData = getNodeData(latestInfo);
+ if (nodeData.ignoreAlphaJumps) return null;
final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0;
final float newAlpha = newInfo != null ? newInfo.alpha : 0;
final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha);
if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) {
- throw new AssertionError(
- String.format(
- "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)"
- + ", threshold: %s, view: %s",
- alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo));
+ nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children.
+ return String.format(
+ "Alpha jump detected: alpha change: %s (%s -> %s), threshold: %s",
+ alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD);
}
+ return null;
}
}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
new file mode 100644
index 0000000..09e2f65
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util.viewcapture_analysis;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Detector of one kind of anomaly.
+ */
+abstract class AnomalyDetector {
+ // Index of this detector in ViewCaptureAnalyzer.ANOMALY_DETECTORS
+ public int detectorOrdinal;
+
+ /**
+ * Element of the tree of ignored nodes.
+ * If the "children" map is empty, then this node should be ignored, i.e. the analysis shouldn't
+ * run for it.
+ * I.e. ignored nodes correspond to the leaves in the ignored nodes tree.
+ */
+ protected static class IgnoreNode {
+ // Map from child node identities to ignore-nodes for these children.
+ public final Map<String, IgnoreNode> children = new HashMap<>();
+ }
+
+ // Converts the list of full paths of nodes to ignore to a more efficient tree of ignore-nodes.
+ protected static IgnoreNode buildIgnoreNodesTree(Iterable<String> pathsToIgnore) {
+ final IgnoreNode root = new IgnoreNode();
+ for (String pathToIgnore : pathsToIgnore) {
+ // Scan the diag path of an ignored node and add its elements into the tree.
+ IgnoreNode currentIgnoreNode = root;
+ for (String part : pathToIgnore.split("\\|")) {
+ // Ensure that the child of the node is added to the tree.
+ IgnoreNode child = currentIgnoreNode.children.get(part);
+ if (child == null) {
+ currentIgnoreNode.children.put(part, child = new IgnoreNode());
+ }
+ currentIgnoreNode = child;
+ }
+ }
+ return root;
+ }
+
+ /**
+ * Initializes fields of the node that are specific to the anomaly detected by this
+ * detector.
+ */
+ abstract void initializeNode(@NonNull ViewCaptureAnalyzer.AnalysisNode info);
+
+ /**
+ * Detects anomalies by looking at the last occurrence of a view, and the current one.
+ * null value means that the view. 'oldInfo' and 'newInfo' cannot be both null.
+ * If an anomaly is detected, an exception will be thrown.
+ *
+ * @param oldInfo the view, as seen in the last frame that contained it in the view
+ * hierarchy before 'currentFrame'. 'null' means that the view is first seen
+ * in the 'currentFrame'.
+ * @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that
+ * the view is not present in the 'currentFrame', but was present in the previous
+ * frame.
+ * @param frameN number of the current frame.
+ * @return Anomaly diagnostic message if an anomaly has been detected; null otherwise.
+ */
+ abstract String detectAnomalies(
+ @Nullable ViewCaptureAnalyzer.AnalysisNode oldInfo,
+ @Nullable ViewCaptureAnalyzer.AnalysisNode newInfo, int frameN,
+ long frameTimeNs);
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
new file mode 100644
index 0000000..eef1bc8
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util.viewcapture_analysis;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
+
+import java.util.List;
+
+/**
+ * Anomaly detector that triggers an error when a view flashes, i.e. appears or disappears for a too
+ * short period of time.
+ */
+final class FlashDetector extends AnomalyDetector {
+ // Maximum time period of a view visibility or invisibility that is recognized as a flash.
+ private static final int FLASH_DURATION_MS = 300;
+
+ // Commonly used parts of the paths to ignore.
+ private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
+ private static final String DRAG_LAYER =
+ CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
+ private static final String RECENTS_DRAG_LAYER =
+ CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
+
+ private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
+ CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
+ DRAG_LAYER
+ + "SearchContainerView:id/apps_view|AllAppsRecyclerView:id/apps_list_view"
+ + "|BubbleTextView:id/icon",
+ DRAG_LAYER + "LauncherRecentsView:id/overview_panel|TaskView|TextView",
+ DRAG_LAYER
+ + "LauncherAllAppsContainerView:id/apps_view|AllAppsRecyclerView:id"
+ + "/apps_list_view|BubbleTextView:id/icon",
+ CONTENT
+ + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ + "|WidgetCellPreview:id/widget_preview_container|WidgetImageView:id"
+ + "/widget_preview",
+ CONTENT
+ + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ + "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge",
+ RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView|IconView:id/icon",
+ DRAG_LAYER + "SearchContainerView:id/apps_view",
+ DRAG_LAYER + "LauncherDragView"
+ ));
+
+ // Per-AnalysisNode data that's specific to this detector.
+ private static class NodeData {
+ public boolean ignoreFlashes;
+
+ // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
+ // ignored.
+ // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
+ // children.
+ public IgnoreNode ignoreNode;
+ }
+
+ private NodeData getNodeData(AnalysisNode info) {
+ return (NodeData) info.detectorsData[detectorOrdinal];
+ }
+
+ @Override
+ void initializeNode(AnalysisNode info) {
+ final NodeData nodeData = new NodeData();
+ info.detectorsData[detectorOrdinal] = nodeData;
+
+ // If the parent view ignores flashes, its descendants will too.
+ final boolean parentIgnoresFlashes = info.parent != null && getNodeData(
+ info.parent).ignoreFlashes;
+ if (parentIgnoresFlashes) {
+ nodeData.ignoreFlashes = true;
+ return;
+ }
+
+ // Parent view doesn't ignore flashes.
+ // Initialize this AnalysisNode's ignore-node with the corresponding child of the
+ // ignore-node of the parent, if present.
+ final IgnoreNode parentIgnoreNode = info.parent != null
+ ? getNodeData(info.parent).ignoreNode
+ : IGNORED_NODES_ROOT;
+ nodeData.ignoreNode = parentIgnoreNode != null
+ ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
+ // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
+ nodeData.ignoreFlashes =
+ nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
+ }
+
+ @Override
+ String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
+ long frameTimeNs) {
+ // Should we check when a view was visible for a short period, then its alpha became 0?
+ // Then 'lastVisible' time should be the last one still visible?
+ // Check only transitions of alpha between 0 and 1?
+
+ // If this is the first time ever when we see the view, there have been no flashes yet.
+ if (oldInfo == null) return null;
+
+ // A flash requires a view to go from the full visibility to no-visibility and then back,
+ // or vice versa.
+ // If the last time the view was seen before the current frame, it didn't have full
+ // visibility; no flash can possibly be detected at the current frame.
+ if (oldInfo.alpha < 1) return null;
+
+ final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
+ final NodeData nodeData = getNodeData(latestInfo);
+ if (nodeData.ignoreFlashes) return null;
+
+ // Once the view becomes invisible, see for how long it was visible prior to that. If it
+ // was visible only for a short interval of time, it's a flash.
+ if (
+ // View is invisible in the current frame
+ newInfo == null
+ // When the view became visible last time, it was a transition from
+ // no-visibility to full visibility.
+ && oldInfo.timeBecameVisibleNs != -1) {
+ final long wasVisibleTimeMs = (frameTimeNs - oldInfo.timeBecameVisibleNs) / 1000000;
+
+ if (wasVisibleTimeMs <= FLASH_DURATION_MS) {
+ nodeData.ignoreFlashes = true; // No need to report flashes in children.
+ return
+ String.format(
+ "View was visible for a too short period of time %dms, which is a"
+ + " flash",
+ wasVisibleTimeMs
+ );
+ }
+ }
+
+ // Once a view becomes visible, see for how long it was invisible prior to that. If it
+ // was invisible only for a short interval of time, it's a flash.
+ if (
+ // The view is fully visible now
+ newInfo != null && newInfo.alpha >= 1
+ // The view wasn't visible in the previous frame
+ && frameN != oldInfo.frameN + 1) {
+ // We can assert the below condition because at this point, we know that
+ // oldInfo.alpha >= 1, i.e. it disappeared abruptly.
+ assertTrue("oldInfo.timeBecameInvisibleNs must not be -1",
+ oldInfo.timeBecameInvisibleNs != -1);
+
+ final long wasInvisibleTimeMs = (frameTimeNs - oldInfo.timeBecameInvisibleNs) / 1000000;
+ if (wasInvisibleTimeMs <= FLASH_DURATION_MS) {
+ nodeData.ignoreFlashes = true; // No need to report flashes in children.
+ return
+ String.format(
+ "View was invisible for a too short period of time %dms, which "
+ + "is a flash",
+ wasInvisibleTimeMs);
+ }
+ }
+ return null;
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
index 5a2611c..ccb4a1e 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
@@ -17,9 +17,6 @@
import static android.view.View.VISIBLE;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import com.android.app.viewcapture.data.ExportedData;
import com.android.app.viewcapture.data.FrameData;
import com.android.app.viewcapture.data.ViewNode;
@@ -36,37 +33,15 @@
public class ViewCaptureAnalyzer {
private static final String SCRIM_VIEW_CLASS = "com.android.launcher3.views.ScrimView";
- /**
- * Detector of one kind of anomaly.
- */
- abstract static class AnomalyDetector {
- /**
- * Initializes fields of the node that are specific to the anomaly detected by this
- * detector.
- */
- abstract void initializeNode(@NonNull AnalysisNode info);
-
- /**
- * Detects anomalies by looking at the last occurrence of a view, and the current one.
- * null value means that the view. 'oldInfo' and 'newInfo' cannot be both null.
- * If an anomaly is detected, an exception will be thrown.
- *
- * @param oldInfo the view, as seen in the last frame that contained it in the view
- * hierarchy before 'currentFrame'. 'null' means that the view is first seen
- * in the 'currentFrame'.
- * @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that
- * the view is not present in the 'currentFrame', but was present in earlier
- * frames.
- * @param frameN number of the current frame.
- */
- abstract void detectAnomalies(
- @Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN);
- }
-
// All detectors. They will be invoked in the order listed here.
- private static final Iterable<AnomalyDetector> ANOMALY_DETECTORS = Arrays.asList(
- new AlphaJumpDetector()
- );
+ private static final AnomalyDetector[] ANOMALY_DETECTORS = {
+ new AlphaJumpDetector(),
+ new FlashDetector()
+ };
+
+ static {
+ for (int i = 0; i < ANOMALY_DETECTORS.length; ++i) ANOMALY_DETECTORS[i].detectorOrdinal = i;
+ }
// A view from view capture data converted to a form that's convenient for detecting anomalies.
static class AnalysisNode {
@@ -81,34 +56,56 @@
// Visible scale and alpha, build recursively from the ancestor list.
public float scaleX;
public float scaleY;
- public float alpha;
+ public float alpha; // Always > 0
public int frameN;
+
+ // Timestamp of the frame when this view became abruptly visible, i.e. its alpha became 1
+ // the next frame after it was 0 or the view wasn't visible.
+ // If the view is currently invisible or the last appearance wasn't abrupt, the value is -1.
+ public long timeBecameVisibleNs;
+
+ // Timestamp of the frame when this view became abruptly invisible last time, i.e. its
+ // alpha became 0, or view disappeared, after being 1 in the previous frame.
+ // If the view is currently visible or the last disappearance wasn't abrupt, the value is
+ // -1.
+ public long timeBecameInvisibleNs;
+
public ViewNode viewCaptureNode;
- public boolean ignoreAlphaJumps;
+ // Class name + resource id
+ public String nodeIdentity;
+
+ // Collection of detector-specific data for this node.
+ public final Object[] detectorsData = new Object[ANOMALY_DETECTORS.length];
@Override
public String toString() {
- return String.format("window coordinates: (%s, %s), class path from the root: %s",
- left, top, diagPathFromRoot(this));
+ return String.format("view window coordinates: (%s, %s)", left, top);
}
}
/**
- * Scans a view capture record and throws an error if an anomaly is found.
+ * Scans a view capture record and searches for view animation anomalies. Can find anomalies for
+ * multiple views.
+ * Returns a map from the view path to the anomaly message for the view. Non-empty map means
+ * that anomalies were detected.
*/
- public static void assertNoAnomalies(ExportedData viewCaptureData) {
+ public static Map<String, String> getAnomalies(ExportedData viewCaptureData) {
+ final Map<String, String> anomalies = new HashMap<>();
+
final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS);
final int windowDataCount = viewCaptureData.getWindowDataCount();
for (int i = 0; i < windowDataCount; ++i) {
- analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex);
+ analyzeWindowData(
+ viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies);
}
+ return anomalies;
}
private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData,
- int scrimClassIndex) {
+ int scrimClassIndex, Map<String, String> anomalies) {
// View hash code => Last seen node with this hash code.
// The view is added when we analyze the first frame where it's visible.
// After that, it gets updated for every frame where it's visible.
@@ -117,14 +114,17 @@
for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes,
- scrimClassIndex);
+ scrimClassIndex, anomalies);
}
}
private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData,
- Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
+ Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
+ Map<String, String> anomalies) {
// Analyze the node tree starting from the root.
+ long frameTimeNs = frame.getTimestamp();
analyzeView(
+ frameTimeNs,
frame.getNode(),
/* parent = */ null,
frameN,
@@ -132,26 +132,36 @@
/* topShift = */ 0,
viewCaptureData,
lastSeenNodes,
- scrimClassIndex);
+ scrimClassIndex,
+ anomalies);
- // Analyze transitions when a view visible in the last frame become invisible in the
+ // Analyze transitions when a view visible in the previous frame became invisible in the
// current one.
for (AnalysisNode info : lastSeenNodes.values()) {
if (info.frameN == frameN - 1) {
if (!info.viewCaptureNode.getWillNotDraw()) {
- ANOMALY_DETECTORS.forEach(
- detector -> detector.detectAnomalies(
- /* oldInfo = */ info,
- /* newInfo = */ null,
- frameN));
+ Arrays.stream(ANOMALY_DETECTORS).forEach(
+ detector ->
+ detectAnomaly(
+ detector,
+ frameN,
+ /* oldInfo = */ info,
+ /* newInfo = */ null,
+ anomalies,
+ frameTimeNs)
+ );
}
+ info.timeBecameInvisibleNs = info.alpha == 1 ? frameTimeNs : -1;
+ info.timeBecameVisibleNs = -1;
}
}
}
- private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN,
+ private static void analyzeView(long frameTimeNs, ViewNode viewCaptureNode, AnalysisNode parent,
+ int frameN,
float leftShift, float topShift, ExportedData viewCaptureData,
- Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
+ Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
+ Map<String, String> anomalies) {
// Skip analysis of invisible views
final float parentAlpha = parent != null ? parent.alpha : 1;
final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
@@ -177,6 +187,8 @@
final AnalysisNode newAnalysisNode = new AnalysisNode();
newAnalysisNode.className = viewCaptureData.getClassname(classIndex);
newAnalysisNode.resourceId = viewCaptureNode.getId();
+ newAnalysisNode.nodeIdentity =
+ getNodeIdentity(newAnalysisNode.className, newAnalysisNode.resourceId);
newAnalysisNode.parent = parent;
newAnalysisNode.left = left;
newAnalysisNode.top = top;
@@ -184,14 +196,32 @@
newAnalysisNode.scaleY = scaleY;
newAnalysisNode.alpha = alpha;
newAnalysisNode.frameN = frameN;
+ newAnalysisNode.timeBecameInvisibleNs = -1;
newAnalysisNode.viewCaptureNode = viewCaptureNode;
- ANOMALY_DETECTORS.forEach(detector -> detector.initializeNode(newAnalysisNode));
+ Arrays.stream(ANOMALY_DETECTORS).forEach(
+ detector -> detector.initializeNode(newAnalysisNode));
- // Detect anomalies for the view
final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null
+
+ if (oldAnalysisNode != null && oldAnalysisNode.frameN + 1 == frameN) {
+ // If this view was present in the previous frame, keep the time when it became visible.
+ newAnalysisNode.timeBecameVisibleNs = oldAnalysisNode.timeBecameVisibleNs;
+ } else {
+ // If the view is becoming visible after being invisible, initialize the time when it
+ // became visible with a new value.
+ // If the view became visible abruptly, i.e. alpha jumped from 0 to 1 between the
+ // previous and the current frames, then initialize with the time of the current
+ // frame. Otherwise, use -1.
+ newAnalysisNode.timeBecameVisibleNs = newAnalysisNode.alpha >= 1 ? frameTimeNs : -1;
+ }
+
+ // Detect anomalies for the view.
if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) {
- ANOMALY_DETECTORS.forEach(
- detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN));
+ Arrays.stream(ANOMALY_DETECTORS).forEach(
+ detector ->
+ detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode,
+ anomalies, frameTimeNs)
+ );
}
lastSeenNodes.put(hashcode, newAnalysisNode);
@@ -205,9 +235,23 @@
// transparent.
if (child.getClassnameIndex() == scrimClassIndex) break;
- analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren,
- viewCaptureData, lastSeenNodes,
- scrimClassIndex);
+ analyzeView(frameTimeNs, child, newAnalysisNode, frameN, leftShiftForChildren,
+ topShiftForChildren,
+ viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies);
+ }
+ }
+
+ private static void detectAnomaly(AnomalyDetector detector, int frameN,
+ AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode,
+ Map<String, String> anomalies, long frameTimeNs) {
+ final String maybeAnomaly =
+ detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN, frameTimeNs);
+ if (maybeAnomaly != null) {
+ AnalysisNode latestInfo = newAnalysisNode != null ? newAnalysisNode : oldAnalysisNode;
+ final String viewDiagPath = diagPathFromRoot(latestInfo);
+ if (!anomalies.containsKey(viewDiagPath)) {
+ anomalies.put(viewDiagPath, String.format("%s, %s", maybeAnomaly, latestInfo));
+ }
}
}
@@ -221,18 +265,20 @@
return className.substring(className.lastIndexOf(".") + 1);
}
- static String diagPathFromRoot(AnalysisNode nodeBox) {
- final StringBuilder path = new StringBuilder(diagPathElement(nodeBox));
- for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) {
- path.insert(0, diagPathElement(ancestor) + "|");
+ private static String diagPathFromRoot(AnalysisNode analysisNode) {
+ final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity);
+ for (AnalysisNode ancestor = analysisNode.parent;
+ ancestor != null;
+ ancestor = ancestor.parent) {
+ path.insert(0, ancestor.nodeIdentity + "|");
}
return path.toString();
}
- private static String diagPathElement(AnalysisNode nodeBox) {
+ private static String getNodeIdentity(String className, String resourceId) {
final StringBuilder sb = new StringBuilder();
- sb.append(classNameToSimpleName(nodeBox.className));
- if (!"NO_ID".equals(nodeBox.resourceId)) sb.append(":" + nodeBox.resourceId);
+ sb.append(classNameToSimpleName(className));
+ if (!"NO_ID".equals(resourceId)) sb.append(":" + resourceId);
return sb.toString();
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 399abc7..23d09d4 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -210,6 +210,9 @@
public AppIcon getAppIcon(String appName) {
AppIcon appIcon = tryGetAppIcon(appName);
mLauncher.assertNotNull("Unable to scroll to a clickable icon: " + appName, appIcon);
+ // appIcon.getAppName() checks for content description, so it is possible that it can have
+ // trailing words. So check if the content description contains the appName.
+ mLauncher.assertTrue("Wrong app icon name.", appIcon.getAppName().contains(appName));
return appIcon;
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 0a0cf07..85098c8 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -37,10 +37,14 @@
}
static BySelector getAppIconSelector(String appName, LauncherInstrumentation launcher) {
- return By.clazz(TextView.class).textContains(appName)
+ return By.clazz(TextView.class).desc(makeMultilinePattern(appName))
.pkg(launcher.getLauncherPackageName());
}
+ static BySelector getMenuItemSelector(String text, LauncherInstrumentation launcher) {
+ return By.clazz(TextView.class).text(text).pkg(launcher.getLauncherPackageName());
+ }
+
static BySelector getAnyAppIconSelector() {
return By.clazz(TextView.class);
}
@@ -94,4 +98,24 @@
public String getIconName() {
return getObject().getText();
}
+
+ /**
+ * Return the app name of a icon by the content description. This should be used when trying to
+ * get the name of an app where the text of it is multiline.
+ */
+ @NonNull
+ String getAppName() {
+ return getObject().getContentDescription();
+ }
+
+ /**
+ * Create a regular expression pattern that matches strings starting with the app name, where
+ * spaces in the app name are replaced with zero or more occurrences of the "\s" character
+ * (which represents a whitespace character in regular expressions), followed by any characters
+ * after the app name.
+ */
+ static Pattern makeMultilinePattern(String appName) {
+ return Pattern.compile(appName.replaceAll("\\s+", "\\\\s*") + ".*",
+ Pattern.DOTALL);
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
index 667290f..bbcc6a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
@@ -50,7 +50,7 @@
*/
public AppIconMenuItem getMenuItem(String shortcutText) {
final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
- AppIcon.getAppIconSelector(shortcutText, mLauncher));
+ AppIcon.getMenuItemSelector(shortcutText, mLauncher));
return createMenuItem(menuItem);
}
@@ -59,7 +59,7 @@
*/
public SplitScreenMenuItem getSplitScreenMenuItem() {
final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
- AppIcon.getAppIconSelector("Split screen", mLauncher));
+ AppIcon.getMenuItemSelector("Split screen", mLauncher));
return new SplitScreenMenuItem(mLauncher, menuItem);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 3dab9a8..7dd5827 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -240,76 +240,71 @@
"want to quick switch to the previous app")) {
verifyActiveContainer();
final boolean launcherWasVisible = mLauncher.isLauncherVisible();
- switch (mLauncher.getNavigationModel()) {
- case ZERO_BUTTON: {
- final int startX;
- final int startY;
- final int endX;
- final int endY;
- final int cornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
- if (toRight) {
- // Swipe from the bottom left to the bottom right of the screen.
- startX = cornerRadius;
- startY = getSwipeStartY();
- endX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
- endY = startY;
- } else {
- // Swipe from the bottom right to the bottom left of the screen.
- startX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
- startY = getSwipeStartY();
- endX = cornerRadius;
- endY = startY;
- }
-
- LauncherInstrumentation.GestureScope gestureScope =
- launcherWasVisible
- ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
- : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
- mLauncher.executeAndWaitForEvent(
- () -> mLauncher.linearGesture(
- startX, startY, endX, endY, 20, false, gestureScope),
- event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
- () -> "Quick switch gesture didn't change window state", "swiping");
- break;
+ if (mLauncher.getNavigationModel() == NavigationModel.ZERO_BUTTON
+ || mLauncher.getTrackpadGestureType() == TrackpadGestureType.FOUR_FINGER) {
+ final int startX;
+ final int startY;
+ final int endX;
+ final int endY;
+ final int cornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
+ if (toRight) {
+ // Swipe from the bottom left to the bottom right of the screen.
+ startX = cornerRadius;
+ startY = getSwipeStartY();
+ endX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
+ endY = startY;
+ } else {
+ // Swipe from the bottom right to the bottom left of the screen.
+ startX = mLauncher.getDevice().getDisplayWidth() - cornerRadius;
+ startY = getSwipeStartY();
+ endX = cornerRadius;
+ endY = startY;
}
- case THREE_BUTTON:
- // Double press the recents button.
- UiObject2 recentsButton = mLauncher.waitForNavigationUiObject("recent_apps");
- if (mLauncher.isTablet()) {
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
- LauncherInstrumentation.EVENT_TOUCH_DOWN);
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
- LauncherInstrumentation.EVENT_TOUCH_UP);
- }
- if (mLauncher.isTrackpadGestureEnabled()) {
- mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
- mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
- }
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
- mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL,
- "clicking Recents button for the first time");
- mLauncher.getOverview();
- if (mLauncher.isTablet()) {
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
- LauncherInstrumentation.EVENT_TOUCH_DOWN);
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
- LauncherInstrumentation.EVENT_TOUCH_UP);
- }
- if (mLauncher.isTrackpadGestureEnabled()) {
- mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
- mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
- }
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
- mLauncher.executeAndWaitForEvent(
- () -> recentsButton.click(),
- event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
- () -> "Pressing recents button didn't change window state",
- "clicking Recents button for the second time");
- break;
+ LauncherInstrumentation.GestureScope gestureScope =
+ launcherWasVisible
+ ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+ : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
+ mLauncher.executeAndWaitForEvent(
+ () -> mLauncher.linearGesture(
+ startX, startY, endX, endY, 20, false, gestureScope),
+ event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+ () -> "Quick switch gesture didn't change window state", "swiping");
+ } else {
+ // Double press the recents button.
+ UiObject2 recentsButton = mLauncher.waitForNavigationUiObject("recent_apps");
+ if (mLauncher.isTablet()) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_DOWN);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_UP);
+ }
+ if (mLauncher.isTrackpadGestureEnabled()) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
+ }
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+ mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL,
+ "clicking Recents button for the first time");
+ mLauncher.getOverview();
+ if (mLauncher.isTablet()) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_DOWN);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_UP);
+ }
+ if (mLauncher.isTrackpadGestureEnabled()) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
+ }
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+ mLauncher.executeAndWaitForEvent(
+ () -> recentsButton.click(),
+ event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+ () -> "Pressing recents button didn't change window state",
+ "clicking Recents button for the second time");
}
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
- return;
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index a59eff7..9b4d273 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -37,6 +37,7 @@
import androidx.test.uiautomator.Condition;
import androidx.test.uiautomator.UiDevice;
+import com.android.launcher3.testing.shared.ResourceUtils;
import com.android.launcher3.testing.shared.TestProtocol;
/**
@@ -50,6 +51,8 @@
// UNSTASHED_TASKBAR_HANDLE_HINT_SCALE value from TaskbarStashController.
private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
+ private static final int STASHED_TASKBAR_BOTTOM_EDGE_DP = 1;
+
private final Condition<UiDevice, Boolean> mStashedTaskbarHintScaleCondition =
device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
TestProtocol.TEST_INFO_RESPONSE_FIELD) - UNSTASHED_TASKBAR_HANDLE_HINT_SCALE
@@ -209,7 +212,7 @@
*
* <p>This unstashing occurs when not actively hovering the taskbar.
*/
- public void hoverScreenBottomEdgeToUnstashTaskbar() {
+ public Taskbar hoverScreenBottomEdgeToUnstashTaskbar() {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"cursor hover entering screen edge to unstash taskbar")) {
@@ -226,13 +229,15 @@
mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+
+ return new Taskbar(mLauncher);
}
}
/**
* Emulate the cursor hovering the taskbar to get unstash hint, then hovering below to unstash.
*/
- public void hoverBelowHintedTaskbarToUnstash() {
+ public Taskbar hoverBelowHintedTaskbarToUnstash() {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"cursor hover entering stashed taskbar")) {
@@ -254,6 +259,7 @@
new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+ return new Taskbar(mLauncher);
}
}
}
@@ -288,4 +294,45 @@
}
}
}
+
+ /**
+ * Emulate the cursor clicking the stashed taskbar to go home.
+ */
+ public Workspace clickStashedTaskbarToGoHome() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "cursor hover entering stashed taskbar")) {
+ long downTime = SystemClock.uptimeMillis();
+ int stashedTaskbarBottomEdge = ResourceUtils.pxFromDp(STASHED_TASKBAR_BOTTOM_EDGE_DP,
+ mLauncher.getResources().getDisplayMetrics());
+ Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+ mLauncher.getRealDisplaySize().y - stashedTaskbarBottomEdge - 1);
+ mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+ new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+
+ mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+ LauncherInstrumentation.WAIT_TIME_MS);
+
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "cursor clicking stashed taskbar to go home")) {
+ mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+ new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+ null);
+ mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
+ new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+ LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+ mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_PRESS,
+ new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+ null);
+ mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_RELEASE,
+ new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+ null);
+ mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP,
+ new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+ LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+
+ return mLauncher.getWorkspace();
+ }
+ }
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 89f141f..b929293 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -99,17 +99,22 @@
private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 15;
private static final int GESTURE_STEP_MS = 16;
- static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
- static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
- private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
+ static final Pattern EVENT_TOUCH_DOWN = getTouchEventPatternWithPointerCount("ACTION_DOWN");
+ static final Pattern EVENT_TOUCH_UP = getTouchEventPatternWithPointerCount("ACTION_UP");
+ private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPatternWithPointerCount(
+ "ACTION_CANCEL");
static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
static final Pattern EVENT_START = Pattern.compile("start:");
static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
- static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPatternTIS("ACTION_CANCEL");
+ static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPattern(
+ "TouchInteractionService.onInputEvent", "ACTION_CANCEL");
static final Pattern EVENT_HOVER_ENTER_TIS = getTouchEventPatternTIS("ACTION_HOVER_ENTER");
static final Pattern EVENT_HOVER_EXIT_TIS = getTouchEventPatternTIS("ACTION_HOVER_EXIT");
+ static final Pattern EVENT_BUTTON_PRESS_TIS = getTouchEventPatternTIS("ACTION_BUTTON_PRESS");
+ static final Pattern EVENT_BUTTON_RELEASE_TIS =
+ getTouchEventPatternTIS("ACTION_BUTTON_RELEASE");
private static final Pattern EVENT_KEY_BACK_DOWN =
getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
@@ -173,7 +178,7 @@
void close();
}
- private static final String WORKSPACE_RES_ID = "workspace";
+ 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 = "primary_widgets_list_view";
@@ -209,30 +214,35 @@
private int mPointerCount = 0;
private static Pattern getTouchEventPattern(String prefix, String action) {
- return getTouchEventPattern(prefix, action, 1);
+ return Pattern.compile(
+ prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+ + ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?");
}
- private static Pattern getTouchEventPattern(String prefix, String action, int pointerCount) {
+ private static Pattern getTouchEventPatternWithPointerCount(String prefix, String action,
+ int pointerCount) {
return Pattern.compile(
prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+ ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount="
+ pointerCount);
}
- private static Pattern getTouchEventPattern(String action) {
- return getTouchEventPattern("Touch event", action);
+ private static Pattern getTouchEventPatternWithPointerCount(String action) {
+ return getTouchEventPatternWithPointerCount("Touch event", action, 1);
}
- private static Pattern getTouchEventPattern(String action, int pointerCount) {
- return getTouchEventPattern("Touch event", action, pointerCount);
+ private static Pattern getTouchEventPatternWithPointerCount(String action, int pointerCount) {
+ return getTouchEventPatternWithPointerCount("Touch event", action, pointerCount);
}
private static Pattern getTouchEventPatternTIS(String action) {
- return getTouchEventPattern("TouchInteractionService.onInputEvent", action);
+ return getTouchEventPatternWithPointerCount("TouchInteractionService.onInputEvent", action,
+ 1);
}
private static Pattern getTouchEventPatternTIS(String action, int pointerCount) {
- return getTouchEventPattern("TouchInteractionService.onInputEvent", action, pointerCount);
+ return getTouchEventPatternWithPointerCount("TouchInteractionService.onInputEvent", action,
+ pointerCount);
}
private static Pattern getKeyEventPattern(String action, String keyCode) {
@@ -1811,11 +1821,14 @@
&& gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
&& gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE
&& (!isTrackpadGesture || isTwoFingerTrackpadGesture)) {
- expectEvent(TestProtocol.SEQUENCE_MAIN,
- getTouchEventPattern("ACTION_POINTER_DOWN", mPointerCount));
+ expectEvent(TestProtocol.SEQUENCE_MAIN, getTouchEventPatternWithPointerCount(
+ "ACTION_POINTER_DOWN", mPointerCount));
}
- expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
- "ACTION_POINTER_DOWN", mPointerCount));
+ if (hasTIS && (isTrackpadGestureEnabled()
+ || getNavigationModel() != NavigationModel.THREE_BUTTON)) {
+ expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
+ "ACTION_POINTER_DOWN", mPointerCount));
+ }
pointerCount = mPointerCount;
break;
case MotionEvent.ACTION_POINTER_UP:
@@ -1823,17 +1836,26 @@
&& gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER
&& gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE
&& (!isTrackpadGesture || isTwoFingerTrackpadGesture)) {
- expectEvent(TestProtocol.SEQUENCE_MAIN,
- getTouchEventPattern("ACTION_POINTER_UP", mPointerCount));
- }
- // When the gesture is handled outside, it's cancelled within launcher.
- if (gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
- && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
- expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
+ expectEvent(TestProtocol.SEQUENCE_MAIN, getTouchEventPatternWithPointerCount(
"ACTION_POINTER_UP", mPointerCount));
}
+ // When the gesture is handled outside, it's cancelled within launcher.
+ if (hasTIS && (isTrackpadGestureEnabled()
+ || getNavigationModel() != NavigationModel.THREE_BUTTON)) {
+ if (gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
+ && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
+ expectEvent(TestProtocol.SEQUENCE_TIS, getTouchEventPatternTIS(
+ "ACTION_POINTER_UP", mPointerCount));
+ }
+ }
mPointerCount--;
break;
+ case MotionEvent.ACTION_BUTTON_PRESS:
+ expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_BUTTON_PRESS_TIS);
+ break;
+ case MotionEvent.ACTION_BUTTON_RELEASE:
+ expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_BUTTON_RELEASE_TIS);
+ break;
}
final MotionEvent event = isTrackpadGesture
@@ -1841,6 +1863,10 @@
downTime, currentTime, action, point.x, point.y, pointerCount,
mTrackpadGestureType)
: getMotionEvent(downTime, currentTime, action, point.x, point.y);
+ if (action == MotionEvent.ACTION_BUTTON_PRESS
+ || action == MotionEvent.ACTION_BUTTON_RELEASE) {
+ event.setActionButton(MotionEvent.BUTTON_PRIMARY);
+ }
assertTrue("injectInputEvent failed",
mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
event.recycle();
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index e349620..7c29a6c 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -50,6 +50,24 @@
}
}
+ /** Taps the app info item from the overview task menu and returns the LaunchedAppState
+ * representing the App info settings page. */
+ @NonNull
+ public LaunchedAppState tapAppInfoMenuItem() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "before tapping the app info menu item")) {
+ mLauncher.clickLauncherObject(
+ mLauncher.findObjectInContainer(mMenu, By.text("App info")));
+
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "tapped app info menu item")) {
+ mLauncher.waitUntilSystemLauncherObjectGone("overview_panel");
+ return new LaunchedAppState(mLauncher);
+ }
+ }
+ }
+
/** Returns true if an item matching the given string is present in the menu. */
public boolean hasMenuItem(String expectedMenuItemText) {
UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));