Merge "Revert "Disable gesture nav while PiP anim is running"" into main
diff --git a/Android.bp b/Android.bp
index 13a926b..e358005 100644
--- a/Android.bp
+++ b/Android.bp
@@ -19,6 +19,17 @@
min_launcher3_sdk_version = "30"
+// Targets that don't inherit framework aconfig libs (i.e., those that don't set
+// `platform_apis: true`) must manually link them.
+java_defaults {
+ name: "launcher-non-platform-apis-defaults",
+ static_libs: [
+ "android.os.flags-aconfig-java",
+ "android.appwidget.flags-aconfig-java",
+ "com.android.window.flags.window-aconfig-java",
+ ]
+}
+
// Common source files used to build launcher (java and kotlin)
// All sources are split so they can be reused in many other libraries/apps in other folders
@@ -141,7 +152,6 @@
static_libs: [
"LauncherPluginLib",
"launcher_quickstep_log_protos_lite",
- "android.os.flags-aconfig-java",
"androidx-constraintlayout_constraintlayout",
"androidx.recyclerview_recyclerview",
"androidx.dynamicanimation_dynamicanimation",
@@ -163,8 +173,6 @@
"kotlinx_coroutines",
"com_android_launcher3_flags_lib",
"com_android_wm_shell_flags_lib",
- "android.appwidget.flags-aconfig-java",
- "com.android.window.flags.window-aconfig-java",
],
manifest: "AndroidManifest-common.xml",
sdk_version: "current",
@@ -179,6 +187,7 @@
//
android_app {
name: "Launcher3",
+ defaults: ["launcher-non-platform-apis-defaults"],
static_libs: [
"Launcher3ResLib",
@@ -190,7 +199,7 @@
],
optimize: {
- proguard_flags_files: ["proguard.flags"],
+ proguard_flags_files: [":launcher-proguard-rules"],
// Proguard is disable for testing. Derivarive prjects to keep proguard enabled
enabled: false,
},
@@ -302,7 +311,9 @@
static_libs: ["Launcher3QuickStepLib"],
optimize: {
- enabled: false,
+ proguard_flags_files: [":launcher-proguard-rules"],
+ enabled: true,
+ shrink_resources: true,
},
platform_apis: true,
@@ -349,6 +360,7 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
enabled: true,
+ shrink_resources: true,
},
privileged: true,
@@ -385,6 +397,7 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
enabled: true,
+ shrink_resources: true,
},
privileged: true,
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index f1f9966..21b9863 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -23,13 +23,6 @@
}
flag {
- name: "enable_grid_only_overview"
- namespace: "launcher"
- description: "Enable a grid-only overview without a focused task."
- bug: "257950105"
-}
-
-flag {
name: "enable_cursor_hover_states"
namespace: "launcher"
description: "Enables cursor hover states for certain elements."
@@ -44,13 +37,6 @@
}
flag {
- name: "enable_overview_icon_menu"
- namespace: "launcher"
- description: "Enable updated overview icon and menu within task."
- bug: "257950105"
-}
-
-flag {
name: "enable_focus_outline"
namespace: "launcher"
description: "Enables focus states outline for launcher."
@@ -238,13 +224,6 @@
}
flag {
- name: "enable_refactor_task_thumbnail"
- namespace: "launcher"
- description: "Enables rewritten version of TaskThumbnailViews in Overview"
- bug: "331753115"
-}
-
-flag {
name: "enable_handle_delayed_gesture_callbacks"
namespace: "launcher"
description: "Enables additional handling for delayed mid-gesture callbacks"
@@ -311,8 +290,29 @@
}
flag {
+ name: "enable_container_return_animations"
+ namespace: "launcher"
+ description: "Enables the container return animation mirroring launches."
+ bug: "341017746"
+}
+
+flag {
name: "floating_search_bar"
namespace: "launcher"
description: "Search bar persists at the bottom of the screen across Launcher states"
bug: "346408388"
}
+
+flag {
+ name: "multiline_search_bar"
+ namespace: "launcher"
+ description: "Search bar can wrap to multi-line"
+ bug: "341795751"
+}
+
+flag {
+ name: "enable_new_archiving_icon"
+ namespace: "launcher"
+ description: "Archived apps will use new icon in app title"
+ bug: "350758155"
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
new file mode 100644
index 0000000..f9327fe
--- /dev/null
+++ b/aconfig/launcher_overview.aconfig
@@ -0,0 +1,23 @@
+package: "com.android.launcher3"
+container: "system_ext"
+
+flag {
+ name: "enable_grid_only_overview"
+ namespace: "launcher_overview"
+ description: "Enable a grid-only overview without a focused task."
+ bug: "257950105"
+}
+
+flag {
+ name: "enable_overview_icon_menu"
+ namespace: "launcher_overview"
+ description: "Enable updated overview icon and menu within task."
+ bug: "257950105"
+}
+
+flag {
+ name: "enable_refactor_task_thumbnail"
+ namespace: "launcher_overview"
+ description: "Enables rewritten version of TaskThumbnailViews in Overview"
+ bug: "331753115"
+}
diff --git a/go/quickstep/res/values-fa/strings.xml b/go/quickstep/res/values-fa/strings.xml
index 47786e9..8453d4e 100644
--- a/go/quickstep/res/values-fa/strings.xml
+++ b/go/quickstep/res/values-fa/strings.xml
@@ -14,7 +14,7 @@
<string name="assistant_not_selected_text" msgid="3244613673884359276">"برای گوش کردن به نوشتار در صفحهنمایشتان یا ترجمه کردن آن، یکی از برنامههای دستیار دیجیتالی را در «تنظیمات» انتخاب کنید"</string>
<string name="assistant_not_supported_title" msgid="1675788067597484142">"برای استفاده از این ویژگی، دستیارتان را تغییر دهید"</string>
<string name="assistant_not_supported_text" msgid="1708031078549268884">"برای گوش کردن به نوشتار در صفحهنمایشتان یا ترجمه کردن آن، برنامه دستیار دیجیتالیتان را در «تنظیمات» تغییر دهید"</string>
- <string name="tooltip_listen" msgid="7634466447860989102">"برای گوش کردن به نوشتار در این صفحه، اینجا ضربه بزنید"</string>
- <string name="tooltip_translate" msgid="4184845868901542567">"برای ترجمه نوشتار در این صفحه، اینجا ضربه بزنید"</string>
+ <string name="tooltip_listen" msgid="7634466447860989102">"برای گوش کردن به نوشتار در این صفحه، اینجا تکضرب بزنید"</string>
+ <string name="tooltip_translate" msgid="4184845868901542567">"برای ترجمه نوشتار در این صفحه، اینجا تکضرب بزنید"</string>
<string name="toast_p2p_app_not_shareable" msgid="7229739094132131536">"نمیتوان این برنامه را همرسانی کرد"</string>
</resources>
diff --git a/go/quickstep/res/values-fr-rCA/strings.xml b/go/quickstep/res/values-fr-rCA/strings.xml
index 2cc9d8f..e48faeb 100644
--- a/go/quickstep/res/values-fr-rCA/strings.xml
+++ b/go/quickstep/res/values-fr-rCA/strings.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_share_drop_target_label" msgid="5804774105974539508">"Partager application"</string>
+ <string name="app_share_drop_target_label" msgid="5804774105974539508">"Partager appli"</string>
<string name="action_listen" msgid="2370304050784689486">"Écouter"</string>
<string name="action_translate" msgid="8028378961867277746">"Traduire"</string>
<string name="action_search" msgid="6269564710943755464">"Lentille"</string>
@@ -9,12 +9,12 @@
<string name="dialog_cancel" msgid="6464336969134856366">"ANNULER"</string>
<string name="dialog_settings" msgid="6564397136021186148">"PARAMÈTRES"</string>
<string name="niu_actions_confirmation_title" msgid="3863451714863526143">"Traduire ou écouter le texte à l\'écran"</string>
- <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"Des renseignements comme du texte sur votre écran, des adresses Web et des captures d\'écran peuvent être partagés avec Google.\n\nPour modifier les renseignements que vous partagez, accédez à "<b>"Paramètres > Applications > Applications par défaut > Application d\'assistant numérique"</b>"."</string>
+ <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"Des renseignements comme du texte sur votre écran, des adresses Web et des captures d\'écran peuvent être partagés avec Google.\n\nPour modifier les renseignements que vous partagez, accédez à "<b>"Paramètres > Applis > Applis par défaut > Appli d\'assistant numérique"</b>"."</string>
<string name="assistant_not_selected_title" msgid="5017072974603345228">"Choisir un assistant pour utiliser cette fonctionnalité"</string>
- <string name="assistant_not_selected_text" msgid="3244613673884359276">"Pour écouter ou traduire le texte affiché sur votre écran, choisissez l\'application d\'un assistant numérique dans les paramètres"</string>
+ <string name="assistant_not_selected_text" msgid="3244613673884359276">"Pour écouter ou traduire le texte affiché sur votre écran, choisissez l\'appli d\'un assistant numérique dans les paramètres"</string>
<string name="assistant_not_supported_title" msgid="1675788067597484142">"Modifier votre assistant pour utiliser cette fonctionnalité"</string>
- <string name="assistant_not_supported_text" msgid="1708031078549268884">"Pour écouter ou traduire le texte affiché sur votre écran, modifiez l\'application de votre assistant numérique dans les paramètres"</string>
+ <string name="assistant_not_supported_text" msgid="1708031078549268884">"Pour écouter ou traduire le texte affiché sur votre écran, modifiez l\'appli de votre assistant numérique dans les paramètres"</string>
<string name="tooltip_listen" msgid="7634466447860989102">"Touchez ce bouton pour écouter le texte affiché sur cet écran"</string>
<string name="tooltip_translate" msgid="4184845868901542567">"Touchez ce bouton pour traduire le texte affiché sur cet écran"</string>
- <string name="toast_p2p_app_not_shareable" msgid="7229739094132131536">"Cette application ne peut pas être partagée"</string>
+ <string name="toast_p2p_app_not_shareable" msgid="7229739094132131536">"Cette appli ne peut pas être partagée"</string>
</resources>
diff --git a/go/quickstep/res/values-nb/strings.xml b/go/quickstep/res/values-nb/strings.xml
index 662b544..6299cc8 100644
--- a/go/quickstep/res/values-nb/strings.xml
+++ b/go/quickstep/res/values-nb/strings.xml
@@ -9,7 +9,7 @@
<string name="dialog_cancel" msgid="6464336969134856366">"AVBRYT"</string>
<string name="dialog_settings" msgid="6564397136021186148">"INNSTILLINGER"</string>
<string name="niu_actions_confirmation_title" msgid="3863451714863526143">"Oversett eller lytt til tekst på skjermen"</string>
- <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"Informasjon som tekst på skjermen, nettadresser og skjermdumper kan deles med Google.\n\nFor å endre hvilken informasjon du deler, gå til "<b>"Innstillinger > Apper > Standardapper > Digital assistent-app"</b>"."</string>
+ <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"Informasjon som tekst på skjermen, nettadresser og skjermbilder kan deles med Google.\n\nFor å endre hvilken informasjon du deler, gå til "<b>"Innstillinger > Apper > Standardapper > Digital assistent-app"</b>"."</string>
<string name="assistant_not_selected_title" msgid="5017072974603345228">"Velg en assistent for å bruke denne funksjonen"</string>
<string name="assistant_not_selected_text" msgid="3244613673884359276">"For å høre eller oversette tekst på skjermen, velg en digital assistent-app i innstillingene"</string>
<string name="assistant_not_supported_title" msgid="1675788067597484142">"Endre assistenten for å bruke denne funksjonen"</string>
diff --git a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
index 26ca06a..0fb9718 100644
--- a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
+++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
@@ -31,6 +31,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.drawable.ColorDrawable;
@@ -47,6 +48,7 @@
import android.widget.TextView;
import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.BaseActivity;
@@ -56,9 +58,8 @@
import com.android.quickstep.util.AssistContentRequester;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.views.GoOverviewActionsView;
-import com.android.quickstep.views.TaskView.TaskContainer;
+import com.android.quickstep.views.TaskContainer;
import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
import java.lang.annotation.Retention;
@@ -131,7 +132,7 @@
* Called when the current task is interactive for the user
*/
@Override
- public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+ public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
boolean rotated) {
if (mDialog != null && mDialog.isShowing()) {
// Redraw the dialog in case the layout changed
diff --git a/quickstep/res/drawable-hdpi/nav_background.9.png b/quickstep/res/drawable-hdpi/nav_background.9.png
new file mode 100644
index 0000000..a09e654
--- /dev/null
+++ b/quickstep/res/drawable-hdpi/nav_background.9.png
Binary files differ
diff --git a/quickstep/res/drawable-mdpi/nav_background.9.png b/quickstep/res/drawable-mdpi/nav_background.9.png
new file mode 100644
index 0000000..aa74153
--- /dev/null
+++ b/quickstep/res/drawable-mdpi/nav_background.9.png
Binary files differ
diff --git a/quickstep/res/drawable-xhdpi/nav_background.9.png b/quickstep/res/drawable-xhdpi/nav_background.9.png
new file mode 100644
index 0000000..3b52195
--- /dev/null
+++ b/quickstep/res/drawable-xhdpi/nav_background.9.png
Binary files differ
diff --git a/quickstep/res/drawable-xxhdpi/nav_background.9.png b/quickstep/res/drawable-xxhdpi/nav_background.9.png
new file mode 100644
index 0000000..b35183c
--- /dev/null
+++ b/quickstep/res/drawable-xxhdpi/nav_background.9.png
Binary files differ
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index cc3b30e..46c1332 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -28,7 +28,7 @@
launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
- <include layout="@layout/task_thumbnail" />
+ <include layout="@layout/task_thumbnail_deprecated" />
<!-- Filtering affects only alpha instead of the visibility since visibility can be altered
separately through RecentsView#resetFromSplitSelectionState() -->
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 89e9b3d..453057c 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -36,15 +36,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
- <!--
- TODO(b249371338): DesktopTaskView extends from TaskView. TaskView expects TaskThumbnailView
- and IconView with these ids to be present. Need to refactor RecentsView to accept child
- views that do not inherint from TaskView only or create a generic TaskView that have
- N number of tasks.
- -->
- <include layout="@layout/task_thumbnail"
- android:visibility="gone" />
-
<ViewStub
android:id="@+id/icon"
android:inflatedId="@id/icon"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 87a0f70..708aa3c 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -33,9 +33,9 @@
launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
- <include layout="@layout/task_thumbnail"/>
+ <include layout="@layout/task_thumbnail_deprecated"/>
- <include layout="@layout/task_thumbnail"
+ <include layout="@layout/task_thumbnail_deprecated"
android:id="@+id/bottomright_snapshot" />
<!-- Filtering affects only alpha instead of the visibility since visibility can be altered
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index f1a3d62..34640e6 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -13,8 +13,29 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.quickstep.views.TaskThumbnailViewDeprecated
+<com.android.quickstep.task.thumbnail.TaskThumbnailView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/snapshot"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
\ No newline at end of file
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/task_thumbnail"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no"
+ android:visibility="gone"/>
+
+ <com.android.quickstep.task.thumbnail.LiveTileView
+ android:id="@+id/task_thumbnail_live_tile"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
+
+ <View
+ android:id="@+id/task_thumbnail_scrim"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/overview_foreground_scrim_color"
+ android:alpha="0" />
+
+</com.android.quickstep.task.thumbnail.TaskThumbnailView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_thumbnail_deprecated.xml b/quickstep/res/layout/task_thumbnail_deprecated.xml
new file mode 100644
index 0000000..f1a3d62
--- /dev/null
+++ b/quickstep/res/layout/task_thumbnail_deprecated.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.quickstep.views.TaskThumbnailViewDeprecated
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/snapshot"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/quickstep/res/raw-h480dp/all_set_page_bg.json b/quickstep/res/raw-h480dp/all_set_page_bg.json
deleted file mode 100644
index f2998a0..0000000
--- a/quickstep/res/raw-h480dp/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":701,"h":841,"nm":"SUW_WelcomeScreen_FoldableOpen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.949},"o":{"x":0.167,"y":0.167},"t":0,"s":[181.172,148.425,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.038},"t":95,"s":[181.172,-68.377,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[181.172,148.425,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[21.6,21.6,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9.3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.927]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[458.803]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.05]},"t":95,"s":[536.803]},{"t":180,"s":[458.803]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[707.143]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.069]},"t":95,"s":[639.643]},{"t":180,"s":[707.143]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-30,30,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[350.5,420.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw600dp-land/all_set_page_bg.json b/quickstep/res/raw-sw600dp-land/all_set_page_bg.json
deleted file mode 100644
index 63b64da..0000000
--- a/quickstep/res/raw-sw600dp-land/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":841,"h":701,"nm":"SUW_WelcomeScreen_FoldableOpen_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.939},"o":{"x":0.167,"y":0.167},"t":0,"s":[140.975,228.318,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.045},"t":95,"s":[140.975,47.65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[140.975,228.318,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[18,18,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[11.111],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[11.111],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.913]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[639]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.06]},"t":95,"s":[704]},{"t":180,"s":[639]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.12]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[527.25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.083]},"t":95,"s":[471]},{"t":180,"s":[527.25]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-25,25,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[6],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[6],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420.5,350.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw600dp/all_set_page_bg.json b/quickstep/res/raw-sw600dp/all_set_page_bg.json
deleted file mode 100644
index f2998a0..0000000
--- a/quickstep/res/raw-sw600dp/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":701,"h":841,"nm":"SUW_WelcomeScreen_FoldableOpen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.949},"o":{"x":0.167,"y":0.167},"t":0,"s":[181.172,148.425,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.038},"t":95,"s":[181.172,-68.377,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[181.172,148.425,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[21.6,21.6,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9.3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.927]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[458.803]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.05]},"t":95,"s":[536.803]},{"t":180,"s":[458.803]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[707.143]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.069]},"t":95,"s":[639.643]},{"t":180,"s":[707.143]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-30,30,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[350.5,420.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw720dp-land/all_set_page_bg.json b/quickstep/res/raw-sw720dp-land/all_set_page_bg.json
deleted file mode 100644
index a994b0f..0000000
--- a/quickstep/res/raw-sw720dp-land/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":1280,"h":800,"nm":"SUW_WelcomeScreen_Tablet_Landscape_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,540,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[375.832,-1006.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[375.832,-1811,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[375.832,-1006.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[110,110,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[57]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[75]},{"t":180,"s":[57]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2618]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[2442]},{"t":180,"s":[2618]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[891]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[694]},{"t":180,"s":[891]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[120,120,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw720dp/all_set_page_bg.json b/quickstep/res/raw-sw720dp/all_set_page_bg.json
deleted file mode 100644
index 1030ffa..0000000
--- a/quickstep/res/raw-sw720dp/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":800,"h":1280,"nm":"SUW_WelcomeScreen_Tablet_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,528,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[999.832,-2238.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[999.832,-3043,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[999.832,-2238.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[200,200,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-39]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[-21]},{"t":180,"s":[-39]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1490]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[1314]},{"t":180,"s":[1490]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2967]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[2770]},{"t":180,"s":[2967]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[168,168,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-w600dp-h900dp/all_set_page_bg.json b/quickstep/res/raw-w600dp-h900dp/all_set_page_bg.json
new file mode 100644
index 0000000..b1a3bbe
--- /dev/null
+++ b/quickstep/res/raw-w600dp-h900dp/all_set_page_bg.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":800,"h":1280,"nm":"SUW_WelcomeScreen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,528,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[999.832,-2238.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[999.832,-3043,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[999.832,-2238.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[200,200,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-39]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[-21]},{"t":180,"s":[-39]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1490]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[1314]},{"t":180,"s":[1490]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2967]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[2770]},{"t":180,"s":[2967]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[168,168,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json b/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json
index ae1b560..81de7a2 100644
--- a/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json
+++ b/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json
@@ -1 +1 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":841,"h":701,"nm":"SUW_WelcomeScreen_FelixOpen_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.939},"o":{"x":0.167,"y":0.167},"t":0,"s":[140.975,228.318,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.045},"t":95,"s":[140.975,47.65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[140.975,228.318,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[18,18,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[11.111],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[11.111],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.913]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[639]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.06]},"t":95,"s":[704]},{"t":180,"s":[639]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.12]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[527.25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.083]},"t":95,"s":[471]},{"t":180,"s":[527.25]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-25,25,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[6],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[6],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420.5,350.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
+{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":1280,"h":800,"nm":"SUW_WelcomeScreen_Landscape_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,540,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[375.832,-1006.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95, "s":[375.832,-1811,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[375.832,-1006.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[110,110,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[57]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[75]},{"t":180,"s":[57]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2618]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[2442]},{"t":180,"s":[2618]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[891]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[694]},{"t":180,"s":[891]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[120,120,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-land/all_set_page_bg.json b/quickstep/res/raw-w840dp/all_set_page_bg.json
similarity index 100%
rename from quickstep/res/raw-land/all_set_page_bg.json
rename to quickstep/res/raw-w840dp/all_set_page_bg.json
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index eedb29e..b84f646 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -87,7 +87,7 @@
<string name="gesture_tutorial_try_again" msgid="65962545858556697">"Try again"</string>
<string name="gesture_tutorial_nice" msgid="2936275692616928280">"Nice!"</string>
<string name="gesture_tutorial_step" msgid="1279786122817620968">"Tutorial <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
- <string name="allset_title" msgid="5021126669778966707">"Ready!"</string>
+ <string name="allset_title" msgid="5021126669778966707">"All set!"</string>
<string name="allset_hint" msgid="459504134589971527">"Swipe up to go home"</string>
<string name="allset_button_hint" msgid="2395219947744706291">"Tap the home button to go to your home screen"</string>
<string name="allset_description_generic" msgid="5385500062202019855">"You’re ready to start using your <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index eedb29e..b84f646 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -87,7 +87,7 @@
<string name="gesture_tutorial_try_again" msgid="65962545858556697">"Try again"</string>
<string name="gesture_tutorial_nice" msgid="2936275692616928280">"Nice!"</string>
<string name="gesture_tutorial_step" msgid="1279786122817620968">"Tutorial <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
- <string name="allset_title" msgid="5021126669778966707">"Ready!"</string>
+ <string name="allset_title" msgid="5021126669778966707">"All set!"</string>
<string name="allset_hint" msgid="459504134589971527">"Swipe up to go home"</string>
<string name="allset_button_hint" msgid="2395219947744706291">"Tap the home button to go to your home screen"</string>
<string name="allset_description_generic" msgid="5385500062202019855">"You’re ready to start using your <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index eedb29e..b84f646 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -87,7 +87,7 @@
<string name="gesture_tutorial_try_again" msgid="65962545858556697">"Try again"</string>
<string name="gesture_tutorial_nice" msgid="2936275692616928280">"Nice!"</string>
<string name="gesture_tutorial_step" msgid="1279786122817620968">"Tutorial <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
- <string name="allset_title" msgid="5021126669778966707">"Ready!"</string>
+ <string name="allset_title" msgid="5021126669778966707">"All set!"</string>
<string name="allset_hint" msgid="459504134589971527">"Swipe up to go home"</string>
<string name="allset_button_hint" msgid="2395219947744706291">"Tap the home button to go to your home screen"</string>
<string name="allset_description_generic" msgid="5385500062202019855">"You’re ready to start using your <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml
index b296080..bafc2d5 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -89,7 +89,7 @@
<string name="gesture_tutorial_step" msgid="1279786122817620968">"آموزش گامبهگام <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="allset_title" msgid="5021126669778966707">"همه چیز آماده است!"</string>
<string name="allset_hint" msgid="459504134589971527">"برای رفتن به صفحه اصلی، تند بهبالا بکشید"</string>
- <string name="allset_button_hint" msgid="2395219947744706291">"برای رفتن به صفحه اصلی، روی دکمه صفحه اصلی ضربه بزنید"</string>
+ <string name="allset_button_hint" msgid="2395219947744706291">"برای رفتن به صفحه اصلی، روی دکمه صفحه اصلی تکضرب بزنید"</string>
<string name="allset_description_generic" msgid="5385500062202019855">"آمادهاید از <xliff:g id="DEVICE">%1$s</xliff:g> خود استفاده کنید"</string>
<string name="default_device_name" msgid="6660656727127422487">"دستگاه"</string>
<string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"تنظیمات پیمایش سیستم"</annotation></string>
diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index edfb59e..9510494 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -23,34 +23,34 @@
<string name="recent_task_option_freeform" msgid="48863056265284071">"Forme libre"</string>
<string name="recent_task_option_desktop" msgid="8280879717125435668">"Ordinateur de bureau"</string>
<string name="recents_empty_message" msgid="7040467240571714191">"Aucun élément récent"</string>
- <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Paramètres d\'utilisation de l\'application"</string>
+ <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Paramètres d\'utilisation de l\'appli"</string>
<string name="recents_clear_all" msgid="5328176793634888831">"Tout effacer"</string>
- <string name="accessibility_recent_apps" msgid="4058661986695117371">"Applications récentes"</string>
+ <string name="accessibility_recent_apps" msgid="4058661986695117371">"Applis récentes"</string>
<string name="task_view_closed" msgid="9170038230110856166">"Tâche fermée"</string>
<string name="task_contents_description_with_remaining_time" msgid="4479688746574672685">"<xliff:g id="TASK_DESCRIPTION">%1$s</xliff:g> : <xliff:g id="REMAINING_TIME">%2$s</xliff:g>"</string>
<string name="shorter_duration_less_than_one_minute" msgid="4722015666335015336">"< 1 min"</string>
<string name="time_left_for_app" msgid="3111996412933644358">"Il reste <xliff:g id="TIME">%1$s</xliff:g> aujourd\'hui"</string>
- <string name="title_app_suggestions" msgid="4185902664111965088">"Suggestions d\'applications"</string>
- <string name="all_apps_prediction_tip" msgid="2672336544844936186">"Vos prédictions d\'applications"</string>
- <string name="hotseat_edu_title_migrate" msgid="306578144424489980">"Obtenir des suggestions d\'applications dans la rangée du bas de votre écran d\'accueil"</string>
- <string name="hotseat_edu_title_migrate_landscape" msgid="3633942953997845243">"Retrouvez des suggestions d\'applications dans la rangée des favoris de votre écran d\'accueil"</string>
- <string name="hotseat_edu_message_migrate" msgid="8927179260533775320">"Accédez facilement aux applications que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applications dans la rangée du bas seront déplacées vers votre écran d\'accueil."</string>
- <string name="hotseat_edu_message_migrate_landscape" msgid="4248943380443387697">"Accédez facilement aux applications que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applications dans la rangée des favoris seront déplacées vers votre écran d\'accueil."</string>
- <string name="hotseat_edu_accept" msgid="1611544083278999837">"Obtenir des suggestions d\'applications"</string>
+ <string name="title_app_suggestions" msgid="4185902664111965088">"Suggestions d\'applis"</string>
+ <string name="all_apps_prediction_tip" msgid="2672336544844936186">"Vos prédictions d\'applis"</string>
+ <string name="hotseat_edu_title_migrate" msgid="306578144424489980">"Obtenir des suggestions d\'applis dans la rangée du bas de votre écran d\'accueil"</string>
+ <string name="hotseat_edu_title_migrate_landscape" msgid="3633942953997845243">"Retrouvez des suggestions d\'applis dans la rangée des favoris de votre écran d\'accueil"</string>
+ <string name="hotseat_edu_message_migrate" msgid="8927179260533775320">"Accédez facilement aux applis que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applis dans la rangée du bas seront déplacées vers votre écran d\'accueil."</string>
+ <string name="hotseat_edu_message_migrate_landscape" msgid="4248943380443387697">"Accédez facilement aux applis que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applis dans la rangée des favoris seront déplacées vers votre écran d\'accueil."</string>
+ <string name="hotseat_edu_accept" msgid="1611544083278999837">"Obtenir des suggestions d\'applis"</string>
<string name="hotseat_edu_dismiss" msgid="2781161822780201689">"Non merci"</string>
<string name="hotseat_prediction_settings" msgid="6246554993566070818">"Paramètres"</string>
- <string name="hotseat_auto_enrolled" msgid="522100018967146807">"Les applications les plus utilisées s\'affichent ici et changent en fonction des habitudes"</string>
- <string name="hotseat_tip_no_empty_slots" msgid="1325212677738179185">"Faites glisser des applications hors de la rangée du bas pour obtenir des suggestions d\'applications"</string>
- <string name="hotseat_tip_gaps_filled" msgid="3035673010274223538">"Applications suggérées ajoutées à l\'espace vide"</string>
- <string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"Les suggestions d\'applications sont activées"</string>
- <string name="hotsaet_tip_prediction_disabled" msgid="1506426298884658491">"Les suggestions d\'applications sont désactivées"</string>
- <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Application prédite : <xliff:g id="TITLE">%1$s</xliff:g>"</string>
+ <string name="hotseat_auto_enrolled" msgid="522100018967146807">"Les applis les plus utilisées s\'affichent ici et changent en fonction des habitudes"</string>
+ <string name="hotseat_tip_no_empty_slots" msgid="1325212677738179185">"Faites glisser des applis hors de la rangée du bas pour obtenir des suggestions d\'applis"</string>
+ <string name="hotseat_tip_gaps_filled" msgid="3035673010274223538">"Applis suggérées ajoutées à l\'espace vide"</string>
+ <string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"Les suggestions d\'applis sont activées"</string>
+ <string name="hotsaet_tip_prediction_disabled" msgid="1506426298884658491">"Les suggestions d\'applis sont désactivées"</string>
+ <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Appli prédite : <xliff:g id="TITLE">%1$s</xliff:g>"</string>
<string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Faites pivoter votre appareil"</string>
<string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Veuillez faire pivoter votre appareil pour terminer le tutoriel de navigation par gestes"</string>
<string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Assurez-vous de balayer l\'écran à partir de l\'extrémité droite ou gauche"</string>
<string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Assurez-vous de balayer l\'écran à partir de l\'extrémité droite ou gauche vers le centre, puis allons-y"</string>
<string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Vous avez appris à balayer de la droite pour revenir en arrière. Apprenez comment changer d\'appli."</string>
- <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Vous avez appris le geste de retour en arrière. Maintenant, apprenez comment changer d\'application."</string>
+ <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Vous avez appris le geste de retour en arrière. Maintenant, apprenez comment changer d\'appli."</string>
<string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Vous avez appris le geste de retour en arrière"</string>
<string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Assurez-vous de ne pas balayer trop près du bas de l\'écran"</string>
<string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Modifiez la sensibilité du geste de retour dans Paramètres"</string>
@@ -74,11 +74,11 @@
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Essayez de tenir la fenêtre plus longtemps avant de relâcher"</string>
<string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Assurez-vous de balayer l\'écran vers le haut, puis de faire une pause"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Vous avez appris à utiliser les gestes. Pour les désactiver, accédez au menu Paramètres."</string>
- <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Vous avez appris le geste de changement d\'application"</string>
- <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Balayez pour basculer entre les applications"</string>
- <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Pour changer d\'application, balayez l\'écran de bas en haut, maintenez le doigt dessus, puis relâchez-le."</string>
+ <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Vous avez appris le geste de changement d\'appli"</string>
+ <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Balayez pour basculer entre les applis"</string>
+ <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Pour changer d\'appli, balayez l\'écran de bas en haut, maintenez le doigt dessus, puis relâchez-le."</string>
<string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Pour changer d\'appli, balayez l\'écran de bas en haut avec deux doigts, maintenez-les et relâchez-les."</string>
- <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Changer d\'application"</string>
+ <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Changer d\'appli"</string>
<string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Balayez l\'écran de bas en haut, maintenez le doigt en place, puis relâchez-le"</string>
<string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Bien joué!"</string>
<string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Terminé"</string>
@@ -98,14 +98,14 @@
<string name="action_split" msgid="2098009717623550676">"Partager"</string>
<string name="action_save_app_pair" msgid="5974823919237645229">"Enr. paire d\'applis"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Toucher une autre appli pour partager l\'écran"</string>
- <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choisir une autre application pour utiliser l\'Écran divisé"</string>
+ <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choisir une autre appli pour utiliser l\'Écran divisé"</string>
<string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Annuler"</b></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="split_widgets_not_supported" msgid="1355743038053053866">"Les widgets ne sont actuellement pas pris en charge. Veuillez sélectionner une autre application"</string>
+ <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choisir une autre appli pour utiliser l\'écran partagé"</string>
+ <string name="blocked_by_policy" msgid="2071401072261365546">"L\'appli ou votre organisation n\'autorise pas cette action"</string>
+ <string name="split_widgets_not_supported" msgid="1355743038053053866">"Les widgets ne sont actuellement pas pris en charge. Veuillez sélectionner une autre appli"</string>
<string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorer le tutoriel sur la navigation?"</string>
- <string name="skip_tutorial_dialog_subtitle" msgid="544063326241955662">"Vous trouverez le tutoriel dans l\'application <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="skip_tutorial_dialog_subtitle" msgid="544063326241955662">"Vous trouverez le tutoriel dans l\'appli <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="gesture_tutorial_action_button_label_cancel" msgid="3809842569351264108">"Annuler"</string>
<string name="gesture_tutorial_action_button_label_skip" msgid="394452764989751960">"Ignorer"</string>
<string name="accessibility_rotate_button" msgid="4771825231336502943">"Faire pivoter l\'écran"</string>
@@ -137,7 +137,7 @@
<string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Séparateur de la barre des tâches"</string>
<string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Déplacer vers le coin supérieur gauche de l\'écran"</string>
<string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer vers le coin inférieur droit de l\'écran"</string>
- <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre application.}one{Afficher # autre application.}other{Afficher # autres applications.}}"</string>
+ <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre appli.}one{Afficher # autre appli.}other{Afficher # autres applis.}}"</string>
<string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Afficher # appli de bureau.}one{Afficher # appli de bureau.}other{Afficher # applis de bureau.}}"</string>
<string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> et <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
<string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bulle"</string>
diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml
index 1f4275a..c27b7f8 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -141,7 +141,7 @@
<string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{데스크톱 앱 #개를 표시합니다.}other{데스크톱 앱 #개를 표시합니다.}}"</string>
<string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> 및 <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
<string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"풍선"</string>
- <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"오버플로"</string>
+ <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"더보기"</string>
<string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>의 <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> 외 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>개"</string>
</resources>
diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml
index 7a4c7e9..fe2e4a4 100644
--- a/quickstep/res/values-mn/strings.xml
+++ b/quickstep/res/values-mn/strings.xml
@@ -141,7 +141,7 @@
<string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Компьютерын # аппыг харуулна уу.}other{Компьютерын # аппыг харуулна уу.}}"</string>
<string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> болон <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
<string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Бөмбөлөг"</string>
- <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Урт цэс"</string>
+ <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"Илүү хэсэг"</string>
<string name="bubble_bar_bubble_description" msgid="1882466152448446446">"<xliff:g id="APP_NAME">%2$s</xliff:g>-с ирсэн <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g> болон бусад <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g>"</string>
</resources>
diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml
index dd3d16e..6aa755a 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -94,7 +94,7 @@
<string name="default_device_name" msgid="6660656727127422487">"enheten"</string>
<string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Innstillinger for systemnavigasjon"</annotation></string>
<string name="action_share" msgid="2648470652637092375">"Del"</string>
- <string name="action_screenshot" msgid="8171125848358142917">"Skjermdump"</string>
+ <string name="action_screenshot" msgid="8171125848358142917">"Skjermbilde"</string>
<string name="action_split" msgid="2098009717623550676">"Del opp"</string>
<string name="action_save_app_pair" msgid="5974823919237645229">"Lagre apptilkobling"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Trykk på en annen app for å bruke delt skjerm"</string>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index 47498cc..e4d07bd 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -113,7 +113,7 @@
<string name="taskbar_edu_splitscreen" msgid="5605512479258053350">"Arraste uma app para o lado para usar 2 apps em simultâneo"</string>
<string name="taskbar_edu_stashing" msgid="5645461372669217294">"Deslize lentamente para cima para ver a Barra de tarefas"</string>
<string name="taskbar_edu_suggestions" msgid="8215044496435527982">"Receba sugestões de apps baseadas na sua rotina"</string>
- <string name="taskbar_edu_pinning" msgid="6708550858580071558">"Mantenha o divisor premido para fixar a Barra de tarefas"</string>
+ <string name="taskbar_edu_pinning" msgid="6708550858580071558">"Mantenha o divisor pressionado para fixar a Barra de tarefas"</string>
<string name="taskbar_edu_features" msgid="3320337287472848162">"Faça mais com a Barra de tarefas"</string>
<string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Mostre sempre a Barra de tarefas"</string>
<string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Para mostrar sempre a Barra de tarefas no fundo do ecrã, toque sem soltar no divisor"</string>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index a89227e..79ea299 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -141,7 +141,7 @@
<string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{显示 # 款桌面应用。}other{显示 # 款桌面应用。}}"</string>
<string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g>和<xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
<string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"气泡框"</string>
- <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"菜单"</string>
+ <string name="bubble_bar_overflow_description" msgid="8617628132733151708">"溢出式气泡框"</string>
<string name="bubble_bar_bubble_description" msgid="1882466152448446446">"来自“<xliff:g id="APP_NAME">%2$s</xliff:g>”的<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_bar_description_multiple_bubbles" msgid="3922207715357143648">"<xliff:g id="BUBBLE_BAR_BUBBLE_DESCRIPTION">%1$s</xliff:g>以及另外 <xliff:g id="BUBBLE_COUNT">%2$d</xliff:g> 个"</string>
</resources>
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 15180ef..d973149 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -238,5 +238,12 @@
@Override
@UiThread
default void onAnimationCancelled() {}
+
+ /**
+ * Returns whether this animation factory supports a tightly coupled return animation.
+ */
+ default boolean supportsReturnTransition() {
+ return false;
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index fae281a..5a74f4a 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -43,6 +43,7 @@
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
+import static com.android.launcher3.Flags.enableContainerReturnAnimations;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
@@ -68,6 +69,7 @@
import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
import static com.android.quickstep.util.AnimUtils.clampToDuration;
import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
+import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary;
import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
@@ -181,6 +183,9 @@
*/
public class QuickstepTransitionManager implements OnDeviceProfileChangeListener {
+ private static final String TRANSITION_COOKIE_PREFIX =
+ "com.android.launcher3.QuickstepTransitionManager_activityLaunch";
+
private static final boolean ENABLE_SHELL_STARTING_SURFACE =
SystemProperties.getBoolean("persist.debug.shell_starting_surface", true);
@@ -333,17 +338,7 @@
restartedListener.register(onEndCallback::executeAllAndDestroy);
onEndCallback.add(restartedListener::unregister);
- mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback);
- ItemInfo tag = (ItemInfo) v.getTag();
- if (tag != null && tag.shouldUseBackgroundAnimation()) {
- ContainerAnimationRunner containerAnimationRunner = ContainerAnimationRunner.from(
- v, mLauncher, mStartingWindowListener, onEndCallback);
- if (containerAnimationRunner != null) {
- mAppLaunchRunner = containerAnimationRunner;
- }
- }
- RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(
- mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */);
+ RemoteAnimationRunnerCompat runner = createAppLaunchRunner(v, onEndCallback);
// Note that this duration is a guess as we do not know if the animation will be a
// recents launch or not for sure until we know the opening app targets.
@@ -360,10 +355,95 @@
IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
+
+ IBinder cookie = mAppLaunchRunner.supportsReturnTransition()
+ ? ((ContainerAnimationRunner) mAppLaunchRunner).getCookie() : null;
+ addLaunchCookie(cookie, (ItemInfo) v.getTag(), options);
+
+ // Register the return animation so it can be triggered on back from the app to home.
+ maybeRegisterAppReturnTransition(v);
+
return new ActivityOptionsWrapper(options, onEndCallback);
}
/**
+ * Selects the appropriate type of launch runner for the given view, builds it, and returns it.
+ * {@link QuickstepTransitionManager#mAppLaunchRunner} is updated as a by-product of this
+ * method.
+ */
+ private RemoteAnimationRunnerCompat createAppLaunchRunner(View v, RunnableList onEndCallback) {
+ ItemInfo tag = (ItemInfo) v.getTag();
+ ContainerAnimationRunner containerRunner = null;
+ if (tag != null && tag.shouldUseBackgroundAnimation()) {
+ // The cookie should only override the default used by launcher if container return
+ // animations are enabled.
+ ActivityTransitionAnimator.TransitionCookie cookie =
+ checkReturnAnimationsFlags()
+ ? new ActivityTransitionAnimator.TransitionCookie(
+ TRANSITION_COOKIE_PREFIX + tag.id)
+ : null;
+ ContainerAnimationRunner launchAnimationRunner =
+ ContainerAnimationRunner.fromView(
+ v, cookie, true /* forLaunch */, mLauncher, mStartingWindowListener,
+ onEndCallback);
+
+ if (launchAnimationRunner != null) {
+ containerRunner = launchAnimationRunner;
+ }
+ }
+
+ mAppLaunchRunner = containerRunner != null
+ ? containerRunner : new AppLaunchAnimationRunner(v, onEndCallback);
+ return new LauncherAnimationRunner(
+ mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */);
+ }
+
+ /**
+ * If container return animations are enabled and the current launch runner is itself a
+ * {@link ContainerAnimationRunner}, registers a matching return animation that de-registers
+ * itself after it has run once or is made obsolete by the view going away.
+ */
+ private void maybeRegisterAppReturnTransition(View v) {
+ if (!checkReturnAnimationsFlags() || !mAppLaunchRunner.supportsReturnTransition()) {
+ return;
+ }
+
+ ActivityTransitionAnimator.TransitionCookie cookie =
+ ((ContainerAnimationRunner) mAppLaunchRunner).getCookie();
+ RunnableList onEndCallback = new RunnableList();
+ ContainerAnimationRunner runner =
+ ContainerAnimationRunner.fromView(
+ v, cookie, false /* forLaunch */, mLauncher, mStartingWindowListener,
+ onEndCallback);
+ RemoteTransition transition =
+ new RemoteTransition(
+ new LauncherAnimationRunner(
+ mHandler, runner, true /* startAtFrontOfQueue */
+ ).toRemoteTransition()
+ );
+
+ SystemUiProxy.INSTANCE.get(mLauncher).registerRemoteTransition(
+ transition, ContainerAnimationRunner.buildBackToHomeFilter(cookie, mLauncher));
+ ContainerAnimationRunner.setUpRemoteAnimationCleanup(
+ v, transition, onEndCallback, mLauncher);
+ }
+
+ /**
+ * Adds a new launch cookie for the activity launch if supported.
+ * Prioritizes the explicitly provided cookie, falling back on extracting one from the given
+ * {@link ItemInfo} if necessary.
+ */
+ private void addLaunchCookie(IBinder cookie, ItemInfo info, ActivityOptions options) {
+ if (cookie == null) {
+ cookie = mLauncher.getLaunchCookie(info);
+ }
+
+ if (cookie != null) {
+ options.setLaunchCookie(cookie);
+ }
+ }
+
+ /**
* Whether the launch is a recents app transition and we should do a launch animation
* from the recents view. Note that if the remote animation targets are not provided, this
* may not always be correct as we may resolve the opening app to a task when the animation
@@ -1728,6 +1808,10 @@
}
}
+ private static boolean checkReturnAnimationsFlags() {
+ return enableContainerReturnAnimations() && returnAnimationFrameworkLibrary();
+ }
+
/**
* Remote animation runner for animation from the app to Launcher, including recents.
*/
@@ -1844,38 +1928,45 @@
/** The delegate runner that handles the actual animation. */
private final RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> mDelegate;
+ @Nullable
+ private final ActivityTransitionAnimator.TransitionCookie mCookie;
+
private ContainerAnimationRunner(
- RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> delegate) {
+ RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> delegate,
+ ActivityTransitionAnimator.TransitionCookie cookie) {
mDelegate = delegate;
+ mCookie = cookie;
}
@Nullable
- private static ContainerAnimationRunner from(View v, Launcher launcher,
- StartingWindowListener startingWindowListener, RunnableList onEndCallback) {
- View viewToUse = findLaunchableViewWithBackground(v);
- if (viewToUse == null) {
- return null;
+ ActivityTransitionAnimator.TransitionCookie getCookie() {
+ return mCookie;
+ }
+
+ @Nullable
+ static ContainerAnimationRunner fromView(
+ View v,
+ ActivityTransitionAnimator.TransitionCookie cookie,
+ boolean forLaunch,
+ Launcher launcher,
+ StartingWindowListener startingWindowListener,
+ RunnableList onEndCallback) {
+ if (!forLaunch && !checkReturnAnimationsFlags()) {
+ throw new IllegalStateException(
+ "forLaunch cannot be false when the enableContainerReturnAnimations or "
+ + "returnAnimationFrameworkLibrary flag is disabled");
}
- // The CUJ is logged by the click handler, so we don't log it inside the animation
- // library.
- ActivityTransitionAnimator.Controller controllerDelegate =
- ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */);
-
- if (controllerDelegate == null) {
- return null;
- }
-
- // This wrapper allows us to override the default value, telling the controller that the
- // current window is below the animating window.
+ // First the controller is created. This is used by the runner to animate the
+ // origin/target view.
ActivityTransitionAnimator.Controller controller =
- new DelegateTransitionAnimatorController(controllerDelegate) {
- @Override
- public boolean isBelowAnimatingWindow() {
- return true;
- }
- };
+ buildController(v, cookie, forLaunch);
+ if (controller == null) {
+ return null;
+ }
+ // The callback is used to make sure that we use the right color to fade between view
+ // and the window.
ActivityTransitionAnimator.Callback callback = task -> {
final int backgroundColor =
startingWindowListener.mBackgroundColor == Color.TRANSPARENT
@@ -1894,7 +1985,52 @@
return new ContainerAnimationRunner(
new ActivityTransitionAnimator.AnimationDelegate(
- MAIN_EXECUTOR, controller, callback, listener));
+ MAIN_EXECUTOR, controller, callback, listener),
+ cookie);
+ }
+
+ /**
+ * Constructs a {@link ActivityTransitionAnimator.Controller} that can be used by a
+ * {@link ContainerAnimationRunner} to animate a view into an opening window or from a
+ * closing one.
+ */
+ @Nullable
+ private static ActivityTransitionAnimator.Controller buildController(
+ View v, ActivityTransitionAnimator.TransitionCookie cookie, boolean isLaunching) {
+ View viewToUse = findLaunchableViewWithBackground(v);
+ if (viewToUse == null) {
+ return null;
+ }
+
+ // The CUJ is logged by the click handler, so we don't log it inside the animation
+ // library. TODO: figure out return CUJ.
+ ActivityTransitionAnimator.Controller controllerDelegate =
+ ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */);
+
+ if (controllerDelegate == null) {
+ return null;
+ }
+
+ // This wrapper allows us to override the default value, telling the controller that the
+ // current window is below the animating window as well as information about the return
+ // animation.
+ return new DelegateTransitionAnimatorController(controllerDelegate) {
+ @Override
+ public boolean isLaunching() {
+ return isLaunching;
+ }
+
+ @Override
+ public boolean isBelowAnimatingWindow() {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public ActivityTransitionAnimator.TransitionCookie getTransitionCookie() {
+ return cookie;
+ }
+ };
}
/**
@@ -1916,6 +2052,67 @@
return (T) current;
}
+ /**
+ * Builds the filter used by WM Shell to match app closing transitions (only back, no home
+ * button/gesture) to the given launch cookie.
+ */
+ static TransitionFilter buildBackToHomeFilter(
+ ActivityTransitionAnimator.TransitionCookie cookie, Launcher launcher) {
+ // Closing activity must include the cookie in its list of launch cookies.
+ TransitionFilter.Requirement appRequirement = new TransitionFilter.Requirement();
+ appRequirement.mActivityType = ACTIVITY_TYPE_STANDARD;
+ appRequirement.mLaunchCookie = cookie;
+ appRequirement.mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+ // Opening activity must be Launcher.
+ TransitionFilter.Requirement launcherRequirement = new TransitionFilter.Requirement();
+ launcherRequirement.mActivityType = ACTIVITY_TYPE_HOME;
+ launcherRequirement.mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+ launcherRequirement.mTopActivity = launcher.getComponentName();
+ // Transition types CLOSE and TO_BACK match the back button/gesture but not the home
+ // button/gesture.
+ TransitionFilter filter = new TransitionFilter();
+ filter.mTypeSet = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+ filter.mRequirements =
+ new TransitionFilter.Requirement[]{appRequirement, launcherRequirement};
+ return filter;
+ }
+
+ /**
+ * Creates various conditions to ensure that the given transition is cleaned up correctly
+ * when necessary:
+ * - if the transition has run, it is the callback that unregisters it;
+ * - if the associated view is detached before the transition has had an opportunity to run,
+ * a {@link View.OnAttachStateChangeListener} allows us to do the same (and removes
+ * itself).
+ */
+ static void setUpRemoteAnimationCleanup(
+ View v, RemoteTransition transition, RunnableList callback, Launcher launcher) {
+ View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(@NonNull View v) {}
+
+ @Override
+ public void onViewDetachedFromWindow(@NonNull View v) {
+ SystemUiProxy.INSTANCE.get(launcher)
+ .unregisterRemoteTransition(transition);
+ v.removeOnAttachStateChangeListener(this);
+ }
+ };
+
+ // Remove the animation as soon as it has run once.
+ callback.add(() -> {
+ SystemUiProxy.INSTANCE.get(launcher).unregisterRemoteTransition(transition);
+ if (v != null) {
+ v.removeOnAttachStateChangeListener(listener);
+ }
+ });
+
+ // Remove the animation when the view is detached from the hierarchy.
+ // This is so that if back is not invoked (e.g. if we go back home through the home
+ // gesture) we don't have obsolete transitions staying registered.
+ v.addOnAttachStateChangeListener(listener);
+ }
+
@Override
public void onAnimationStart(int transit, RemoteAnimationTarget[] appTargets,
RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets,
@@ -1928,6 +2125,11 @@
public void onAnimationCancelled() {
mDelegate.onAnimationCancelled();
}
+
+ @Override
+ public boolean supportsReturnTransition() {
+ return true;
+ }
}
/**
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 7e52ea1..44d8a5c 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -20,6 +20,7 @@
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
+import static com.android.launcher3.Flags.enablePredictiveBackGesture;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -33,6 +34,9 @@
import android.view.View;
import android.view.WindowInsetsController;
import android.view.WindowManager;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
+import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -44,16 +48,17 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -79,6 +84,15 @@
*/
private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets";
/**
+ * Intent extra for the string representing the title displayed within the picker header.
+ */
+ private static final String EXTRA_PICKER_TITLE = "picker_title";
+ /**
+ * Intent extra for the string representing the description displayed within the picker header.
+ */
+ private static final String EXTRA_PICKER_DESCRIPTION = "picker_description";
+
+ /**
* A unique identifier of the surface hosting the widgets;
* <p>"widgets" is reserved for home screen surface.</p>
* <p>"widgets_hub" is reserved for glanceable hub surface.</p>
@@ -86,6 +100,12 @@
private static final String EXTRA_UI_SURFACE = "ui_surface";
private static final Pattern UI_SURFACE_PATTERN =
Pattern.compile("^(widgets|widgets_hub)$");
+
+ /**
+ * User ids that should be filtered out of the widget lists created by this activity.
+ */
+ private static final String EXTRA_USER_ID_FILTER = "filtered_user_ids";
+
private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
private WidgetsModel mModel;
private LauncherAppState mApp;
@@ -100,6 +120,16 @@
// Widgets existing on the host surface.
@NonNull
private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>();
+ @Nullable
+ private String mTitle;
+ @Nullable
+ private String mDescription;
+
+ /** A set of user ids that should be filtered out from the selected widgets. */
+ @NonNull
+ Set<Integer> mFilteredUserIds = new HashSet<>();
+ @Nullable
+ private WidgetsFullSheet mWidgetSheet;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -120,15 +150,26 @@
WindowInsetsController wc = mDragLayer.getWindowInsetsController();
wc.hide(navigationBars() + statusBars());
- BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true);
- widgetSheet.disableNavBarScrim(true);
- widgetSheet.addOnCloseListener(this::finish);
-
parseIntentExtras();
refreshAndBindWidgets();
}
+ @Override
+ protected void registerBackDispatcher() {
+ if (!enablePredictiveBackGesture()) {
+ super.registerBackDispatcher();
+ return;
+ }
+
+ getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+ new BackAnimationCallback());
+ }
+
private void parseIntentExtras() {
+ mTitle = getIntent().getStringExtra(EXTRA_PICKER_TITLE);
+ mDescription = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION);
+
// A value of 0 for either size means that no filtering will occur in that dimension. If
// both values are 0, then no size filtering will occur.
mDesiredWidgetWidth =
@@ -149,6 +190,12 @@
if (addedWidgets != null) {
mAddedWidgets = addedWidgets;
}
+ ArrayList<Integer> filteredUsers = getIntent().getIntegerArrayListExtra(
+ EXTRA_USER_ID_FILTER);
+ mFilteredUserIds.clear();
+ if (filteredUsers != null) {
+ mFilteredUserIds.addAll(filteredUsers);
+ }
}
@NonNull
@@ -224,7 +271,8 @@
};
}
- /** Updates the model with widgets and provides them after applying the provided filter. */
+ /** Updates the model with widgets, applies filters and launches the widgets sheet once
+ * widgets are available */
private void refreshAndBindWidgets() {
MODEL_EXECUTOR.execute(() -> {
LauncherAppState app = LauncherAppState.getInstance(this);
@@ -240,6 +288,9 @@
}
);
bindWidgets(allWidgets);
+ // Open sheet once widgets are available, so that it doesn't interrupt the open
+ // animation.
+ openWidgetsSheet();
if (mUiSurface != null) {
Map<ComponentKey, WidgetItem> allWidgetItems = allWidgets.stream()
.filter(entry -> entry instanceof WidgetsListContentEntry)
@@ -260,8 +311,20 @@
MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
}
+ private void openWidgetsSheet() {
+ MAIN_EXECUTOR.execute(() -> {
+ mWidgetSheet = WidgetsFullSheet.show(this, true);
+ mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
+ mWidgetSheet.disableNavBarScrim(true);
+ mWidgetSheet.addOnCloseListener(this::finish);
+ });
+ }
+
private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
- MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets));
+ // Bind recommendations once picker has finished open animation.
+ MAIN_EXECUTOR.getHandler().postDelayed(
+ () -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets),
+ mDeviceProfile.bottomSheetOpenDuration);
}
@Override
@@ -272,12 +335,64 @@
}
}
+ /**
+ * Animation callback for different predictive back animation states for the widget picker.
+ */
+ private class BackAnimationCallback implements OnBackAnimationCallback {
+ @Nullable
+ OnBackAnimationCallback mActiveOnBackAnimationCallback;
+
+ @Override
+ public void onBackStarted(@NonNull BackEvent backEvent) {
+ if (mActiveOnBackAnimationCallback != null) {
+ mActiveOnBackAnimationCallback.onBackCancelled();
+ }
+ if (mWidgetSheet != null) {
+ mActiveOnBackAnimationCallback = mWidgetSheet;
+ mActiveOnBackAnimationCallback.onBackStarted(backEvent);
+ }
+ }
+
+ @Override
+ public void onBackInvoked() {
+ if (mActiveOnBackAnimationCallback == null) {
+ return;
+ }
+ mActiveOnBackAnimationCallback.onBackInvoked();
+ mActiveOnBackAnimationCallback = null;
+ }
+
+ @Override
+ public void onBackProgressed(@NonNull BackEvent backEvent) {
+ if (mActiveOnBackAnimationCallback == null) {
+ return;
+ }
+ mActiveOnBackAnimationCallback.onBackProgressed(backEvent);
+ }
+
+ @Override
+ public void onBackCancelled() {
+ if (mActiveOnBackAnimationCallback == null) {
+ return;
+ }
+ mActiveOnBackAnimationCallback.onBackCancelled();
+ mActiveOnBackAnimationCallback = null;
+ }
+ };
+
private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
final AppWidgetProviderInfo info = widget.widgetInfo;
if (info == null) {
return rejectWidget(widget, "shortcut");
}
+ if (mFilteredUserIds.contains(widget.user.getIdentifier())) {
+ return rejectWidget(
+ widget,
+ "widget user: %d is being filtered",
+ widget.user.getIdentifier());
+ }
+
if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
return rejectWidget(
widget,
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index 28bc01c..fb17f15 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -83,10 +83,8 @@
private final Handler mWorkerHandler;
private final ContentObserver mContentObserver;
- private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver =
- new SimpleBroadcastReceiver(t -> restartObserver());
- private final SimpleBroadcastReceiver mAppAddRemoveReceiver =
- new SimpleBroadcastReceiver(this::onAppPackageChanged);
+ private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver;
+ private final SimpleBroadcastReceiver mAppAddRemoveReceiver;
private final Object mModelLock = new Object();
// Maps the action Id to the corresponding RemoteAction
@@ -101,6 +99,11 @@
mWorkerHandler = new Handler(TextUtils.isEmpty(mWellbeingProviderPkg)
? Executors.UI_HELPER_EXECUTOR.getLooper()
: Executors.getPackageExecutor(mWellbeingProviderPkg).getLooper());
+ mWellbeingAppChangeReceiver =
+ new SimpleBroadcastReceiver(mWorkerHandler, t -> restartObserver());
+ mAppAddRemoveReceiver =
+ new SimpleBroadcastReceiver(mWorkerHandler, this::onAppPackageChanged);
+
mContentObserver = new ContentObserver(mWorkerHandler) {
@Override
@@ -135,8 +138,8 @@
public void close() {
if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
mWorkerHandler.post(() -> {
- mWellbeingAppChangeReceiver.unregisterReceiverSafelySync(mContext);
- mAppAddRemoveReceiver.unregisterReceiverSafelySync(mContext);
+ mWellbeingAppChangeReceiver.unregisterReceiverSafely(mContext);
+ mAppAddRemoveReceiver.unregisterReceiverSafely(mContext);
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
});
}
diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
index 5730273..8c98bab 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
@@ -59,12 +59,18 @@
public class WidgetPredictionsRequester {
private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20;
private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets";
+ // container/screenid/[positionx,positiony]/[spanx,spany]
+ // Matches the format passed used by PredictionHelper; But, position and size values aren't
+ // used, so, we pass default values.
+ @VisibleForTesting
+ static final String LAUNCH_LOCATION = "workspace/1/[0,0]/[2,2]";
@Nullable
private AppPredictor mAppPredictor;
private final Context mContext;
@NonNull
private final String mUiSurface;
+ private boolean mPredictionsAvailable;
@NonNull
private final Map<ComponentKey, WidgetItem> mAllWidgets;
@@ -76,8 +82,8 @@
}
/**
- * Requests predictions from the app predictions manager and registers the provided callback to
- * receive updates when predictions are available.
+ * Requests one time predictions from the app predictions manager and invokes provided callback
+ * once predictions are available.
*
* @param existingWidgets widgets that are currently added to the surface;
* @param callback consumer of prediction results to be called when predictions are
@@ -85,7 +91,7 @@
*/
public void request(List<AppWidgetProviderInfo> existingWidgets,
Consumer<List<ItemInfo>> callback) {
- Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface);
+ Bundle bundle = buildBundleForPredictionSession(existingWidgets);
Predicate<WidgetItem> filter = notOnUiSurfaceFilter(existingWidgets);
MODEL_EXECUTOR.execute(() -> {
@@ -111,17 +117,14 @@
* Returns a bundle that can be passed in a prediction session
*
* @param addedWidgets widgets that are already added by the user in the ui surface
- * @param uiSurface a unique identifier of the surface hosting widgets; format
- * "widgets_xx"; note - "widgets" is reserved for home screen surface.
*/
@VisibleForTesting
- static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets,
- String uiSurface) {
+ static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets) {
Bundle bundle = new Bundle();
ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>();
for (AppWidgetProviderInfo info : addedWidgets) {
ComponentName componentName = info.provider;
- AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName);
+ AppTargetEvent appTargetEvent = buildAppTargetEvent(info, componentName);
addedAppTargetEvents.add(appTargetEvent);
}
bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents);
@@ -133,13 +136,13 @@
* predictor.
* Also see {@link PredictionHelper}
*/
- private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info,
+ private static AppTargetEvent buildAppTargetEvent(AppWidgetProviderInfo info,
ComponentName componentName) {
AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName());
AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(),
/*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build();
- return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
- .setLaunchLocation(uiSurface).build();
+ return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN).setLaunchLocation(
+ LAUNCH_LOCATION).build();
}
/**
@@ -159,10 +162,14 @@
@WorkerThread
private void bindPredictions(List<AppTarget> targets, Predicate<WidgetItem> filter,
Consumer<List<ItemInfo>> callback) {
- List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, filter);
- List<ItemInfo> mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions);
+ if (!mPredictionsAvailable) {
+ mPredictionsAvailable = true;
+ List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, filter);
+ List<ItemInfo> mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions);
- MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions));
+ MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions));
+ MODEL_EXECUTOR.execute(this::clear);
+ }
}
/**
@@ -214,5 +221,6 @@
mAppPredictor.destroy();
mAppPredictor = null;
}
+ mPredictionsAvailable = false;
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 358d703..46501c4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -245,11 +245,20 @@
}
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
- mModel.getThumbnailCache().updateThumbnailInBackground(task, callback);
+ mModel.getThumbnailCache().getThumbnailInBackground(task,
+ thumbnailData -> {
+ task.thumbnail = thumbnailData;
+ callback.accept(thumbnailData);
+ });
}
void updateIconInBackground(Task task, Consumer<Task> callback) {
- mModel.getIconCache().updateIconInBackground(task, callback);
+ mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> {
+ task.icon = icon;
+ task.titleDescription = contentDescription;
+ task.title = title;
+ callback.accept(task);
+ });
}
void onCloseComplete() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 5d47212..0ba5de1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -46,6 +46,7 @@
import androidx.core.content.res.ResourcesCompat;
import com.android.app.animation.Interpolators;
+import com.android.internal.jank.Cuj;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
@@ -53,6 +54,7 @@
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import java.util.HashMap;
import java.util.List;
@@ -331,6 +333,8 @@
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
+ InteractionJankMonitorWrapper.begin(
+ KeyboardQuickSwitchView.this, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
setClipToPadding(false);
setOutlineProvider(new ViewOutlineProvider() {
@Override
@@ -366,12 +370,19 @@
}
@Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setClipToPadding(true);
setOutlineProvider(outlineProvider);
invalidateOutline();
mOpenAnimation = null;
+ InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
}
});
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 73819b3..d411ba6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -16,6 +16,7 @@
package com.android.launcher3.taskbar;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.view.KeyEvent;
import android.view.animation.AnimationUtils;
import android.window.RemoteTransition;
@@ -23,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.jank.Cuj;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
@@ -30,6 +32,7 @@
import com.android.quickstep.util.SlideInRemoteTransition;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import java.io.PrintWriter;
@@ -93,18 +96,28 @@
protected void closeQuickSwitchView(boolean animate) {
if (isCloseAnimationRunning()) {
- // Let currently-running animation finish.
if (!animate) {
mCloseAnimation.end();
}
+ // Let currently-running animation finish.
return;
}
if (!animate) {
+ InteractionJankMonitorWrapper.begin(
+ mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
onCloseComplete();
return;
}
mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation();
+ mCloseAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ InteractionJankMonitorWrapper.begin(
+ mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
+ }
+ });
mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
mCloseAnimation.start();
}
@@ -142,16 +155,26 @@
return -1;
}
+ Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin(
+ mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
+ Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
+ Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
TaskbarActivityContext context = mControllers.taskbarActivityContext;
RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
context.getDeviceProfile().overviewPageSpacing,
QuickStepContract.getWindowCornerRadius(context),
AnimationUtils.loadInterpolator(
- context, android.R.interpolator.fast_out_extra_slow_in)),
+ context, android.R.interpolator.fast_out_extra_slow_in),
+ onStartCallback,
+ onFinishCallback),
"SlideInTransition");
mControllers.taskbarActivityContext.handleGroupTaskLaunch(
- task, remoteTransition, mOnDesktop);
+ task,
+ remoteTransition,
+ mOnDesktop,
+ onStartCallback,
+ onFinishCallback);
return -1;
}
@@ -159,6 +182,7 @@
mCloseAnimation = null;
mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
mControllerCallbacks.onCloseComplete();
+ InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
}
protected void onDestroy() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 63e1e01..0fa3fbc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -41,6 +41,7 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -106,6 +107,7 @@
import com.android.systemui.shared.rotation.FloatingRotationButton;
import com.android.systemui.shared.rotation.RotationButton;
import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -154,6 +156,8 @@
public static final int ALPHA_INDEX_SUW = 2;
private static final int NUM_ALPHA_CHANNELS = 3;
+ private static final long AUTODIM_TIMEOUT_MS = 2250;
+
private final ArrayList<StatePropertyHolder> mPropertyHolders = new ArrayList<>();
private final ArrayList<ImageView> mAllButtons = new ArrayList<>();
private int mState;
@@ -162,6 +166,7 @@
private final @Nullable Context mNavigationBarPanelContext;
private final WindowManagerProxy mWindowManagerProxy;
private final NearestTouchFrame mNavButtonsView;
+ private final Handler mHandler;
private final LinearLayout mNavButtonContainer;
// Used for IME+A11Y buttons
private final ViewGroup mEndContextualContainer;
@@ -183,7 +188,7 @@
this::updateNavButtonInAppDisplayProgressForSysui);
/** Expected nav button dark intensity communicated via the framework. */
private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat(
- this::updateNavButtonColor);
+ this::onDarkIntensityChanged);
/** {@code 1} if the Taskbar background color is fully opaque. */
private final AnimatedFloat mOnTaskbarBackgroundNavButtonColorOverride = new AnimatedFloat(
this::updateNavButtonColor);
@@ -219,12 +224,19 @@
private ImageView mRecentsButton;
private Space mSpace;
+ private TaskbarTransitions mTaskbarTransitions;
+ private @BarTransitions.TransitionMode int mTransitionMode;
+
+ private final Runnable mAutoDim = () -> mTaskbarTransitions.setAutoDim(true);
+
public NavbarButtonsViewController(TaskbarActivityContext context,
- @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) {
+ @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView,
+ Handler handler) {
mContext = context;
mNavigationBarPanelContext = navigationBarPanelContext;
mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext);
mNavButtonsView = navButtonsView;
+ mHandler = handler;
mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons);
mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons);
@@ -234,6 +246,10 @@
mOnBackgroundIconColor = Utilities.isDarkTheme(context)
? context.getColor(R.color.taskbar_nav_icon_light_color)
: context.getColor(R.color.taskbar_nav_icon_dark_color);
+
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
+ }
}
/**
@@ -345,6 +361,9 @@
R.bool.floating_rotation_button_position_left);
mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
mRotationButtonListener);
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions.init();
+ }
applyState();
mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
@@ -605,6 +624,48 @@
mBackButton.setAccessibilityDelegate(accessibilityDelegate);
}
+ public void setWallpaperVisible(boolean isVisible) {
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions.setWallpaperVisibility(isVisible);
+ }
+ }
+
+ public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
+ mTransitionMode = barMode;
+ if (checkBarModes) {
+ checkNavBarModes();
+ }
+ }
+
+ public void checkNavBarModes() {
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
+ mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden);
+ }
+ }
+
+ public void finishBarAnimations() {
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions.finishAnimations();
+ }
+ }
+
+ public void touchAutoDim(boolean reset) {
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions.setAutoDim(false);
+ mHandler.removeCallbacks(mAutoDim);
+ if (reset) {
+ mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS);
+ }
+ }
+ }
+
+ public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) {
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions.transitionTo(barMode, animate);
+ }
+ }
+
/** Use to set the translationY for the all nav+contextual buttons */
public AnimatedFloat getTaskbarNavButtonTranslationY() {
return mTaskbarNavButtonTranslationY;
@@ -680,8 +741,7 @@
mDarkIconColorOnHome);
final int iconColor;
- if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && enableTaskbarOnPhones()
- && mContext.isPhoneMode()) {
+ if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode()) {
iconColor = sysUiNavButtonIconColorOnHome;
} else {
// Override the color from framework if nav buttons are over an opaque Taskbar surface.
@@ -703,6 +763,13 @@
}
}
+ private void onDarkIntensityChanged() {
+ updateNavButtonColor();
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
+ }
+ }
+
protected ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType,
ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) {
return addButton(drawableId, buttonType, parent, navButtonController, id,
@@ -1048,6 +1115,9 @@
+ mOnBackgroundNavButtonColorOverrideMultiplier.value);
mNavButtonsView.dumpLogs(prefix + "\t", pw);
+ if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+ mTaskbarTransitions.dumpLogs(prefix + "\t", pw);
+ }
}
private static String getStateString(int flags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 5020206..6d54abd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -43,6 +43,8 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
import static com.android.wm.shell.Flags.enableTinyTaskbar;
+import static java.lang.invoke.MethodHandles.Lookup.PROTECTED;
+
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.app.ActivityOptions;
@@ -93,6 +95,7 @@
import com.android.launcher3.model.data.AppPairInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.TaskItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.popup.PopupDataProvider;
@@ -139,6 +142,7 @@
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.systemui.unfold.updates.RotationChangeProvider;
@@ -275,7 +279,8 @@
mControllers = new TaskbarControllers(this,
new TaskbarDragController(this),
buttonController,
- new NavbarButtonsViewController(this, mNavigationBarPanelContext, navButtonsView),
+ new NavbarButtonsViewController(this, mNavigationBarPanelContext, navButtonsView,
+ getMainThreadHandler()),
rotationButtonController,
new TaskbarDragLayerController(this, mDragLayer),
new TaskbarViewController(this, taskbarView),
@@ -790,6 +795,11 @@
*/
public void setUIController(@NonNull TaskbarUIController uiController) {
mControllers.setUiController(uiController);
+ if (mControllers.bubbleControllers.isEmpty()) {
+ // if the bubble bar was visible in a previous configuration of taskbar and is being
+ // recreated now without bubbles, clean up any bubble bar adjustments from hotseat
+ bubbleBarVisibilityChanged(/* isVisible= */ false);
+ }
}
/**
@@ -799,6 +809,27 @@
mControllers.taskbarStashController.setSetupUIVisible(isVisible);
}
+ public void setWallpaperVisible(boolean isVisible) {
+ mControllers.navbarButtonsViewController.setWallpaperVisible(isVisible);
+ }
+
+ public void checkNavBarModes() {
+ mControllers.navbarButtonsViewController.checkNavBarModes();
+ }
+
+ public void finishBarAnimations() {
+ mControllers.navbarButtonsViewController.finishBarAnimations();
+ }
+
+ public void touchAutoDim(boolean reset) {
+ mControllers.navbarButtonsViewController.touchAutoDim(reset);
+ }
+
+ public void transitionTo(@BarTransitions.TransitionMode int barMode,
+ boolean animate) {
+ mControllers.navbarButtonsViewController.transitionTo(barMode, animate);
+ }
+
/**
* Called when this instance of taskbar is no longer needed
*/
@@ -876,6 +907,9 @@
mControllers.rotationButtonController.onBehaviorChanged(displayId, behavior);
}
+ public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
+ mControllers.navbarButtonsViewController.onTransitionModeUpdated(barMode, checkBarModes);
+ }
public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity()
.updateValue(darkIntensity);
@@ -1104,6 +1138,11 @@
mControllers.uiController.onTaskbarIconLaunched(api);
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
}
+ } else if (tag instanceof TaskItemInfo info) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId()));
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
+ /* stash= */ true);
} else if (tag instanceof WorkspaceItemInfo) {
// Tapping a launchable icon on Taskbar
WorkspaceItemInfo info = (WorkspaceItemInfo) tag;
@@ -1187,22 +1226,44 @@
}
}
+ public void handleGroupTaskLaunch(
+ GroupTask task,
+ @Nullable RemoteTransition remoteTransition,
+ boolean onDesktop) {
+ handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
+ }
+
/**
* Launches the given GroupTask with the following behavior:
* - If the GroupTask is a DesktopTask, launch the tasks in that Desktop.
* - If {@code onDesktop}, bring the given GroupTask to the front.
* - If the GroupTask is a single task, launch it via startActivityFromRecents.
* - Otherwise, we assume the GroupTask is a Split pair and launch them together.
+ * <p>
+ * Given start and/or finish callbacks, they will be run before an after the app launch
+ * respectively in cases where we can't use the remote transition, otherwise we will assume that
+ * these callbacks are included in the remote transition.
*/
- public void handleGroupTaskLaunch(GroupTask task, @Nullable RemoteTransition remoteTransition,
- boolean onDesktop) {
+ public void handleGroupTaskLaunch(
+ GroupTask task,
+ @Nullable RemoteTransition remoteTransition,
+ boolean onDesktop,
+ @Nullable Runnable onStartCallback,
+ @Nullable Runnable onFinishCallback) {
if (task instanceof DesktopTask) {
UI_HELPER_EXECUTOR.execute(() ->
SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
remoteTransition));
} else if (onDesktop) {
- UI_HELPER_EXECUTOR.execute(() ->
- SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id));
+ UI_HELPER_EXECUTOR.execute(() -> {
+ if (onStartCallback != null) {
+ onStartCallback.run();
+ }
+ SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id);
+ if (onFinishCallback != null) {
+ onFinishCallback.run();
+ }
+ });
} else if (task.task2 == null) {
UI_HELPER_EXECUTOR.execute(() -> {
ActivityOptions activityOptions =
@@ -1489,7 +1550,8 @@
return mIsNavBarKidsMode && isThreeButtonNav();
}
- protected boolean isNavBarForceVisible() {
+ @VisibleForTesting(otherwise = PROTECTED)
+ public boolean isNavBarForceVisible() {
return mIsNavBarForceVisible;
}
@@ -1623,6 +1685,10 @@
return mControllers.uiController.canToggleHomeAllApps();
}
+ boolean isIconAlignedWithHotseat() {
+ return mControllers.uiController.isIconAlignedWithHotseat();
+ }
+
@VisibleForTesting
public TaskbarControllers getControllers() {
return mControllers;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 58c5e83..0645972 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -282,6 +282,11 @@
}
uiController.dumpLogs(prefix + "\t", pw);
rotationButtonController.dumpLogs(prefix + "\t", pw);
+ if (bubbleControllers.isPresent()) {
+ bubbleControllers.get().dump(pw);
+ } else {
+ pw.println(String.format("%s\t%s", prefix, "Bubble controllers are empty."));
+ }
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index c45c667..7f9d8a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -27,6 +27,7 @@
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.animation.Interpolator
+import android.window.OnBackInvokedDispatcher
import androidx.core.view.updateLayoutParams
import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
@@ -66,11 +67,14 @@
/** Container where the tooltip's body should be inflated. */
lateinit var content: ViewGroup
private set
+
private lateinit var arrow: View
/** Callback invoked when the tooltip is being closed. */
var onCloseCallback: () -> Unit = {}
private var openCloseAnimator: AnimatorSet? = null
+ /** Used to set whether users can tap outside the current tooltip window to dismiss it */
+ var allowTouchDismissal = true
/** Animates the tooltip into view. */
fun show() {
@@ -134,14 +138,25 @@
override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0
override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
- if (ev?.action == ACTION_DOWN && !activityContext.dragLayer.isEventOverView(this, ev)) {
+ if (
+ ev?.action == ACTION_DOWN &&
+ !activityContext.dragLayer.isEventOverView(this, ev) &&
+ allowTouchDismissal
+ ) {
close(true)
}
return false
}
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ findOnBackInvokedDispatcher()
+ ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this)
+ }
+
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
+ findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this)
Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0)
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 5cbd5c9..d57c483 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -86,10 +86,13 @@
!activityContext.isPhoneMode &&
!activityContext.isTinyTaskbar
}
+
private val isOpen: Boolean
get() = tooltip?.isOpen ?: false
+
val isBeforeTooltipFeaturesStep: Boolean
get() = isTooltipEnabled && tooltipStep <= TOOLTIP_STEP_FEATURES
+
private lateinit var controllers: TaskbarControllers
// Keep track of whether the user has seen the Search Edu
@@ -152,6 +155,7 @@
tooltipStep = TOOLTIP_STEP_NONE
inflateTooltip(R.layout.taskbar_edu_features)
tooltip?.run {
+ allowTouchDismissal = false
val splitscreenAnim = requireViewById<LottieAnimationView>(R.id.splitscreen_animation)
val suggestionsAnim = requireViewById<LottieAnimationView>(R.id.suggestions_animation)
val pinningAnim = requireViewById<LottieAnimationView>(R.id.pinning_animation)
@@ -216,6 +220,7 @@
inflateTooltip(R.layout.taskbar_edu_pinning)
tooltip?.run {
+ allowTouchDismissal = true
requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
.supportLightTheme()
@@ -260,6 +265,7 @@
userHasSeenSearchEdu = true
inflateTooltip(R.layout.taskbar_edu_search)
tooltip?.run {
+ allowTouchDismissal = true
requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)
showDisclosureText(eduSubtitle)
@@ -332,7 +338,9 @@
}
/** Closes the current [tooltip]. */
- fun hide() = tooltip?.close(true)
+ fun hide() {
+ tooltip?.close(true)
+ }
/** Initializes [tooltip] with content from [contentResId]. */
private fun inflateTooltip(@LayoutRes contentResId: Int) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index 0443197..dd14109 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -40,6 +40,7 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.views.ArrowTipView;
@@ -73,6 +74,8 @@
} else if (mHoverView instanceof FolderIcon
&& ((FolderIcon) mHoverView).mInfo.title != null) {
mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString();
+ } else if (mHoverView instanceof AppPairIcon) {
+ mToolTipText = ((AppPairIcon) mHoverView).getTitleTextView().getText().toString();
} else {
mToolTipText = null;
}
@@ -156,6 +159,10 @@
if (mHoverView == null || mToolTipText == null) {
return;
}
+ // Do not show tooltip if taskbar icons are transitioning to hotseat.
+ if (mActivity.isIconAlignedWithHotseat()) {
+ return;
+ }
if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 051bdc8..f411e79 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -39,7 +39,6 @@
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
@@ -72,6 +71,7 @@
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.AssistUtils;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
@@ -114,11 +114,12 @@
private WindowManager mWindowManager;
private FrameLayout mTaskbarRootLayout;
private boolean mAddedWindow;
+ private boolean mIsSuspended;
private final TaskbarNavButtonController mNavButtonController;
private final ComponentCallbacks mComponentCallbacks;
private final SimpleBroadcastReceiver mShutdownReceiver =
- new SimpleBroadcastReceiver(i -> destroyExistingTaskbar());
+ new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyExistingTaskbar());
// The source for this provider is set when Launcher is available
// We use 'non-destroyable' version here so the original provider won't be destroyed
@@ -155,7 +156,7 @@
private boolean mUserUnlocked = false;
private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver =
- new SimpleBroadcastReceiver(this::showTaskbarFromBroadcast);
+ new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast);
private final AllAppsActionManager mAllAppsActionManager;
@@ -304,17 +305,15 @@
.register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
mContext.registerComponentCallbacks(mComponentCallbacks);
- mShutdownReceiver.registerAsync(mContext, Intent.ACTION_SHUTDOWN);
+ mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
UI_HELPER_EXECUTOR.execute(() -> {
mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
mContext,
SYSTEM_ACTION_ID_TASKBAR,
new Intent(ACTION_SHOW_TASKBAR).setPackage(mContext.getPackageName()),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
- mContext.registerReceiver(
- mTaskbarBroadcastReceiver,
- new IntentFilter(ACTION_SHOW_TASKBAR),
- RECEIVER_NOT_EXPORTED);
+ mTaskbarBroadcastReceiver.register(
+ mContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
});
debugWhyTaskbarNotDestroyed("TaskbarManager created");
@@ -442,6 +441,8 @@
*/
@VisibleForTesting
public synchronized void recreateTaskbar() {
+ if (mIsSuspended) return;
+
Trace.beginSection("recreateTaskbar");
try {
DeviceProfile dp = mUserUnlocked ?
@@ -519,6 +520,36 @@
}
}
+ public void setWallpaperVisible(boolean isVisible) {
+ mSharedState.wallpaperVisible = isVisible;
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.setWallpaperVisible(isVisible);
+ }
+ }
+
+ public void checkNavBarModes() {
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.checkNavBarModes();
+ }
+ }
+
+ public void finishBarAnimations() {
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.finishBarAnimations();
+ }
+ }
+
+ public void touchAutoDim(boolean reset) {
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.touchAutoDim(reset);
+ }
+ }
+
+ public void transitionTo(@BarTransitions.TransitionMode int barMode,
+ boolean animate) {
+ mTaskbarActivityContext.transitionTo(barMode, animate);
+ }
+
private boolean isTaskbarEnabled(DeviceProfile deviceProfile) {
return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent;
}
@@ -546,6 +577,13 @@
}
}
+ public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
+ mSharedState.barMode = barMode;
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.onTransitionModeUpdated(barMode, checkBarModes);
+ }
+ }
+
public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
mSharedState.navButtonsDarkIntensity = darkIntensity;
if (mTaskbarActivityContext != null) {
@@ -582,7 +620,7 @@
public void destroy() {
debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
- mTaskbarBroadcastReceiver.unregisterReceiverSafelyAsync(mContext);
+ mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
destroyExistingTaskbar();
removeTaskbarRootViewFromWindow();
if (mUserUnlocked) {
@@ -594,7 +632,7 @@
.unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
mContext.unregisterComponentCallbacks(mComponentCallbacks);
- mShutdownReceiver.unregisterReceiverSafelyAsync(mContext);
+ mShutdownReceiver.unregisterReceiverSafely(mContext);
}
public @Nullable TaskbarActivityContext getCurrentActivityContext() {
@@ -610,8 +648,22 @@
}
}
+ /**
+ * Removes Taskbar from the window manager and prevents recreation if {@code true}.
+ * <p>
+ * Suspending is for testing purposes only; avoid calling this method in production.
+ */
@VisibleForTesting
- void addTaskbarRootViewToWindow() {
+ public void setSuspended(boolean isSuspended) {
+ mIsSuspended = isSuspended;
+ if (mIsSuspended) {
+ removeTaskbarRootViewFromWindow();
+ } else {
+ addTaskbarRootViewToWindow();
+ }
+ }
+
+ private void addTaskbarRootViewToWindow() {
if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
mWindowManager.addView(mTaskbarRootLayout,
mTaskbarActivityContext.getWindowLayoutParams());
@@ -619,8 +671,7 @@
}
}
- @VisibleForTesting
- void removeTaskbarRootViewFromWindow() {
+ private void removeTaskbarRootViewFromWindow() {
if (enableTaskbarNoRecreate() && mAddedWindow) {
mWindowManager.removeViewImmediate(mTaskbarRootLayout);
mAddedWindow = false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 0b7ae39..5024cd8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -196,26 +196,26 @@
final TaskbarRecentAppsController recentAppsController =
mControllers.taskbarRecentAppsController;
hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
- Set<String> runningPackages = recentAppsController.getRunningAppPackages();
- Set<String> minimizedPackages = recentAppsController.getMinimizedAppPackages();
+ Set<Integer> runningTaskIds = recentAppsController.getRunningTaskIds();
+ Set<Integer> minimizedTaskIds = recentAppsController.getMinimizedTaskIds();
if (mDeferUpdatesForSUW) {
ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
mDeferredUpdates = () ->
commitHotseatItemUpdates(finalHotseatItemInfos,
- recentAppsController.getShownTasks(), runningPackages,
- minimizedPackages);
+ recentAppsController.getShownTasks(), runningTaskIds,
+ minimizedTaskIds);
} else {
commitHotseatItemUpdates(hotseatItemInfos,
- recentAppsController.getShownTasks(), runningPackages, minimizedPackages);
+ recentAppsController.getShownTasks(), runningTaskIds, minimizedTaskIds);
}
}
private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
- Set<String> runningPackages, Set<String> minimizedPackages) {
+ Set<Integer> runningTaskIds, Set<Integer> minimizedTaskIds) {
mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
mControllers.taskbarViewController.updateIconViewsRunningStates(
- runningPackages, minimizedPackages);
+ runningTaskIds, minimizedTaskIds);
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index d26a36d..ea091ca 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -166,8 +166,12 @@
if (buttonType == BUTTON_SPACE) {
return false;
}
- // Provide the same haptic feedback that the system offers for virtual keys.
- view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+
+ // Provide the same haptic feedback that the system offers for long press.
+ // The haptic feedback from long pressing on the home button is handled by circle to search.
+ if (buttonType != BUTTON_HOME) {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
switch (buttonType) {
case BUTTON_HOME:
logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS);
@@ -179,10 +183,12 @@
return true;
case BUTTON_BACK:
logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
- return backRecentsLongpress(buttonType);
+ backRecentsLongpress(buttonType);
+ return true;
case BUTTON_RECENTS:
logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS);
- return backRecentsLongpress(buttonType);
+ backRecentsLongpress(buttonType);
+ return true;
case BUTTON_IME_SWITCH:
default:
return false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index fc3b4c7..5c08116 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -18,17 +18,17 @@
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags.enableRecentsInTaskbar
import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.TaskItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
import com.android.launcher3.util.CancellableTask
import com.android.quickstep.RecentsModel
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
-import com.android.systemui.shared.recents.model.Task
import com.android.window.flags.Flags.enableDesktopWindowingMode
import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
import java.io.PrintWriter
-import java.util.function.Consumer
/**
* Provides recent apps functionality, when the Taskbar Recent Apps section is enabled. Behavior:
@@ -60,9 +60,13 @@
// Initialized in init.
private lateinit var controllers: TaskbarControllers
- private var shownHotseatItems: List<ItemInfo> = emptyList()
+ var shownHotseatItems: List<ItemInfo> = emptyList()
+ private set
+
private var allRecentTasks: List<GroupTask> = emptyList()
private var desktopTask: DesktopTask? = null
+ // Keeps track of the order in which running tasks appear.
+ private var orderedRunningTaskIds = emptyList<Int>()
var shownTasks: List<GroupTask> = emptyList()
private set
@@ -72,9 +76,9 @@
private val isInDesktopMode: Boolean
get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
- val runningAppPackages: Set<String>
+ val runningTaskIds: Set<Int>
/**
- * Returns the package names of apps that should be indicated as "running" to the user.
+ * Returns the task IDs of apps that should be indicated as "running" to the user.
* Specifically, we return all the open tasks if we are in Desktop mode, else emptySet().
*/
get() {
@@ -82,22 +86,19 @@
return emptySet()
}
val tasks = desktopTask?.tasks ?: return emptySet()
- return tasks.map { task -> task.key.packageName }.toSet()
+ return tasks.map { task -> task.key.id }.toSet()
}
- val minimizedAppPackages: Set<String>
+ val minimizedTaskIds: Set<Int>
/**
- * Returns the package names of apps that should be indicated as "minimized" to the user.
- * Specifically, we return all the running packages where all the tasks in that package are
- * minimized (not visible).
+ * Returns the task IDs for the tasks that should be indicated as "minimized" to the user.
*/
get() {
if (!canShowRunningApps || !isInDesktopMode) {
return emptySet()
}
val desktopTasks = desktopTask?.tasks ?: return emptySet()
- val packageToTasks = desktopTasks.groupBy { it.key.packageName }
- return packageToTasks.filterValues { tasks -> tasks.all { !it.isVisible } }.keys
+ return desktopTasks.filter { !it.isVisible }.map { task -> task.key.id }.toSet()
}
private val recentTasksChangedListener =
@@ -139,59 +140,118 @@
.filter { itemInfo -> !itemInfo.isPredictedItem }
.toMutableList()
+ if (isInDesktopMode && canShowRunningApps) {
+ shownHotseatItems =
+ updateHotseatItemsFromRunningTasks(
+ getOrderedAndWrappedDesktopTasks(),
+ shownHotseatItems
+ )
+ }
+
onRecentsOrHotseatChanged()
return shownHotseatItems.toTypedArray()
}
+ private fun getOrderedAndWrappedDesktopTasks(): List<GroupTask> {
+ val tasks = desktopTask?.tasks ?: emptyList()
+ // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
+ val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index }
+ val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] })
+ return sortedTasks.map { GroupTask(it) }
+ }
+
private fun reloadRecentTasksIfNeeded() {
if (!recentsModel.isTaskListValid(taskListChangeId)) {
taskListChangeId =
recentsModel.getTasks { tasks ->
allRecentTasks = tasks
+ val oldRunningTaskdIds = runningTaskIds
+ val oldMinimizedTaskIds = minimizedTaskIds
desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
- onRecentsOrHotseatChanged()
- controllers.taskbarViewController.commitRunningAppsToUI()
+ val runningTasksChanged = oldRunningTaskdIds != runningTaskIds
+ val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds
+ if (
+ onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged
+ ) {
+ controllers.taskbarViewController.commitRunningAppsToUI()
+ }
}
}
}
- private fun onRecentsOrHotseatChanged() {
+ /**
+ * Updates [shownTasks] when Recents or Hotseat changes.
+ *
+ * @return Whether [shownTasks] changed.
+ */
+ private fun onRecentsOrHotseatChanged(): Boolean {
+ val oldShownTasks = shownTasks
+ orderedRunningTaskIds = updateOrderedRunningTaskIds()
shownTasks =
if (isInDesktopMode) {
computeShownRunningTasks()
} else {
computeShownRecentTasks()
}
+ val shownTasksChanged = oldShownTasks != shownTasks
+ if (!shownTasksChanged) {
+ return shownTasksChanged
+ }
for (groupTask in shownTasks) {
for (task in groupTask.tasks) {
- val callback =
- Consumer<Task> { controllers.taskbarViewController.onTaskUpdated(it) }
- val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback)
+ val cancellableTask =
+ recentsModel.iconCache.getIconInBackground(task) {
+ icon,
+ contentDescription,
+ title ->
+ task.icon = icon
+ task.titleDescription = contentDescription
+ task.title = title
+ controllers.taskbarViewController.onTaskUpdated(task)
+ }
if (cancellableTask != null) {
iconLoadRequests.add(cancellableTask)
}
}
}
+ return shownTasksChanged
+ }
+
+ private fun updateOrderedRunningTaskIds(): MutableList<Int> {
+ val desktopTaskAsList = getOrderedAndWrappedDesktopTasks()
+ val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id }
+ var newOrder =
+ orderedRunningTaskIds
+ .filter { it in desktopTaskIds } // Only keep the tasks that are still running
+ .toMutableList()
+ // Add new tasks not already listed
+ newOrder.addAll(desktopTaskIds.filter { it !in newOrder })
+ return newOrder
}
private fun computeShownRunningTasks(): List<GroupTask> {
if (!canShowRunningApps) {
return emptyList()
}
- val tasks = desktopTask?.tasks ?: emptyList()
- // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
- var desktopTaskAsList = tasks.map { GroupTask(it) }
- // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too.
- desktopTaskAsList = dedupeHotseatTasks(desktopTaskAsList, shownHotseatItems)
- val desktopPackages = desktopTaskAsList.map { it.packageNames }
- // Remove any missing Tasks.
- val newShownTasks = shownTasks.filter { it.packageNames in desktopPackages }.toMutableList()
- val newShownPackages = newShownTasks.map { it.packageNames }
+ val desktopTaskAsList = getOrderedAndWrappedDesktopTasks()
+ val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id }
+ val shownTaskIds = shownTasks.map { it.task1.key.id }
+ // TODO(b/315344726 Multi-instance support): only show one icon per package once we support
+ // taskbar multi-instance menus
+ val shownHotseatItemTaskIds =
+ shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId }
+ // Remove any newly-missing Tasks, and actual group-tasks
+ val newShownTasks =
+ shownTasks
+ .filter { !it.hasMultipleTasks() }
+ .filter { it.task1.key.id in desktopTaskIds }
+ .toMutableList()
// Add any new Tasks, maintaining the order from previous shownTasks.
- newShownTasks.addAll(desktopTaskAsList.filter { it.packageNames !in newShownPackages })
- return newShownTasks.toList()
+ newShownTasks.addAll(desktopTaskAsList.filter { it.task1.key.id !in shownTaskIds })
+ // Remove any tasks already covered by Hotseat icons
+ return newShownTasks.filter { it.task1.key.id !in shownHotseatItemTaskIds }
}
private fun computeShownRecentTasks(): List<GroupTask> {
@@ -220,6 +280,25 @@
}
}
+ /**
+ * Returns the hotseat items updated so that any item that points to a package with a running
+ * task also references that task.
+ */
+ private fun updateHotseatItemsFromRunningTasks(
+ groupTasks: List<GroupTask>,
+ shownHotseatItems: List<ItemInfo>
+ ): List<ItemInfo> =
+ shownHotseatItems.map { itemInfo ->
+ if (itemInfo is TaskItemInfo) {
+ itemInfo
+ } else {
+ val foundTask =
+ groupTasks.find { task -> task.task1.key.packageName == itemInfo.targetPackage }
+ ?: return@map itemInfo
+ TaskItemInfo(foundTask.task1.key.id, itemInfo as WorkspaceItemInfo)
+ }
+ }
+
override fun dumpLogs(prefix: String, pw: PrintWriter) {
pw.println("$prefix TaskbarRecentAppsController:")
pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
@@ -228,8 +307,8 @@
pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}")
pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}")
pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}")
- pw.println("$prefix\trunningTasks=$runningAppPackages")
- pw.println("$prefix\tminimizedTasks=$minimizedAppPackages")
+ pw.println("$prefix\trunningTaskIds=$runningTaskIds")
+ pw.println("$prefix\tminimizedTaskIds=$minimizedTaskIds")
}
private val GroupTask.packageNames: List<String>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
index edaeb63..77bd35f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
@@ -56,12 +56,17 @@
// TaskbarManager#onNavButtonsDarkIntensityChanged()
public float navButtonsDarkIntensity;
+ // TaskbarManager#onTransitionModeUpdated()
+ public int barMode;
+
// TaskbarManager#onNavigationBarLumaSamplingEnabled()
public int mLumaSamplingDisplayId = DEFAULT_DISPLAY;
public boolean mIsLumaSamplingEnabled = true;
public boolean setupUIVisible = false;
+ public boolean wallpaperVisible = false;
+
public boolean allAppsVisible = false;
// LauncherTaskbarUIController#mTaskbarInAppDisplayProgressMultiProp
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index fa2d907..64fb04b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -31,6 +31,7 @@
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
@@ -1018,7 +1019,7 @@
long startDelay = 0;
updateStateForFlag(FLAG_STASHED_IN_APP_SYSUI, hasAnyFlag(systemUiStateFlags,
- SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE));
+ SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE | SYSUI_STATE_DIALOG_SHOWING));
boolean stashForBubbles = hasAnyFlag(FLAG_IN_OVERVIEW)
&& hasAnyFlag(systemUiStateFlags, SYSUI_STATE_BUBBLES_EXPANDED)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java
new file mode 100644
index 0000000..615db01
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar;
+
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
+
+import java.io.PrintWriter;
+
+/** Manages task bar transitions */
+public class TaskbarTransitions extends BarTransitions implements
+ TaskbarControllers.LoggableTaskbarController {
+
+ private final TaskbarActivityContext mContext;
+
+ private boolean mWallpaperVisible;
+
+ private boolean mLightsOut;
+ private boolean mAutoDim;
+ private View mNavButtons;
+ private float mDarkIntensity;
+
+ private final NearestTouchFrame mView;
+
+ public TaskbarTransitions(TaskbarActivityContext context, NearestTouchFrame view) {
+ super(view, R.drawable.nav_background);
+
+ mContext = context;
+ mView = view;
+ }
+
+ void init() {
+ mView.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ mNavButtons = mView.findViewById(R.id.end_nav_buttons);
+ applyLightsOut(false, true);
+ });
+ mNavButtons = mView.findViewById(R.id.end_nav_buttons);
+
+ applyModeBackground(-1, getMode(), false /*animate*/);
+ applyLightsOut(false /*animate*/, true /*force*/);
+ if (mContext.isPhoneButtonNavMode()) {
+ mBarBackground.setOverrideAlpha(1);
+ }
+ }
+
+ void setWallpaperVisibility(boolean visible) {
+ mWallpaperVisible = visible;
+ applyLightsOut(true, false);
+ }
+
+ @Override
+ public void setAutoDim(boolean autoDim) {
+ // Ensure we aren't in gestural nav if we are triggering auto dim
+ if (autoDim && !mContext.isPhoneButtonNavMode()) {
+ return;
+ }
+ if (mAutoDim == autoDim) return;
+ mAutoDim = autoDim;
+ applyLightsOut(true, false);
+ }
+
+ @Override
+ protected void onTransition(int oldMode, int newMode, boolean animate) {
+ super.onTransition(oldMode, newMode, animate);
+ applyLightsOut(animate, false /*force*/);
+ }
+
+ private void applyLightsOut(boolean animate, boolean force) {
+ // apply to lights out
+ applyLightsOut(isLightsOut(getMode()), animate, force);
+ }
+
+ private void applyLightsOut(boolean lightsOut, boolean animate, boolean force) {
+ if (!force && lightsOut == mLightsOut) return;
+
+ mLightsOut = lightsOut;
+ if (mNavButtons == null) return;
+
+ // ok, everyone, stop it right there
+ mNavButtons.animate().cancel();
+
+ // Bump percentage by 10% if dark.
+ float darkBump = mDarkIntensity / 10;
+ final float navButtonsAlpha = lightsOut ? 0.6f + darkBump : 1f;
+
+ if (!animate) {
+ mNavButtons.setAlpha(navButtonsAlpha);
+ } else {
+ final int duration = lightsOut ? LIGHTS_OUT_DURATION : LIGHTS_IN_DURATION;
+ mNavButtons.animate()
+ .alpha(navButtonsAlpha)
+ .setDuration(duration)
+ .start();
+ }
+ }
+
+ void onDarkIntensityChanged(float darkIntensity) {
+ mDarkIntensity = darkIntensity;
+ if (mAutoDim) {
+ applyLightsOut(false, true);
+ }
+ }
+
+ @Override
+ public void dumpLogs(String prefix, PrintWriter pw) {
+ pw.println(prefix + "TaskbarTransitions:");
+
+ pw.println(prefix + "\tmMode=" + getMode());
+ pw.println(prefix + "\tmAlwaysOpaque: " + isAlwaysOpaque());
+ pw.println(prefix + "\tmWallpaperVisible: " + mWallpaperVisible);
+ pw.println(prefix + "\tmLightsOut: " + mLightsOut);
+ pw.println(prefix + "\tmAutoDim: " + mAutoDim);
+ pw.println(prefix + "\tbg overrideAlpha: " + mBarBackground.getOverrideAlpha());
+ pw.println(prefix + "\tbg color: " + mBarBackground.getColor());
+ pw.println(prefix + "\tbg frame: " + mBarBackground.getFrame());
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index ce281c3..a2278ec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -43,8 +43,8 @@
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.TISBindHelper;
import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -269,8 +269,8 @@
foundTaskView,
foundTask,
taskContainer.getIconView().getDrawable(),
- taskContainer.getThumbnailViewDeprecated(),
- taskContainer.getThumbnailViewDeprecated().getThumbnail(),
+ taskContainer.getSnapshotView(),
+ taskContainer.getSplitAnimationThumbnail(),
null /* intent */,
null /* user */,
info);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index e59a016..527e3a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -63,6 +63,7 @@
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.TaskItemInfo;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LauncherBindableItemsContainer;
@@ -515,35 +516,38 @@
return mTaskbarView.getTaskbarDividerView();
}
- /** Updates which icons are marked as running given the Set of currently running packages. */
- public void updateIconViewsRunningStates(Set<String> runningPackages,
- Set<String> minimizedPackages) {
+ /**
+ * Updates which icons are marked as running or minimized given the Sets of currently running
+ * and minimized tasks.
+ */
+ public void updateIconViewsRunningStates(Set<Integer> runningTaskIds,
+ Set<Integer> minimizedTaskIds) {
for (View iconView : getIconViews()) {
if (iconView instanceof BubbleTextView btv) {
btv.updateRunningState(
- getRunningAppState(btv, runningPackages, minimizedPackages));
+ getRunningAppState(btv, runningTaskIds, minimizedTaskIds));
}
}
}
private BubbleTextView.RunningAppState getRunningAppState(
BubbleTextView btv,
- Set<String> runningPackages,
- Set<String> minimizedPackages) {
+ Set<Integer> runningTaskIds,
+ Set<Integer> minimizedTaskIds) {
Object tag = btv.getTag();
- if (tag instanceof ItemInfo itemInfo) {
- if (minimizedPackages.contains(itemInfo.getTargetPackage())) {
+ if (tag instanceof TaskItemInfo itemInfo) {
+ if (minimizedTaskIds.contains(itemInfo.getTaskId())) {
return BubbleTextView.RunningAppState.MINIMIZED;
}
- if (runningPackages.contains(itemInfo.getTargetPackage())) {
+ if (runningTaskIds.contains(itemInfo.getTaskId())) {
return BubbleTextView.RunningAppState.RUNNING;
}
}
if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
- if (minimizedPackages.contains(groupTask.task1.key.getPackageName())) {
+ if (minimizedTaskIds.contains(groupTask.task1.key.id)) {
return BubbleTextView.RunningAppState.MINIMIZED;
}
- if (runningPackages.contains(groupTask.task1.key.getPackageName())) {
+ if (runningTaskIds.contains(groupTask.task1.key.id)) {
return BubbleTextView.RunningAppState.RUNNING;
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 15e4578..a3832cd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -322,27 +322,45 @@
|| mImeVisibilityChecker.isImeVisible();
BubbleBarBubble bubbleToSelect = null;
- if (!update.removedBubbles.isEmpty()) {
- for (int i = 0; i < update.removedBubbles.size(); i++) {
- RemovedBubble removedBubble = update.removedBubbles.get(i);
- BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
- if (bubble != null) {
- mBubbleBarViewController.removeBubble(bubble);
- } else {
- Log.w(TAG, "trying to remove bubble that doesn't exist: "
- + removedBubble.getKey());
+
+ if (update.addedBubble != null && update.removedBubbles.size() == 1) {
+ // we're adding and removing a bubble at the same time. handle this as a single update.
+ RemovedBubble removedBubble = update.removedBubbles.get(0);
+ BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey());
+ mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+ if (bubbleToRemove != null) {
+ mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble,
+ bubbleToRemove, isExpanding, suppressAnimation);
+ } else {
+ mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
+ suppressAnimation);
+ Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
+ }
+ } else {
+ if (!update.removedBubbles.isEmpty()) {
+ for (int i = 0; i < update.removedBubbles.size(); i++) {
+ RemovedBubble removedBubble = update.removedBubbles.get(i);
+ BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
+ if (bubble != null) {
+ mBubbleBarViewController.removeBubble(bubble);
+ } else {
+ Log.w(TAG, "trying to remove bubble that doesn't exist: "
+ + removedBubble.getKey());
+ }
}
}
- }
- if (update.addedBubble != null) {
- mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
- mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation);
- if (isCollapsed) {
- // If we're collapsed, the most recently added bubble will be selected.
- bubbleToSelect = update.addedBubble;
+ if (update.addedBubble != null) {
+ mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
+ mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
+ suppressAnimation);
}
-
}
+
+ if (update.addedBubble != null && isCollapsed) {
+ // If we're collapsed, the most recently added bubble will be selected.
+ bubbleToSelect = update.addedBubble;
+ }
+
if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) {
// Iterate in reverse because new bubbles are added in front and the list is in order.
for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
@@ -376,7 +394,8 @@
BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
// If we're not stashed, we're visible so animate
bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
- mBubbleBarViewController.animateBubbleNotification(bb, /* isExpanding= */ false);
+ mBubbleBarViewController.animateBubbleNotification(
+ bb, /* isExpanding= */ false, /* isUpdate= */ true);
}
if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
// Create the new list
@@ -427,6 +446,13 @@
}
}
+ /**
+ * Removes the given bubble from the backing list of bubbles after it was dismissed by the user.
+ */
+ public void onBubbleDismissed(BubbleView bubble) {
+ mBubbles.remove(bubble.getBubble().getKey());
+ }
+
/** Tells WMShell to show the currently selected bubble. */
public void showSelectedBubble() {
if (getSelectedBubbleKey() != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 0ea5031..fd989b1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -46,9 +46,10 @@
import com.android.launcher3.anim.SpringAnimationBuilder;
import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
import com.android.launcher3.util.DisplayController;
-import com.android.wm.shell.Flags;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@@ -182,6 +183,8 @@
@Nullable
private BubbleView mDraggedBubbleView;
+ @Nullable
+ private BubbleView mDismissedByDragBubbleView;
private float mAlphaDuringDrag = 1f;
private Controller mController;
@@ -256,10 +259,6 @@
if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) {
return;
}
- if (!Flags.animateBubbleSizeChange()) {
- setIconSizeAndPadding(newIconSize, newBubbleBarPadding);
- return;
- }
if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) {
mScalePaddingAnimator.cancel();
}
@@ -626,6 +625,7 @@
/**
* 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
*/
@@ -665,7 +665,9 @@
}
/** Add a new bubble to the bubble bar. */
- public void addBubble(View bubble, FrameLayout.LayoutParams lp) {
+ public void addBubble(View bubble) {
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
+ Gravity.LEFT);
if (isExpanded()) {
// if we're expanded scale the new bubble in
bubble.setScaleX(0f);
@@ -702,14 +704,58 @@
}
}
+ /** Add a new bubble and remove an old bubble from the bubble bar. */
+ public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) {
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
+ Gravity.LEFT);
+ if (!isExpanded()) {
+ removeView(removedBubble);
+ addView(addedBubble, 0, lp);
+ return;
+ }
+ addedBubble.setScaleX(0f);
+ addedBubble.setScaleY(0f);
+ addView(addedBubble, 0, lp);
+
+ int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
+ int indexOfBubbleToRemove = indexOfChild(removedBubble);
+
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ removeView(removedBubble);
+ updateWidth();
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ addedBubble.setScaleX(1);
+ addedBubble.setScaleY(1);
+ removedBubble.setScaleX(0);
+ removedBubble.setScaleY(0);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ addedBubble.setScaleX(animatedFraction);
+ addedBubble.setScaleY(animatedFraction);
+ removedBubble.setScaleX(1 - animatedFraction);
+ removedBubble.setScaleY(1 - animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ mBubbleAnimator.animateNewAndRemoveOld(indexOfSelectedBubble, indexOfBubbleToRemove,
+ listener);
+ }
+
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
- if (getChildCount() + 1 > MAX_BUBBLES) {
- // the last child view is the overflow bubble and we shouldn't remove that. remove the
- // second to last child view.
- removeViewInLayout(getChildAt(getChildCount() - 2));
- }
super.addView(child, index, params);
updateWidth();
updateBubbleAccessibilityStates();
@@ -720,6 +766,10 @@
public void removeBubble(View bubble) {
if (isExpanded()) {
// TODO b/347062801 - animate the bubble bar if the last bubble is removed
+ final boolean dismissedByDrag = mDraggedBubbleView == bubble;
+ if (dismissedByDrag) {
+ mDismissedByDragBubbleView = mDraggedBubbleView;
+ }
int bubbleCount = getChildCount();
mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -739,8 +789,11 @@
@Override
public void onAnimationUpdate(float animatedFraction) {
- bubble.setScaleX(1 - animatedFraction);
- bubble.setScaleY(1 - animatedFraction);
+ // don't update the scale if this bubble was dismissed by drag
+ if (!dismissedByDrag) {
+ bubble.setScaleX(1 - animatedFraction);
+ bubble.setScaleY(1 - animatedFraction);
+ }
updateBubblesLayoutProperties(mBubbleBarLocation);
invalidate();
}
@@ -771,6 +824,7 @@
updateWidth();
updateBubbleAccessibilityStates();
updateContentDescription();
+ mDismissedByDragBubbleView = null;
}
private void updateWidth() {
@@ -817,7 +871,7 @@
float elevationState = (1 - widthState);
for (int i = 0; i < bubbleCount; i++) {
BubbleView bv = (BubbleView) getChildAt(i);
- if (bv == mDraggedBubbleView) {
+ if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
// Skip the dragged bubble. Its translation is managed by the drag controller.
continue;
}
@@ -842,22 +896,29 @@
float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
bv.setZ(fullElevationForChild * elevationState);
+ // only update the dot scale if we're expanding or collapsing
+ // TODO b/351904597: update the dot for the first bubble after removal and reorder
+ // since those might happen when the bar is collapsed and will need their dot back
+ if (mWidthAnimator.isRunning()) {
+ bv.setDotScale(widthState);
+ }
+
if (mIsBarExpanded) {
// If bar is on the right, account for bubble bar expanding and shifting left
final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
// where the bubble will end up when the animation ends
final float targetX = expandedX + expandedBarShift;
bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
- // When we're expanded, we're not stacked so we're not behind the stack
- bv.setBehindStack(false, animate);
+ // When we're expanded, the badge is visible for all bubbles
+ bv.updateBadgeVisibility(/* show= */ true);
bv.setAlpha(1);
} else {
// If bar is on the right, account for bubble bar expanding and shifting left
final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
final float targetX = collapsedX + collapsedBarShift;
bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
- // If we're not the first bubble we're behind the stack
- bv.setBehindStack(i > 0, animate);
+ // The badge is always visible for the first bubble
+ bv.updateBadgeVisibility(/* show= */ i == 0);
// If we're fully collapsed, hide all bubbles except for the first 2. If there are
// only 2 bubbles, hide the second bubble as well because it's the overflow.
if (widthState == 0) {
@@ -911,7 +972,7 @@
final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
float translationX;
if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
- return mBubbleAnimator.getExpandedBubbleTranslationX(bubbleIndex) + mBubbleBarPadding;
+ return mBubbleAnimator.getBubbleTranslationX(bubbleIndex) + mBubbleBarPadding;
} else if (onLeft) {
translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
} else {
@@ -920,8 +981,7 @@
return translationX - getScaleIconShift();
}
- private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount,
- boolean onLeft) {
+ private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) {
if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
return 0;
}
@@ -932,7 +992,9 @@
bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
? mIconOverlapAmount : 0);
} else {
- translationX = mBubbleBarPadding + (bubbleIndex == 0 ? 0 : mIconOverlapAmount);
+ translationX = mBubbleBarPadding + (
+ bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED
+ ? 0 : mIconOverlapAmount);
}
return translationX - getScaleIconShift();
}
@@ -999,6 +1061,8 @@
mDraggedBubbleView = view;
if (view != null) {
view.setZ(mDragElevation);
+ // we started dragging a bubble. reset the bubble that was previously dismissed by drag
+ mDismissedByDragBubbleView = null;
}
setIsDragging(view != null);
}
@@ -1248,6 +1312,37 @@
});
}
+ /** Dumps the current state of BubbleBarView. */
+ public void dump(PrintWriter pw) {
+ pw.println("BubbleBarView state:");
+ pw.println(" visibility: " + getVisibility());
+ pw.println(" translation Y: " + getTranslationY());
+ pw.println(" bubbles in bar (childCount = " + getChildCount() + ")");
+ for (BubbleView bubbleView: getBubbles()) {
+ BubbleBarItem bubble = bubbleView.getBubble();
+ String key = bubble == null ? "null" : bubble.getKey();
+ pw.println(" bubble key: " + key);
+ }
+ pw.println(" isExpanded: " + isExpanded());
+ pw.println(" mIsAnimatingNewBubble: " + mIsAnimatingNewBubble);
+ if (mBubbleAnimator != null) {
+ pw.println(" mBubbleAnimator.isRunning(): " + mBubbleAnimator.isRunning());
+ pw.println(" mBubbleAnimator is null");
+ }
+ pw.println(" mDragging: " + mDragging);
+ }
+
+ private List<BubbleView> getBubbles() {
+ List<BubbleView> bubbles = new ArrayList<>();
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child instanceof BubbleView bubble) {
+ bubbles.add(bubble);
+ }
+ }
+ return bubbles;
+ }
+
/** Interface for BubbleBarView to communicate with its controller. */
interface Controller {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index da0826b..0f9de16 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -25,10 +25,8 @@
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
-import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
-import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -45,6 +43,7 @@
import com.android.quickstep.SystemUiProxy;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@@ -386,7 +385,7 @@
/**
* Removes the provided bubble from the bubble bar.
*/
- public void removeBubble(BubbleBarItem b) {
+ public void removeBubble(BubbleBarBubble b) {
if (b != null) {
mBarView.removeBubble(b.getView());
} else {
@@ -394,13 +393,23 @@
}
}
+ /** Adds a new bubble and removes an old bubble at the same time. */
+ public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble,
+ BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation) {
+ mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView());
+ addedBubble.getView().setOnClickListener(mBubbleClickListener);
+ mBubbleDragController.setupBubbleView(addedBubble.getView());
+ if (!suppressAnimation) {
+ animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false);
+ }
+ }
+
/**
* Adds the provided bubble to the bubble bar.
*/
public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation) {
if (b != null) {
- mBarView.addBubble(
- b.getView(), new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT));
+ mBarView.addBubble(b.getView());
b.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(b.getView());
@@ -419,18 +428,19 @@
}
return;
}
- animateBubbleNotification(bubble, isExpanding);
+ animateBubbleNotification(bubble, isExpanding, /* isUpdate= */ false);
} else {
Log.w(TAG, "addBubble, bubble was null!");
}
}
/** Animates the bubble bar to notify the user about a bubble change. */
- public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding) {
+ public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding,
+ boolean isUpdate) {
boolean isInApp = mTaskbarStashController.isInApp();
// if this is the first bubble, animate to the initial state. one bubble is the overflow
// so check for at most 2 children.
- if (mBarView.getChildCount() <= 2) {
+ if (mBarView.getChildCount() <= 2 && !isUpdate) {
mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
return;
}
@@ -518,6 +528,12 @@
mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
}
+ /** Notifies {@link BubbleBarView} that the dragged bubble was dismissed. */
+ public void onBubbleDragDismissed(BubbleView bubble) {
+ mBubbleBarController.onBubbleDismissed(bubble);
+ mBarView.removeBubble(bubble);
+ }
+
/**
* Notifies {@link BubbleBarView} that drag and all animations are finished.
*/
@@ -584,4 +600,19 @@
/** Called when bounds have changed */
void onBoundsChanged();
}
+
+ /** Dumps the state of BubbleBarViewController. */
+ public void dump(PrintWriter pw) {
+ pw.println("Bubble bar view controller state:");
+ pw.println(" mHiddenForSysui: " + mHiddenForSysui);
+ pw.println(" mHiddenForNoBubbles: " + mHiddenForNoBubbles);
+ pw.println(" mShouldShowEducation: " + mShouldShowEducation);
+ pw.println(" mBubbleBarTranslationY.value: " + mBubbleBarTranslationY.value);
+ pw.println(" mBubbleBarSwipeUpTranslationY: " + mBubbleBarSwipeUpTranslationY);
+ if (mBarView != null) {
+ mBarView.dump(pw);
+ } else {
+ pw.println(" Bubble bar view is null!");
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 32d6375..03140fe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -18,6 +18,8 @@
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.util.RunnableList;
+import java.io.PrintWriter;
+
/**
* Hosts various bubble controllers to facilitate passing between one another.
*/
@@ -94,4 +96,9 @@
bubbleStashedHandleViewController.onDestroy();
bubbleBarController.onDestroy();
}
+
+ /** Dumps bubble controllers state. */
+ public void dump(PrintWriter pw) {
+ bubbleBarViewController.dump(pw);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index efc747c..8316b5b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -153,6 +153,7 @@
@Override
protected void onDragDismiss() {
mBubblePinController.onDragEnd();
+ mBubbleBarViewController.onBubbleDragDismissed(bubbleView);
mBubbleBarViewController.onBubbleDragEnd();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 185f85f..74f58ac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -36,6 +36,8 @@
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import java.io.PrintWriter;
+
/**
* Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to
* create a cohesive animation between stashed/unstashed states.
@@ -456,4 +458,13 @@
public void setHandleTranslationY(float ty) {
mHandleViewController.setTranslationYForSwipe(ty);
}
+
+ /** Dumps the state of BubbleStashController. */
+ public void dump(PrintWriter pw) {
+ pw.println("Bubble stash controller state:");
+ pw.println(" mIsStashed: " + mIsStashed);
+ pw.println(" mBubblesShowingOnOverview: " + mBubblesShowingOnOverview);
+ pw.println(" mBubblesShowingOnHome: " + mBubblesShowingOnHome);
+ pw.println(" mIsSysuiLocked: " + mIsSysuiLocked);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 2f92fbb..4c468bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -20,6 +20,7 @@
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Outline;
+import android.graphics.Path;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -35,8 +36,6 @@
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;
-import java.util.EnumSet;
-
// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
/**
@@ -47,25 +46,9 @@
public static final int DEFAULT_PATH_SIZE = 100;
- /**
- * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
- * another. If any of these flags are set, the dot will not be shown.
- * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
- */
- enum SuppressionFlag {
- // TODO: (b/277815200) implement flyout
- // Suppressed because the flyout is visible - it will morph into the dot via animation.
- FLYOUT_VISIBLE,
- // Suppressed because this bubble is behind others in the collapsed stack.
- BEHIND_STACK,
- }
-
- private final EnumSet<SuppressionFlag> mSuppressionFlags =
- EnumSet.noneOf(SuppressionFlag.class);
-
private final ImageView mBubbleIcon;
private final ImageView mAppIcon;
- private final int mBubbleSize;
+ private int mBubbleSize;
private float mDragTranslationX;
private float mOffsetX;
@@ -107,8 +90,6 @@
setLayoutDirection(LAYOUT_DIRECTION_LTR);
LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
-
- mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
mBubbleIcon = findViewById(R.id.icon_view);
mAppIcon = findViewById(R.id.app_icon_view);
@@ -125,11 +106,21 @@
}
private void getOutline(Outline outline) {
+ updateBubbleSizeAndDotRender();
final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
final int inset = (mBubbleSize - normalizedSize) / 2;
outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
}
+ private void updateBubbleSizeAndDotRender() {
+ int updatedBubbleSize = Math.min(getWidth(), getHeight());
+ if (updatedBubbleSize == mBubbleSize) return;
+ mBubbleSize = updatedBubbleSize;
+ if (mBubble == null || mBubble instanceof BubbleBarOverflow) return;
+ Path dotPath = ((BubbleBarBubble) mBubble).getDotPath();
+ mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE);
+ }
+
/**
* Set translation-x while this bubble is being dragged.
* Translation applied to the view is a sum of {@code translationX} and offset defined by
@@ -159,6 +150,12 @@
applyDragTranslation();
}
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ updateBubbleSizeAndDotRender();
+ }
+
private void applyDragTranslation() {
setTranslationX(mDragTranslationX + mOffsetX);
}
@@ -230,7 +227,7 @@
}
}
- void updateBadgeVisibility() {
+ void updateBadgeVisibility(boolean show) {
if (mBubble instanceof BubbleBarOverflow) {
// The overflow bubble does not have a badge, so just bail.
return;
@@ -241,39 +238,24 @@
? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
: 0;
mAppIcon.setTranslationX(translationX);
- mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
- }
-
- /** Sets whether this bubble is in the stack & not the first bubble. **/
- void setBehindStack(boolean behindStack, boolean animate) {
- if (behindStack) {
- mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
- } else {
- mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
- }
- updateDotVisibility(animate);
- updateBadgeVisibility();
- }
-
- /** Whether this bubble is in the stack & not the first bubble. **/
- boolean isBehindStack() {
- return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
+ mAppIcon.setVisibility(show ? VISIBLE : GONE);
}
/** Whether the dot indicating unseen content in a bubble should be shown. */
private boolean shouldDrawDot() {
boolean bubbleHasUnseenContent = mBubble != null
&& mBubble instanceof BubbleBarBubble
- && mSuppressionFlags.isEmpty()
&& !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
-
// Always render the dot if it's animating, since it could be animating out. Otherwise, show
// it if the bubble wants to show it, and we aren't suppressing it.
return bubbleHasUnseenContent || mDotIsAnimating;
}
/** How big the dot should be, fraction from 0 to 1. */
- private void setDotScale(float fraction) {
+ void setDotScale(float fraction) {
+ if (!shouldDrawDot()) {
+ return;
+ }
mDotScale = fraction;
invalidate();
}
@@ -283,14 +265,14 @@
*/
private void animateDotScale() {
float toScale = shouldDrawDot() ? 1f : 0f;
- mDotIsAnimating = true;
+ boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0;
- // Don't restart the animation if we're already animating to the given value.
- if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
- mDotIsAnimating = false;
+ // Don't restart the animation if we're already animating to the given value or if the dot
+ // scale is not changing
+ if ((mDotIsAnimating && mAnimatingToDotScale == toScale) || !isDotScaleChanging) {
return;
}
-
+ mDotIsAnimating = true;
mAnimatingToDotScale = toScale;
final boolean showDot = toScale > 0f;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
index 7672743..8af8ffb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -56,6 +56,20 @@
animator.start()
}
+ fun animateNewAndRemoveOld(
+ selectedBubbleIndex: Int,
+ removedBubbleIndex: Int,
+ listener: Listener
+ ) {
+ animator = createAnimator(listener)
+ state =
+ State.AddingAndRemoving(
+ selectedBubbleIndex = selectedBubbleIndex,
+ removedBubbleIndex = removedBubbleIndex
+ )
+ animator.start()
+ }
+
private fun createAnimator(listener: Listener): ValueAnimator {
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
animator.addUpdateListener { animation ->
@@ -83,28 +97,35 @@
}
/**
- * The translation X of the bubble at index [bubbleIndex] according to the progress of the
- * animation.
+ * The translation X of the bubble at index [bubbleIndex] when the bubble bar is expanded
+ * according to the progress of this animation.
*
* Callers should verify that the animation is running before calling this.
*
* @see isRunning
*/
- fun getExpandedBubbleTranslationX(bubbleIndex: Int): Float {
+ fun getBubbleTranslationX(bubbleIndex: Int): Float {
return when (val state = state) {
State.Idle -> 0f
is State.AddingBubble ->
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = bubbleIndex,
scalingBubbleIndex = 0,
bubbleScale = animator.animatedFraction
)
is State.RemovingBubble ->
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = bubbleIndex,
scalingBubbleIndex = state.bubbleIndex,
bubbleScale = 1 - animator.animatedFraction
)
+ is State.AddingAndRemoving ->
+ getBubbleTranslationXWhileAddingBubbleAtLimit(
+ bubbleIndex = bubbleIndex,
+ removedBubbleIndex = state.removedBubbleIndex,
+ addedBubbleScale = animator.animatedFraction,
+ removedBubbleScale = 1 - animator.animatedFraction
+ )
}
}
@@ -121,6 +142,14 @@
State.Idle -> 0f
is State.AddingBubble -> animator.animatedFraction
is State.RemovingBubble -> 1 - animator.animatedFraction
+ is State.AddingAndRemoving -> {
+ // since we're adding a bubble and removing another bubble, their sizes together
+ // equal to a single bubble. the width is the same as having bubbleCount - 1
+ // bubbles at full scale.
+ val totalSpace = (bubbleCount - 2) * expandedBarIconSpacing
+ val totalIconSize = (bubbleCount - 1) * iconSize
+ return totalIconSize + totalSpace
+ }
}
// When this animator is running the bubble bar is expanded so it's safe to assume that we
// have at least 2 bubbles, but should update the logic to support optional overflow.
@@ -144,7 +173,7 @@
State.Idle -> 0f
is State.AddingBubble -> {
val tx =
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = state.selectedBubbleIndex,
scalingBubbleIndex = 0,
bubbleScale = animator.animatedFraction
@@ -152,6 +181,17 @@
tx + iconSize / 2f
}
is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
+ is State.AddingAndRemoving -> {
+ // we never remove the selected bubble, so the arrow stays pointing to its center
+ val tx =
+ getBubbleTranslationXWhileAddingBubbleAtLimit(
+ bubbleIndex = state.selectedBubbleIndex,
+ removedBubbleIndex = state.removedBubbleIndex,
+ addedBubbleScale = animator.animatedFraction,
+ removedBubbleScale = 1 - animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ }
}
}
@@ -160,7 +200,7 @@
// if we're not removing the selected bubble, the selected bubble doesn't change so just
// return the translation X of the selected bubble and add half icon
val tx =
- getExpandedBubbleTranslationXWhileScalingBubble(
+ getBubbleTranslationXWhileScalingBubble(
bubbleIndex = state.selectedBubbleIndex,
scalingBubbleIndex = state.bubbleIndex,
bubbleScale = 1 - animator.animatedFraction
@@ -208,7 +248,7 @@
* @param scalingBubbleIndex the index of the bubble that is animating
* @param bubbleScale the current scale of the animating bubble
*/
- private fun getExpandedBubbleTranslationXWhileScalingBubble(
+ private fun getBubbleTranslationXWhileScalingBubble(
bubbleIndex: Int,
scalingBubbleIndex: Int,
bubbleScale: Float
@@ -256,6 +296,68 @@
}
}
+ private fun getBubbleTranslationXWhileAddingBubbleAtLimit(
+ bubbleIndex: Int,
+ removedBubbleIndex: Int,
+ addedBubbleScale: Float,
+ removedBubbleScale: Float
+ ): Float {
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ // the bubbles are scaling from the center, so we need to adjust their translation so
+ // that the distance to the adjacent bubble scales at the same rate.
+ val addedBubblePivotAdjustment = -(1 - addedBubbleScale) * iconSize / 2f
+ val removedBubblePivotAdjustment = -(1 - removedBubbleScale) * iconSize / 2f
+
+ return if (onLeft) {
+ // this is how many bubbles there are to the left of the current bubble.
+ // when the bubble bar is on the right the added bubble is the right-most bubble so it
+ // doesn't affect the translation of any other bubble.
+ // when the removed bubble is to the left of the current bubble, we need to subtract it
+ // from bubblesToLeft and use removedBubbleScale instead when calculating the
+ // translation.
+ val bubblesToLeft = bubbleCount - bubbleIndex - 1
+ when {
+ bubbleIndex == 0 ->
+ // this is the added bubble and it's the right-most bubble. account for all the
+ // other bubbles -- including the removed bubble -- and adjust for the added
+ // bubble pivot.
+ (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing +
+ addedBubblePivotAdjustment
+ bubbleIndex < removedBubbleIndex ->
+ // the removed bubble is to the left so account for it
+ (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
+ bubbleIndex == removedBubbleIndex -> {
+ // this is the removed bubble. all the bubbles to the left are at full scale
+ // but we need to scale the spacing between the removed bubble and the bubble to
+ // its left because the removed bubble disappears towards the left side
+ val totalIconSize = bubblesToLeft * iconSize
+ val totalSpacing =
+ (bubblesToLeft - 1 + removedBubbleScale) * expandedBarIconSpacing
+ totalIconSize + totalSpacing + removedBubblePivotAdjustment
+ }
+ else ->
+ // both added and removed bubbles are to the right so they don't affect the tx
+ bubblesToLeft * iconAndSpacing
+ }
+ } else {
+ when {
+ bubbleIndex == 0 -> addedBubblePivotAdjustment // we always add bubbles at index 0
+ bubbleIndex < removedBubbleIndex ->
+ // the bar is on the right and the removed bubble is on the right. the current
+ // bubble is unaffected by the removed bubble. only need to factor in the added
+ // bubble's scale.
+ iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale)
+ bubbleIndex == removedBubbleIndex ->
+ // the bar is on the right, and this is the animating bubble.
+ iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale) +
+ removedBubblePivotAdjustment
+ else ->
+ // both the added and the removed bubbles are to the left of the current bubble
+ iconAndSpacing * (bubbleIndex - 2 + addedBubbleScale + removedBubbleScale)
+ }
+ }
+ }
+
val isRunning: Boolean
get() = state != State.Idle
@@ -277,6 +379,10 @@
/** Whether the bubble being removed is also the last bubble. */
val removingLastBubble: Boolean
) : State
+
+ /** A new bubble is being added and an old bubble is being removed from the bubble bar. */
+ data class AddingAndRemoving(val selectedBubbleIndex: Int, val removedBubbleIndex: Int) :
+ State
}
/** Callbacks for the animation. */
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 037f2f6..be6f690 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1200,7 +1200,6 @@
: Display.DEFAULT_DISPLAY);
activityOptions.options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- addLaunchCookie(item, activityOptions.options);
return activityOptions;
}
@@ -1225,19 +1224,6 @@
}
/**
- * Adds a new launch cookie for the activity launch if supported.
- *
- * @param info the item info for the launch
- * @param opts the options to set the launchCookie on.
- */
- public void addLaunchCookie(ItemInfo info, ActivityOptions opts) {
- IBinder launchCookie = getLaunchCookie(info);
- if (launchCookie != null) {
- opts.setLaunchCookie(launchCookie);
- }
- }
-
- /**
* Return a new launch cookie for the activity launch if supported.
*
* @param info the item info for the launch
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index dc6365b..181cba0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -79,6 +79,7 @@
import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_DISCOVERY_TIP_COUNT
import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
+import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
import com.android.launcher3.util.PluginManagerWrapper
import com.android.launcher3.util.StartActivityParams
import com.android.launcher3.util.UserIconInfo
@@ -394,6 +395,7 @@
HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey
)
addOnboardPref("Taskbar Education", TASKBAR_EDU_TOOLTIP_STEP.sharedPrefKey)
+ addOnboardPref("Taskbar Search Education", TASKBAR_SEARCH_EDU_SEEN.sharedPrefKey)
addOnboardPref("All Apps Visited Count", ALL_APPS_VISITED_COUNT.sharedPrefKey)
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 0368f3a..3a39cf2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -110,6 +110,7 @@
// taskbar icons disappearing before hotseat icons show up.
float scrimUpperBoundFromSplit =
QuickstepTransitionManager.getTaskbarToHomeDuration() / (float) config.duration;
+ scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f);
config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
config.setInterpolator(ANIM_SCRIM_FADE,
fromState == OVERVIEW_SPLIT_SELECT
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 93e4fbd..31e4e33 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -151,7 +151,7 @@
int sysuiFlags = 0;
TaskView tv = mOverviewPanel.getTaskViewAt(0);
if (tv != null) {
- sysuiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+ sysuiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
}
mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags);
} else {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 4bc3c16..3c7f335 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -252,7 +252,7 @@
mTaskBeingDragged, maxDuration, currentInterpolator);
// Since the thumbnail is what is filling the screen, based the end displacement on it.
- View thumbnailView = mTaskBeingDragged.getFirstThumbnailViewDeprecated();
+ View thumbnailView = mTaskBeingDragged.getFirstSnapshotView();
mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
mEndDisplacement = secondaryLayerDimension - mTempCords[1];
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index fb2a982..f020c8f 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -138,8 +138,8 @@
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
+import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -152,8 +152,6 @@
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
-import kotlin.Unit;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -163,6 +161,8 @@
import java.util.OptionalInt;
import java.util.function.Consumer;
+import kotlin.Unit;
+
/**
* Handles the navigation gestures when Launcher is the default home activity.
*/
@@ -926,7 +926,7 @@
TaskView runningTask = mRecentsView.getRunningTaskView();
TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
int centermostTaskFlags = centermostTask == null ? 0
- : centermostTask.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+ : centermostTask.getTaskContainers().getFirst().getSysUiStatusNavFlags();
boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
boolean quickswitchThresholdPassed = centermostTask != runningTask;
@@ -2423,58 +2423,65 @@
if (mRecentsAnimationController == null) {
return;
}
+ final Runnable onFinishComplete = () -> {
+ ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+ "AbsSwipeUpHandler.onTasksAppeared: ")
+ .append("force finish recents animation complete; clearing state callback."));
+ mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+ };
+ ActiveGestureLog.CompoundString forceFinishReason = new ActiveGestureLog.CompoundString(
+ "Forcefully finishing recents animation: ");
if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED)
&& !hasStartedTaskBefore(appearedTaskTargets)) {
// This is a special case, if a task is started mid-gesture that wasn't a part of a
// previous quickswitch task launch, then cancel the animation back to the app
RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
TaskInfo taskInfo = appearedTaskTarget.taskInfo;
- ActiveGestureLog.INSTANCE.addLog(
- new ActiveGestureLog.CompoundString("Unexpected task appeared")
- .append(" id=")
+ ActiveGestureLog.INSTANCE.addLog(forceFinishReason
+ .append("Unexpected task appeared id=")
.append(taskInfo.taskId)
.append(" pkg=")
.append(taskInfo.baseIntent.getComponent().getPackageName()));
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ finishRecentsAnimationOnTasksAppeared(onFinishComplete);
return;
}
ActiveGestureLog.CompoundString handleTaskFailureReason =
new ActiveGestureLog.CompoundString("handleTaskAppeared check failed: ");
if (!handleTaskAppeared(appearedTaskTargets, handleTaskFailureReason)) {
- ActiveGestureLog.INSTANCE.addLog(handleTaskFailureReason);
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append(handleTaskFailureReason));
+ finishRecentsAnimationOnTasksAppeared(onFinishComplete);
return;
}
- Optional<RemoteAnimationTarget> taskTargetOptional =
- Arrays.stream(appearedTaskTargets)
- .filter(mGestureState.mLastStartedTaskIdPredicate)
- .findFirst();
- if (!taskTargetOptional.isPresent()) {
- ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ RemoteAnimationTarget[] taskTargets = Arrays.stream(appearedTaskTargets)
+ .filter(mGestureState.mLastStartedTaskIdPredicate)
+ .toArray(RemoteAnimationTarget[]::new);
+ if (taskTargets.length == 0) {
+ ActiveGestureLog.INSTANCE.addLog(
+ forceFinishReason.append("No appeared task matching started task id"));
+ finishRecentsAnimationOnTasksAppeared(onFinishComplete);
return;
}
- RemoteAnimationTarget taskTarget = taskTargetOptional.get();
+ RemoteAnimationTarget taskTarget = taskTargets[0];
TaskView taskView = mRecentsView == null
? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
- if (taskView == null
- || !taskView.getFirstThumbnailViewDeprecated().shouldShowSplashView()) {
- ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state");
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ if (taskView == null || taskView.getTaskContainers().stream().noneMatch(
+ TaskContainer::getShouldShowSplashView)) {
+ ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Splash not needed"));
+ finishRecentsAnimationOnTasksAppeared(onFinishComplete);
return;
}
if (mContainer == null) {
- ActiveGestureLog.INSTANCE.addLog("Activity destroyed");
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Activity destroyed"));
+ finishRecentsAnimationOnTasksAppeared(onFinishComplete);
return;
}
- animateSplashScreenExit(mContainer, appearedTaskTargets, taskTarget.leash);
+ animateSplashScreenExit(mContainer, appearedTaskTargets, taskTargets);
}
private void animateSplashScreenExit(
@NonNull T activity,
@NonNull RemoteAnimationTarget[] appearedTaskTargets,
- @NonNull SurfaceControl leash) {
+ @NonNull RemoteAnimationTarget[] animatingTargets) {
ViewGroup splashView = activity.getDragLayer();
final QuickstepLauncher quickstepLauncher = activity instanceof QuickstepLauncher
? (QuickstepLauncher) activity : null;
@@ -2492,26 +2499,28 @@
}
surfaceApplier.scheduleApply(transaction);
- SplashScreenExitAnimationUtils.startAnimations(splashView, leash,
- mSplashMainWindowShiftLength, new TransactionPool(), new Rect(),
- SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
- /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
- SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Hiding launcher which shows the app surface behind, then
- // finishing recents to the app. After transition finish, showing
- // the views on launcher again, so it can be visible when next
- // animation starts.
- splashView.setAlpha(0);
- if (quickstepLauncher != null) {
- quickstepLauncher.getDepthController()
- .pauseBlursOnWindows(false);
+ for (RemoteAnimationTarget target : animatingTargets) {
+ SplashScreenExitAnimationUtils.startAnimations(splashView, target.leash,
+ mSplashMainWindowShiftLength, new TransactionPool(), target.screenSpaceBounds,
+ SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
+ /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
+ SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Hiding launcher which shows the app surface behind, then
+ // finishing recents to the app. After transition finish, showing
+ // the views on launcher again, so it can be visible when next
+ // animation starts.
+ splashView.setAlpha(0);
+ if (quickstepLauncher != null) {
+ quickstepLauncher.getDepthController()
+ .pauseBlursOnWindows(false);
+ }
+ finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
}
- finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
- }
- });
+ });
+ }
}
private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) {
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index b1d511c..3a8c141 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -42,6 +42,7 @@
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.views.ScrimView;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.ActivityInitListener;
@@ -51,6 +52,7 @@
import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.HashMap;
+import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -269,8 +271,11 @@
} else {
Rect portraitInsets = dp.getInsets();
DisplayController displayController = DisplayController.INSTANCE.get(context);
- Rect deviceRotationInsets = displayController.getInfo().getCurrentBounds().get(
- orientationHandler.getRotation()).insets;
+ @Nullable List<WindowBounds> windowBounds =
+ displayController.getInfo().getCurrentBounds();
+ Rect deviceRotationInsets = windowBounds != null
+ ? windowBounds.get(orientationHandler.getRotation()).insets
+ : new Rect();
// Obtain the landscape/seascape insets, and rotate it to portrait perspective.
orientationHandler.rotateInsets(deviceRotationInsets, outRect);
// Then combine with portrait's insets to leave space for status bar/nav bar in
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index 9c188f3..358f644 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -23,9 +23,9 @@
import com.android.launcher3.popup.SystemShortcut
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.RecentsViewContainer
-import com.android.quickstep.views.TaskView.TaskContainer
+import com.android.quickstep.views.TaskContainer
import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
/** A menu item, "Desktop", that allows the user to bring the current app into Desktop Windowing. */
class DesktopSystemShortcut(
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 1048ea1..b564fa7 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -212,6 +212,9 @@
if (launcher.isStarted() && (isInLiveTileMode() || launcher.hasBeenResumed())) {
return launcher;
}
+ if (isInMinusOne()) {
+ return launcher;
+ }
return null;
}
@@ -289,6 +292,15 @@
&& TopTaskTracker.INSTANCE.get(launcher).getCachedTopTask(false).isHomeTask();
}
+ private boolean isInMinusOne() {
+ QuickstepLauncher launcher = getCreatedContainer();
+
+ return launcher != null
+ && launcher.getStateManager().getState() == NORMAL
+ && !launcher.isStarted()
+ && TopTaskTracker.INSTANCE.get(launcher).getCachedTopTask(false).isHomeTask();
+ }
+
@Override
public void onLaunchTaskFailed() {
QuickstepLauncher launcher = getCreatedContainer();
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 9c64576..d82426f 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -21,6 +21,7 @@
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
import android.content.ActivityNotFoundException;
@@ -55,10 +56,11 @@
public final class OverviewComponentObserver {
private static final String TAG = "OverviewComponentObserver";
+ // We register broadcast receivers on main thread to avoid missing updates.
private final SimpleBroadcastReceiver mUserPreferenceChangeReceiver =
- new SimpleBroadcastReceiver(this::updateOverviewTargets);
+ new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::updateOverviewTargets);
private final SimpleBroadcastReceiver mOtherHomeAppUpdateReceiver =
- new SimpleBroadcastReceiver(this::updateOverviewTargets);
+ new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::updateOverviewTargets);
private final Context mContext;
private final RecentsAnimationDeviceState mDeviceState;
@@ -102,7 +104,7 @@
mConfigChangesMap.append(fallbackComponent.hashCode(), fallbackInfo.configChanges);
} catch (PackageManager.NameNotFoundException ignored) { /* Impossible */ }
- mUserPreferenceChangeReceiver.registerAsync(mContext, ACTION_PREFERRED_ACTIVITY_CHANGED);
+ mUserPreferenceChangeReceiver.register(mContext, ACTION_PREFERRED_ACTIVITY_CHANGED);
updateOverviewTargets();
}
@@ -191,7 +193,7 @@
unregisterOtherHomeAppUpdateReceiver();
mUpdateRegisteredPackage = defaultHome.getPackageName();
- mOtherHomeAppUpdateReceiver.registerPkgActionsAsync(
+ mOtherHomeAppUpdateReceiver.registerPkgActions(
mContext, mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED,
ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED);
}
@@ -203,13 +205,13 @@
* Clean up any registered receivers.
*/
public void onDestroy() {
- mUserPreferenceChangeReceiver.unregisterReceiverSafelyAsync(mContext);
+ mUserPreferenceChangeReceiver.unregisterReceiverSafely(mContext);
unregisterOtherHomeAppUpdateReceiver();
}
private void unregisterOtherHomeAppUpdateReceiver() {
if (mUpdateRegisteredPackage != null) {
- mOtherHomeAppUpdateReceiver.unregisterReceiverSafelyAsync(mContext);
+ mOtherHomeAppUpdateReceiver.unregisterReceiverSafely(mContext);
mUpdateRegisteredPackage = null;
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 66091d4..3d4167a 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -44,6 +44,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -324,7 +325,9 @@
// leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
if (enableDesktopWindowingMode()) {
GroupTask desktopTask = createDesktopTask(rawTask);
- allTasks.add(desktopTask);
+ if (desktopTask != null) {
+ allTasks.add(desktopTask);
+ }
}
continue;
}
@@ -368,8 +371,13 @@
return allTasks;
}
- private DesktopTask createDesktopTask(GroupedRecentTaskInfo recentTaskInfo) {
+ private @Nullable DesktopTask createDesktopTask(GroupedRecentTaskInfo recentTaskInfo) {
ArrayList<Task> tasks = new ArrayList<>(recentTaskInfo.getTaskInfoList().size());
+ int[] minimizedTaskIds = recentTaskInfo.getMinimizedTaskIds();
+ if (minimizedTaskIds.length == recentTaskInfo.getTaskInfoList().size()) {
+ // All Tasks are minimized -> don't create a DesktopTask
+ return null;
+ }
for (ActivityManager.RecentTaskInfo taskInfo : recentTaskInfo.getTaskInfoList()) {
Task.TaskKey key = new Task.TaskKey(taskInfo);
Task task = Task.from(key, taskInfo, false);
@@ -377,6 +385,8 @@
task.positionInParent = taskInfo.positionInParent;
task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds();
task.isVisible = taskInfo.isVisible;
+ task.isMinimized =
+ Arrays.stream(minimizedTaskIds).anyMatch(taskId -> taskId == taskInfo.taskId);
tasks.add(task);
}
return new DesktopTask(tasks);
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 13e9844..18461a6 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -132,6 +132,13 @@
* Init drag layer and overview panel views.
*/
protected void setupViews() {
+ SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
+ // SplitSelectStateController needs to be created before setContentView()
+ mSplitSelectStateController =
+ new SplitSelectStateController(this, mHandler, getStateManager(),
+ null /* depthController */, getStatsLogManager(),
+ systemUiProxy, RecentsModel.INSTANCE.get(this),
+ null /*activityBackCallback*/);
inflateRootView(R.layout.fallback_recents_activity);
setContentView(getRootView());
mDragLayer = findViewById(R.id.drag_layer);
@@ -139,12 +146,6 @@
mFallbackRecentsView = findViewById(R.id.overview_panel);
mActionsView = findViewById(R.id.overview_actions_view);
getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
- SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
- mSplitSelectStateController =
- new SplitSelectStateController(this, mHandler, getStateManager(),
- null /* depthController */, getStatsLogManager(),
- systemUiProxy, RecentsModel.INSTANCE.get(this),
- null /*activityBackCallback*/);
mDragLayer.recreateControllers();
if (enableDesktopWindowingMode()) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 7adce74..cd62265 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -301,6 +301,10 @@
return mNavBarPosition;
}
+ public NavigationMode getMode() {
+ return mMode;
+ }
+
/**
* @return whether the current nav mode is fully gestural.
*/
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index ba33c62..f813d9a 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -26,6 +26,7 @@
import android.graphics.Matrix.ScaleToFit;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.util.Log;
import android.view.RemoteAnimationTarget;
import androidx.annotation.NonNull;
@@ -381,6 +382,8 @@
protected class SpringAnimationRunner extends AnimationSuccessListener
implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
+ private static final String TAG = "SpringAnimationRunner";
+
final Rect mCropRect = new Rect();
final Matrix mMatrix = new Matrix();
@@ -481,10 +484,26 @@
return;
}
mTargetTaskView.setAlpha(mAnimationFactory.isAnimatingIntoIcon() ? 1f : alpha);
- float width = mThumbnailStartBounds.width();
- float height = mThumbnailStartBounds.height();
- float scale = Math.min(currentRect.width(), currentRect.height())
- / Math.min(width, height);
+ float startWidth = mThumbnailStartBounds.width();
+ float startHeight = mThumbnailStartBounds.height();
+ float currentWidth = currentRect.width();
+ float currentHeight = currentRect.height();
+ float scale;
+
+ boolean isStartWidthValid = Float.compare(startWidth, 0f) > 0;
+ boolean isStartHeightValid = Float.compare(startHeight, 0f) > 0;
+ if (isStartWidthValid && isStartHeightValid) {
+ scale = Math.min(currentWidth, currentHeight) / Math.min(startWidth, startHeight);
+ } else {
+ Log.e(TAG, "TaskView starting bounds are invalid: " + mThumbnailStartBounds);
+ if (isStartWidthValid) {
+ scale = currentWidth / startWidth;
+ } else if (isStartHeightValid) {
+ scale = currentHeight / startHeight;
+ } else {
+ scale = 1f;
+ }
+ }
mTargetTaskView.setScaleX(scale);
mTargetTaskView.setScaleY(scale);
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index e6febff..1f6c02c 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -33,6 +33,7 @@
import android.text.TextUtils;
import android.util.SparseArray;
+import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.android.launcher3.R;
@@ -48,6 +49,7 @@
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource;
import com.android.quickstep.util.TaskKeyLruCache;
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.systemui.shared.recents.model.Task;
@@ -55,12 +57,11 @@
import com.android.systemui.shared.system.PackageManagerWrapper;
import java.util.concurrent.Executor;
-import java.util.function.Consumer;
/**
* Manages the caching of task icons and related data.
*/
-public class TaskIconCache implements DisplayInfoChangeListener {
+public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListener {
private final Executor mBgExecutor;
@@ -103,21 +104,22 @@
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
- public CancellableTask updateIconInBackground(Task task, Consumer<Task> callback) {
+ @Override
+ public CancellableTask getIconInBackground(Task task, @NonNull GetTaskIconCallback callback) {
Preconditions.assertUIThread();
if (task.icon != null) {
// Nothing to load, the icon is already loaded
- callback.accept(task);
+ callback.onTaskIconReceived(task.icon, task.titleDescription, task.title);
return null;
}
CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
() -> getCacheEntry(task),
MAIN_EXECUTOR,
result -> {
- task.icon = result.icon;
- task.titleDescription = result.contentDescription;
- task.title = result.title;
- callback.accept(task);
+ callback.onTaskIconReceived(
+ result.icon,
+ result.contentDescription,
+ result.title);
dispatchIconUpdate(task.key.id);
}
);
@@ -280,6 +282,12 @@
public String title = "";
}
+ /** Callback used when retrieving app icons from cache. */
+ public interface GetTaskIconCallback {
+ /** Called when task icon is retrieved. */
+ void onTaskIconReceived(Drawable icon, String contentDescription, String title);
+ }
+
void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) {
mTaskVisualsChangeListener = newListener;
}
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index b183ae3..80902e3 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -16,18 +16,22 @@
package com.android.quickstep;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.os.Build;
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.launcher3.BaseActivity;
@@ -38,17 +42,16 @@
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.Snackbar;
+import com.android.quickstep.task.util.TaskOverlayHelper;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
-import com.android.quickstep.views.TaskThumbnailViewDeprecated;
+import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.ArrayList;
import java.util.List;
@@ -129,39 +132,68 @@
private T mActionsView;
protected ImageActionsApi mImageApi;
+ protected TaskOverlayHelper mHelper;
protected TaskOverlay(TaskContainer taskContainer) {
mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext();
mTaskContainer = taskContainer;
- mImageApi = new ImageActionsApi(
- mApplicationContext, mTaskContainer.getThumbnailViewDeprecated()::getThumbnail);
+ if (enableRefactorTaskThumbnail()) {
+ mHelper = new TaskOverlayHelper(mTaskContainer.getTask(), this);
+ }
+ mImageApi = new ImageActionsApi(mApplicationContext, this::getThumbnail);
+ }
+
+ /**
+ * Initialize the overlay when a Task is bound to the TaskView.
+ */
+ public void init() {
+ if (enableRefactorTaskThumbnail()) {
+ mHelper.init();
+ }
+ }
+
+ /**
+ * Destroy the overlay when the TaskView is recycled.
+ */
+ public void destroy() {
+ if (enableRefactorTaskThumbnail()) {
+ mHelper.destroy();
+ }
+ }
+
+ protected @Nullable Bitmap getThumbnail() {
+ return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().getThumbnail()
+ : mTaskContainer.getThumbnailViewDeprecated().getThumbnail();
+ }
+
+ protected boolean isRealSnapshot() {
+ return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().isRealSnapshot()
+ : mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
}
protected T getActionsView() {
if (mActionsView == null) {
mActionsView = BaseActivity.fromContext(
- mTaskContainer.getThumbnailViewDeprecated().getContext()).findViewById(
+ mTaskContainer.getTaskView().getContext()).findViewById(
R.id.overview_actions_view);
}
return mActionsView;
}
- public TaskThumbnailViewDeprecated getThumbnailView() {
- return mTaskContainer.getThumbnailViewDeprecated();
+ public TaskView getTaskView() {
+ return mTaskContainer.getTaskView();
}
/**
* Called when the current task is interactive for the user
*/
- public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+ public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
boolean rotated) {
getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
if (thumbnail != null) {
getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
- boolean isAllowedByPolicy =
- mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
- getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
+ getActionsView().setCallbacks(new OverlayUICallbacksImpl(isRealSnapshot(), task));
}
}
@@ -172,7 +204,7 @@
*/
public void endLiveTileMode(@NonNull Runnable callback) {
RecentsView recentsView =
- mTaskContainer.getThumbnailViewDeprecated().getTaskView().getRecentsView();
+ mTaskContainer.getTaskView().getRecentsView();
// Task has already been dismissed
if (recentsView == null) return;
recentsView.switchToScreenshot(
@@ -185,8 +217,8 @@
*/
@SuppressLint("NewApi")
protected void saveScreenshot(Task task) {
- if (mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot()) {
- mImageApi.saveScreenshot(mTaskContainer.getThumbnailViewDeprecated().getThumbnail(),
+ if (isRealSnapshot()) {
+ mImageApi.saveScreenshot(getThumbnail(),
getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
} else {
showBlockedByPolicyMessage();
@@ -194,17 +226,14 @@
}
protected void enterSplitSelect() {
- RecentsView overviewPanel =
- mTaskContainer.getThumbnailViewDeprecated().getTaskView().getRecentsView();
+ RecentsView overviewPanel = mTaskContainer.getTaskView().getRecentsView();
// Task has already been dismissed
if (overviewPanel == null) return;
- overviewPanel.initiateSplitSelect(
- mTaskContainer.getThumbnailViewDeprecated().getTaskView());
+ overviewPanel.initiateSplitSelect(mTaskContainer.getTaskView());
}
protected void saveAppPair() {
- GroupedTaskView taskView =
- (GroupedTaskView) mTaskContainer.getThumbnailViewDeprecated().getTaskView();
+ GroupedTaskView taskView = (GroupedTaskView) mTaskContainer.getTaskView();
taskView.getRecentsView().getSplitSelectController().getAppPairsController()
.saveAppPair(taskView);
}
@@ -250,11 +279,11 @@
*/
public Rect getTaskSnapshotBounds() {
int[] location = new int[2];
- mTaskContainer.getThumbnailViewDeprecated().getLocationOnScreen(location);
+ mTaskContainer.getSnapshotView().getLocationOnScreen(location);
return new Rect(location[0], location[1],
- mTaskContainer.getThumbnailViewDeprecated().getWidth() + location[0],
- mTaskContainer.getThumbnailViewDeprecated().getHeight() + location[1]);
+ mTaskContainer.getSnapshotView().getWidth() + location[0],
+ mTaskContainer.getSnapshotView().getHeight() + location[1]);
}
/**
@@ -264,7 +293,36 @@
*/
@RequiresApi(api = Build.VERSION_CODES.Q)
public Insets getTaskSnapshotInsets() {
- return mTaskContainer.getThumbnailViewDeprecated().getScaledInsets();
+ Bitmap thumbnail = getThumbnail();
+ if (thumbnail == null) {
+ return Insets.NONE;
+ }
+
+ RectF bitmapRect = new RectF(
+ 0,
+ 0,
+ thumbnail.getWidth(),
+ thumbnail.getHeight());
+ View snapshotView = mTaskContainer.getSnapshotView();
+ RectF viewRect = new RectF(0, 0, snapshotView.getMeasuredWidth(),
+ snapshotView.getMeasuredHeight());
+
+ // The position helper matrix tells us how to transform the bitmap to fit the view, the
+ // inverse tells us where the view would be in the bitmaps coordinates. The insets are
+ // the difference between the bitmap bounds and the projected view bounds.
+ Matrix boundsToBitmapSpace = new Matrix();
+ Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
+ ? mHelper.getEnabledState().getThumbnailMatrix()
+ : mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
+ thumbnailMatrix.invert(boundsToBitmapSpace);
+ RectF boundsInBitmapSpace = new RectF();
+ boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
+
+ RecentsViewContainer container = RecentsViewContainer.containerFromContext(
+ getTaskView().getContext());
+ int bottomInset = container.getDeviceProfile().isTablet
+ ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
+ return Insets.of(0, 0, 0, bottomInset);
}
/**
@@ -275,14 +333,14 @@
protected void showBlockedByPolicyMessage() {
ActivityContext activityContext = ActivityContext.lookupContext(
- mTaskContainer.getThumbnailViewDeprecated().getContext());
+ mTaskContainer.getTaskView().getContext());
String message = activityContext.getStringCache() != null
? activityContext.getStringCache().disabledByAdminMessage
- : mTaskContainer.getThumbnailViewDeprecated().getContext().getString(
+ : mTaskContainer.getTaskView().getContext().getString(
R.string.blocked_by_policy);
Snackbar.show(BaseActivity.fromContext(
- mTaskContainer.getThumbnailViewDeprecated().getContext()), message, null);
+ mTaskContainer.getTaskView().getContext()), message, null);
}
/** Called when the snapshot has updated its full screen drawing parameters. */
@@ -304,8 +362,7 @@
@Override
public void onClick(View view) {
- saveScreenshot(
- mTaskContainer.getThumbnailViewDeprecated().getTaskView().getFirstTask());
+ saveScreenshot(mTaskContainer.getTaskView().getFirstTask());
dismissTaskMenuView();
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 4691ea9..77124bf 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -20,6 +20,7 @@
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.view.Surface.ROTATION_0;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.window.flags.Flags.enableDesktopWindowingMode;
@@ -55,9 +56,8 @@
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
-import com.android.quickstep.views.TaskThumbnailViewDeprecated;
+import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
@@ -174,7 +174,7 @@
private Handler mHandler;
private final RecentsView mRecentsView;
- private final TaskThumbnailViewDeprecated mThumbnailView;
+ private final TaskContainer mTaskContainer;
private final TaskView mTaskView;
private final LauncherEvent mLauncherEvent;
@@ -186,7 +186,7 @@
mHandler = new Handler(Looper.getMainLooper());
mTaskView = taskContainer.getTaskView();
mRecentsView = container.getOverviewPanel();
- mThumbnailView = taskContainer.getThumbnailViewDeprecated();
+ mTaskContainer = taskContainer;
}
@Override
@@ -220,20 +220,25 @@
};
final int[] position = new int[2];
- mThumbnailView.getLocationOnScreen(position);
- final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
- final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
+ View snapShotView = mTaskContainer.getSnapshotView();
+ snapShotView.getLocationOnScreen(position);
+ final int width = (int) (snapShotView.getWidth() * mTaskView.getScaleX());
+ final int height = (int) (snapShotView.getHeight() * mTaskView.getScaleY());
final Rect taskBounds = new Rect(position[0], position[1],
position[0] + width, position[1] + height);
// Take the thumbnail of the task without a scrim and apply it back after
- float alpha = mThumbnailView.getDimAlpha();
// TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval
- mThumbnailView.setDimAlpha(0);
+ float alpha = 0f;
+ if (!enableRefactorTaskThumbnail()) {
+ alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha();
+ mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(0);
+ }
Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
- taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
- Color.BLACK);
- mThumbnailView.setDimAlpha(alpha);
+ taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK);
+ if (!enableRefactorTaskThumbnail()) {
+ mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(alpha);
+ }
AppTransitionAnimationSpecsFuture future =
new AppTransitionAnimationSpecsFuture(mHandler) {
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 38e927f..3c6c3e4 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -131,8 +131,7 @@
Preconditions.assertUIThread();
// Fetch the thumbnail for this task and put it in the cache
if (task.thumbnail == null) {
- updateThumbnailInBackground(task.key, lowResolution,
- t -> task.thumbnail = t);
+ getThumbnailInBackground(task.key, lowResolution, t -> task.thumbnail = t);
}
}
@@ -145,13 +144,13 @@
}
/**
- * Asynchronously fetches the icon and other task data for the given {@param task}.
+ * Asynchronously fetches the thumbnail for the given {@code task}.
*
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
@Override
- public CancellableTask<ThumbnailData> updateThumbnailInBackground(
+ public CancellableTask<ThumbnailData> getThumbnailInBackground(
Task task, @NonNull Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
@@ -164,10 +163,7 @@
return null;
}
- return updateThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), t -> {
- task.thumbnail = t;
- callback.accept(t);
- });
+ return getThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), callback);
}
/**
@@ -187,7 +183,7 @@
return newSize > oldSize;
}
- private CancellableTask<ThumbnailData> updateThumbnailInBackground(TaskKey key,
+ private CancellableTask<ThumbnailData> getThumbnailInBackground(TaskKey key,
boolean lowResolution, Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index ecd84f8..bd44283 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -80,7 +80,6 @@
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskThumbnailViewDeprecated;
import com.android.quickstep.views.TaskView;
import com.android.systemui.animation.RemoteAnimationTargetCompat;
import com.android.systemui.shared.recents.model.Task;
@@ -334,7 +333,7 @@
// During animation we apply transformation on the thumbnailView (and not the rootView)
// to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is:
// Mt K(0)` K(t) Mt`
- TaskThumbnailViewDeprecated[] thumbnails = v.getThumbnailViews();
+ View[] thumbnails = v.getSnapshotViews();
// In case simulator copies and thumbnail size do no match, ensure we get the lesser.
// This ensures we do not create arrays with empty elements or attempt to references
@@ -344,7 +343,7 @@
Matrix[] mt = new Matrix[matrixSize];
Matrix[] mti = new Matrix[matrixSize];
for (int i = 0; i < matrixSize; i++) {
- TaskThumbnailViewDeprecated ttv = thumbnails[i];
+ View ttv = thumbnails[i];
RectF localBounds = new RectF(0, 0, ttv.getWidth(), ttv.getHeight());
float[] tvBoundsMapped = new float[]{0, 0, ttv.getWidth(), ttv.getHeight()};
getDescendantCoordRelativeToAncestor(ttv, ttv.getRootView(), tvBoundsMapped, false);
@@ -391,7 +390,7 @@
out.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- for (TaskThumbnailViewDeprecated ttv : thumbnails) {
+ for (View ttv : thumbnails) {
ttv.setAnimationMatrix(null);
}
}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index bfdc3df..2896979 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -42,6 +42,7 @@
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
@@ -101,6 +102,7 @@
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.LockedUserState;
+import com.android.launcher3.util.NavigationMode;
import com.android.launcher3.util.PluginManagerWrapper;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.ScreenOnTracker;
@@ -126,6 +128,7 @@
import com.android.quickstep.views.RecentsViewContainer;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
import com.android.systemui.shared.system.InputConsumerController;
@@ -330,6 +333,49 @@
});
}
+ @BinderThread
+ @Override
+ public void updateWallpaperVisibility(int displayId, boolean visible) {
+ MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
+ executeForTaskbarManager(
+ taskbarManager -> taskbarManager.setWallpaperVisible(visible))
+ ));
+ }
+
+ @BinderThread
+ @Override
+ public void checkNavBarModes() {
+ MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
+ executeForTaskbarManager(TaskbarManager::checkNavBarModes)
+ ));
+ }
+
+ @BinderThread
+ @Override
+ public void finishBarAnimations() {
+ MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
+ executeForTaskbarManager(TaskbarManager::finishBarAnimations)
+ ));
+ }
+
+ @BinderThread
+ @Override
+ public void touchAutoDim(boolean reset) {
+ MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
+ executeForTaskbarManager(taskbarManager -> taskbarManager.touchAutoDim(reset))
+ ));
+ }
+
+ @BinderThread
+ @Override
+ public void transitionTo(@BarTransitions.TransitionMode int barMode,
+ boolean animate) {
+ MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
+ executeForTaskbarManager(
+ taskbarManager -> taskbarManager.transitionTo(barMode, animate))
+ ));
+ }
+
/**
* Preloads the Overview activity.
* <p>
@@ -359,6 +405,12 @@
}
@Override
+ public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
+ executeForTaskbarManager(taskbarManager ->
+ taskbarManager.onTransitionModeUpdated(barMode, checkBarModes));
+ }
+
+ @Override
public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
executeForTaskbarManager(taskbarManager ->
taskbarManager.onNavButtonsDarkIntensityChanged(darkIntensity));
@@ -571,6 +623,8 @@
private InputManager mInputManager;
private final Set<Integer> mTrackpadsConnected = new ArraySet<>();
+ private NavigationMode mGestureStartNavMode = null;
+
@Override
public void onCreate() {
super.onCreate();
@@ -785,14 +839,26 @@
TestLogging.recordMotionEvent(
TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
- boolean isUserUnlocked = LockedUserState.get(this).isUserUnlocked();
- if (!isUserUnlocked || (mDeviceState.isButtonNavMode()
- && !isTrackpadMotionEvent(event))) {
+ if (!LockedUserState.get(this).isUserUnlocked()) {
+ ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
+ .append("Cannot process input event: user is locked"));
+ return;
+ }
+
+ NavigationMode currentNavMode = mDeviceState.getMode();
+ if (mGestureStartNavMode != null && mGestureStartNavMode != currentNavMode) {
+ ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
+ .append("Navigation mode switched mid-gesture (")
+ .append(mGestureStartNavMode.name())
+ .append(" -> ")
+ .append(currentNavMode.name())
+ .append("); cancelling gesture."),
+ NAVIGATION_MODE_SWITCHED);
+ event.setAction(ACTION_CANCEL);
+ } else if (mDeviceState.isButtonNavMode() && !isTrackpadMotionEvent(event)) {
ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
.append("Cannot process input event: ")
- .append(!isUserUnlocked
- ? "user is locked"
- : "using 3-button nav and event is not a trackpad event"));
+ .append("using 3-button nav and event is not a trackpad event"));
return;
}
@@ -819,6 +885,12 @@
}
}
+ if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
+ mGestureStartNavMode = currentNavMode;
+ } else if (action == ACTION_UP || action == ACTION_CANCEL) {
+ mGestureStartNavMode = null;
+ }
+
SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
CompoundString reasonString = action == ACTION_DOWN
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 0d450c6..13b6447 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -70,6 +70,9 @@
*/
public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer {
+ private static final String TAG = "OtherActivityInputConsumer";
+ private static final boolean DEBUG = true;
+
public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN";
private static final String UP_EVT = "OtherActivityInputConsumer.UP";
@@ -230,6 +233,9 @@
// Start the window animation on down to give more time for launcher to draw if the
// user didn't start the gesture over the back button
+ if (DEBUG) {
+ Log.d(TAG, "ACTION_DOWN: mIsDeferredDownTarget=" + mIsDeferredDownTarget);
+ }
if (!mIsDeferredDownTarget) {
startTouchTrackingForWindowAnimation(ev.getEventTime());
}
@@ -284,9 +290,18 @@
float horizontalDist = Math.abs(displacementX);
float upDist = -displacement;
- boolean passedSlop = mGestureState.isTrackpadGesture()
- || (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop
- && !mGestureState.isInExtendedSlopRegion());
+ boolean isTrackpadGesture = mGestureState.isTrackpadGesture();
+ float squaredHypot = squaredHypot(displacementX, displacementY);
+ boolean isInExtendedSlopRegion = !mGestureState.isInExtendedSlopRegion();
+ boolean passedSlop = isTrackpadGesture
+ || (squaredHypot >= mSquaredTouchSlop
+ && isInExtendedSlopRegion);
+ if (DEBUG) {
+ Log.d(TAG, "ACTION_MOVE: passedSlop=" + passedSlop
+ + " ( " + isTrackpadGesture
+ + " || (" + squaredHypot + " >= " + mSquaredTouchSlop
+ + " && " + isInExtendedSlopRegion + " ))");
+ }
if (!mPassedSlopOnThisGesture && passedSlop) {
mPassedSlopOnThisGesture = true;
@@ -306,6 +321,9 @@
boolean isLikelyToStartNewTask =
haveNotPassedSlopOnContinuedGesture || swipeWithinQuickSwitchRange;
+ if (DEBUG) {
+ Log.d(TAG, "ACTION_MOVE: mPassedPilferInputSlop=" + mPassedPilferInputSlop);
+ }
if (!mPassedPilferInputSlop) {
if (passedSlop) {
// Horizontal gesture is not allowed in this region
@@ -394,6 +412,10 @@
mInteractionHandler.initWhenReady(
"OtherActivityInputConsumer.startTouchTrackingForWindowAnimation");
+ if (DEBUG) {
+ Log.d(TAG, "startTouchTrackingForWindowAnimation: isRecentsAnimationRunning="
+ + mTaskAnimationManager.isRecentsAnimationRunning());
+ }
if (mTaskAnimationManager.isRecentsAnimationRunning()) {
mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
mActiveCallbacks.removeListener(mCleanupHandler);
@@ -422,6 +444,11 @@
*/
private void finishTouchTracking(MotionEvent ev) {
TraceHelper.INSTANCE.beginSection(UP_EVT);
+ if (DEBUG) {
+ Log.d(TAG, "finishTouchTracking: mPassedWindowMoveSlop=" + mPassedWindowMoveSlop);
+ Log.d(TAG, "finishTouchTracking: mInteractionHandler=" + mInteractionHandler);
+ Log.d(TAG, "finishTouchTracking: ev=" + ev);
+ }
boolean isCanceled = ev.getActionMasked() == ACTION_CANCEL;
if (mPassedWindowMoveSlop && mInteractionHandler != null) {
@@ -444,7 +471,14 @@
// Since we start touch tracking on DOWN, we may reach this state without actually
// starting the gesture. In that case, we need to clean-up an unfinished or un-started
// animation.
+ if (DEBUG) {
+ Log.d(TAG, "finishTouchTracking: mActiveCallbacks=" + mActiveCallbacks);
+ }
if (mActiveCallbacks != null && mInteractionHandler != null) {
+ if (DEBUG) {
+ Log.d(TAG, "finishTouchTracking: isRecentsAnimationRunning="
+ + mTaskAnimationManager.isRecentsAnimationRunning());
+ }
if (mTaskAnimationManager.isRecentsAnimationRunning()) {
// The animation started, but with no movement, in this case, there will be no
// animateToProgress so we have to manually finish here. In the case of
@@ -535,7 +569,13 @@
public void onRecentsAnimationStart(RecentsAnimationController controller,
RecentsAnimationTargets targets) {
+ if (DEBUG) {
+ Log.d(TAG, "FinishImmediatelyHandler: queuing callback");
+ }
Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+ if (DEBUG) {
+ Log.d(TAG, "FinishImmediatelyHandler: running callback");
+ }
controller.finish(false /* toRecents */, null);
});
}
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index c1eef0b..9c4248c 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.recents.data
import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.flow.Flow
interface RecentTasksRepository {
@@ -25,11 +26,17 @@
/**
* Gets the data associated with a task that has id [taskId]. Flow will settle on null if the
- * task was not found.
+ * task was not found. [Task.thumbnail] will settle on null if task is invisible.
*/
fun getTaskDataById(taskId: Int): Flow<Task?>
/**
+ * Gets the [ThumbnailData] associated with a task that has id [taskId]. Flow will settle on
+ * null if the task was not found or is invisible.
+ */
+ fun getThumbnailById(taskId: Int): Flow<ThumbnailData?>
+
+ /**
* Sets the tasks that are visible, indicating that properties relating to visuals need to be
* populated e.g. icons/thumbnails etc.
*/
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index b21a1b4..f73db5a 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -16,7 +16,8 @@
package com.android.quickstep.recents.data
-import com.android.quickstep.TaskIconCache
+import android.graphics.drawable.Drawable
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
@@ -27,6 +28,7 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
@@ -37,7 +39,7 @@
class TasksRepository(
private val recentsModel: RecentTasksDataSource,
private val taskThumbnailDataSource: TaskThumbnailDataSource,
- private val taskIconCache: TaskIconCache,
+ private val taskIconDataSource: TaskIconDataSource,
) : RecentTasksRepository {
private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
private val _taskData =
@@ -45,10 +47,19 @@
private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
private val taskData: Flow<List<Task>> =
- combine(_taskData, getThumbnailQueryResults()) { tasks, results ->
+ combine(_taskData, getThumbnailQueryResults(), getIconQueryResults()) {
+ tasks,
+ thumbnailQueryResults,
+ iconQueryResults ->
tasks.forEach { task ->
// Add retrieved thumbnails + remove unnecessary thumbnails
- task.thumbnail = results[task.key.id]
+ task.thumbnail = thumbnailQueryResults[task.key.id]
+
+ // TODO(b/352331675) don't load icons for DesktopTaskView
+ // Add retrieved icons + remove unnecessary icons
+ task.icon = iconQueryResults[task.key.id]?.icon
+ task.titleDescription = iconQueryResults[task.key.id]?.contentDescription
+ task.title = iconQueryResults[task.key.id]?.title
}
tasks
}
@@ -63,18 +74,21 @@
override fun getTaskDataById(taskId: Int): Flow<Task?> =
taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+ override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
+ getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
+
override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
this.visibleTaskIds.value = visibleTaskIdList.toSet()
}
- /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */
+ /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest =
flow {
emit(task.key.id to task.thumbnail)
val thumbnailDataResult: ThumbnailData? =
suspendCancellableCoroutine { continuation ->
val cancellableTask =
- taskThumbnailDataSource.updateThumbnailInBackground(task) {
+ taskThumbnailDataSource.getThumbnailInBackground(task) {
continuation.resume(it)
}
continuation.invokeOnCancellation { cancellableTask?.cancel() }
@@ -94,12 +108,7 @@
tasks.filter { it.key.id in visibleIds }
}
val visibleThumbnailDataRequests: Flow<List<ThumbnailDataRequest>> =
- visibleTasks.map {
- it.map { visibleTask ->
- val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail }
- getThumbnailDataRequest(taskCopy)
- }
- }
+ visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
return visibleThumbnailDataRequests.flatMapLatest {
thumbnailRequestFlows: List<ThumbnailDataRequest> ->
if (thumbnailRequestFlows.isEmpty()) {
@@ -109,6 +118,59 @@
}
}
}
+
+ /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
+ private fun getIconDataRequest(task: Task): IconDataRequest =
+ flow {
+ emit(task.key.id to task.getTaskIconQueryResponse())
+ val iconDataResponse: TaskIconQueryResponse? =
+ suspendCancellableCoroutine { continuation ->
+ val cancellableTask =
+ taskIconDataSource.getIconInBackground(task) {
+ icon,
+ contentDescription,
+ title ->
+ continuation.resume(
+ TaskIconQueryResponse(icon, contentDescription, title)
+ )
+ }
+ continuation.invokeOnCancellation { cancellableTask?.cancel() }
+ }
+ emit(task.key.id to iconDataResponse)
+ }
+ .distinctUntilChanged()
+
+ private fun getIconQueryResults(): Flow<Map<Int, TaskIconQueryResponse?>> {
+ val visibleTasks =
+ combine(_taskData, visibleTaskIds) { tasks, visibleIds ->
+ tasks.filter { it.key.id in visibleIds }
+ }
+ val visibleIconDataRequests: Flow<List<IconDataRequest>> =
+ visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
+ return visibleIconDataRequests.flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
+ if (iconRequestFlows.isEmpty()) {
+ flowOf(emptyMap())
+ } else {
+ combine(iconRequestFlows) { it.toMap() }
+ }
+ }
+ }
}
-typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
+private data class TaskIconQueryResponse(
+ val icon: Drawable,
+ val contentDescription: String,
+ val title: String
+)
+
+private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
+ val iconVal = icon ?: return null
+ val titleDescriptionVal = titleDescription ?: return null
+ val titleVal = title ?: return null
+
+ return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal)
+}
+
+private typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
+
+private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index 28212cf..fdb62df 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -24,4 +24,11 @@
// This is typically a View concern but it is used to invalidate rendering in other Views
val scale = MutableStateFlow(1f)
+
+ // Whether the current RecentsView state supports task overlays.
+ // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM.
+ val overlayEnabled = MutableStateFlow(false)
+
+ // The settled set of visible taskIds that is updated after RecentsView scroll settles.
+ val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt
new file mode 100644
index 0000000..45b3687
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.util.AttributeSet
+import android.view.View
+
+class LiveTileView : View {
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr)
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), CLEAR_PAINT)
+ }
+
+ companion object {
+ private val CLEAR_PAINT =
+ Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
new file mode 100644
index 0000000..feee11f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+
+/** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
+sealed class TaskOverlayUiState {
+ data object Disabled : TaskOverlayUiState()
+
+ data class Enabled(
+ val isRealSnapshot: Boolean,
+ val thumbnail: Bitmap?,
+ val thumbnailMatrix: Matrix
+ ) : TaskOverlayUiState()
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index dbe2b19..c71b9e7 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -18,18 +18,19 @@
import android.content.Context
import android.content.res.Configuration
-import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Outline
-import android.graphics.Paint
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import android.widget.ImageView
import androidx.annotation.ColorInt
+import androidx.core.view.isVisible
+import com.android.launcher3.R
import com.android.launcher3.Utilities
+import com.android.launcher3.util.ViewPool
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
@@ -42,7 +43,7 @@
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
-class TaskThumbnailView : View {
+class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
// TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
// to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
// This is using a lazy for now because the dependencies cannot be obtained without DI.
@@ -51,19 +52,19 @@
RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
.getOverviewPanel<RecentsView<*, *>>()
TaskThumbnailViewModel(
- recentsView.mRecentsViewData,
+ recentsView.mRecentsViewData!!,
(parent as TaskView).taskViewData,
(parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
- recentsView.mTasksRepository,
+ recentsView.mTasksRepository!!,
)
}
- private var uiState: TaskThumbnailUiState = Uninitialized
- private var inheritedScale: Float = 1f
- private var dimProgress: Float = 0f
+ private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
+ private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
+ private val thumbnail: ImageView by lazy { findViewById(R.id.task_thumbnail) }
- private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
- private val scrimPaint = Paint().apply { color = Color.BLACK }
+ private var inheritedScale: Float = 1f
+
private val _measuredBounds = Rect()
private val measuredBounds: Rect
get() {
@@ -71,15 +72,15 @@
return _measuredBounds
}
- private var cornerRadius: Float = TaskCornerRadius.get(context)
+ private var overviewCornerRadius: Float = TaskCornerRadius.get(context)
private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
- constructor(context: Context?) : super(context)
+ constructor(context: Context) : super(context)
- constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(
- context: Context?,
+ context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
) : super(context, attrs, defStyleAttr)
@@ -89,18 +90,22 @@
// TODO(b/335396935) replace MainScope with shorter lifecycle.
MainScope().launch {
viewModel.uiState.collect { viewModelUiState ->
- uiState = viewModelUiState
- invalidate()
+ resetViews()
+ when (viewModelUiState) {
+ is Uninitialized -> {}
+ is LiveTile -> drawLiveWindow()
+ is Snapshot -> drawSnapshot(viewModelUiState)
+ is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
+ }
}
}
MainScope().launch {
viewModel.dimProgress.collect { dimProgress ->
// TODO(b/348195366) Add fade in/out for scrim
- this@TaskThumbnailView.dimProgress = dimProgress
- invalidate()
+ scrimView.alpha = dimProgress * MAX_SCRIM_ALPHA
}
}
- MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } }
+ MainScope().launch { viewModel.cornerRadiusProgress.collect { invalidateOutline() } }
MainScope().launch {
viewModel.inheritedScale.collect { viewModelInheritedScale ->
inheritedScale = viewModelInheritedScale
@@ -117,56 +122,47 @@
}
}
- override fun onDraw(canvas: Canvas) {
- when (val uiStateVal = uiState) {
- is Uninitialized -> drawBackgroundOnly(canvas, Color.BLACK)
- is LiveTile -> drawTransparentUiState(canvas)
- is Snapshot -> drawSnapshotState(canvas, uiStateVal)
- is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor)
- }
-
- if (dimProgress > 0) {
- drawScrim(canvas)
- }
- }
-
- private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) {
- backgroundPaint.color = backgroundColor
- canvas.drawRect(measuredBounds, backgroundPaint)
+ override fun onRecycle() {
+ // Do nothing
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
- cornerRadius = TaskCornerRadius.get(context)
+ overviewCornerRadius = TaskCornerRadius.get(context)
fullscreenCornerRadius = QuickStepContract.getWindowCornerRadius(context)
invalidateOutline()
}
- private fun drawTransparentUiState(canvas: Canvas) {
- canvas.drawRect(measuredBounds, CLEAR_PAINT)
+ private fun resetViews() {
+ liveTileView.isVisible = false
+ thumbnail.isVisible = false
+ scrimView.alpha = 0f
+ setBackgroundColor(Color.BLACK)
}
- private fun drawSnapshotState(canvas: Canvas, snapshot: Snapshot) {
- drawBackgroundOnly(canvas, snapshot.backgroundColor)
- canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null)
+ private fun drawBackground(@ColorInt background: Int) {
+ setBackgroundColor(background)
}
- private fun drawScrim(canvas: Canvas) {
- scrimPaint.alpha = (dimProgress * MAX_SCRIM_ALPHA).toInt()
- canvas.drawRect(measuredBounds, scrimPaint)
+ private fun drawLiveWindow() {
+ liveTileView.isVisible = true
+ }
+
+ private fun drawSnapshot(snapshot: Snapshot) {
+ drawBackground(snapshot.backgroundColor)
+ thumbnail.setImageBitmap(snapshot.bitmap)
+ thumbnail.isVisible = true
}
private fun getCurrentCornerRadius() =
Utilities.mapRange(
- viewModel.recentsFullscreenProgress.value,
- cornerRadius,
+ viewModel.cornerRadiusProgress.value,
+ overviewCornerRadius,
fullscreenCornerRadius
) / inheritedScale
- companion object {
- private val CLEAR_PAINT =
- Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
- private const val MAX_SCRIM_ALPHA = (0.4f * 255).toInt()
+ private companion object {
+ const val MAX_SCRIM_ALPHA = 0.4f
}
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
index fe21174..d8729a6 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
@@ -31,6 +31,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
@@ -47,7 +48,13 @@
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
private var boundTaskIsRunning = false
- val recentsFullscreenProgress = recentsViewData.fullscreenProgress
+ /**
+ * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
+ * corner radius.
+ */
+ val cornerRadiusProgress =
+ if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
+ else MutableStateFlow(1f).asStateFlow()
val inheritedScale =
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
recentsScale * taskScale
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
new file mode 100644
index 0000000..ab699c6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail.data
+
+import com.android.launcher3.util.CancellableTask
+import com.android.quickstep.TaskIconCache.GetTaskIconCallback
+import com.android.systemui.shared.recents.model.Task
+
+interface TaskIconDataSource {
+ fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>?
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
index 55598f0..986acbe 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
@@ -22,7 +22,7 @@
import java.util.function.Consumer
interface TaskThumbnailDataSource {
- fun updateThumbnailInBackground(
+ fun getThumbnailInBackground(
task: Task,
callback: Consumer<ThumbnailData>
): CancellableTask<ThumbnailData>?
diff --git a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
new file mode 100644
index 0000000..e8dd04c3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.util
+
+import android.graphics.Bitmap
+import com.android.quickstep.recents.data.RecentTasksRepository
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+/** Use case for retrieving thumbnail. */
+class GetThumbnailUseCase(private val taskRepository: RecentTasksRepository) {
+ /** Returns the latest thumbnail associated with [taskId] if loaded, or null otherwise */
+ fun run(taskId: Int): Bitmap? = runBlocking {
+ taskRepository.getThumbnailById(taskId).firstOrNull()?.thumbnail
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
new file mode 100644
index 0000000..5e55e2e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.util
+
+import android.util.Log
+import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+/**
+ * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper
+ * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM.
+ */
+class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) {
+ private lateinit var job: Job
+ private var uiState: TaskOverlayUiState = Disabled
+
+ // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
+ // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
+ // This is using a lazy for now because the dependencies cannot be obtained without DI.
+ private val taskOverlayViewModel by lazy {
+ val recentsView =
+ RecentsViewContainer.containerFromContext<RecentsViewContainer>(
+ overlay.taskView.context
+ )
+ .getOverviewPanel<RecentsView<*, *>>()
+ TaskOverlayViewModel(task, recentsView.mRecentsViewData!!, recentsView.mTasksRepository!!)
+ }
+
+ // TODO(b/331753115): TaskOverlay should listen for state changes and react.
+ val enabledState: Enabled
+ get() = uiState as Enabled
+
+ fun init() {
+ // TODO(b/335396935): This should be changed to TaskView's scope.
+ job =
+ MainScope().launch {
+ taskOverlayViewModel.overlayState.collect {
+ uiState = it
+ if (it is Enabled) {
+ Log.d(
+ TAG,
+ "initOverlay - taskId: ${task.key.id}, thumbnail: ${it.thumbnail}"
+ )
+ overlay.initOverlay(
+ task,
+ it.thumbnail,
+ it.thumbnailMatrix,
+ /* rotated= */ false
+ )
+ } else {
+ Log.d(TAG, "reset - taskId: ${task.key.id}")
+ overlay.reset()
+ }
+ }
+ }
+ }
+
+ fun destroy() {
+ job.cancel()
+ uiState = Disabled
+ overlay.reset()
+ }
+
+ companion object {
+ private const val TAG = "TaskOverlayHelper"
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
new file mode 100644
index 0000000..47f32fb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import android.graphics.Matrix
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** View model for TaskOverlay */
+class TaskOverlayViewModel(
+ task: Task,
+ recentsViewData: RecentsViewData,
+ tasksRepository: RecentTasksRepository,
+) {
+ val overlayState =
+ combine(
+ recentsViewData.overlayEnabled,
+ recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
+ tasksRepository.getThumbnailById(task.key.id)
+ ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
+ if (isOverlayEnabled && isFullyVisible) {
+ Enabled(
+ isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
+ thumbnailData?.thumbnail,
+ // TODO(b/343101424): Use PreviewPositionHelper, listen from a common source
+ // with
+ // TaskThumbnailView.
+ Matrix.IDENTITY_MATRIX
+ )
+ } else {
+ Disabled
+ }
+ }
+ .distinctUntilChanged()
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
index a8b5112..7a9ecf2 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
@@ -16,9 +16,14 @@
package com.android.quickstep.task.viewmodel
+import com.android.quickstep.views.TaskViewType
import kotlinx.coroutines.flow.MutableStateFlow
-class TaskViewData {
+class TaskViewData(taskViewType: TaskViewType) {
// This is typically a View concern but it is used to invalidate rendering in other Views
val scale = MutableStateFlow(1f)
+
+ // TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM.
+ /** Whether outline of TaskView is formed by outline thumbnail view(s). */
+ val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP
}
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 3140fff..2398e66 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -40,7 +40,7 @@
SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
- QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED,
+ QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED, NAVIGATION_MODE_SWITCHED,
/**
* These GestureEvents are specifically associated to state flags that get set in
@@ -299,6 +299,13 @@
+ "the current page index and index 0 were missing.",
writer);
break;
+ case NAVIGATION_MODE_SWITCHED:
+ errorDetected |= printErrorIfTrue(
+ true,
+ prefix,
+ /* errorMessage= */ "Navigation mode switched mid-gesture.",
+ writer);
+ break;
case EXPECTING_TASK_APPEARED:
case MOTION_DOWN:
case SET_END_TARGET:
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 6f9cbfd..c3d74bb 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -22,7 +22,6 @@
import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
import static com.android.launcher3.model.data.AppInfo.PACKAGE_KEY_COMPARATOR;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SUPPORTS_MULTI_INSTANCE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -45,8 +44,6 @@
import com.android.internal.jank.Cuj;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.R;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.apppairs.AppPairIcon;
@@ -69,6 +66,7 @@
import com.android.quickstep.TaskUtils;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.views.GroupedTaskView;
+import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
@@ -135,7 +133,7 @@
}
GroupedTaskView gtv = (GroupedTaskView) taskView;
- List<TaskView.TaskContainer> containers = gtv.getTaskContainers();
+ List<TaskContainer> containers = gtv.getTaskContainers();
ComponentKey taskKey1 = TaskUtils.getLaunchComponentKeyForTask(
containers.get(0).getTask().key);
ComponentKey taskKey2 = TaskUtils.getLaunchComponentKeyForTask(
@@ -172,7 +170,7 @@
*/
public void saveAppPair(GroupedTaskView gtv) {
InteractionJankMonitorWrapper.begin(gtv, Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR);
- List<TaskView.TaskContainer> containers = gtv.getTaskContainers();
+ List<TaskContainer> containers = gtv.getTaskContainers();
WorkspaceItemInfo recentsInfo1 = containers.get(0).getItemInfo();
WorkspaceItemInfo recentsInfo2 = containers.get(1).getItemInfo();
WorkspaceItemInfo app1 = resolveAppPairWorkspaceInfo(recentsInfo1);
diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
index c26fc0c5..38ae303 100644
--- a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
+++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
@@ -18,6 +18,8 @@
import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
import static android.content.Intent.ACTION_TIME_CHANGED;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -50,7 +52,7 @@
private final Context mContext;
private final SimpleBroadcastReceiver mReceiver =
- new SimpleBroadcastReceiver(this::onClockEventReceived);
+ new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onClockEventReceived);
private final ArrayMap<BroadcastReceiver, Handler> mTimeEventReceivers = new ArrayMap<>();
private final List<ContentObserver> mFormatObservers = new ArrayList<>();
@@ -62,7 +64,7 @@
private AsyncClockEventDelegate(Context context) {
super(context);
mContext = context;
- mReceiver.registerAsync(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
+ mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
}
@Override
@@ -123,6 +125,6 @@
public void close() {
mDestroyed = true;
SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this);
- mReceiver.unregisterReceiverSafelyAsync(mContext);
+ mReceiver.unregisterReceiverSafely(mContext);
}
}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
index 85238ed..7e51fcf 100644
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
@@ -50,7 +50,7 @@
private val disappearanceDurationMs: Long,
private val interpolator: Interpolator,
) {
- private val borderAnimationProgress = AnimatedFloat { updateOutline() }
+ private val borderAnimationProgress = AnimatedFloat { _ -> updateOutline() }
private val borderPaint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = borderColor
@@ -224,6 +224,7 @@
val borderWidth: Float
get() = borderWidthPx * animationProgress
+
val alignmentAdjustment: Float
// Outset the border by half the width to create an outwards-growth animation
get() = -borderWidth / 2f + alignmentAdjustmentInset
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index 8d99069..a727aa2 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -18,10 +18,11 @@
import androidx.annotation.NonNull;
-import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskViewType;
import com.android.systemui.shared.recents.model.Task;
import java.util.List;
+import java.util.Objects;
/**
* A {@link Task} container that can contain N number of tasks that are part of the desktop in
@@ -33,7 +34,7 @@
public final List<Task> tasks;
public DesktopTask(@NonNull List<Task> tasks) {
- super(tasks.get(0), null, null, TaskView.Type.DESKTOP);
+ super(tasks.get(0), null, null, TaskViewType.DESKTOP);
this.tasks = tasks;
}
@@ -68,4 +69,16 @@
return "type=" + taskViewType + " tasks=" + tasks;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DesktopTask that)) return false;
+ if (!super.equals(o)) return false;
+ return Objects.equals(tasks, that.tasks);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), tasks);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index 945ffe3..fba08a9 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -20,12 +20,13 @@
import androidx.annotation.Nullable;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskViewType;
import com.android.systemui.shared.recents.model.Task;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
/**
* A {@link Task} container that can contain one or two tasks, depending on if the two tasks
@@ -38,19 +39,18 @@
public final Task task2;
@Nullable
public final SplitBounds mSplitBounds;
- @TaskView.Type
- public final int taskViewType;
+ public final TaskViewType taskViewType;
public GroupTask(@NonNull Task task) {
this(task, null, null);
}
public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) {
- this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE);
+ this(t1, t2, splitBounds, t2 != null ? TaskViewType.GROUPED : TaskViewType.SINGLE);
}
protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds,
- @TaskView.Type int taskViewType) {
+ TaskViewType taskViewType) {
task1 = t1;
task2 = t2;
mSplitBounds = splitBounds;
@@ -91,4 +91,17 @@
return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof GroupTask that)) return false;
+ return taskViewType == that.taskViewType && Objects.equals(task1,
+ that.task1) && Objects.equals(task2, that.task2)
+ && Objects.equals(mSplitBounds, that.mSplitBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(task1, task2, mSplitBounds, taskViewType);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
index dbeedd3..ece9583 100644
--- a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
+++ b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
@@ -36,6 +36,8 @@
private val pageSpacing: Int,
private val cornerRadius: Float,
private val interpolator: TimeInterpolator,
+ private val onStartCallback: Runnable,
+ private val onFinishCallback: Runnable,
) : RemoteTransitionStub() {
private val animationDurationMs = 500L
@@ -68,6 +70,7 @@
startT.setCrop(leash, chg.endAbsBounds).setCornerRadius(leash, cornerRadius)
}
}
+ onStartCallback.run()
startT.apply()
anim.addUpdateListener {
@@ -97,6 +100,7 @@
val t = Transaction()
try {
finishCB.onTransitionFinished(null, t)
+ onFinishCallback.run()
} catch (e: RemoteException) {
// Ignore
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 7ea04b1..e31a828 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -27,8 +27,10 @@
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.content.Context
import android.graphics.Bitmap
+import android.graphics.Color
import android.graphics.Rect
import android.graphics.RectF
+import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.view.RemoteAnimationTarget
import android.view.SurfaceControl
@@ -43,6 +45,7 @@
import com.android.app.animation.Interpolators
import com.android.launcher3.DeviceProfile
import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.InsettableFrameLayout
import com.android.launcher3.QuickstepTransitionManager
import com.android.launcher3.R
@@ -67,9 +70,9 @@
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.RecentsViewContainer
import com.android.quickstep.views.SplitInstructionsView
+import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskThumbnailViewDeprecated
import com.android.quickstep.views.TaskView
-import com.android.quickstep.views.TaskView.TaskContainer
import com.android.quickstep.views.TaskViewIcon
import com.android.wm.shell.shared.TransitionUtil
import java.util.Optional
@@ -118,9 +121,9 @@
if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) {
val drawable = getDrawable(container.iconView, splitSelectSource)
return SplitAnimInitProps(
- container.thumbnailViewDeprecated,
- container.thumbnailViewDeprecated.thumbnail,
- drawable!!,
+ container.snapshotView,
+ container.splitAnimationThumbnail,
+ drawable,
fadeWithThumbnail = true,
isStagedTask = true,
iconView = container.iconView.asView()
@@ -137,9 +140,9 @@
taskView.taskContainers.first().let {
val drawable = getDrawable(it.iconView, splitSelectSource)
return SplitAnimInitProps(
- it.thumbnailViewDeprecated,
- it.thumbnailViewDeprecated.thumbnail,
- drawable!!,
+ it.snapshotView,
+ it.splitAnimationThumbnail,
+ drawable,
fadeWithThumbnail = true,
isStagedTask = true,
iconView = it.iconView.asView()
@@ -151,41 +154,51 @@
/**
* Returns the drawable that's provided in iconView, however if that is null it falls back to
* the drawable that's in splitSelectSource. TaskView's icon drawable can be null if the
- * TaskView is scrolled far enough off screen
+ * TaskView is scrolled far enough off screen.
*
- * @return [Drawable]
+ * @return the [Drawable] icon, or a translucent drawable if none was found
*/
- fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable? {
- if (iconView.drawable == null && splitSelectSource != null) {
- return splitSelectSource.drawable
- }
- return iconView.drawable
+ fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable {
+ val drawable =
+ if (iconView.drawable == null && splitSelectSource != null) splitSelectSource.drawable
+ else iconView.drawable
+ return drawable ?: ColorDrawable(Color.TRANSPARENT)
}
/**
* When selecting first app from split pair, second app's thumbnail remains. This animates the
* second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying it
- * with [TaskThumbnailViewDeprecated]'s splashView. Adds animations to the provided builder.
- * Note: The app that **was not** selected as the first split app should be the container that's
- * passed through.
+ * with [TaskContainer]'s splashView. Adds animations to the provided builder. Note: The app
+ * that **was not** selected as the first split app should be the container that's passed
+ * through.
*
* @param builder Adds animation to this
- * @param taskIdAttributeContainer container of the app that **was not** selected
+ * @param taskContainer container of the app that **was not** selected
* @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair
- * (opposite of that representing [taskIdAttributeContainer])
+ * (opposite of that representing [taskContainer])
*/
fun addInitialSplitFromPair(
- taskIdAttributeContainer: TaskContainer,
+ taskContainer: TaskContainer,
builder: PendingAnimation,
deviceProfile: DeviceProfile,
taskViewWidth: Int,
taskViewHeight: Int,
isPrimaryTaskSplitting: Boolean
) {
- val thumbnail = taskIdAttributeContainer.thumbnailViewDeprecated
- val iconView: View = taskIdAttributeContainer.iconView.asView()
- builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailViewDeprecated.SPLASH_ALPHA, 1f))
- thumbnail.setShowSplashForSplitSelection(true)
+ val snapshot = taskContainer.snapshotView
+ val iconView: View = taskContainer.iconView.asView()
+ // TODO(334826842): Switch to splash state in TaskThumbnailView
+ if (!enableRefactorTaskThumbnail()) {
+ val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated
+ builder.add(
+ ObjectAnimator.ofFloat(
+ thumbnailViewDeprecated,
+ TaskThumbnailViewDeprecated.SPLASH_ALPHA,
+ 1f
+ )
+ )
+ thumbnailViewDeprecated.setShowSplashForSplitSelection(true)
+ }
// With the new `IconAppChipView`, we always want to keep the chip pinned to the
// top left of the task / thumbnail.
if (enableOverviewIconMenu()) {
@@ -202,14 +215,10 @@
}
if (deviceProfile.isLeftRightSplit) {
// Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
- val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f
- val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width
+ val centerThumbnailTranslationX: Float = (taskViewWidth - snapshot.width) / 2f
+ val finalScaleX: Float = taskViewWidth.toFloat() / snapshot.width
builder.add(
- ObjectAnimator.ofFloat(
- thumbnail,
- TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X,
- centerThumbnailTranslationX
- )
+ ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, centerThumbnailTranslationX)
)
if (!enableOverviewIconMenu()) {
// icons are anchored from Gravity.END, so need to use negative translation
@@ -218,21 +227,15 @@
ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX)
)
}
- builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX))
+ builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_X, finalScaleX))
// Reset other dimensions
// TODO(b/271468547), can't set Y translate to 0, need to account for top space
- thumbnail.scaleY = 1f
+ snapshot.scaleY = 1f
val translateYResetVal: Float =
if (!isPrimaryTaskSplitting) 0f
else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
- builder.add(
- ObjectAnimator.ofFloat(
- thumbnail,
- TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y,
- translateYResetVal
- )
- )
+ builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, translateYResetVal))
} else {
val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
// Center view first so scaling happens uniformly, alternatively we can move pivotY to 0
@@ -247,36 +250,26 @@
// thumbnail needs to take that into account. We should migrate to only using
// translations otherwise this asymmetry causes problems..
if (isPrimaryTaskSplitting) {
- centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
+ centerThumbnailTranslationY = (thumbnailSize - snapshot.height) / 2f
centerThumbnailTranslationY +=
deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
} else {
- centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
+ centerThumbnailTranslationY = (thumbnailSize - snapshot.height) / 2f
}
- val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height
+ val finalScaleY: Float = thumbnailSize.toFloat() / snapshot.height
builder.add(
- ObjectAnimator.ofFloat(
- thumbnail,
- TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y,
- centerThumbnailTranslationY
- )
+ ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_Y, centerThumbnailTranslationY)
)
if (!enableOverviewIconMenu()) {
// icons are anchored from Gravity.END, so need to use negative translation
builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f))
}
- builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY))
+ builder.add(ObjectAnimator.ofFloat(snapshot, View.SCALE_Y, finalScaleY))
// Reset other dimensions
- thumbnail.scaleX = 1f
- builder.add(
- ObjectAnimator.ofFloat(
- thumbnail,
- TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X,
- 0f
- )
- )
+ snapshot.scaleX = 1f
+ builder.add(ObjectAnimator.ofFloat(snapshot, View.TRANSLATION_X, 0f))
}
}
@@ -495,7 +488,8 @@
depthController: DepthController?,
info: TransitionInfo?,
t: Transaction?,
- finishCallback: Runnable
+ finishCallback: Runnable,
+ cornerRadius: Float
) {
if (info == null && t == null) {
// (Legacy animation) Tapping a split tile in Overview
@@ -542,7 +536,13 @@
val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info)
if (appPairLaunchingAppIndex == -1) {
// Launch split app pair animation
- composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+ composeIconSplitLaunchAnimator(
+ launchingIconView,
+ info,
+ t,
+ finishCallback,
+ cornerRadius
+ )
} else {
composeFullscreenIconSplitLaunchAnimator(
launchingIconView,
@@ -559,7 +559,14 @@
"unexpected null"
}
- composeFadeInSplitLaunchAnimator(initialTaskId, secondTaskId, info, t, finishCallback)
+ composeFadeInSplitLaunchAnimator(
+ initialTaskId,
+ secondTaskId,
+ info,
+ t,
+ finishCallback,
+ cornerRadius
+ )
}
}
@@ -659,15 +666,15 @@
* To find the root shell leash that we want to fade in, we do the following: The Changes we
* receive in transitionInfo are structured like this
*
- * Root (grandparent)
+ * (0) Root (grandparent)
* |
- * |--> Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW)
+ * |--> (1) Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW)
* | |
- * | --> App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW)
+ * | --> (1a) App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW)
* |--> Divider
- * |--> Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW)
+ * |--> (2) Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW)
* |
- * --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
+ * --> (2a) App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
*
* We want to animate the Root (grandparent) so that it affects both apps and the divider. To do
* this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the left-side ones,
@@ -682,7 +689,8 @@
launchingIconView: AppPairIcon,
transitionInfo: TransitionInfo,
t: Transaction,
- finishCallback: Runnable
+ finishCallback: Runnable,
+ windowRadius: Float
) {
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
// use the scale-up animation
@@ -702,48 +710,26 @@
// Create an AnimatorSet that will run both shell and launcher transitions together
val launchAnimation = AnimatorSet()
- var rootCandidate: Change? = null
- for (change in transitionInfo.changes) {
- val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+ val splitRoots: Pair<Change, List<Change>>? =
+ SplitScreenUtils.extractTopParentAndChildren(transitionInfo)
+ check(splitRoots != null) { "Could not find split roots" }
- // TODO (b/316490565): Replace this logic when SplitBounds is available to
- // startAnimation() and we can know the precise taskIds of launching tasks.
- // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
- if (
- taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
- (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
- ) {
- // Check if it is a left/top app.
- val isLeftTopApp =
- (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
- (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
- if (isLeftTopApp) {
- // Found one!
- rootCandidate = change
- break
- }
- }
- }
-
- // If we could not find a proper root candidate, something went wrong.
- check(rootCandidate != null) { "Could not find a split root candidate" }
+ // Will point to change (0) in diagram above
+ val mainRootCandidate = splitRoots.first
+ // Will contain changes (1) and (2) in diagram above
+ val leafRoots: List<Change> = splitRoots.second
// Find the place where our left/top app window meets the divider (used for the
// launcher side animation)
+ val leftTopApp =
+ leafRoots.single { change ->
+ (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
+ (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
+ }
val dividerPos =
- if (dp.isLeftRightSplit) rootCandidate.endAbsBounds.right
- else rootCandidate.endAbsBounds.bottom
-
- // Recurse up the tree until parent is null, then we've found our root.
- var parentToken: WindowContainerToken? = rootCandidate.parent
- while (parentToken != null) {
- rootCandidate = transitionInfo.getChange(parentToken) ?: break
- parentToken = rootCandidate.parent
- }
-
- // Make sure nothing weird happened, like getChange() returning null.
- check(rootCandidate != null) { "Failed to find a root leash" }
+ if (dp.isLeftRightSplit) leftTopApp.endAbsBounds.right
+ else leftTopApp.endAbsBounds.bottom
// Create a new floating view in Launcher, positioned above the launching icon
val drawableArea = launchingIconView.iconDrawableArea
@@ -762,9 +748,26 @@
)
floatingView.bringToFront()
- launchAnimation.play(
- getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate)
+ val iconLaunchValueAnimator =
+ getIconLaunchValueAnimator(
+ t,
+ dp,
+ finishCallback,
+ launcher,
+ floatingView,
+ mainRootCandidate
+ )
+ iconLaunchValueAnimator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
+ for (c in leafRoots) {
+ t.setCornerRadius(c.leash, windowRadius)
+ t.apply()
+ }
+ }
+ }
)
+ launchAnimation.play(iconLaunchValueAnimator)
launchAnimation.start()
}
@@ -1037,7 +1040,8 @@
secondTaskId: Int,
transitionInfo: TransitionInfo,
t: Transaction,
- finishCallback: Runnable
+ finishCallback: Runnable,
+ cornerRadius: Float
) {
var splitRoot1: Change? = null
var splitRoot2: Change? = null
@@ -1115,6 +1119,7 @@
override fun onAnimationStart(animation: Animator) {
for (leash in openingTargets) {
animTransaction.show(leash).setAlpha(leash, 0.0f)
+ animTransaction.setCornerRadius(leash, cornerRadius)
}
animTransaction.apply()
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index 38bbe60..d58cb91 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -16,11 +16,21 @@
package com.android.quickstep.util
+import android.util.Log
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.TransitionInfo.FLAG_FIRST_CUSTOM
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.wm.shell.util.SplitBounds
+import java.lang.IllegalStateException
class SplitScreenUtils {
companion object {
+ private const val TAG = "SplitScreenUtils"
+
// TODO(b/254378592): Remove these methods when the two classes are reunited
/** Converts the shell version of SplitBounds to the launcher version */
@JvmStatic
@@ -31,28 +41,58 @@
null
} else {
SplitConfigurationOptions.SplitBounds(
- shellSplitBounds.leftTopBounds, shellSplitBounds.rightBottomBounds,
- shellSplitBounds.leftTopTaskId, shellSplitBounds.rightBottomTaskId,
+ shellSplitBounds.leftTopBounds,
+ shellSplitBounds.rightBottomBounds,
+ shellSplitBounds.leftTopTaskId,
+ shellSplitBounds.rightBottomTaskId,
shellSplitBounds.snapPosition
)
}
}
- /** Converts the launcher version of SplitBounds to the shell version */
- @JvmStatic
- fun convertLauncherSplitBoundsToShell(
- launcherSplitBounds: SplitConfigurationOptions.SplitBounds?
- ): SplitBounds? {
- return if (launcherSplitBounds == null) {
- null
+ /**
+ * Given a TransitionInfo, generates the tree structure for those changes and extracts out
+ * the top most root and it's two immediate children.
+ * Changes can be provided in any order.
+ *
+ * @return a [Pair] where first -> top most split root,
+ * second -> [List] of 2, leftTop/bottomRight stage roots
+ */
+ fun extractTopParentAndChildren(transitionInfo: TransitionInfo):
+ Pair<Change, List<Change>>? {
+ val parentToChildren = mutableMapOf<Change, MutableList<Change>>()
+ val hasParent = mutableSetOf<Change>()
+ // filter out anything that isn't opening and the divider
+ val taskChanges: List<Change> = transitionInfo.changes
+ .filter { change -> (change.mode == TRANSIT_OPEN ||
+ change.mode == TRANSIT_TO_FRONT) && change.flags < FLAG_FIRST_CUSTOM}
+ .toList()
+
+ // 1. Build Parent-Child Relationships
+ for (change in taskChanges) {
+ // TODO (b/316490565): Replace this logic when SplitBounds is available to
+ // startAnimation() and we can know the precise taskIds of launching tasks.
+ change.parent?.let { parent ->
+ parentToChildren
+ .getOrPut(transitionInfo.getChange(parent)!!) { mutableListOf() }
+ .add(change)
+ hasParent.add(change)
+ }
+ }
+
+ // 2. Find Top Parent
+ val topParent = taskChanges.firstOrNull { it !in hasParent }
+
+ // 3. Extract Immediate Children
+ return if (topParent != null) {
+ val immediateChildren = parentToChildren.getOrDefault(topParent, emptyList())
+ if (immediateChildren.size != 2) {
+ throw IllegalStateException("incorrect split stage root size")
+ }
+ Pair(topParent, immediateChildren)
} else {
- SplitBounds(
- launcherSplitBounds.leftTopBounds,
- launcherSplitBounds.rightBottomBounds,
- launcherSplitBounds.leftTopTaskId,
- launcherSplitBounds.rightBottomTaskId,
- launcherSplitBounds.snapPosition
- )
+ Log.w(TAG, "No top parent found")
+ null
}
}
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 7e7c794..d906bb3 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -104,6 +104,7 @@
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import com.android.wm.shell.splitscreen.ISplitSelectListener;
@@ -778,7 +779,8 @@
info, t, () -> {
finishAdapter.run();
cleanup(true /*success*/);
- });
+ },
+ QuickStepContract.getWindowCornerRadius(mContainer.asContext()));
});
}
@@ -826,7 +828,8 @@
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
Runnable finishedCallback) {
postAsyncCallback(mHandler,
- () -> mSplitAnimationController.playSplitLaunchAnimation(mLaunchingTaskView,
+ () -> mSplitAnimationController
+ .playSplitLaunchAnimation(mLaunchingTaskView,
mLaunchingIconView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
nonApps, mStateManager, mDepthController, null /* info */, null /* t */,
() -> {
@@ -835,7 +838,8 @@
mSuccessCallback.accept(true);
}
resetState();
- }));
+ },
+ QuickStepContract.getWindowCornerRadius(mContainer.asContext())));
}
@Override
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 5e42b90..27fb31d 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -138,12 +138,13 @@
mLauncher, mLauncher.getDragLayer(),
controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(),
null /* icon */, startingTaskRect);
+ Task task = Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
+ false /* isLocked */);
RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
.getIconCache()
- .updateIconInBackground(
- Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
- false /* isLocked */),
- (task) -> floatingTaskView.setIcon(task.icon));
+ .getIconInBackground(
+ task,
+ (icon, contentDescription, title) -> floatingTaskView.setIcon(icon));
floatingTaskView.setAlpha(1);
floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
false /* fadeWithThumbnail */, true /* isStagedTask */);
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 88c3a08..56e91ed 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -139,6 +139,10 @@
final float aspectRatio = destinationBounds.width() / (float) destinationBounds.height();
String reasonForCreateOverlay = null; // For debugging purpose.
+
+ // Slightly larger app bounds to allow for off by 1 pixel source-rect-hint errors.
+ Rect overflowAppBounds = new Rect(appBounds.left - 1, appBounds.top - 1,
+ appBounds.right + 1, appBounds.bottom + 1);
if (sourceRectHint.isEmpty()) {
reasonForCreateOverlay = "Source rect hint is empty";
} else if (sourceRectHint.width() < destinationBounds.width()
@@ -149,7 +153,7 @@
// animation in this case.
reasonForCreateOverlay = "Source rect hint is too small " + sourceRectHint;
sourceRectHint.setEmpty();
- } else if (!appBounds.contains(sourceRectHint)) {
+ } else if (!overflowAppBounds.contains(sourceRectHint)) {
// This is a situation in which the source hint rect is outside the app bounds, so it is
// not a valid rectangle to use for cropping app surface
reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint;
@@ -453,8 +457,9 @@
}
// adjust the mSourceRectHint / mAppBounds by display cutout if applicable.
if (mSourceRectHint != null && mDisplayCutoutInsets != null) {
- if (mFromRotation == Surface.ROTATION_0 && mDisplayCutoutInsets.top >= 0) {
- // TODO: this is to special case the issues on Pixel Foldable device(s).
+ if (mFromRotation == Surface.ROTATION_0) {
+ // TODO: this is to special case the issues on Foldable device
+ // with display cutout.
mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
} else if (mFromRotation == Surface.ROTATION_90) {
mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 49f4e5f..d9b7d20 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -27,7 +27,6 @@
import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
-import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell;
import android.animation.TimeInterpolator;
import android.content.Context;
@@ -247,8 +246,6 @@
} else {
mStagePosition = runningTarget.taskId == splitInfo.leftTopTaskId
? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT;
- mPositionHelper.setSplitBounds(convertLauncherSplitBoundsToShell(mSplitBounds),
- mStagePosition);
}
calculateTaskSize();
}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 4c78e21..9ce2277 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -24,9 +24,11 @@
import android.graphics.drawable.shapes.RoundRectShape
import android.util.AttributeSet
import android.util.Log
+import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
import com.android.launcher3.util.RunnableList
import com.android.launcher3.util.SplitConfigurationOptions
@@ -40,7 +42,7 @@
/** TaskView that contains all tasks that are part of the desktop. */
class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
- TaskView(context, attrs) {
+ TaskView(context, attrs, type = TaskViewType.DESKTOP) {
private val snapshotDrawParams =
object : FullscreenDrawParams(context) {
@@ -48,11 +50,11 @@
override fun computeTaskCornerRadius(context: Context) =
computeWindowCornerRadius(context)
}
- private val taskThumbnailViewPool =
+ private val taskThumbnailViewDeprecatedPool =
ViewPool<TaskThumbnailViewDeprecated>(
context,
this,
- R.layout.task_thumbnail,
+ R.layout.task_thumbnail_deprecated,
VIEW_POOL_MAX_SIZE,
VIEW_POOL_INITIAL_SIZE
)
@@ -89,6 +91,65 @@
childCountAtInflation = childCount
}
+ /** Updates this desktop task to the gives task list defined in `tasks` */
+ fun bind(
+ tasks: List<Task>,
+ orientedState: RecentsOrientedState,
+ taskOverlayFactory: TaskOverlayFactory
+ ) {
+ if (DEBUG) {
+ val sb = StringBuilder()
+ sb.append("bind tasks=").append(tasks.size).append("\n")
+ tasks.forEach { sb.append(" key=${it.key}\n") }
+ Log.d(TAG, sb.toString())
+ }
+ cancelPendingLoadTasks()
+ taskContainers =
+ tasks.map { task ->
+ val snapshotView =
+ if (enableRefactorTaskThumbnail()) {
+ LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false)
+ } else {
+ taskThumbnailViewDeprecatedPool.view
+ }
+
+ addView(
+ snapshotView,
+ // Add snapshotView to the front after initial views e.g. icon and
+ // background.
+ childCountAtInflation,
+ LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ )
+ TaskContainer(
+ this,
+ task,
+ snapshotView,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+ digitalWellBeingToast = null,
+ showWindowsView = null,
+ taskOverlayFactory
+ )
+ }
+ taskContainers.forEach { it.bind() }
+ setOrientationState(orientedState)
+ }
+
+ override fun onRecycle() {
+ super.onRecycle()
+ visibility = VISIBLE
+ taskContainers.forEach {
+ if (!enableRefactorTaskThumbnail()) {
+ removeView(it.thumbnailViewDeprecated)
+ taskThumbnailViewDeprecatedPool.recycle(it.thumbnailViewDeprecated)
+ }
+ }
+ }
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val containerWidth = MeasureSpec.getSize(widthMeasureSpec)
@@ -128,7 +189,7 @@
}
val thumbWidth = (taskSize.width() * scaleWidth).toInt()
val thumbHeight = (taskSize.height() * scaleHeight).toInt()
- it.thumbnailViewDeprecated.measure(
+ it.snapshotView.measure(
MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY)
)
@@ -139,8 +200,8 @@
var taskY = (positionInParent.y * scaleHeight).toInt()
// move task down by margin size
taskY += thumbnailTopMarginPx
- it.thumbnailViewDeprecated.x = taskX.toFloat()
- it.thumbnailViewDeprecated.y = taskY.toFloat()
+ it.snapshotView.x = taskX.toFloat()
+ it.snapshotView.y = taskY.toFloat()
if (DEBUG) {
Log.d(
TAG,
@@ -151,76 +212,6 @@
}
}
- override fun onRecycle() {
- super.onRecycle()
- visibility = VISIBLE
- }
-
- /** Updates this desktop task to the gives task list defined in `tasks` */
- fun bind(
- tasks: List<Task>,
- orientedState: RecentsOrientedState,
- taskOverlayFactory: TaskOverlayFactory
- ) {
- if (DEBUG) {
- val sb = StringBuilder()
- sb.append("bind tasks=").append(tasks.size).append("\n")
- tasks.forEach { sb.append(" key=${it.key}\n") }
- Log.d(TAG, sb.toString())
- }
- cancelPendingLoadTasks()
-
- if (!isTaskContainersInitialized()) {
- taskContainers = arrayListOf()
- }
- val taskContainers = taskContainers as ArrayList
- taskContainers.ensureCapacity(tasks.size)
- tasks.forEachIndexed { index, task ->
- val thumbnailViewDeprecated: TaskThumbnailViewDeprecated
- if (index >= taskContainers.size) {
- thumbnailViewDeprecated = taskThumbnailViewPool.view
- // Add thumbnailView from to position after the initial child views.
- addView(
- thumbnailViewDeprecated,
- childCountAtInflation,
- LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- )
- } else {
- thumbnailViewDeprecated = taskContainers[index].thumbnailViewDeprecated
- }
- val taskContainer =
- TaskContainer(
- task,
- // TODO(b/338360089): Support new TTV for DesktopTaskView
- thumbnailView = null,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
- digitalWellBeingToast = null,
- showWindowsView = null,
- taskOverlayFactory
- )
- if (index >= taskContainers.size) {
- taskContainers.add(taskContainer)
- } else {
- taskContainers[index] = taskContainer
- }
- taskContainer.bind()
- }
- repeat(taskContainers.size - tasks.size) {
- with(taskContainers.removeLast()) {
- removeView(thumbnailViewDeprecated)
- taskThumbnailViewPool.recycle(thumbnailViewDeprecated)
- }
- }
-
- setOrientationState(orientedState)
- }
-
override fun needsUpdate(dataChange: Int, flag: Int) =
if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 4a5b9e4..9f268a0 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -332,12 +332,12 @@
(FrameLayout.LayoutParams) mBanner.getLayoutParams();
DeviceProfile deviceProfile = mContainer.getDeviceProfile();
layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
- mTaskView.getFirstThumbnailViewDeprecated().getLayoutParams()).bottomMargin;
+ mTaskView.getFirstSnapshotView().getLayoutParams()).bottomMargin;
RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
Pair<Float, Float> translations = orientationHandler
.getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
- mTaskView.getThumbnailViews(), mTask.key.id, mBanner);
+ mTaskView.getSnapshotViews(), mTask.key.id, mBanner);
mSplitOffsetTranslationX = translations.first;
mSplitOffsetTranslationY = translations.second;
updateTranslationY();
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index e024995..6bbd6b2 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -57,6 +57,7 @@
private val container: RecentsViewContainer
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG)
// Animation interpolators
protected val expandXInterpolator: Interpolator
@@ -105,13 +106,15 @@
)
// Find device-specific measurements
- deviceCornerRadius = QuickStepContract.getWindowCornerRadius(container.asContext())
+ val resources = context.resources
+ deviceCornerRadius = QuickStepContract.getWindowCornerRadius(context)
deviceHalfDividerSize =
- container.asContext().resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
+ resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
val dividerCenterPos = dividerPos + deviceHalfDividerSize
desiredSplitRatio =
if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx
else dividerCenterPos / dp.heightPx
+ dividerPaint.color = resources.getColor(R.color.taskbar_background_dark, null /*theme*/)
}
override fun draw(canvas: Canvas) {
@@ -153,8 +156,12 @@
val leftSide = RectF(0f, 0f, dividerCenterPos - changingDividerSize, height)
// The right half of the background image
val rightSide = RectF(dividerCenterPos + changingDividerSize, 0f, width, height)
+ // Middle part is for divider background
+ val middleRect = RectF(leftSide.right - deviceHalfDividerSize, 0f,
+ rightSide.left + deviceHalfDividerSize, height)
// Draw background
+ canvas.drawRect(middleRect, dividerPaint)
drawCustomRoundedRect(
canvas,
leftSide,
@@ -251,8 +258,12 @@
val topSide = RectF(0f, 0f, width, dividerCenterPos - changingDividerSize)
// The bottom half of the background image
val bottomSide = RectF(0f, dividerCenterPos + changingDividerSize, width, height)
+ // Middle part is for divider background
+ val middleRect = RectF(0f, topSide.bottom - deviceHalfDividerSize,
+ width, bottomSide.top + deviceHalfDividerSize)
// Draw background
+ canvas.drawRect(middleRect, dividerPaint)
drawCustomRoundedRect(
canvas,
topSide,
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 6296b0e..6523ba7 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -33,10 +33,8 @@
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.util.RecentsOrientedState
-import com.android.quickstep.util.SplitScreenUtils.Companion.convertLauncherSplitBoundsToShell
import com.android.quickstep.util.SplitSelectStateController
import com.android.systemui.shared.recents.model.Task
-import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
import com.android.systemui.shared.system.InteractionJankMonitorWrapper
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition
@@ -51,7 +49,7 @@
* (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included).
*/
class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
- TaskView(context, attrs) {
+ TaskView(context, attrs, type = TaskViewType.GROUPED) {
// TODO(b/336612373): Support new TTV for GroupedTaskView
var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null
private set
@@ -70,36 +68,20 @@
val initSplitTaskId = getThisTaskCurrentlyInSplitSelection()
if (initSplitTaskId == INVALID_TASK_ID) {
pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
- taskContainers[0].thumbnailViewDeprecated,
- taskContainers[1].thumbnailViewDeprecated,
+ taskContainers[0].snapshotView,
+ taskContainers[1].snapshotView,
widthSize,
heightSize,
splitBoundsConfig,
container.deviceProfile,
layoutDirection == LAYOUT_DIRECTION_RTL
)
- // Should we be having a separate translation step apart from the measuring above?
- // The following only applies to large screen for now, but for future reference
- // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary
- // translation directions
- taskContainers[0]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateX(taskContainers[0].thumbnailViewDeprecated.translationX)
- taskContainers[0]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateY(taskContainers[0].thumbnailViewDeprecated.translationY)
- taskContainers[1]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateX(taskContainers[1].thumbnailViewDeprecated.translationX)
- taskContainers[1]
- .thumbnailViewDeprecated
- .applySplitSelectTranslateY(taskContainers[1].thumbnailViewDeprecated.translationY)
} else {
// Currently being split with this taskView, let the non-split selected thumbnail
// take up full thumbnail area
taskContainers
.firstOrNull { it.task.key.id != initSplitTaskId }
- ?.thumbnailViewDeprecated
+ ?.snapshotView
?.measure(
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(
@@ -147,23 +129,7 @@
)
taskContainers.forEach { it.bind() }
- this.splitBoundsConfig =
- splitBoundsConfig?.also {
- taskContainers[0]
- .thumbnailViewDeprecated
- .previewPositionHelper
- .setSplitBounds(
- convertLauncherSplitBoundsToShell(it),
- PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT
- )
- taskContainers[1]
- .thumbnailViewDeprecated
- .previewPositionHelper
- .setSplitBounds(
- convertLauncherSplitBoundsToShell(it),
- PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT
- )
- }
+ this.splitBoundsConfig = splitBoundsConfig
taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) }
setOrientationState(orientedState)
}
@@ -230,8 +196,8 @@
taskContainers[0].iconView.asView(),
taskContainers[1].iconView.asView(),
taskIconHeight,
- taskContainers[0].thumbnailViewDeprecated.measuredWidth,
- taskContainers[0].thumbnailViewDeprecated.measuredHeight,
+ taskContainers[0].snapshotView.measuredWidth,
+ taskContainers[0].snapshotView.measuredHeight,
measuredHeight,
measuredWidth,
isRtl,
@@ -326,7 +292,7 @@
// Check which of the two apps was selected
if (
taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) ||
- taskContainers[1].thumbnailViewDeprecated.containsPoint(lastTouchDownPosition)
+ taskContainers[1].snapshotView.containsPoint(lastTouchDownPosition)
) {
return 1
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index d806e3d..cb8ee06 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -135,7 +135,6 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Flags;
import com.android.launcher3.Insettable;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.PagedView;
@@ -212,7 +211,6 @@
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.quickstep.util.TransformParams;
import com.android.quickstep.util.VibrationConstants;
-import com.android.quickstep.views.TaskView.TaskContainer;
import com.android.systemui.plugins.ResourceProvider;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -223,17 +221,19 @@
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.common.pip.IPipAnimationListener;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import kotlin.Unit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -461,7 +461,9 @@
private static final float FOREGROUND_SCRIM_TINT = 0.32f;
+ @Nullable
public final RecentsViewData mRecentsViewData = new RecentsViewData();
+ @Nullable
public final TasksRepository mTasksRepository;
protected final RecentsOrientedState mOrientationState;
@@ -1016,14 +1018,17 @@
@Nullable
public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
if (mHandleTaskStackChanges) {
- TaskView taskView = getTaskViewByTaskId(taskId);
- if (taskView != null) {
- for (TaskContainer container : taskView.getTaskContainers()) {
- if (container == null || taskId != container.getTask().key.id) {
- continue;
+ // TODO(b/342560598): Handle onTaskThumbnailChanged for new TTV.
+ if (!enableRefactorTaskThumbnail()) {
+ TaskView taskView = getTaskViewByTaskId(taskId);
+ if (taskView != null) {
+ for (TaskContainer container : taskView.getTaskContainers()) {
+ if (container == null || taskId != container.getTask().key.id) {
+ continue;
+ }
+ container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
+ thumbnailData);
}
- container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
- thumbnailData);
}
}
}
@@ -1060,6 +1065,10 @@
@Nullable
public TaskView updateThumbnail(
HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow) {
+ if (enableRefactorTaskThumbnail()) {
+ // TODO(b/342560598): Handle updateThumbnail for new TTV.
+ return null;
+ }
TaskView updatedTaskView = null;
for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
Integer id = entry.getKey();
@@ -1817,7 +1826,7 @@
// If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE
// to be a temporary container for the remaining task.
TaskView taskView = getTaskViewFromPool(
- isRemovalNeeded ? TaskView.Type.SINGLE : groupTask.taskViewType);
+ isRemovalNeeded ? TaskViewType.SINGLE : groupTask.taskViewType);
if (taskView instanceof GroupedTaskView) {
boolean firstTaskIsLeftTopTask =
groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
@@ -1826,8 +1835,13 @@
((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
mTaskOverlayFactory, groupTask.mSplitBounds);
} else if (taskView instanceof DesktopTaskView) {
- ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks,
- mOrientationState, mTaskOverlayFactory);
+ // Minimized tasks should not be shown in Overview
+ List<Task> nonMinimizedTasks =
+ ((DesktopTask) groupTask).tasks.stream()
+ .filter(task -> !task.isMinimized)
+ .toList();
+ ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState,
+ mTaskOverlayFactory);
mDesktopTaskView = (DesktopTaskView) taskView;
} else {
Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
@@ -2386,12 +2400,11 @@
if (containers.isEmpty()) {
continue;
}
- int index = indexOfChild(taskView);
boolean visible;
if (showAsGrid()) {
visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
} else {
- visible = lower <= index && index <= upper;
+ visible = lower <= i && i <= upper;
}
if (visible) {
// Default update all non-null tasks, then remove running ones
@@ -2422,7 +2435,7 @@
}
taskView.onTaskListVisibilityChanged(true /* visible */, changes);
}
- mHasVisibleTaskData.put(task.key.id, visible);
+ mHasVisibleTaskData.put(task.key.id, true);
}
} else {
for (TaskContainer container : containers) {
@@ -2589,16 +2602,16 @@
* Handle the edge case where Recents could increment task count very high over long
* period of device usage. Probably will never happen, but meh.
*/
- private TaskView getTaskViewFromPool(@TaskView.Type int type) {
+ private TaskView getTaskViewFromPool(TaskViewType type) {
TaskView taskView;
switch (type) {
- case TaskView.Type.GROUPED:
+ case GROUPED:
taskView = mGroupedTaskViewPool.getView();
break;
- case TaskView.Type.DESKTOP:
+ case DESKTOP:
taskView = mDesktopTaskViewPool.getView();
break;
- case TaskView.Type.SINGLE:
+ case SINGLE:
default:
taskView = mTaskViewPool.getView();
}
@@ -2829,12 +2842,12 @@
// Add an empty view for now until the task plan is loaded and applied
final TaskView taskView;
if (needDesktopTask) {
- taskView = getTaskViewFromPool(TaskView.Type.DESKTOP);
+ taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length);
((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks),
mOrientationState, mTaskOverlayFactory);
} else if (needGroupTaskView) {
- taskView = getTaskViewFromPool(TaskView.Type.GROUPED);
+ taskView = getTaskViewFromPool(TaskViewType.GROUPED);
mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]};
// When we create a placeholder task view mSplitBoundsConfig will be null, but with
// the actual app running we won't need to show the thumbnail until all the tasks
@@ -2842,7 +2855,7 @@
((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
} else {
- taskView = getTaskViewFromPool(TaskView.Type.SINGLE);
+ taskView = getTaskViewFromPool(TaskViewType.SINGLE);
// The temporary running task is only used for the duration between the start of the
// gesture and the task list is loaded and applied
mTmpRunningTasks = new Task[]{runningTasks[0]};
@@ -2910,7 +2923,7 @@
int prevRunningTaskViewId = mRunningTaskViewId;
mRunningTaskViewId = runningTaskViewId;
- if (Flags.enableRefactorTaskThumbnail()) {
+ if (enableRefactorTaskThumbnail()) {
TaskView previousRunningTaskView = getTaskViewFromTaskViewId(prevRunningTaskViewId);
if (previousRunningTaskView != null) {
previousRunningTaskView.notifyIsRunningTaskUpdated();
@@ -3803,7 +3816,7 @@
anim.setFloat(taskView, taskView.getSecondaryDismissTranslationProperty(),
secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
dismissTranslationInterpolationEnd));
- anim.setFloat(taskView, TaskView.SCALE_AND_DIM_OUT, 0f,
+ anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
} else {
float primaryTranslation =
@@ -4856,14 +4869,16 @@
== mSplitSelectStateController.getInitialTaskId();
TaskContainer taskContainer = mSplitHiddenTaskView
.getTaskContainers().get(primaryTaskSelected ? 1 : 0);
- TaskThumbnailViewDeprecated thumbnail = taskContainer.getThumbnailViewDeprecated();
mSplitSelectStateController.getSplitAnimationController()
.addInitialSplitFromPair(taskContainer, builder,
mContainer.getDeviceProfile(),
mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
primaryTaskSelected);
builder.addOnFrameCallback(() ->{
- thumbnail.refreshSplashView();
+ // TODO(b/334826842): Handle splash icon for new TTV.
+ if (!enableRefactorTaskThumbnail()) {
+ taskContainer.getThumbnailViewDeprecated().refreshSplashView();
+ }
mSplitHiddenTaskView.updateSnapshotRadius();
});
} else if (isInitiatingSplitFromTaskView) {
@@ -5255,7 +5270,7 @@
updateGridProperties();
updateScrollSynchronously();
- int targetSysUiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+ int targetSysUiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
final boolean[] passedOverviewThreshold = new boolean[] {false};
ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
progressAnim.addUpdateListener(animator -> {
@@ -5342,6 +5357,43 @@
updateCurrentTaskActionsVisibility();
loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
updateEnabledOverlays();
+
+ if (enableRefactorTaskThumbnail()) {
+ int screenStart = 0;
+ int screenEnd = 0;
+ int centerPageIndex = 0;
+ if (showAsGrid()) {
+ screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
+ int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
+ screenEnd = screenStart + pageOrientedSize;
+ } else {
+ centerPageIndex = getPageNearestToCenterOfScreen();
+ }
+
+ Set<Integer> fullyVisibleTaskIds = new HashSet<>();
+
+ // Update the task data for the in/visible children
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView taskView = requireTaskViewAt(i);
+ List<TaskContainer> containers = taskView.getTaskContainers();
+ if (containers.isEmpty()) {
+ continue;
+ }
+ boolean isFullyVisible;
+ if (showAsGrid()) {
+ isFullyVisible = isTaskViewFullyWithinBounds(taskView, screenStart,
+ screenEnd);
+ } else {
+ isFullyVisible = i == centerPageIndex;
+ }
+ if (isFullyVisible) {
+ List<Integer> taskIds = containers.stream().map(
+ taskContainer -> taskContainer.getTask().key.id).toList();
+ fullyVisibleTaskIds.addAll(taskIds);
+ }
+ }
+ mRecentsViewData.getSettledFullyVisibleTaskIds().setValue(fullyVisibleTaskIds);
+ }
}
@Override
@@ -5892,6 +5944,7 @@
if (mOverlayEnabled != overlayEnabled) {
mOverlayEnabled = overlayEnabled;
updateEnabledOverlays();
+ mRecentsViewData.getOverlayEnabled().setValue(overlayEnabled);
}
}
@@ -5943,6 +5996,12 @@
}
private void switchToScreenshotInternal(Runnable onFinishRunnable) {
+ // TODO(b/342560598): Handle switchToScreenshot for new TTV.
+ if (enableRefactorTaskThumbnail()) {
+ onFinishRunnable.run();
+ return;
+ }
+
TaskView taskView = getRunningTaskView();
if (taskView == null) {
onFinishRunnable.run();
@@ -6030,7 +6089,7 @@
* tasks to be dimmed while other elements in the recents view are left alone.
*/
public void showForegroundScrim(boolean show) {
- // TODO(b/335606129) Add scrim response into new TTV - this is called from overlay
+ // TODO(b/349601769) Add scrim response into new TTV - this is called from overlay
if (!show && mColorTint == 0) {
if (mTintingAnimator != null) {
mTintingAnimator.cancel();
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
new file mode 100644
index 0000000..74d120f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.view.View
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.model.data.ItemInfoWithIcon
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.TransformingTouchDelegate
+import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.TaskUtils
+import com.android.quickstep.task.thumbnail.TaskThumbnail
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.task.util.GetThumbnailUseCase
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.systemui.shared.recents.model.Task
+
+/** Holder for all Task dependent information. */
+class TaskContainer(
+ val taskView: TaskView,
+ val task: Task,
+ val snapshotView: View,
+ val iconView: TaskViewIcon,
+ /**
+ * This technically can be a vanilla [android.view.TouchDelegate] class, however that class
+ * requires setting the touch bounds at construction, so we'd repeatedly be created many
+ * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows touch
+ * delegated bounds only to be updated.
+ */
+ val iconTouchDelegate: TransformingTouchDelegate,
+ /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */
+ @SplitConfigurationOptions.StagePosition val stagePosition: Int,
+ val digitalWellBeingToast: DigitalWellBeingToast?,
+ val showWindowsView: View?,
+ taskOverlayFactory: TaskOverlayFactory
+) {
+ val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
+ val taskContainerData = TaskContainerData()
+
+ private val getThumbnailUseCase by lazy {
+ // TODO(b/335649589): Ideally create and obtain this from DI.
+ val recentsView =
+ RecentsViewContainer.containerFromContext<RecentsViewContainer>(
+ overlay.taskView.context
+ )
+ .getOverviewPanel<RecentsView<*, *>>()
+ GetThumbnailUseCase(recentsView.mTasksRepository!!)
+ }
+
+ init {
+ if (enableRefactorTaskThumbnail()) {
+ require(snapshotView is TaskThumbnailView)
+ } else {
+ require(snapshotView is TaskThumbnailViewDeprecated)
+ }
+ }
+
+ val splitAnimationThumbnail: Bitmap?
+ get() =
+ if (enableRefactorTaskThumbnail()) {
+ getThumbnailUseCase.run(task.key.id)
+ } else {
+ thumbnailViewDeprecated.thumbnail
+ }
+
+ val thumbnailView: TaskThumbnailView
+ get() {
+ require(enableRefactorTaskThumbnail())
+ return snapshotView as TaskThumbnailView
+ }
+
+ val thumbnailViewDeprecated: TaskThumbnailViewDeprecated
+ get() {
+ require(!enableRefactorTaskThumbnail())
+ return snapshotView as TaskThumbnailViewDeprecated
+ }
+
+ // TODO(b/334826842): Support shouldShowSplashView for new TTV.
+ val shouldShowSplashView: Boolean
+ get() =
+ if (enableRefactorTaskThumbnail()) false
+ else thumbnailViewDeprecated.shouldShowSplashView()
+
+ // TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
+ val sysUiStatusNavFlags: Int
+ get() =
+ if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
+
+ /** Builds proto for logging */
+ val itemInfo: WorkspaceItemInfo
+ get() =
+ WorkspaceItemInfo().apply {
+ itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK
+ container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER
+ val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key)
+ user = componentKey.user
+ intent = Intent().setComponent(componentKey.componentName)
+ title = task.title
+ taskView.recentsView?.let { screenId = it.indexOfChild(taskView) }
+ if (privateSpaceRestrictAccessibilityDrag()) {
+ if (
+ UserCache.getInstance(taskView.context)
+ .getUserInfo(componentKey.user)
+ .isPrivate
+ ) {
+ runtimeStatusFlags =
+ runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE
+ }
+ }
+ }
+
+ fun bind() {
+ if (enableRefactorTaskThumbnail()) {
+ bindThumbnailView()
+ } else {
+ thumbnailViewDeprecated.bind(task, overlay)
+ }
+ overlay.init()
+ }
+
+ fun destroy() {
+ digitalWellBeingToast?.destroy()
+ if (enableRefactorTaskThumbnail()) {
+ taskView.removeView(thumbnailView)
+ }
+ overlay.destroy()
+ }
+
+ // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
+ // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
+ fun bindThumbnailView() {
+ // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
+ // this should be decided inside TaskThumbnailViewModel.
+ thumbnailView.viewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
+ }
+
+ fun setOverlayEnabled(enabled: Boolean) {
+ if (!enableRefactorTaskThumbnail()) {
+ thumbnailViewDeprecated.setOverlayEnabled(enabled)
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 4f446b2..63bc509 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -55,7 +55,6 @@
import com.android.quickstep.TaskUtils;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.TaskCornerRadius;
-import com.android.quickstep.views.TaskView.TaskContainer;
/**
* Contains options for a recent task when long-pressing its icon.
@@ -238,12 +237,12 @@
mContainer.getDragLayer().getDescendantRectRelativeToSelf(
enableOverviewIconMenu()
? getIconView().findViewById(R.id.icon_view_menu_anchor)
- : taskContainer.getThumbnailViewDeprecated(),
+ : taskContainer.getSnapshotView(),
sTempRect);
Rect insets = mContainer.getDragLayer().getInsets();
BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
params.width = orientationHandler.getTaskMenuWidth(
- taskContainer.getThumbnailViewDeprecated(), deviceProfile,
+ taskContainer.getSnapshotView(), deviceProfile,
taskContainer.getStagePosition());
// Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
params.gravity = Gravity.LEFT;
@@ -277,10 +276,10 @@
// Margin that insets the menuView inside the taskView
float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
- mTaskContainer.getThumbnailViewDeprecated(), deviceProfile, taskInsetMargin,
+ mTaskContainer.getSnapshotView(), deviceProfile, taskInsetMargin,
getIconView()));
setTranslationY(orientationHandler.getTaskMenuY(
- thumbnailAlignedY, mTaskContainer.getThumbnailViewDeprecated(),
+ thumbnailAlignedY, mTaskContainer.getSnapshotView(),
mTaskContainer.getStagePosition(), this, taskInsetMargin,
getIconView()));
}
@@ -316,7 +315,7 @@
.createRevealAnimator(this, closing, revealAnimationStartProgress);
mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
: Interpolators.DECELERATE);
-
+ AnimatorSet.Builder openCloseAnimatorBuilder = mOpenCloseAnimator.play(mRevealAnimator);
if (enableOverviewIconMenu()) {
IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
@@ -334,11 +333,13 @@
closing ? mMenuTranslationYBeforeOpen
: mMenuTranslationYBeforeOpen + additionalTranslationY);
translationYAnim.setInterpolator(EMPHASIZED);
+ openCloseAnimatorBuilder.with(translationYAnim);
ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
iconAppChip.getMenuTranslationY(),
MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY);
menuTranslationYAnim.setInterpolator(EMPHASIZED);
+ openCloseAnimatorBuilder.with(menuTranslationYAnim);
float additionalTranslationX = 0;
if (mContainer.getDeviceProfile().isLandscape
@@ -354,20 +355,15 @@
closing ? mMenuTranslationXBeforeOpen
: mMenuTranslationXBeforeOpen - additionalTranslationX);
translationXAnim.setInterpolator(EMPHASIZED);
+ openCloseAnimatorBuilder.with(translationXAnim);
ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat(
iconAppChip.getMenuTranslationX(),
MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX);
menuTranslationXAnim.setInterpolator(EMPHASIZED);
-
- mOpenCloseAnimator.playTogether(translationYAnim, translationXAnim,
- menuTranslationXAnim, menuTranslationYAnim);
+ openCloseAnimatorBuilder.with(menuTranslationXAnim);
}
- mOpenCloseAnimator.playTogether(mRevealAnimator,
- ObjectAnimator.ofFloat(
- mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA,
- closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
- ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+ openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
if (enableRefactorTaskThumbnail()) {
mRevealAnimator.addUpdateListener(animation -> {
float animatedFraction = animation.getAnimatedFraction();
@@ -375,6 +371,10 @@
mTaskContainer.getTaskContainerData()
.getTaskMenuOpenProgress().setValue(openProgress);
});
+ } else {
+ openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat(
+ mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA,
+ closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA));
}
mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
@Override
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index 659cc0c..e10d38c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -37,7 +37,6 @@
import com.android.launcher3.popup.SystemShortcut
import com.android.launcher3.util.Themes
import com.android.quickstep.TaskOverlayFactory
-import com.android.quickstep.views.TaskView.TaskContainer
class TaskMenuViewWithArrow<T> : ArrowPopup<T> where T : RecentsViewContainer, T : Context {
companion object {
@@ -58,7 +57,9 @@
}
constructor(context: Context) : super(context)
+
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
constructor(
context: Context,
attrs: AttributeSet,
@@ -80,6 +81,7 @@
private var alignedOptionIndex: Int = 0
private val extraSpaceForRowAlignment: Int
get() = optionMeasuredHeight * alignedOptionIndex
+
private val menuPaddingEnd = context.resources.getDimensionPixelSize(R.dimen.task_card_margin)
private lateinit var taskView: TaskView
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index 4283d0e..56ca043 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -28,16 +28,13 @@
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
-import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Property;
@@ -45,7 +42,6 @@
import android.widget.ImageView;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.DeviceProfile;
@@ -99,36 +95,6 @@
}
};
- /** Use to animate thumbnail translationX while first app in split selection is initiated */
- public static final Property<TaskThumbnailViewDeprecated, Float> SPLIT_SELECT_TRANSLATE_X =
- new FloatProperty<TaskThumbnailViewDeprecated>("splitSelectTranslateX") {
- @Override
- public void setValue(TaskThumbnailViewDeprecated thumbnail,
- float splitSelectTranslateX) {
- thumbnail.applySplitSelectTranslateX(splitSelectTranslateX);
- }
-
- @Override
- public Float get(TaskThumbnailViewDeprecated thumbnailView) {
- return thumbnailView.mSplitSelectTranslateX;
- }
- };
-
- /** Use to animate thumbnail translationY while first app in split selection is initiated */
- public static final Property<TaskThumbnailViewDeprecated, Float> SPLIT_SELECT_TRANSLATE_Y =
- new FloatProperty<TaskThumbnailViewDeprecated>("splitSelectTranslateY") {
- @Override
- public void setValue(TaskThumbnailViewDeprecated thumbnail,
- float splitSelectTranslateY) {
- thumbnail.applySplitSelectTranslateY(splitSelectTranslateY);
- }
-
- @Override
- public Float get(TaskThumbnailViewDeprecated thumbnailView) {
- return thumbnailView.mSplitSelectTranslateY;
- }
- };
-
private final RecentsViewContainer mContainer;
private TaskOverlay<?> mOverlay;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -160,8 +126,6 @@
private boolean mOverlayEnabled;
/** Used as a placeholder when the original thumbnail animates out to. */
private boolean mShowSplashForSplitSelection;
- private float mSplitSelectTranslateX;
- private float mSplitSelectTranslateY;
public TaskThumbnailViewDeprecated(Context context) {
this(context, null);
@@ -201,17 +165,6 @@
}
/**
- * Sets TaskOverlay without binding a task.
- *
- * @deprecated Should only be used when using new
- * {@link com.android.quickstep.task.thumbnail.TaskThumbnailView}.
- */
- @Deprecated
- public void setTaskOverlay(TaskOverlay<?> overlay) {
- mOverlay = overlay;
- }
-
- /**
* Updates the thumbnail.
*
* @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
@@ -298,40 +251,6 @@
return mDimAlpha;
}
- /**
- * Get the scaled insets that are being used to draw the task view. This is a subsection of
- * the full snapshot.
- *
- * @return the insets in snapshot bitmap coordinates.
- */
- @RequiresApi(api = Build.VERSION_CODES.Q)
- public Insets getScaledInsets() {
- if (mThumbnailData == null) {
- return Insets.NONE;
- }
-
- RectF bitmapRect = new RectF(
- 0,
- 0,
- mThumbnailData.getThumbnail().getWidth(),
- mThumbnailData.getThumbnail().getHeight());
- RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
-
- // The position helper matrix tells us how to transform the bitmap to fit the view, the
- // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
- // difference between the bitmap bounds and the projected view bounds.
- Matrix boundsToBitmapSpace = new Matrix();
- mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
- RectF boundsInBitmapSpace = new RectF();
- boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
-
- DeviceProfile dp = mContainer.getDeviceProfile();
- int bottomInset = dp.isTablet
- ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
- return Insets.of(0, 0, 0, bottomInset);
- }
-
-
@SystemUiControllerFlags
public int getSysUiStatusNavFlags() {
if (mThumbnailData != null) {
@@ -415,31 +334,6 @@
}
}
- /** See {@link #SPLIT_SELECT_TRANSLATE_X} */
- protected void applySplitSelectTranslateX(float splitSelectTranslateX) {
- mSplitSelectTranslateX = splitSelectTranslateX;
- applyTranslateX();
- }
-
- /** See {@link #SPLIT_SELECT_TRANSLATE_Y} */
- protected void applySplitSelectTranslateY(float splitSelectTranslateY) {
- mSplitSelectTranslateY = splitSelectTranslateY;
- applyTranslateY();
- }
-
- private void applyTranslateX() {
- setTranslationX(mSplitSelectTranslateX);
- }
-
- private void applyTranslateY() {
- setTranslationY(mSplitSelectTranslateY);
- }
-
- protected void resetViewTransforms() {
- mSplitSelectTranslateX = 0;
- mSplitSelectTranslateY = 0;
- }
-
public TaskView getTaskView() {
return (TaskView) getParent();
}
@@ -544,7 +438,9 @@
*/
private void refreshOverlay() {
if (mOverlayEnabled) {
- mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.getMatrix(),
+ mOverlay.initOverlay(mTask,
+ mThumbnailData != null ? mThumbnailData.getThumbnail() : null,
+ mPreviewPositionHelper.getMatrix(),
mPreviewPositionHelper.isOrientationChanged());
} else {
mOverlay.reset();
@@ -617,6 +513,10 @@
return mThumbnailData.isRealSnapshot && !mTask.isLocked;
}
+ public Matrix getThumbnailMatrix() {
+ return mPreviewPositionHelper.getMatrix();
+ }
+
@Override
public void onRecycle() {
// Do nothing
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 7a3b00f..004003c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -22,7 +22,6 @@
import android.annotation.IdRes
import android.app.ActivityOptions
import android.content.Context
-import android.content.Intent
import android.graphics.Canvas
import android.graphics.PointF
import android.graphics.Rect
@@ -32,6 +31,7 @@
import android.util.FloatProperty
import android.util.Log
import android.view.Display
+import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.View.OnClickListener
@@ -50,16 +50,12 @@
import com.android.launcher3.Flags.enableGridOnlyOverview
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
-import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag
-import com.android.launcher3.LauncherSettings
import com.android.launcher3.R
import com.android.launcher3.Utilities
+import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH
import com.android.launcher3.logging.StatsLogManager.LauncherEvent
import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.ItemInfoWithIcon
-import com.android.launcher3.model.data.WorkspaceItemInfo
-import com.android.launcher3.pm.UserCache
import com.android.launcher3.testing.TestLogging
import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.util.CancellableTask
@@ -82,13 +78,9 @@
import com.android.quickstep.RemoteAnimationTargets
import com.android.quickstep.TaskAnimationManager
import com.android.quickstep.TaskOverlayFactory
-import com.android.quickstep.TaskOverlayFactory.TaskOverlay
-import com.android.quickstep.TaskUtils
import com.android.quickstep.TaskViewUtils
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
-import com.android.quickstep.task.thumbnail.TaskThumbnail
import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
@@ -112,7 +104,8 @@
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
focusBorderAnimator: BorderAnimator? = null,
- hoverBorderAnimator: BorderAnimator? = null
+ hoverBorderAnimator: BorderAnimator? = null,
+ type: TaskViewType = TaskViewType.SINGLE
) : FrameLayout(context, attrs), ViewPool.Reusable {
/**
* Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which
@@ -122,24 +115,13 @@
@IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
annotation class TaskDataChanges
- /** Type of task view */
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(Type.SINGLE, Type.GROUPED, Type.DESKTOP)
- annotation class Type {
- companion object {
- const val SINGLE = 1
- const val GROUPED = 2
- const val DESKTOP = 3
- }
- }
-
- val taskViewData = TaskViewData()
+ val taskViewData = TaskViewData(type)
val taskIds: IntArray
/** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
get() = taskContainers.map { it.task.key.id }.toIntArray()
- val thumbnailViews: Array<TaskThumbnailViewDeprecated>
- get() = taskContainers.map { it.thumbnailViewDeprecated }.toTypedArray()
+ val snapshotViews: Array<View>
+ get() = taskContainers.map { it.snapshotView }.toTypedArray()
val isGridTask: Boolean
/** Returns whether the task is part of overview grid and not being focused. */
@@ -166,9 +148,9 @@
get() = taskContainers[0].task
@get:Deprecated("Use [taskContainers] instead.")
- val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated
- /** Returns the first thumbnailView of the TaskView. */
- get() = taskContainers[0].thumbnailViewDeprecated
+ val firstSnapshotView: View
+ /** Returns the first snapshotView of the TaskView. */
+ get() = taskContainers[0].snapshotView
@get:Deprecated("Use [taskContainers] instead.")
val firstItemInfo: ItemInfo
@@ -439,17 +421,17 @@
focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN)
private val focusTransitionScaleAndDim =
focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_SCALE_AND_DIM)
+
/**
- * Variant of [focusTransitionScaleAndDim] that has a built-in interpolator, to be used with
- * [com.android.launcher3.anim.PendingAnimation] via [SCALE_AND_DIM_OUT] only. PendingAnimation
- * doesn't support interpolator per animation, so we'll have to interpolate inside the property.
+ * Returns an animator of [focusTransitionScaleAndDim] that transition out with a built-in
+ * interpolator.
*/
- private var focusTransitionScaleAndDimOut = focusTransitionScaleAndDim.value
- set(value) {
- field = value
- focusTransitionScaleAndDim.value =
- FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(field)
- }
+ fun getFocusTransitionScaleAndDimOutAnimator(): ObjectAnimator =
+ AnimatedFloat { v ->
+ focusTransitionScaleAndDim.value =
+ FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(v)
+ }
+ .animateToValue(1f, 0f)
private var iconAndDimAnimator: ObjectAnimator? = null
// The current background requests to load the task thumbnail and icon
@@ -681,23 +663,21 @@
taskOverlayFactory: TaskOverlayFactory
): TaskContainer {
val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!!
- val thumbnailView: TaskThumbnailView?
- if (enableRefactorTaskThumbnail()) {
- val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated)
- thumbnailView =
- TaskThumbnailView(context).apply {
- layoutParams = thumbnailViewDeprecated.layoutParams
- addView(this, indexOfSnapshotView)
+ val snapshotView =
+ if (enableRefactorTaskThumbnail()) {
+ thumbnailViewDeprecated.visibility = GONE
+ val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated)
+ LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false).also {
+ addView(it, indexOfSnapshotView, thumbnailViewDeprecated.layoutParams)
}
- thumbnailViewDeprecated.visibility = GONE
- } else {
- thumbnailView = null
- }
+ } else {
+ thumbnailViewDeprecated
+ }
val iconView = getOrInflateIconView(iconViewId)
return TaskContainer(
+ this,
task,
- thumbnailView,
- thumbnailViewDeprecated,
+ snapshotView,
iconView,
TransformingTouchDelegate(iconView.asView()),
stagePosition,
@@ -719,8 +699,6 @@
.inflate() as TaskViewIcon
}
- protected fun isTaskContainersInitialized() = this::taskContainers.isInitialized
-
fun containsMultipleTasks() = taskContainers.size > 1
/**
@@ -855,7 +833,8 @@
taskContainers.forEach {
if (visible) {
recentsModel.thumbnailCache
- .updateThumbnailInBackground(it.task) { thumbnailData ->
+ .getThumbnailInBackground(it.task) { thumbnailData ->
+ it.task.thumbnail = thumbnailData
it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
}
?.also { request -> pendingThumbnailLoadRequests.add(request) }
@@ -871,12 +850,15 @@
taskContainers.forEach {
if (visible) {
recentsModel.iconCache
- .updateIconInBackground(it.task) { task ->
- setIcon(it.iconView, task.icon)
+ .getIconInBackground(it.task) { icon, contentDescription, title ->
+ it.task.icon = icon
+ it.task.titleDescription = contentDescription
+ it.task.title = title
+ setIcon(it.iconView, icon)
if (enableOverviewIconMenu()) {
- setText(it.iconView, task.title)
+ setText(it.iconView, title)
}
- it.digitalWellBeingToast?.initialize(task)
+ it.digitalWellBeingToast?.initialize(it.task)
}
?.also { request -> pendingIconLoadRequests.add(request) }
} else {
@@ -1197,10 +1179,10 @@
this,
container.task,
container.iconView.drawable,
- container.thumbnailViewDeprecated,
- container.thumbnailViewDeprecated.thumbnail, /* intent */
- null, /* user */
- null,
+ container.snapshotView,
+ container.splitAnimationThumbnail,
+ /* intent */ null,
+ /* user */ null,
container.itemInfo
)
}
@@ -1398,10 +1380,8 @@
}
open fun setOverlayEnabled(overlayEnabled: Boolean) {
- // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView.
- // and if it's still necessary we should support that in the new TTV class.
if (!enableRefactorTaskThumbnail()) {
- taskContainers.forEach { it.thumbnailViewDeprecated.setOverlayEnabled(overlayEnabled) }
+ taskContainers.forEach { it.setOverlayEnabled(overlayEnabled) }
}
}
@@ -1476,7 +1456,9 @@
protected open fun updateSnapshotRadius() {
updateCurrentFullscreenParams()
taskContainers.forEach {
- it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+ if (!enableRefactorTaskThumbnail()) {
+ it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+ }
it.overlay.setFullscreenParams(getThumbnailFullscreenParams())
}
}
@@ -1512,6 +1494,10 @@
gridTranslationY = 0f
boxTranslationY = 0f
nonGridPivotTranslationX = 0f
+ taskContainers.forEach {
+ it.snapshotView.translationX = 0f
+ it.snapshotView.translationY = 0f
+ }
resetViewTransforms()
}
@@ -1537,10 +1523,6 @@
alpha = stableAlpha
setIconScaleAndDim(1f)
setColorTint(0f, 0)
- if (!enableRefactorTaskThumbnail()) {
- // TODO(b/335399428) add split select functionality to new TTV
- taskContainers.forEach { it.thumbnailViewDeprecated.resetViewTransforms() }
- }
}
private fun getGridTrans(endTranslation: Float) =
@@ -1599,78 +1581,6 @@
override fun close() {}
}
- /** Holder for all Task dependent information. */
- inner class TaskContainer(
- val task: Task,
- val thumbnailView: TaskThumbnailView?,
- val thumbnailViewDeprecated: TaskThumbnailViewDeprecated,
- val iconView: TaskViewIcon,
- /**
- * This technically can be a vanilla [android.view.TouchDelegate] class, however that class
- * requires setting the touch bounds at construction, so we'd repeatedly be created many
- * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows
- * touch delegated bounds only to be updated.
- */
- val iconTouchDelegate: TransformingTouchDelegate,
- /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */
- @StagePosition val stagePosition: Int,
- val digitalWellBeingToast: DigitalWellBeingToast?,
- val showWindowsView: View?,
- taskOverlayFactory: TaskOverlayFactory
- ) {
- val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
- val taskContainerData = TaskContainerData()
-
- val snapshotView: View
- get() = thumbnailView ?: thumbnailViewDeprecated
-
- /** Builds proto for logging */
- val itemInfo: WorkspaceItemInfo
- get() =
- WorkspaceItemInfo().apply {
- itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK
- container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER
- val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key)
- user = componentKey.user
- intent = Intent().setComponent(componentKey.componentName)
- title = task.title
- recentsView?.let { screenId = it.indexOfChild(this@TaskView) }
- if (privateSpaceRestrictAccessibilityDrag()) {
- if (
- UserCache.getInstance(context).getUserInfo(componentKey.user).isPrivate
- ) {
- runtimeStatusFlags =
- runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE
- }
- }
- }
-
- val taskView: TaskView
- get() = this@TaskView
-
- fun destroy() {
- digitalWellBeingToast?.destroy()
- thumbnailView?.let { taskView.removeView(it) }
- }
-
- fun bind() {
- if (enableRefactorTaskThumbnail() && thumbnailView != null) {
- thumbnailViewDeprecated.setTaskOverlay(overlay)
- bindThumbnailView()
- } else {
- thumbnailViewDeprecated.bind(task, overlay)
- }
- }
-
- // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
- // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
- fun bindThumbnailView() {
- // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
- // this should be decided inside TaskThumbnailViewModel.
- thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, isRunningTask))
- }
- }
-
companion object {
private const val TAG = "TaskView"
const val FLAG_UPDATE_ICON = 1
@@ -1706,16 +1616,6 @@
override fun get(taskView: TaskView) = taskView.focusTransitionProgress
}
- @JvmField
- val SCALE_AND_DIM_OUT: FloatProperty<TaskView> =
- object : FloatProperty<TaskView>("scaleAndDimFastOut") {
- override fun setValue(taskView: TaskView, v: Float) {
- taskView.focusTransitionScaleAndDimOut = v
- }
-
- override fun get(taskView: TaskView) = taskView.focusTransitionScaleAndDimOut
- }
-
private val SPLIT_SELECT_TRANSLATION_X: FloatProperty<TaskView> =
object : FloatProperty<TaskView>("splitSelectTranslationX") {
override fun setValue(taskView: TaskView, v: Float) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskViewType.kt b/quickstep/src/com/android/quickstep/views/TaskViewType.kt
new file mode 100644
index 0000000..b2a32a9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskViewType.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+/** Type of the [TaskView] */
+enum class TaskViewType {
+ SINGLE,
+ GROUPED,
+ DESKTOP
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
index 039dce4..4ea74df 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
@@ -30,6 +30,7 @@
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetPredictionsRequester.LAUNCH_LOCATION
import com.android.launcher3.model.WidgetPredictionsRequester.buildBundleForPredictionSession
import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions
import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter
@@ -103,7 +104,7 @@
fun buildBundleForPredictionSession_includesAddedAppWidgets() {
val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo, widget2Info)
- val bundle = buildBundleForPredictionSession(existingWidgets, TEST_UI_SURFACE)
+ val bundle = buildBundleForPredictionSession(existingWidgets)
val addedWidgetsBundleExtra =
bundle.getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, AppTarget::class.java)
@@ -213,7 +214,7 @@
.setClassName(providerClassName)
.build()
return AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
- .setLaunchLocation(TEST_UI_SURFACE)
+ .setLaunchLocation(LAUNCH_LOCATION)
.build()
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt
deleted file mode 100644
index 1c900b8..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.taskbar
-
-import android.app.Instrumentation
-import android.app.PendingIntent
-import android.content.Context
-import android.content.IIntentSender
-import android.content.Intent
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.ServiceTestRule
-import com.android.launcher3.LauncherAppState
-import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
-import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
-import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
-import com.android.quickstep.AllAppsActionManager
-import com.android.quickstep.TouchInteractionService
-import com.android.quickstep.TouchInteractionService.TISBinder
-import org.junit.Assume.assumeTrue
-import org.junit.rules.MethodRule
-import org.junit.runners.model.FrameworkMethod
-import org.junit.runners.model.Statement
-
-/**
- * Manages the Taskbar lifecycle for unit tests.
- *
- * Tests need to provide their target [context] through the constructor.
- *
- * See [InjectController] for grabbing controller(s) under test with minimal boilerplate.
- *
- * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is
- * that code that is executed on the main thread in production should also happen on that thread
- * when tested.
- *
- * `@UiThreadTest` is a simple way to run an entire test body on the main thread. But if a test
- * executes code that appends message(s) to the main thread's `MessageQueue`, the annotation will
- * prevent those messages from being processed until after the test body finishes.
- *
- * To test pending messages, instead use something like [Instrumentation.runOnMainSync] to perform
- * only sections of the test body on the main thread synchronously:
- * ```
- * @Test
- * fun example() {
- * instrumentation.runOnMainSync { doWorkThatPostsMessage() }
- * // Second lambda will not execute until message is processed.
- * instrumentation.runOnMainSync { verifyMessageResults() }
- * }
- * ```
- */
-class TaskbarUnitTestRule(private val context: Context) : MethodRule {
- private val instrumentation = InstrumentationRegistry.getInstrumentation()
- private val serviceTestRule = ServiceTestRule()
-
- private lateinit var taskbarManager: TaskbarManager
- private lateinit var target: Any
-
- val activityContext: TaskbarActivityContext
- get() {
- return taskbarManager.currentActivityContext
- ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.")
- }
-
- override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement {
- return object : Statement() {
- override fun evaluate() {
- this@TaskbarUnitTestRule.target = target
-
- instrumentation.runOnMainSync {
- assumeTrue(
- LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent
- )
- }
-
- // Check for existing Taskbar instance from Launcher process.
- val launcherTaskbarManager: TaskbarManager? =
- if (!isRunningInRobolectric) {
- try {
- val tisBinder =
- serviceTestRule.bindService(
- Intent(context, TouchInteractionService::class.java)
- ) as? TISBinder
- tisBinder?.taskbarManager
- } catch (_: Exception) {
- null
- }
- } else {
- null
- }
-
- instrumentation.runOnMainSync {
- taskbarManager =
- TaskbarManager(
- context,
- AllAppsActionManager(context, UI_HELPER_EXECUTOR) {
- PendingIntent(IIntentSender.Default())
- },
- object : TaskbarNavButtonCallbacks {},
- )
- }
-
- try {
- // Replace Launcher Taskbar window with test instance.
- instrumentation.runOnMainSync {
- launcherTaskbarManager?.removeTaskbarRootViewFromWindow()
- taskbarManager.onUserUnlocked() // Required to complete initialization.
- }
-
- injectControllers()
- base.evaluate()
- } finally {
- // Revert Taskbar window.
- instrumentation.runOnMainSync {
- taskbarManager.destroy()
- launcherTaskbarManager?.addTaskbarRootViewToWindow()
- }
- }
- }
- }
- }
-
- /** Simulates Taskbar recreation lifecycle. */
- fun recreateTaskbar() {
- taskbarManager.recreateTaskbar()
- injectControllers()
- }
-
- private fun injectControllers() {
- val controllers = activityContext.controllers
- val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
- target.javaClass.fields
- .filter { it.isAnnotationPresent(InjectController::class.java) }
- .forEach {
- it.set(
- target,
- controllerFieldsByType[it.type]?.get(controllers)
- ?: throw NoSuchElementException("Failed to find controller for ${it.type}"),
- )
- }
- }
-
- /**
- * Annotates test controller fields to inject the corresponding controllers from the current
- * [TaskbarControllers] instance.
- *
- * Controllers are injected during test setup and upon calling [recreateTaskbar].
- *
- * Multiple controllers can be injected if needed.
- */
- @Retention(AnnotationRetention.RUNTIME)
- @Target(AnnotationTarget.FIELD)
- annotation class InjectController
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
index 9a514bf..2f0b446 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
@@ -20,19 +20,20 @@
import android.content.ComponentName
import android.content.Intent
import android.os.Process
-import androidx.test.annotation.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.BubbleTextView
import com.android.launcher3.appprediction.PredictionRowView
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.notification.NotificationKeyData
-import com.android.launcher3.taskbar.TaskbarUnitTestRule
-import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.TestUtil
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -42,84 +43,96 @@
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
class TaskbarAllAppsControllerTest {
- @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(getInstrumentation().targetContext)
+ @get:Rule
+ val taskbarUnitTestRule =
+ TaskbarUnitTestRule(
+ this,
+ TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
+ )
@get:Rule val animatorTestRule = AnimatorTestRule(this)
@InjectController lateinit var allAppsController: TaskbarAllAppsController
@InjectController lateinit var overlayController: TaskbarOverlayController
@Test
- @UiThreadTest
fun testToggle_once_showsAllApps() {
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync { allAppsController.toggle() }
assertThat(allAppsController.isOpen).isTrue()
}
@Test
- @UiThreadTest
fun testToggle_twice_closesAllApps() {
- allAppsController.toggle()
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync {
+ allAppsController.toggle()
+ allAppsController.toggle()
+ }
assertThat(allAppsController.isOpen).isFalse()
}
@Test
- @UiThreadTest
fun testToggle_taskbarRecreated_allAppsReopened() {
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync { allAppsController.toggle() }
taskbarUnitTestRule.recreateTaskbar()
assertThat(allAppsController.isOpen).isTrue()
}
@Test
- @UiThreadTest
fun testSetApps_beforeOpened_cachesInfo() {
- allAppsController.setApps(TEST_APPS, 0, emptyMap())
- allAppsController.toggle()
+ val overlayContext =
+ TestUtil.getOnUiThread {
+ allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ allAppsController.toggle()
+ overlayController.requestWindow()
+ }
- val overlayContext = overlayController.requestWindow()
assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
}
@Test
- @UiThreadTest
fun testSetApps_afterOpened_updatesStore() {
- allAppsController.toggle()
- allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ val overlayContext =
+ TestUtil.getOnUiThread {
+ allAppsController.toggle()
+ allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ overlayController.requestWindow()
+ }
- val overlayContext = overlayController.requestWindow()
assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
}
@Test
- @UiThreadTest
fun testSetPredictedApps_beforeOpened_cachesInfo() {
- allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
- allAppsController.toggle()
-
val predictedApps =
- overlayController
- .requestWindow()
- .appsView
- .floatingHeaderView
- .findFixedRowByType(PredictionRowView::class.java)
- .predictedApps
+ TestUtil.getOnUiThread {
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+ allAppsController.toggle()
+
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .predictedApps
+ }
+
assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
}
@Test
- @UiThreadTest
fun testSetPredictedApps_afterOpened_cachesInfo() {
- allAppsController.toggle()
- allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
-
val predictedApps =
- overlayController
- .requestWindow()
- .appsView
- .floatingHeaderView
- .findFixedRowByType(PredictionRowView::class.java)
- .predictedApps
+ TestUtil.getOnUiThread {
+ allAppsController.toggle()
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .predictedApps
+ }
+
assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
}
@@ -135,36 +148,38 @@
}
// Ensure the recycler view fully inflates before trying to grab an icon.
- getInstrumentation().runOnMainSync {
- val btv =
+ val btv =
+ TestUtil.getOnUiThread {
overlayController
.requestWindow()
.appsView
.activeRecyclerView
.findViewHolderForAdapterPosition(0)
?.itemView as? BubbleTextView
- assertThat(btv?.hasDot()).isTrue()
- }
+ }
+ assertThat(btv?.hasDot()).isTrue()
}
@Test
- @UiThreadTest
fun testUpdateNotificationDots_predictedApp_hasDot() {
- allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync {
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+ allAppsController.toggle()
+ taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
+ PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
+ NotificationKeyData("key"),
+ )
+ }
- taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
- PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
- NotificationKeyData("key"),
- )
-
- val predictionRowView =
- overlayController
- .requestWindow()
- .appsView
- .floatingHeaderView
- .findFixedRowByType(PredictionRowView::class.java)
- val btv = predictionRowView.getChildAt(0) as BubbleTextView
+ val btv =
+ TestUtil.getOnUiThread {
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .getChildAt(0) as BubbleTextView
+ }
assertThat(btv.hasDot()).isTrue()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
index 20bd617..d5a76a2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -80,6 +80,31 @@
assertThat(bubbleAnimator.isRunning).isFalse()
}
+ @Test
+ fun animateNewAndRemoveOld_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateNewAndRemoveOld(
+ selectedBubbleIndex = 3,
+ removedBubbleIndex = 2,
+ listener
+ )
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
private class TestBubbleAnimatorListener : BubbleAnimator.Listener {
override fun onAnimationUpdate(animatedFraction: Float) {}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
index 918ec7d..f946d4d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
@@ -18,7 +18,6 @@
import android.app.ActivityManager.RunningTaskInfo
import android.view.MotionEvent
-import androidx.test.annotation.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP
@@ -26,11 +25,12 @@
import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY
import com.android.launcher3.AbstractFloatingView.hasOpenView
import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.launcher3.taskbar.TaskbarUnitTestRule
-import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
-import com.android.launcher3.views.BaseDragLayer
+import com.android.launcher3.util.TestUtil.getOnUiThread
import com.android.systemui.shared.system.TaskStackChangeListeners
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -41,81 +41,81 @@
@EmulatedDevices(["pixelFoldable2023"])
class TaskbarOverlayControllerTest {
- @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(getInstrumentation().targetContext)
+ @get:Rule
+ val taskbarUnitTestRule =
+ TaskbarUnitTestRule(
+ this,
+ TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
+ )
@InjectController lateinit var overlayController: TaskbarOverlayController
private val taskbarContext: TaskbarActivityContext
get() = taskbarUnitTestRule.activityContext
@Test
- @UiThreadTest
fun testRequestWindow_twice_reusesWindow() {
- val context1 = overlayController.requestWindow()
- val context2 = overlayController.requestWindow()
+ val (context1, context2) =
+ getOnUiThread {
+ Pair(overlayController.requestWindow(), overlayController.requestWindow())
+ }
assertThat(context1).isSameInstanceAs(context2)
}
@Test
- @UiThreadTest
fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() {
- val context1 = overlayController.requestWindow()
- overlayController.hideWindow()
+ val context1 = getOnUiThread { overlayController.requestWindow() }
+ getInstrumentation().runOnMainSync { overlayController.hideWindow() }
- val context2 = overlayController.requestWindow()
+ val context2 = getOnUiThread { overlayController.requestWindow() }
assertThat(context1).isNotSameInstanceAs(context2)
}
@Test
- @UiThreadTest
fun testRequestWindow_afterHidingOverlay_createsNewWindow() {
- val context1 = overlayController.requestWindow()
- TestOverlayView.show(context1)
- overlayController.hideWindow()
+ val context1 = getOnUiThread { overlayController.requestWindow() }
+ getInstrumentation().runOnMainSync {
+ TestOverlayView.show(context1)
+ overlayController.hideWindow()
+ }
- val context2 = overlayController.requestWindow()
+ val context2 = getOnUiThread { overlayController.requestWindow() }
assertThat(context1).isNotSameInstanceAs(context2)
}
@Test
- @UiThreadTest
fun testRequestWindow_addsProxyView() {
- TestOverlayView.show(overlayController.requestWindow())
+ getInstrumentation().runOnMainSync {
+ TestOverlayView.show(overlayController.requestWindow())
+ }
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
}
@Test
- @UiThreadTest
fun testRequestWindow_closeProxyView_closesOverlay() {
- val overlay = TestOverlayView.show(overlayController.requestWindow())
- AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
+ getInstrumentation().runOnMainSync {
+ AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)
+ }
assertThat(overlay.isOpen).isFalse()
}
@Test
fun testRequestWindow_attachesDragLayer() {
- lateinit var dragLayer: BaseDragLayer<*>
- getInstrumentation().runOnMainSync {
- dragLayer = overlayController.requestWindow().dragLayer
- }
-
+ val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
// Allow drag layer to attach before checking.
getInstrumentation().runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() }
}
@Test
- @UiThreadTest
fun testHideWindow_closesOverlay() {
- val overlay = TestOverlayView.show(overlayController.requestWindow())
- overlayController.hideWindow()
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
+ getInstrumentation().runOnMainSync { overlayController.hideWindow() }
assertThat(overlay.isOpen).isFalse()
}
@Test
fun testHideWindow_detachesDragLayer() {
- lateinit var dragLayer: BaseDragLayer<*>
- getInstrumentation().runOnMainSync {
- dragLayer = overlayController.requestWindow().dragLayer
- }
+ val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
// Wait for drag layer to be attached to window before hiding.
getInstrumentation().runOnMainSync {
@@ -125,44 +125,45 @@
}
@Test
- @UiThreadTest
fun testTwoOverlays_closeOne_windowStaysOpen() {
- val context = overlayController.requestWindow()
- val overlay1 = TestOverlayView.show(context)
- val overlay2 = TestOverlayView.show(context)
+ val (overlay1, overlay2) =
+ getOnUiThread {
+ val context = overlayController.requestWindow()
+ Pair(TestOverlayView.show(context), TestOverlayView.show(context))
+ }
- overlay1.close(false)
+ getInstrumentation().runOnMainSync { overlay1.close(false) }
assertThat(overlay2.isOpen).isTrue()
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
}
@Test
- @UiThreadTest
fun testTwoOverlays_closeAll_closesWindow() {
- val context = overlayController.requestWindow()
- val overlay1 = TestOverlayView.show(context)
- val overlay2 = TestOverlayView.show(context)
+ val (overlay1, overlay2) =
+ getOnUiThread {
+ val context = overlayController.requestWindow()
+ Pair(TestOverlayView.show(context), TestOverlayView.show(context))
+ }
- overlay1.close(false)
- overlay2.close(false)
+ getInstrumentation().runOnMainSync {
+ overlay1.close(false)
+ overlay2.close(false)
+ }
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse()
}
@Test
- @UiThreadTest
fun testRecreateTaskbar_closesWindow() {
- TestOverlayView.show(overlayController.requestWindow())
+ getInstrumentation().runOnMainSync {
+ TestOverlayView.show(overlayController.requestWindow())
+ }
taskbarUnitTestRule.recreateTaskbar()
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse()
}
@Test
fun testTaskMovedToFront_closesOverlay() {
- lateinit var overlay: TestOverlayView
- getInstrumentation().runOnMainSync {
- overlay = TestOverlayView.show(overlayController.requestWindow())
- }
-
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo())
// Make sure TaskStackChangeListeners' Handler posts the callback before checking state.
getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() }
@@ -170,9 +171,8 @@
@Test
fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() {
- lateinit var overlay: TestOverlayView
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
getInstrumentation().runOnMainSync {
- overlay = TestOverlayView.show(overlayController.requestWindow())
taskbarContext.controllers.sharedState?.allAppsVisible = false
}
@@ -182,9 +182,8 @@
@Test
fun testTaskStackChanged_allAppsOpen_closesOverlay() {
- lateinit var overlay: TestOverlayView
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
getInstrumentation().runOnMainSync {
- overlay = TestOverlayView.show(overlayController.requestWindow())
taskbarContext.controllers.sharedState?.allAppsVisible = true
}
@@ -193,33 +192,39 @@
}
@Test
- @UiThreadTest
fun testUpdateLauncherDeviceProfile_overlayNotRebindSafe_closesOverlay() {
- val overlayContext = overlayController.requestWindow()
- val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_OPTIONS_POPUP }
+ val context = getOnUiThread { overlayController.requestWindow() }
+ val overlay = getOnUiThread {
+ TestOverlayView.show(context).apply { type = TYPE_OPTIONS_POPUP }
+ }
- overlayController.updateLauncherDeviceProfile(
- overlayController.launcherDeviceProfile
- .toBuilder(overlayContext)
- .setGestureMode(false)
- .build()
- )
+ getInstrumentation().runOnMainSync {
+ overlayController.updateLauncherDeviceProfile(
+ overlayController.launcherDeviceProfile
+ .toBuilder(context)
+ .setGestureMode(false)
+ .build()
+ )
+ }
assertThat(overlay.isOpen).isFalse()
}
@Test
- @UiThreadTest
fun testUpdateLauncherDeviceProfile_overlayRebindSafe_overlayStaysOpen() {
- val overlayContext = overlayController.requestWindow()
- val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_TASKBAR_ALL_APPS }
+ val context = getOnUiThread { overlayController.requestWindow() }
+ val overlay = getOnUiThread {
+ TestOverlayView.show(context).apply { type = TYPE_TASKBAR_ALL_APPS }
+ }
- overlayController.updateLauncherDeviceProfile(
- overlayController.launcherDeviceProfile
- .toBuilder(overlayContext)
- .setGestureMode(false)
- .build()
- )
+ getInstrumentation().runOnMainSync {
+ overlayController.updateLauncherDeviceProfile(
+ overlayController.launcherDeviceProfile
+ .toBuilder(context)
+ .setGestureMode(false)
+ .build()
+ )
+ }
assertThat(overlay.isOpen).isTrue()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
similarity index 60%
rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index 3b53cdc..c48947e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.rules
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode
-import com.android.launcher3.taskbar.TaskbarModeRule.TaskbarMode
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
import com.android.launcher3.util.NavigationMode
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -40,7 +40,7 @@
* Make sure this rule precedes any rules that depend on [DisplayController], or else the instance
* might be inconsistent across the test lifecycle.
*/
-class TaskbarModeRule(private val context: SandboxContext) : TestRule {
+class TaskbarModeRule(private val context: TaskbarWindowSandboxContext) : TestRule {
/** The selected Taskbar mode. */
enum class Mode {
TRANSIENT,
@@ -60,23 +60,25 @@
override fun evaluate() {
val mode = taskbarMode.mode
- context.putObject(
- DisplayController.INSTANCE,
- object : DisplayController(context) {
- override fun getInfo(): Info {
- return spy(super.getInfo()) {
- on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT)
- on { isPinnedTaskbar } doReturn (mode == Mode.PINNED)
- on { navigationMode } doReturn
- when (mode) {
- Mode.TRANSIENT,
- Mode.PINNED -> NavigationMode.NO_BUTTON
- Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS
- }
+ getInstrumentation().runOnMainSync {
+ context.applicationContext.putObject(
+ DisplayController.INSTANCE,
+ object : DisplayController(context) {
+ override fun getInfo(): Info {
+ return spy(super.getInfo()) {
+ on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT)
+ on { isPinnedTaskbar } doReturn (mode == Mode.PINNED)
+ on { navigationMode } doReturn
+ when (mode) {
+ Mode.TRANSIENT,
+ Mode.PINNED -> NavigationMode.NO_BUTTON
+ Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS
+ }
+ }
}
- }
- },
- )
+ },
+ )
+ }
base.evaluate()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
similarity index 87%
rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
index 7dfbb9a..f75e542 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
@@ -14,17 +14,16 @@
* limitations under the License.
*/
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.rules
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.InvariantDeviceProfile
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.PINNED
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.THREE_BUTTONS
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.TRANSIENT
-import com.android.launcher3.taskbar.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.LauncherMultivalentJUnit
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
import com.android.launcher3.util.NavigationMode
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -34,7 +33,7 @@
@RunWith(LauncherMultivalentJUnit::class)
class TaskbarModeRuleTest {
- private val context = SandboxContext(getInstrumentation().targetContext)
+ private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
@get:Rule val taskbarModeRule = TaskbarModeRule(context)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
new file mode 100644
index 0000000..8a64949
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.rules
+
+import android.app.Instrumentation
+import android.app.PendingIntent
+import android.content.IIntentSender
+import android.content.Intent
+import android.provider.Settings
+import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE
+import android.provider.Settings.Secure.USER_SETUP_COMPLETE
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ServiceTestRule
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
+import com.android.launcher3.util.ModelTestExtensions.loadModelSync
+import com.android.launcher3.util.TestUtil
+import com.android.quickstep.AllAppsActionManager
+import com.android.quickstep.TouchInteractionService
+import com.android.quickstep.TouchInteractionService.TISBinder
+import org.junit.Assume.assumeTrue
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Manages the Taskbar lifecycle for unit tests.
+ *
+ * Tests should pass in themselves as [testInstance]. They also need to provide their target
+ * [context] through the constructor.
+ *
+ * See [InjectController] for grabbing controller(s) under test with minimal boilerplate.
+ *
+ * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is
+ * that code that is executed on the main thread in production should also happen on that thread
+ * when tested.
+ *
+ * `@UiThreadTest` is incompatible with this rule. The annotation causes this rule to run on the
+ * main thread, but it needs to be run on the test thread for it to work properly. Instead, only run
+ * code that requires the main thread using something like [Instrumentation.runOnMainSync] or
+ * [TestUtil.getOnUiThread].
+ *
+ * ```
+ * @Test
+ * fun example() {
+ * instrumentation.runOnMainSync { doWorkThatPostsMessage() }
+ * // Second lambda will not execute until message is processed.
+ * instrumentation.runOnMainSync { verifyMessageResults() }
+ * }
+ * ```
+ */
+class TaskbarUnitTestRule(
+ private val testInstance: Any,
+ private val context: TaskbarWindowSandboxContext,
+) : TestRule {
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val serviceTestRule = ServiceTestRule()
+
+ private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE)
+ private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE)
+ private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule)
+
+ private lateinit var taskbarManager: TaskbarManager
+
+ val activityContext: TaskbarActivityContext
+ get() {
+ return taskbarManager.currentActivityContext
+ ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.")
+ }
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return settingRules.apply(createStatement(base, description), description)
+ }
+
+ private fun createStatement(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+
+ // Only run test when Taskbar is enabled.
+ instrumentation.runOnMainSync {
+ assumeTrue(
+ LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent
+ )
+ }
+
+ // Process secure setting annotations.
+ instrumentation.runOnMainSync {
+ userSetupCompleteRule.putInt(
+ if (description.getAnnotation(UserSetupMode::class.java) != null) {
+ 0
+ } else {
+ 1
+ }
+ )
+ kidsModeRule.putInt(
+ if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
+ )
+ }
+
+ // Check for existing Taskbar instance from Launcher process.
+ val launcherTaskbarManager: TaskbarManager? =
+ if (!isRunningInRobolectric) {
+ try {
+ val tisBinder =
+ serviceTestRule.bindService(
+ Intent(context, TouchInteractionService::class.java)
+ ) as? TISBinder
+ tisBinder?.taskbarManager
+ } catch (_: Exception) {
+ null
+ }
+ } else {
+ null
+ }
+
+ taskbarManager =
+ TestUtil.getOnUiThread {
+ TaskbarManager(
+ context,
+ AllAppsActionManager(context, UI_HELPER_EXECUTOR) {
+ PendingIntent(IIntentSender.Default())
+ },
+ object : TaskbarNavButtonCallbacks {},
+ )
+ }
+
+ try {
+ LauncherAppState.getInstance(context).model.loadModelSync()
+
+ // Replace Launcher Taskbar window with test instance.
+ instrumentation.runOnMainSync {
+ launcherTaskbarManager?.setSuspended(true)
+ taskbarManager.onUserUnlocked() // Required to complete initialization.
+ }
+
+ injectControllers()
+ base.evaluate()
+ } finally {
+ // Revert Taskbar window.
+ instrumentation.runOnMainSync {
+ taskbarManager.destroy()
+ launcherTaskbarManager?.setSuspended(false)
+ }
+ }
+ }
+ }
+ }
+
+ /** Simulates Taskbar recreation lifecycle. */
+ fun recreateTaskbar() {
+ instrumentation.runOnMainSync { taskbarManager.recreateTaskbar() }
+ injectControllers()
+ }
+
+ private fun injectControllers() {
+ val controllers = activityContext.controllers
+ val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
+ testInstance.javaClass.fields
+ .filter { it.isAnnotationPresent(InjectController::class.java) }
+ .forEach {
+ it.set(
+ testInstance,
+ controllerFieldsByType[it.type]?.get(controllers)
+ ?: throw NoSuchElementException("Failed to find controller for ${it.type}"),
+ )
+ }
+ }
+
+ /**
+ * Annotates test controller fields to inject the corresponding controllers from the current
+ * [TaskbarControllers] instance.
+ *
+ * Controllers are injected during test setup and upon calling [recreateTaskbar].
+ *
+ * Multiple controllers can be injected if needed.
+ */
+ @Retention(AnnotationRetention.RUNTIME)
+ @Target(AnnotationTarget.FIELD)
+ annotation class InjectController
+
+ /** Overrides [USER_SETUP_COMPLETE] to be `false` for tests. */
+ @Retention(AnnotationRetention.RUNTIME)
+ @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+ annotation class UserSetupMode
+
+ /** Overrides [NAV_BAR_KIDS_MODE] to be `true` for tests. */
+ @Retention(AnnotationRetention.RUNTIME)
+ @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+ annotation class NavBarKidsMode
+
+ /** Rule for Taskbar integer-based secure settings. */
+ private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule {
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ val originalValue =
+ Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0)
+ try {
+ base.evaluate()
+ } finally {
+ instrumentation.runOnMainSync { putInt(originalValue) }
+ }
+ }
+ }
+ }
+
+ /** Puts [value] into secure settings under [settingName]. */
+ fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
new file mode 100644
index 0000000..234e499
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.rules
+
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarKeyguardController
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarStashController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023"])
+class TaskbarUnitTestRuleTest {
+
+ private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+ @Test
+ fun testSetup_taskbarInitialized() {
+ onSetup { assertThat(activityContext).isInstanceOf(TaskbarActivityContext::class.java) }
+ }
+
+ @Test
+ fun testRecreateTaskbar_activityContextChanged() {
+ onSetup {
+ val context1 = activityContext
+ recreateTaskbar()
+ val context2 = activityContext
+ assertThat(context1).isNotSameInstanceAs(context2)
+ }
+ }
+
+ @Test
+ fun testTeardown_taskbarDestroyed() {
+ val testRule = TaskbarUnitTestRule(this, context)
+ testRule.apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+ assertThrows(RuntimeException::class.java) { testRule.activityContext }
+ }
+
+ @Test
+ fun testInjectController_validControllerType_isInjected() {
+ val testClass =
+ object {
+ @InjectController lateinit var controller: TaskbarStashController
+ val isInjected: Boolean
+ get() = ::controller.isInitialized
+ }
+
+ TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+
+ onSetup(TaskbarUnitTestRule(testClass, context)) {
+ assertThat(testClass.isInjected).isTrue()
+ }
+ }
+
+ @Test
+ fun testInjectController_multipleControllers_areInjected() {
+ val testClass =
+ object {
+ @InjectController lateinit var controller1: TaskbarStashController
+ @InjectController lateinit var controller2: TaskbarKeyguardController
+ val areInjected: Boolean
+ get() = ::controller1.isInitialized && ::controller2.isInitialized
+ }
+
+ onSetup(TaskbarUnitTestRule(testClass, context)) {
+ assertThat(testClass.areInjected).isTrue()
+ }
+ }
+
+ @Test
+ fun testInjectController_invalidControllerType_exceptionThrown() {
+ val testClass =
+ object {
+ @InjectController lateinit var manager: TaskbarManager // Not a controller.
+ }
+
+ // We cannot use #assertThrows because we also catch an assumption violated exception when
+ // running #evaluate on devices that do not support Taskbar.
+ val result =
+ try {
+ TaskbarUnitTestRule(testClass, context)
+ .apply(EMPTY_STATEMENT, DESCRIPTION)
+ .evaluate()
+ } catch (e: NoSuchElementException) {
+ e
+ }
+ assertThat(result).isInstanceOf(NoSuchElementException::class.java)
+ }
+
+ @Test
+ fun testInjectController_recreateTaskbar_controllerChanged() {
+ val testClass =
+ object {
+ @InjectController lateinit var controller: TaskbarStashController
+ }
+
+ onSetup(TaskbarUnitTestRule(testClass, context)) {
+ val controller1 = testClass.controller
+ recreateTaskbar()
+ val controller2 = testClass.controller
+ assertThat(controller1).isNotSameInstanceAs(controller2)
+ }
+ }
+
+ @Test
+ fun testUserSetupMode_default_isComplete() {
+ onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() }
+ }
+
+ @Test
+ fun testUserSetupMode_withAnnotation_isIncomplete() {
+ @UserSetupMode class Mode
+ onSetup(description = Description.createSuiteDescription(Mode::class.java)) {
+ assertThat(activityContext.isUserSetupComplete).isFalse()
+ }
+ }
+
+ @Test
+ fun testNavBarKidsMode_default_navBarNotForcedVisible() {
+ onSetup { assertThat(activityContext.isNavBarForceVisible).isFalse() }
+ }
+
+ @Test
+ fun testNavBarKidsMode_withAnnotation_navBarForcedVisible() {
+ @NavBarKidsMode class Mode
+ onSetup(description = Description.createSuiteDescription(Mode::class.java)) {
+ assertThat(activityContext.isNavBarForceVisible).isTrue()
+ }
+ }
+
+ /**
+ * Executes [runTest] after the [testRule] setup phase completes.
+ *
+ * A [description] can also be provided to mimic annotating a test or test class.
+ */
+ private fun onSetup(
+ testRule: TaskbarUnitTestRule = TaskbarUnitTestRule(this, context),
+ description: Description = DESCRIPTION,
+ runTest: TaskbarUnitTestRule.() -> Unit,
+ ) {
+ testRule
+ .apply(
+ object : Statement() {
+ override fun evaluate() = runTest(testRule)
+ },
+ description,
+ )
+ .evaluate()
+ }
+
+ private companion object {
+ private val EMPTY_STATEMENT =
+ object : Statement() {
+ override fun evaluate() = Unit
+ }
+ private val DESCRIPTION =
+ Description.createSuiteDescription(TaskbarUnitTestRuleTest::class.java)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
new file mode 100644
index 0000000..321e7a9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.rules
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.Bundle
+import android.view.Display
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+
+/**
+ * Sandbox wrapper where [createWindowContext] provides contexts that are still sandboxed within
+ * [application].
+ *
+ * Taskbar can create window contexts, which need to operate under the same sandbox application, but
+ * [Context.getApplicationContext] by default returns the actual application. For this reason,
+ * [SandboxContext] overrides [getApplicationContext] to return itself, which prevents leaving the
+ * sandbox. [SandboxContext] and the real application have different sets of
+ * [MainThreadInitializedObject] instances, so overriding the application prevents the latter set
+ * from leaking into the sandbox. Similarly, this implementation overrides [getApplicationContext]
+ * to return the original sandboxed [application], and it wraps created windowed contexts to
+ * propagate this [application].
+ */
+class TaskbarWindowSandboxContext
+private constructor(private val application: SandboxContext, base: Context) : ContextWrapper(base) {
+
+ override fun createWindowContext(type: Int, options: Bundle?): Context {
+ return TaskbarWindowSandboxContext(application, super.createWindowContext(type, options))
+ }
+
+ override fun createWindowContext(display: Display, type: Int, options: Bundle?): Context {
+ return TaskbarWindowSandboxContext(
+ application,
+ super.createWindowContext(display, type, options),
+ )
+ }
+
+ override fun getApplicationContext() = application
+
+ companion object {
+ /** Creates a [TaskbarWindowSandboxContext] to sandbox [base] for Taskbar tests. */
+ fun create(base: Context): TaskbarWindowSandboxContext {
+ return SandboxContext(base).let { TaskbarWindowSandboxContext(it, it) }
+ }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
new file mode 100644
index 0000000..ad4b4de
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.rules
+
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class TaskbarWindowSandboxContextTest {
+
+ private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+ @Test
+ fun testCreateWindowContext_applicationContextSandboxed() {
+ val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+ assertThat(windowContext.applicationContext).isInstanceOf(SandboxContext::class.java)
+ }
+
+ @Test
+ fun testCreateWindowContext_nested_applicationContextSandboxed() {
+ val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+ val nestedContext = windowContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+ assertThat(nestedContext.applicationContext).isInstanceOf(SandboxContext::class.java)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
new file mode 100644
index 0000000..242bc73
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.data
+
+import android.graphics.drawable.Drawable
+import com.android.launcher3.util.CancellableTask
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import org.mockito.kotlin.mock
+
+class FakeTaskIconDataSource : TaskIconDataSource {
+
+ val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mock() }
+ val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
+ var shouldLoadSynchronously: Boolean = true
+
+ /** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */
+ override fun getIconInBackground(
+ task: Task,
+ callback: TaskIconCache.GetTaskIconCallback
+ ): CancellableTask<*>? {
+ val wrappedCallback = {
+ callback.onTaskIconReceived(
+ taskIdToDrawable.getValue(task.key.id),
+ "content desc ${task.key.id}",
+ "title ${task.key.id}"
+ )
+ }
+ if (shouldLoadSynchronously) {
+ wrappedCallback()
+ } else {
+ taskIdToUpdatingTask[task.key.id] = wrappedCallback
+ }
+ return null
+ }
+}
+
+fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) {
+ assertThat(icon).isEqualTo(fakeTaskIconDataSource.taskIdToDrawable[key.id])
+ assertThat(titleDescription).isEqualTo("content desc ${key.id}")
+ assertThat(title).isEqualTo("title ${key.id}")
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index b66b735..30fc491 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -32,7 +32,7 @@
var shouldLoadSynchronously: Boolean = true
/** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
- override fun updateThumbnailInBackground(
+ override fun getThumbnailInBackground(
task: Task,
callback: Consumer<ThumbnailData>
): CancellableTask<ThumbnailData>? {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index e160627..19990a8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -32,6 +32,9 @@
override fun getTaskDataById(taskId: Int): Flow<Task?> =
getAllTaskData().map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+ override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
+ getTaskDataById(taskId).map { it?.thumbnail }
+
override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
visibleTasks.value = visibleTaskIdList
tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index c28a85a..88fa190 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -18,7 +18,6 @@
import android.content.ComponentName
import android.content.Intent
-import com.android.quickstep.TaskIconCache
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.systemui.shared.recents.model.Task
@@ -31,7 +30,6 @@
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
-import org.mockito.kotlin.mock
@OptIn(ExperimentalCoroutinesApi::class)
class TasksRepositoryTest {
@@ -44,10 +42,10 @@
)
private val recentsModel = FakeRecentTasksDataSource()
private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
- private val taskIconCache = mock<TaskIconCache>()
+ private val taskIconDataSource = FakeTaskIconDataSource()
private val systemUnderTest =
- TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache)
+ TasksRepository(recentsModel, taskThumbnailDataSource, taskIconDataSource)
@Test
fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest {
@@ -81,6 +79,22 @@
}
@Test
+ fun setVisibleTasksPopulatesIcons() = runTest {
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+ // .drop(1) to ignore initial null content before from thumbnail was loaded.
+ systemUnderTest
+ .getTaskDataById(1)
+ .drop(1)
+ .first()!!
+ .assertHasIconDataFromSource(taskIconDataSource)
+ systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
+ }
+
+ @Test
fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest {
recentsModel.seedTasks(defaultTaskList)
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
@@ -101,7 +115,28 @@
}
@Test
- fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest {
+ fun changingVisibleTasksContainsAlreadyPopulatedIcons() = runTest {
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+ // .drop(1) to ignore initial null content before from icon was loaded.
+ systemUnderTest
+ .getTaskDataById(2)
+ .drop(1)
+ .first()!!
+ .assertHasIconDataFromSource(taskIconDataSource)
+
+ // Prevent new loading of Drawables
+ taskThumbnailDataSource.shouldLoadSynchronously = false
+ systemUnderTest.setVisibleTasks(listOf(2, 3))
+
+ systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
+ }
+
+ @Test
+ fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() = runTest {
recentsModel.seedTasks(defaultTaskList)
val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
systemUnderTest.getAllTaskData(forceRefresh = true)
@@ -109,14 +144,20 @@
systemUnderTest.setVisibleTasks(listOf(1, 2))
// .drop(1) to ignore initial null content before from thumbnail was loaded.
- assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail)
- .isEqualTo(bitmap2)
+ val task2 = systemUnderTest.getTaskDataById(2).drop(1).first()!!
+ assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
+ task2.assertHasIconDataFromSource(taskIconDataSource)
// Prevent new loading of Bitmaps
taskThumbnailDataSource.shouldLoadSynchronously = false
+ taskIconDataSource.shouldLoadSynchronously = false
systemUnderTest.setVisibleTasks(listOf(0, 1))
- assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
+ val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
+ assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
+ assertThat(task2AfterVisibleTasksChanged.icon).isNull()
+ assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull()
+ assertThat(task2AfterVisibleTasksChanged.title).isNull()
}
@Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index a394b65..b78f871 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -30,6 +30,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
@@ -42,12 +43,14 @@
@RunWith(AndroidJUnit4::class)
class TaskThumbnailViewModelTest {
+ private var taskViewType = TaskViewType.SINGLE
private val recentsViewData = RecentsViewData()
- private val taskViewData = TaskViewData()
+ private val taskViewData by lazy { TaskViewData(taskViewType) }
private val taskContainerData = TaskContainerData()
private val tasksRepository = FakeTasksRepository()
- private val systemUnderTest =
+ private val systemUnderTest by lazy {
TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
+ }
private val tasks = (0..5).map(::createTaskWithId)
@@ -66,14 +69,26 @@
}
@Test
- fun setRecentsFullscreenProgress_thenProgressIsPassedThrough() = runTest {
+ fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() = runTest {
recentsViewData.fullscreenProgress.value = 0.5f
- assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.5f)
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
recentsViewData.fullscreenProgress.value = 0.6f
- assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.6f)
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f)
+ }
+
+ @Test
+ fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() = runTest {
+ taskViewType = TaskViewType.DESKTOP
+ recentsViewData.fullscreenProgress.value = 0.5f
+
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
+
+ recentsViewData.fullscreenProgress.value = 0.6f
+
+ assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
}
@Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
new file mode 100644
index 0000000..414f8ca
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.util
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModelTest
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [GetThumbnailUseCase] */
+class GetThumbnailUseCaseTest {
+ private val task =
+ Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+ colorBackground = Color.BLACK
+ }
+ private val thumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ }
+ )
+
+ private val tasksRepository = FakeTasksRepository()
+ private val systemUnderTest = GetThumbnailUseCase(tasksRepository)
+
+ @Test
+ fun taskNotSeeded_returnsNull() {
+ assertThat(systemUnderTest.run(TASK_ID)).isNull()
+ }
+
+ @Test
+ fun taskNotLoaded_returnsNull() {
+ tasksRepository.seedTasks(listOf(task))
+
+ assertThat(systemUnderTest.run(TASK_ID)).isNull()
+ }
+
+ @Test
+ fun taskNotVisible_returnsNull() {
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
+
+ assertThat(systemUnderTest.run(TASK_ID)).isNull()
+ }
+
+ @Test
+ fun taskVisible_returnsThumbnail() {
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(listOf(TaskOverlayViewModelTest.TASK_ID))
+
+ assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
+ }
+
+ companion object {
+ const val TASK_ID = 0
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
new file mode 100644
index 0000000..40482c4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Matrix
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [TaskOverlayViewModel] */
+@RunWith(AndroidJUnit4::class)
+class TaskOverlayViewModelTest {
+ private val task =
+ Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+ colorBackground = Color.BLACK
+ }
+ private val thumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ }
+ )
+ private val recentsViewData = RecentsViewData()
+ private val tasksRepository = FakeTasksRepository()
+ private val systemUnderTest = TaskOverlayViewModel(task, recentsViewData, tasksRepository)
+
+ @Test
+ fun initialStateIsDisabled() = runTest {
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
+
+ @Test
+ fun recentsViewOverlayDisabled_Disabled() = runTest {
+ recentsViewData.overlayEnabled.value = false
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
+
+ @Test
+ fun taskNotFullyVisible_Disabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf()
+
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
+
+ @Test
+ fun noThumbnail_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ task.isLocked = false
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = false,
+ thumbnail = null,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ @Test
+ fun withThumbnail_RealSnapshot_NotLocked_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ thumbnailData.isRealSnapshot = true
+ task.isLocked = false
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = true,
+ thumbnail = thumbnailData.thumbnail,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ @Test
+ fun withThumbnail_RealSnapshot_Locked_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ thumbnailData.isRealSnapshot = true
+ task.isLocked = true
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = false,
+ thumbnail = thumbnailData.thumbnail,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ @Test
+ fun withThumbnail_FakeSnapshot_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ thumbnailData.isRealSnapshot = false
+ task.isLocked = false
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = false,
+ thumbnail = thumbnailData.thumbnail,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ companion object {
+ const val TASK_ID = 0
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
new file mode 100644
index 0000000..7aed579
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.content.ComponentName
+import android.content.Intent
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class DesktopTaskTest {
+
+ @Test
+ fun testDesktopTask_sameInstance_isEqual() {
+ val task = DesktopTask(createTasks(1))
+ assertThat(task).isEqualTo(task)
+ }
+
+ @Test
+ fun testDesktopTask_identicalConstructor_isEqual() {
+ val task1 = DesktopTask(createTasks(1))
+ val task2 = DesktopTask(createTasks(1))
+ assertThat(task1).isEqualTo(task2)
+ }
+
+ @Test
+ fun testDesktopTask_copy_isEqual() {
+ val task1 = DesktopTask(createTasks(1))
+ val task2 = task1.copy()
+ assertThat(task1).isEqualTo(task2)
+ }
+
+ @Test
+ fun testDesktopTask_differentId_isNotEqual() {
+ val task1 = DesktopTask(createTasks(1))
+ val task2 = DesktopTask(createTasks(2))
+ assertThat(task1).isNotEqualTo(task2)
+ }
+
+ @Test
+ fun testDesktopTask_differentLength_isNotEqual() {
+ val task1 = DesktopTask(createTasks(1))
+ val task2 = DesktopTask(createTasks(1, 2))
+ assertThat(task1).isNotEqualTo(task2)
+ }
+
+ private fun createTasks(vararg ids: Int): List<Task> {
+ return ids.map { Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 0)) }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
new file mode 100644
index 0000000..f11cd0b
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.content.ComponentName
+import android.content.Intent
+import android.graphics.Rect
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+import com.android.wm.shell.common.split.SplitScreenConstants
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class GroupTaskTest {
+
+ @Test
+ fun testGroupTask_sameInstance_isEqual() {
+ val task = GroupTask(createTask(1))
+ assertThat(task).isEqualTo(task)
+ }
+
+ @Test
+ fun testGroupTask_identicalConstructor_isEqual() {
+ val task1 = GroupTask(createTask(1))
+ val task2 = GroupTask(createTask(1))
+ assertThat(task1).isEqualTo(task2)
+ }
+
+ @Test
+ fun testGroupTask_copy_isEqual() {
+ val task1 = GroupTask(createTask(1))
+ val task2 = task1.copy()
+ assertThat(task1).isEqualTo(task2)
+ }
+
+ @Test
+ fun testGroupTask_differentId_isNotEqual() {
+ val task1 = GroupTask(createTask(1))
+ val task2 = GroupTask(createTask(2))
+ assertThat(task1).isNotEqualTo(task2)
+ }
+
+ @Test
+ fun testGroupTask_equalSplitTasks_isEqual() {
+ val splitBounds =
+ SplitConfigurationOptions.SplitBounds(
+ Rect(),
+ Rect(),
+ 1,
+ 2,
+ SplitScreenConstants.SNAP_TO_50_50
+ )
+ val task1 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
+ val task2 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
+ assertThat(task1).isEqualTo(task2)
+ }
+
+ @Test
+ fun testGroupTask_differentSplitTasks_isNotEqual() {
+ val splitBounds1 =
+ SplitConfigurationOptions.SplitBounds(
+ Rect(),
+ Rect(),
+ 1,
+ 2,
+ SplitScreenConstants.SNAP_TO_50_50
+ )
+ val splitBounds2 =
+ SplitConfigurationOptions.SplitBounds(
+ Rect(),
+ Rect(),
+ 1,
+ 2,
+ SplitScreenConstants.SNAP_TO_30_70
+ )
+ val task1 = GroupTask(createTask(1), createTask(2), splitBounds1, TaskViewType.GROUPED)
+ val task2 = GroupTask(createTask(1), createTask(2), splitBounds2, TaskViewType.GROUPED)
+ assertThat(task1).isNotEqualTo(task2)
+ }
+
+ @Test
+ fun testGroupTask_differentType_isNotEqual() {
+ val task1 = GroupTask(createTask(1), null, null, TaskViewType.SINGLE)
+ val task2 = GroupTask(createTask(1), null, null, TaskViewType.DESKTOP)
+ assertThat(task1).isNotEqualTo(task2)
+ }
+
+ private fun createTask(id: Int): Task {
+ return Task(Task.TaskKey(id, 0, Intent(), ComponentName("", ""), 0, 0))
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index d40f8ab..f3cde52 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -33,9 +33,8 @@
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.quickstep.views.GroupedTaskView
import com.android.quickstep.views.IconView
-import com.android.quickstep.views.TaskThumbnailViewDeprecated
+import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskView
-import com.android.quickstep.views.TaskView.TaskContainer
import com.android.systemui.shared.recents.model.Task
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -59,7 +58,7 @@
private val mockSplitSelectStateController: SplitSelectStateController = mock()
// TaskView
private val mockTaskView: TaskView = mock()
- private val mockThumbnailView: TaskThumbnailViewDeprecated = mock()
+ private val mockSnapshotView: View = mock()
private val mockBitmap: Bitmap = mock()
private val mockIconView: IconView = mock()
private val mockTaskViewDrawable: Drawable = mock()
@@ -87,8 +86,8 @@
@Before
fun setup() {
- whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView)
- whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap)
+ whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView)
+ whenever(mockTaskContainer.splitAnimationThumbnail).thenReturn(mockBitmap)
whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable)
whenever(mockTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer })
@@ -180,7 +179,6 @@
whenever(mockTaskContainer.task).thenReturn(mockTask)
whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
- whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView)
whenever(mockTask.getKey()).thenReturn(mockTaskKey)
whenever(mockTaskKey.getId()).thenReturn(taskId)
whenever(mockSplitSelectStateController.initialTaskId).thenReturn(taskId)
@@ -227,7 +225,8 @@
depthController,
null /* info */,
null /* t */,
- {} /* finishCallback */
+ {} /* finishCallback */,
+ 1f /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -263,7 +262,8 @@
depthController,
transitionInfo,
transaction,
- {} /* finishCallback */
+ {} /* finishCallback */,
+ 1f /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -276,7 +276,7 @@
whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
doNothing()
.whenever(spySplitAnimationController)
- .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+ .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any())
doReturn(-1).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any())
spySplitAnimationController.playSplitLaunchAnimation(
@@ -291,11 +291,12 @@
depthController,
transitionInfo,
transaction,
- {} /* finishCallback */
+ {} /* finishCallback */,
+ 1f /* cornerRadius */
)
verify(spySplitAnimationController)
- .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+ .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any())
}
@Test
@@ -319,7 +320,8 @@
depthController,
transitionInfo,
transaction,
- {} /* finishCallback */
+ {} /* finishCallback */,
+ 1f /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -346,7 +348,8 @@
depthController,
transitionInfo,
transaction,
- {} /* finishCallback */
+ {} /* finishCallback */,
+ 1f /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -373,7 +376,8 @@
depthController,
transitionInfo,
transaction,
- {} /* finishCallback */
+ {} /* finishCallback */,
+ 1f /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -385,7 +389,7 @@
val spySplitAnimationController = spy(splitAnimationController)
doNothing()
.whenever(spySplitAnimationController)
- .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+ .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
spySplitAnimationController.playSplitLaunchAnimation(
null /* launchingTaskView */,
@@ -399,10 +403,11 @@
depthController,
transitionInfo,
transaction,
- {} /* finishCallback */
+ {} /* finishCallback */,
+ 1f /* cornerRadius */
)
verify(spySplitAnimationController)
- .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+ .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
}
}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
index 9ed3906..ef3a833 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -39,6 +39,7 @@
import androidx.test.filters.SmallTest;
import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
@@ -66,6 +67,7 @@
@Mock private MotionEvent mMotionEvent;
@Mock private BubbleTextView mHoverBubbleTextView;
@Mock private FolderIcon mHoverFolderIcon;
+ @Mock private AppPairIcon mAppPairIcon;
@Mock private Display mDisplay;
@Mock private TaskbarDragLayer mTaskbarDragLayer;
private Folder mSpyFolderView;
@@ -85,6 +87,7 @@
when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
+ when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(false);
when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
@@ -213,6 +216,49 @@
assertThat(hoverHandled).isFalse();
}
+ @Test
+ public void onHover_hoverEnterAppPair_revealToolTip() {
+ when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+ when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+
+ boolean hoverHandled =
+ mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
+ waitForIdleSync();
+
+ assertThat(hoverHandled).isTrue();
+ verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+ true);
+ }
+
+ @Test
+ public void onHover_hoverExitAppPair_closeToolTip() {
+ when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+ when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+
+ boolean hoverHandled =
+ mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
+ waitForIdleSync();
+
+ assertThat(hoverHandled).isTrue();
+ verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+ false);
+ }
+
+ @Test
+ public void onHover_hoverEnterIconAlignedWithHotseat_noReveal() {
+ when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+ when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+ when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true);
+
+ boolean hoverHandled =
+ mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
+ waitForIdleSync();
+
+ assertThat(hoverHandled).isTrue();
+ verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+ true);
+ }
+
private void waitForIdleSync() {
mTestableLooper.processAllMessages();
}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 486dc68..27e761a 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -26,6 +26,7 @@
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.TaskItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.quickstep.RecentsModel
import com.android.quickstep.RecentsModel.RecentTasksChangedListener
@@ -44,6 +45,7 @@
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -56,7 +58,6 @@
@Mock private lateinit var mockRecentsModel: RecentsModel
@Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
- private var nextTaskId: Int = 500
private var taskListChangeId: Int = 1
private lateinit var recentAppsController: TaskbarRecentAppsController
@@ -78,6 +79,13 @@
val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
recentTasksChangedListener = listenerCaptor.value
+
+ // Make sure updateHotseatItemInfos() is called after commitRunningAppsToUI()
+ whenever(taskbarViewController.commitRunningAppsToUI()).then {
+ recentAppsController.updateHotseatItemInfos(
+ recentAppsController.shownHotseatItems.toTypedArray()
+ )
+ }
}
@Test
@@ -88,7 +96,7 @@
val newHotseatItems =
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = hotseatPackages,
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = emptyList()
)
assertThat(newHotseatItems.map { it?.targetPackage })
@@ -103,7 +111,7 @@
val newHotseatItems =
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = hotseatPackages,
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = emptyList()
)
assertThat(newHotseatItems.map { it?.targetPackage })
@@ -117,7 +125,7 @@
val newHotseatItems =
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = emptyList()
)
val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
@@ -126,13 +134,58 @@
}
@Test
+ fun updateHotseatItemInfos_inDesktopMode_hotseatPackageHasRunningTask_hotseatItemLinksToTask() {
+ setInDesktopMode(true)
+
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+ runningTasks = listOf(createTask(id = 1, HOTSEAT_PACKAGE_1)),
+ recentTaskPackages = emptyList()
+ )
+
+ assertThat(newHotseatItems).hasLength(2)
+ assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+ assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java)
+ val hotseatItem1 = newHotseatItems[0] as TaskItemInfo
+ assertThat(hotseatItem1.taskId).isEqualTo(1)
+ }
+
+ @Test
+ fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_hotseatCoversFirstTask() {
+ setInDesktopMode(true)
+
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+ runningTasks =
+ listOf(
+ createTask(id = 1, HOTSEAT_PACKAGE_1),
+ createTask(id = 2, HOTSEAT_PACKAGE_1)
+ ),
+ recentTaskPackages = emptyList()
+ )
+
+ // First task is in Hotseat Items
+ assertThat(newHotseatItems).hasLength(2)
+ assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+ assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java)
+ val hotseatItem1 = newHotseatItems[0] as TaskItemInfo
+ assertThat(hotseatItem1.taskId).isEqualTo(1)
+ // Second task is in shownTasks
+ val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+ assertThat(shownTasks)
+ .containsExactlyElementsIn(listOf(createTask(id = 2, HOTSEAT_PACKAGE_1)))
+ }
+
+ @Test
fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() {
recentAppsController.canShowRecentApps = true
setInDesktopMode(false)
val newHotseatItems =
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = emptyList()
)
val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
@@ -146,7 +199,11 @@
setInDesktopMode(true)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
- runningTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+ runningTasks =
+ listOf(
+ createTask(id = 1, RUNNING_APP_PACKAGE_1),
+ createTask(id = 2, RUNNING_APP_PACKAGE_2)
+ ),
recentTaskPackages = emptyList()
)
assertThat(recentAppsController.shownTasks).isEmpty()
@@ -158,7 +215,7 @@
setInDesktopMode(false)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
)
assertThat(recentAppsController.shownTasks).isEmpty()
@@ -169,11 +226,15 @@
setInDesktopMode(false)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ runningTasks =
+ listOf(
+ createTask(id = 1, RUNNING_APP_PACKAGE_1),
+ createTask(id = 2, RUNNING_APP_PACKAGE_2)
+ ),
recentTaskPackages = emptyList()
)
assertThat(recentAppsController.shownTasks).isEmpty()
- assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ assertThat(recentAppsController.minimizedTaskIds).isEmpty()
}
@Test
@@ -181,7 +242,7 @@
setInDesktopMode(true)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
)
assertThat(recentAppsController.shownTasks).isEmpty()
@@ -190,120 +251,161 @@
@Test
fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() {
setInDesktopMode(true)
- val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = runningTaskPackages,
+ runningTasks = listOf(task1, task2),
recentTaskPackages = emptyList()
)
- val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
- assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
- }
-
- @Test
- fun onRecentTasksChanged_inDesktopMode_runningAppIsHotseatItem_shownTasks_returnsDistinctItems() {
- setInDesktopMode(true)
- prepareHotseatAndRunningAndRecentApps(
- hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
- runningTaskPackages =
- listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
- recentTaskPackages = emptyList()
- )
- val expectedPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
- val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
- assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+ assertThat(shownTasks).containsExactlyElementsIn(listOf(task1, task2))
}
@Test
fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() {
setInDesktopMode(false)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ runningTasks = listOf(task1, task2),
recentTaskPackages = emptyList()
)
- assertThat(recentAppsController.runningAppPackages).isEmpty()
- assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ assertThat(recentAppsController.runningTaskIds).isEmpty()
+ assertThat(recentAppsController.minimizedTaskIds).isEmpty()
}
@Test
fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() {
setInDesktopMode(true)
- val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = runningTaskPackages,
+ runningTasks = listOf(task1, task2),
recentTaskPackages = emptyList()
)
- assertThat(recentAppsController.runningAppPackages)
- .containsExactlyElementsIn(runningTaskPackages)
- assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
+ assertThat(recentAppsController.minimizedTaskIds).isEmpty()
}
@Test
fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() {
setInDesktopMode(true)
- val runningTaskPackages =
- listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val runningTasks =
+ listOf(
+ createTask(id = 1, HOTSEAT_PACKAGE_1),
+ createTask(id = 2, RUNNING_APP_PACKAGE_1),
+ createTask(id = 3, RUNNING_APP_PACKAGE_2)
+ )
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
- runningTaskPackages = runningTaskPackages,
+ runningTasks = runningTasks,
recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
)
- assertThat(recentAppsController.runningAppPackages)
- .containsExactlyElementsIn(runningTaskPackages)
- assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2, 3))
+ assertThat(recentAppsController.minimizedTaskIds).isEmpty()
}
@Test
- fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() {
+ fun onRecentTasksChanged_inDesktopMode_allAppsRunningAndInvisibleAppsMinimized() {
setInDesktopMode(true)
- val runningTaskPackages =
- listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
- val minimizedTaskIndices = setOf(2) // RUNNING_APP_PACKAGE_3
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+ val task3Minimized = createTask(id = 3, RUNNING_APP_PACKAGE_3, isVisible = false)
+ val runningTasks = listOf(task1, task2, task3Minimized)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = runningTaskPackages,
- minimizedTaskIndices = minimizedTaskIndices,
+ runningTasks = runningTasks,
recentTaskPackages = emptyList()
)
- assertThat(recentAppsController.runningAppPackages)
- .containsExactlyElementsIn(runningTaskPackages)
- assertThat(recentAppsController.minimizedAppPackages).containsExactly(RUNNING_APP_PACKAGE_3)
+ assertThat(recentAppsController.runningTaskIds).containsExactly(1, 2, 3)
+ assertThat(recentAppsController.minimizedTaskIds).containsExactly(3)
}
@Test
- fun getMinimizedApps_inDesktopMode_twoTasksSamePackageOneMinimizedReturnsNotMinimized() {
+ fun onRecentTasksChanged_inDesktopMode_samePackage_differentTasks_severalRunningTasks() {
setInDesktopMode(true)
- val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_1)
- val minimizedTaskIndices = setOf(1) // The second RUNNING_APP_PACKAGE_1 task.
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = runningTaskPackages,
- minimizedTaskIndices = minimizedTaskIndices,
+ runningTasks = listOf(task1, task2),
recentTaskPackages = emptyList()
)
- assertThat(recentAppsController.runningAppPackages)
- .containsExactlyElementsIn(runningTaskPackages.toSet())
- assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
}
@Test
fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() {
setInDesktopMode(true)
- val originalOrder = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = originalOrder,
+ runningTasks = listOf(task1, task2),
recentTaskPackages = emptyList()
)
+
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ runningTasks = listOf(task2, task1),
recentTaskPackages = emptyList()
)
- val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
- assertThat(shownPackages).isEqualTo(originalOrder)
+
+ val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+ assertThat(shownTasks).isEqualTo(listOf(task1, task2))
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_multiInstance_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = listOf(task1, task2),
+ recentTaskPackages = emptyList()
+ )
+
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = listOf(task2, task1),
+ recentTaskPackages = emptyList()
+ )
+
+ val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+ assertThat(shownTasks).isEqualTo(listOf(task1, task2))
+ }
+
+ @Test
+ fun updateHotseatItems_inDesktopMode_multiInstanceHotseatPackage_shownItems_maintainsOrder() {
+ setInDesktopMode(true)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
+ runningTasks = listOf(task1, task2),
+ recentTaskPackages = emptyList()
+ )
+ updateRecentTasks( // Trigger a recent-tasks change before calling updateHotseatItems()
+ runningTasks = listOf(task2, task1),
+ recentTaskPackages = emptyList()
+ )
+
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
+ runningTasks = listOf(task2, task1),
+ recentTaskPackages = emptyList()
+ )
+
+ val newHotseatItems = recentAppsController.shownHotseatItems
+ assertThat(newHotseatItems).hasSize(1)
+ assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+ assertThat((newHotseatItems[0] as TaskItemInfo).taskId).isEqualTo(1)
+ val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+ assertThat(shownTasks).isEqualTo(listOf(task2))
}
@Test
@@ -311,12 +413,12 @@
setInDesktopMode(false)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
)
val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -327,15 +429,17 @@
@Test
fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() {
setInDesktopMode(true)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+ val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ runningTasks = listOf(task1, task2),
recentTaskPackages = emptyList()
)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages =
- listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_3),
+ runningTasks = listOf(task2, task1, task3),
recentTaskPackages = emptyList()
)
val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -349,12 +453,12 @@
setInDesktopMode(false)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2)
)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
)
val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -365,15 +469,17 @@
@Test
fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() {
setInDesktopMode(true)
+ val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+ val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages =
- listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3),
+ runningTasks = listOf(task1, task2, task3),
recentTaskPackages = emptyList()
)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ runningTasks = listOf(task2, task1),
recentTaskPackages = emptyList()
)
val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -385,12 +491,12 @@
setInDesktopMode(false)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)
)
val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -401,27 +507,31 @@
@Test
fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() {
setInDesktopMode(false)
- val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = runningTaskPackages,
+ runningTasks = listOf(runningTask1, runningTask2),
recentTaskPackages = recentTaskPackages
)
+
setInDesktopMode(true)
recentTasksChangedListener.onRecentTasksChanged()
val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
- assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+ assertThat(shownPackages).containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
}
@Test
fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() {
setInDesktopMode(true)
- val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = runningTaskPackages,
+ runningTasks = listOf(runningTask1, runningTask2),
recentTaskPackages = recentTaskPackages
)
setInDesktopMode(false)
@@ -437,7 +547,7 @@
setInDesktopMode(false)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
)
val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -449,9 +559,11 @@
@Test
fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() {
setInDesktopMode(false)
+ val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ runningTasks = listOf(runningTask1, runningTask2),
recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
)
val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
@@ -467,7 +579,7 @@
setInDesktopMode(false)
prepareHotseatAndRunningAndRecentApps(
hotseatPackages = emptyList(),
- runningTaskPackages = emptyList(),
+ runningTasks = emptyList(),
recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2)
)
val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
@@ -478,21 +590,111 @@
assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
}
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_noActualChangeToRecents_commitRunningAppsToUI_notCalled() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+ // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_noActualChangeToRunning_commitRunningAppsToUI_notCalled() {
+ setInDesktopMode(true)
+ val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+ val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = listOf(runningTask1, runningTask2),
+ recentTaskPackages = emptyList()
+ )
+ verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+ // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = listOf(runningTask1, runningTask2),
+ recentTaskPackages = emptyList()
+ )
+ verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+ }
+
+ @Test
+ fun onRecentTasksChanged_onlyMinimizedChanges_commitRunningAppsToUI_isCalled() {
+ setInDesktopMode(true)
+ val task1Minimized = createTask(id = 1, RUNNING_APP_PACKAGE_1, isVisible = false)
+ val task2Visible = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+ val task2Minimized = createTask(id = 2, RUNNING_APP_PACKAGE_2, isVisible = false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = listOf(task1Minimized, task2Visible),
+ recentTaskPackages = emptyList()
+ )
+ verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+
+ // Call onRecentTasksChanged() again with a new minimized app, verify we update UI.
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTasks = listOf(task1Minimized, task2Minimized),
+ recentTaskPackages = emptyList()
+ )
+
+ verify(taskbarViewController, times(2)).commitRunningAppsToUI()
+ }
+
+ @Test
+ fun onRecentTasksChanged_hotseatAppStartsRunning_commitRunningAppsToUI_isCalled() {
+ setInDesktopMode(true)
+ val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+ val originalTasks = listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1))
+ val newTasks =
+ listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1), createTask(id = 2, HOTSEAT_PACKAGE_1))
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = hotseatPackages,
+ runningTasks = originalTasks,
+ recentTaskPackages = emptyList()
+ )
+ verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+
+ // Call onRecentTasksChanged() again with a new running app, verify we update UI.
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = hotseatPackages,
+ runningTasks = newTasks,
+ recentTaskPackages = emptyList()
+ )
+
+ verify(taskbarViewController, times(2)).commitRunningAppsToUI()
+ }
+
private fun prepareHotseatAndRunningAndRecentApps(
hotseatPackages: List<String>,
- runningTaskPackages: List<String>,
- minimizedTaskIndices: Set<Int> = emptySet(),
+ runningTasks: List<Task>,
recentTaskPackages: List<String>,
): Array<ItemInfo?> {
val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
- val newHotseatItems =
- recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
- val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices)
+ recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+ updateRecentTasks(runningTasks, recentTaskPackages)
+ return recentAppsController.shownHotseatItems.toTypedArray()
+ }
+
+ private fun updateRecentTasks(
+ runningTasks: List<Task>,
+ recentTaskPackages: List<String>,
+ ) {
val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
val allTasks =
ArrayList<GroupTask>().apply {
- if (runningTasks != null) {
- add(runningTasks)
+ if (!runningTasks.isEmpty()) {
+ add(DesktopTask(ArrayList(runningTasks)))
}
addAll(recentTasks)
}
@@ -504,20 +706,21 @@
.whenever(mockRecentsModel)
.getTasks(any<Consumer<List<GroupTask>>>())
recentTasksChangedListener.onRecentTasksChanged()
- return newHotseatItems
}
private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
- return packageNames.map {
- createTestAppInfo(packageName = it).apply {
- container =
- if (it.startsWith("predicted")) {
- CONTAINER_HOTSEAT_PREDICTION
- } else {
- CONTAINER_HOTSEAT
- }
+ return packageNames
+ .map {
+ createTestAppInfo(packageName = it).apply {
+ container =
+ if (it.startsWith("predicted")) {
+ CONTAINER_HOTSEAT_PREDICTION
+ } else {
+ CONTAINER_HOTSEAT
+ }
+ }
}
- }
+ .map { it.makeWorkspaceItem(taskbarActivityContext) }
}
private fun createTestAppInfo(
@@ -525,40 +728,27 @@
className: String = "testClassName"
) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
- private fun createDesktopTask(
- packageNames: List<String>,
- minimizedTaskIndices: Set<Int>
- ): DesktopTask? {
- if (packageNames.isEmpty()) return null
-
- return DesktopTask(
- ArrayList(
- packageNames.mapIndexed { index, packageName ->
- createTask(packageName, index !in minimizedTaskIndices)
- }
- )
- )
- }
-
private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
- return packageNames.map {
- if (it.startsWith("split")) {
- val splitPackages = it.split("_")
+ return packageNames.map { packageName ->
+ if (packageName.startsWith("split")) {
+ val splitPackages = packageName.split("_")
GroupTask(
- createTask(splitPackages[0]),
- createTask(splitPackages[1]),
+ createTask(100, splitPackages[0]),
+ createTask(101, splitPackages[1]),
/* splitBounds = */ null
)
} else {
- GroupTask(createTask(it))
+ // Use the number at the end of the test packageName as the id.
+ val id = 1000 + packageName[packageName.length - 1].code
+ GroupTask(createTask(id, packageName))
}
}
}
- private fun createTask(packageName: String, isVisible: Boolean = true): Task {
+ private fun createTask(id: Int, packageName: String, isVisible: Boolean = true): Task {
return Task(
Task.TaskKey(
- nextTaskId++,
+ id,
WINDOWING_MODE_FREEFORM,
Intent().apply { `package` = packageName },
ComponentName(packageName, "TestActivity"),
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 50b5df1..d9d5585 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -32,6 +32,7 @@
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory.TaskOverlay
import com.android.quickstep.views.LauncherRecentsView
+import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskThumbnailViewDeprecated
import com.android.quickstep.views.TaskView
import com.android.quickstep.views.TaskViewIcon
@@ -39,7 +40,7 @@
import com.android.systemui.shared.recents.model.Task.TaskKey
import com.android.window.flags.Flags
import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -186,10 +187,10 @@
}
}
- private fun createTaskContainer(task: Task): TaskView.TaskContainer {
- return taskView.TaskContainer(
+ private fun createTaskContainer(task: Task): TaskContainer {
+ return TaskContainer(
+ taskView,
task,
- thumbnailView = null,
thumbnailViewDeprecated,
iconView,
transformingTouchDelegate,
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index 03244eb..5d00255 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -32,6 +32,8 @@
import com.android.launcher3.util.LooperExecutor;
import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.views.TaskViewType;
+import com.android.systemui.shared.recents.model.Task;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import org.junit.Before;
@@ -40,8 +42,11 @@
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
@SmallTest
public class RecentTasksListTest {
@@ -104,4 +109,52 @@
assertEquals(taskDescription, taskList.get(0).task1.taskDescription.getLabel());
assertNull(taskList.get(0).task2.taskDescription.getLabel());
}
+
+ @Test
+ public void loadTasksInBackground_freeformTask_createsDesktopTask() {
+ ActivityManager.RecentTaskInfo[] tasks = {
+ createRecentTaskInfo(1 /* taskId */),
+ createRecentTaskInfo(4 /* taskId */),
+ createRecentTaskInfo(5 /* taskId */)};
+ GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forFreeformTasks(
+ tasks, Collections.emptySet() /* minimizedTaskIds */);
+ when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
+
+ List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
+ Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */);
+
+ assertEquals(1, taskList.size());
+ assertEquals(TaskViewType.DESKTOP, taskList.get(0).taskViewType);
+ List<Task> actualFreeformTasks = taskList.get(0).getTasks();
+ assertEquals(3, actualFreeformTasks.size());
+ assertEquals(1, actualFreeformTasks.get(0).key.id);
+ assertEquals(4, actualFreeformTasks.get(1).key.id);
+ assertEquals(5, actualFreeformTasks.get(2).key.id);
+ }
+
+ @Test
+ public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask() {
+ ActivityManager.RecentTaskInfo[] tasks = {
+ createRecentTaskInfo(1 /* taskId */),
+ createRecentTaskInfo(4 /* taskId */),
+ createRecentTaskInfo(5 /* taskId */)};
+ Set<Integer> minimizedTaskIds =
+ Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet());
+ GroupedRecentTaskInfo recentTaskInfos =
+ GroupedRecentTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
+ when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
+
+ List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
+ Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */);
+
+ assertEquals(0, taskList.size());
+ }
+
+ private ActivityManager.RecentTaskInfo createRecentTaskInfo(int taskId) {
+ ActivityManager.RecentTaskInfo recentTaskInfo = new ActivityManager.RecentTaskInfo();
+ recentTaskInfo.taskId = taskId;
+ return recentTaskInfo;
+ }
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
index 07d8f61..6e25b10 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
@@ -31,6 +31,7 @@
import com.android.launcher3.Launcher;
import com.android.quickstep.views.DigitalWellBeingToast;
import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
import org.junit.Test;
@@ -86,7 +87,7 @@
final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
return getFromLauncher(launcher -> {
- TaskView.TaskContainer taskContainer = task.getTaskContainers().get(0);
+ TaskContainer taskContainer = task.getTaskContainers().get(0);
assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals(
taskContainer.getTask().getTopComponent().getPackageName()));
return taskContainer.getDigitalWellBeingToast();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
index b7fd8be..2087016 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -69,7 +69,6 @@
}
@Test
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/288939273
public void testSplitTaskTapBothIconMenus() {
createAndLaunchASplitPair();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index 8adf793..733ea4e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -72,7 +72,6 @@
}
@Test
- @TestStabilityRule.Stability(flavors = PLATFORM_POSTSUBMIT | LOCAL) // b/295225524
public void testSplitAppFromHomeWithItself() throws Exception {
// Currently only tablets have Taskbar in Overview, so test is only active on tablets
assumeTrue(mLauncher.isTablet());
diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml
deleted file mode 100644
index 3d7ad2b..0000000
--- a/res/anim-v33/shared_x_axis_activity_close_enter.xml
+++ /dev/null
@@ -1,42 +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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false"
- android:showBackdrop="true">
-
- <alpha
- android:fromAlpha="0.0"
- android:toAlpha="1.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_decelerate_interpolator"
- android:startOffset="100"
- android:duration="350" />
-
- <translate
- android:fromXDelta="-25%"
- android:toXDelta="0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml
deleted file mode 100644
index fb63602..0000000
--- a/res/anim-v33/shared_x_axis_activity_close_exit.xml
+++ /dev/null
@@ -1,41 +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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false">
-
- <alpha
- android:fromAlpha="1.0"
- android:toAlpha="0.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_accelerate_interpolator"
- android:startOffset="0"
- android:duration="100" />
-
- <translate
- android:fromXDelta="0"
- android:toXDelta="25%"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml
deleted file mode 100644
index cba74ba..0000000
--- a/res/anim-v33/shared_x_axis_activity_open_enter.xml
+++ /dev/null
@@ -1,42 +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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false"
- android:showBackdrop="true">
-
- <alpha
- android:fromAlpha="0.0"
- android:toAlpha="1.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_decelerate_interpolator"
- android:startOffset="100"
- android:duration="350" />
-
- <translate
- android:fromXDelta="25%"
- android:toXDelta="0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml
deleted file mode 100644
index 22e878d..0000000
--- a/res/anim-v33/shared_x_axis_activity_open_exit.xml
+++ /dev/null
@@ -1,41 +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.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:shareInterpolator="false">
-
- <alpha
- android:fromAlpha="1.0"
- android:toAlpha="0.0"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/standard_accelerate_interpolator"
- android:startOffset="0"
- android:duration="100" />
-
- <translate
- android:fromXDelta="0"
- android:toXDelta="-25%"
- android:fillEnabled="true"
- android:fillBefore="true"
- android:fillAfter="true"
- android:interpolator="@interpolator/emphasized_interpolator"
- android:startOffset="0"
- android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml
new file mode 100644
index 0000000..f336eea
--- /dev/null
+++ b/res/drawable/ic_close_work_edu.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@color/material_color_on_surface"
+ android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
+</vector>
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index 81e94f7..e283d3f 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -18,11 +18,11 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="?androidprv:attr/colorSurfaceVariant" />
+ <solid android:color="@color/material_color_surface_container_low" />
<corners android:radius="@dimen/rounded_button_radius" />
<stroke
android:width="1dp"
- android:color="?androidprv:attr/colorSurfaceVariant" />
+ android:color="@color/material_color_surface_container_low" />
<padding
android:left="@dimen/rounded_button_padding"
android:right="@dimen/rounded_button_padding" />
diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml
index 4a66cac..9bf2b8d 100644
--- a/res/drawable/work_card.xml
+++ b/res/drawable/work_card.xml
@@ -16,9 +16,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="?androidprv:attr/colorSurface" />
+ <solid android:color="@color/material_color_surface_container_highest" />
<corners android:radius="@dimen/work_edu_card_radius" />
</shape>
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index bb2b7bd..ce5eed9 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -48,20 +48,23 @@
android:textSize="24sp" />
<TextView
- android:id="@+id/no_widgets_text"
- style="@style/PrimaryHeadline"
+ android:id="@+id/widget_picker_description"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center"
- android:textSize="18sp"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_below="@id/title"
+ android:maxLines="1"
+ android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+ android:textColor="?attr/widgetPickerDescriptionColor"
android:visibility="gone"
- tools:text="@string/no_widgets_available" />
+ android:lineHeight="20sp"
+ android:textSize="14sp" />
<LinearLayout
android:id="@+id/linear_layout_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_below="@id/title">
+ android:layout_below="@id/widget_picker_description">
<FrameLayout
android:id="@+id/recycler_view_container"
@@ -124,6 +127,16 @@
android:background="@drawable/widgets_surface_background"
android:importantForAccessibility="yes"
android:id="@+id/right_pane">
+ <TextView
+ android:id="@+id/no_widgets_text"
+ style="@style/PrimaryHeadline"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:textSize="18sp"
+ android:visibility="gone"
+ tools:text="@string/no_widgets_available" />
+
<!-- Shown when there are recommendations to display -->
<LinearLayout
android:id="@+id/widget_recommendations_container"
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index 99db8c6..c581ae3 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -54,7 +54,7 @@
android:layout_gravity="center"
android:contentDescription="@string/accessibility_close"
android:background="@android:color/transparent"
- android:src="@drawable/ic_remove_no_shadow" />
+ android:src="@drawable/ic_close_work_edu" />
</FrameLayout>
</LinearLayout>
</com.android.launcher3.allapps.WorkEduCard>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index b39945b..5cbce53 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -38,7 +38,7 @@
<string name="app_pair_needs_unfold" msgid="4588897528143807002">"برای استفاده از این جفت برنامه، دستگاه را باز کنید"</string>
<string name="app_pair_not_available" msgid="3556767440808032031">"جفت برنامه دردسترس نیست"</string>
<string name="long_press_widget_to_add" msgid="3587712543577675817">"برای جابهجا کردن ابزارک، لمس کنید و نگه دارید."</string>
- <string name="long_accessible_way_to_add" msgid="2733588281439571974">"برای جابهجا کردن ابزارک یا استفاده از کنشهای سفارشی، دوضربه بزنید و نگه دارید."</string>
+ <string name="long_accessible_way_to_add" msgid="2733588281439571974">"برای جابهجا کردن ابزارک یا استفاده از کنشهای سفارشی، دو تکضرب بزنید و نگه دارید."</string>
<string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
<string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d عرض در %2$d طول"</string>
<string name="widget_preview_context_description" msgid="9045841361655787574">"ابزارک <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
@@ -67,7 +67,7 @@
<string name="widget_category_note_taking" msgid="3469689394504266039">"یادداشتبرداری"</string>
<string name="widget_add_button_label" msgid="2761267068711937179">"افزودن"</string>
<string name="widget_add_button_content_description" msgid="1810530016360039643">"افزودن ابزارک <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
- <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"برای تغییر تنظیمات ابزارک، ضربه بزنید"</string>
+ <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"برای تغییر تنظیمات ابزارک، تکضرب بزنید"</string>
<string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغییر تنظیمات ابزارک"</string>
<string name="all_apps_search_bar_hint" msgid="1390553134053255246">"جستجوی برنامهها"</string>
<string name="all_apps_loading_message" msgid="5813968043155271636">"درحال بارگیری برنامهها…"</string>
@@ -76,7 +76,7 @@
<string name="all_apps_label" msgid="5015784846527570951">"همه برنامهها"</string>
<string name="notifications_header" msgid="1404149926117359025">"اعلانها"</string>
<string name="long_press_shortcut_to_add" msgid="5405328730817637737">"برای جابهجا کردن میانبر، لمس کنید و نگه دارید."</string>
- <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"برای جابهجا کردن میانبر یا استفاده از کنشهای سفارشی، دوضربه بزنید و نگه دارید."</string>
+ <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"برای جابهجا کردن میانبر یا استفاده از کنشهای سفارشی، دو تکضرب بزنید و نگه دارید."</string>
<string name="out_of_space" msgid="6455557115204099579">"فضای خالی در این صفحه اصلی وجود ندارد"</string>
<string name="hotseat_out_of_space" msgid="7448809638125333693">"فضای بیشتری در سینی موارد دلخواه وجود ندارد"</string>
<string name="all_apps_button_label" msgid="8130441508702294465">"فهرست برنامهها"</string>
@@ -99,7 +99,7 @@
<string name="permdesc_write_settings" msgid="726859348127868466">"به برنامه اجازه میدهد تنظیمات و میانبرهای صفحه اصلی را تغییر دهد."</string>
<string name="gadget_error_text" msgid="740356548025791839">"ابزارک را نمیتوان بار کرد"</string>
<string name="gadget_setup_text" msgid="8348374825537681407">"تنظیمات ابزارک"</string>
- <string name="gadget_complete_setup_text" msgid="309040266978007925">"برای تکمیل راهاندازی ضربه بزنید"</string>
+ <string name="gadget_complete_setup_text" msgid="309040266978007925">"برای تکمیل راهاندازی تکضرب بزنید"</string>
<string name="uninstall_system_app_text" msgid="4172046090762920660">"این برنامه سیستمی است و حذف نصب نمیشود."</string>
<string name="folder_hint_text" msgid="5174843001373488816">"ویرایش نام"</string>
<string name="disabled_app_label" msgid="6673129024321402780">"<xliff:g id="APP_NAME">%1$s</xliff:g> غیرفعال شد"</string>
@@ -108,8 +108,8 @@
<string name="workspace_scroll_format" msgid="8458889198184077399">"صفحه اصلی %1$d از %2$d"</string>
<string name="workspace_new_page" msgid="257366611030256142">"صفحه اصلی جدید"</string>
<string name="folder_opened" msgid="94695026776264709">"پوشه باز شده، <xliff:g id="WIDTH">%1$d</xliff:g> در <xliff:g id="HEIGHT">%2$d</xliff:g>"</string>
- <string name="folder_tap_to_close" msgid="4625795376335528256">"برای بستن پوشه، ضربه بزنید"</string>
- <string name="folder_tap_to_rename" msgid="4017685068016979677">"برای ذخیره تغییر نام، ضربه بزنید"</string>
+ <string name="folder_tap_to_close" msgid="4625795376335528256">"برای بستن پوشه، تکضرب بزنید"</string>
+ <string name="folder_tap_to_rename" msgid="4017685068016979677">"برای ذخیره تغییر نام، تکضرب بزنید"</string>
<string name="folder_closed" msgid="4100806530910930934">"پوشه بسته شد"</string>
<string name="folder_renamed" msgid="1794088362165669656">"نام پوشه به <xliff:g id="NAME">%1$s</xliff:g> تغییر کرد"</string>
<string name="folder_name_format_exact" msgid="8626242716117004803">"پوشه: <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="SIZE">%2$d</xliff:g> مورد"</string>
@@ -139,7 +139,7 @@
<string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> درحال نصب است، <xliff:g id="PROGRESS">%2$s</xliff:g> تکمیل شده است"</string>
<string name="app_downloading_title" msgid="8336702962104482644">"درحال بارگیری <xliff:g id="NAME">%1$s</xliff:g>، <xliff:g id="PROGRESS">%2$s</xliff:g> کامل شد"</string>
<string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> درانتظار نصب"</string>
- <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> بایگانی شده است. برای بارگیری و بازیابی ضربه بزنید."</string>
+ <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> بایگانی شده است. برای بارگیری و بازیابی تکضرب بزنید."</string>
<string name="dialog_update_title" msgid="114234265740994042">"برنامه باید بهروز شود"</string>
<string name="dialog_update_message" msgid="4176784553982226114">"برنامه برای این نماد بهروز نشده است. میتوانید آن را بهصورت دستی بهروز کنید تا میانبر دوباره فعال شود، یا نماد را بردارید."</string>
<string name="dialog_update" msgid="2178028071796141234">"بهروزرسانی"</string>
@@ -187,7 +187,7 @@
<string name="developer_options_filter_hint" msgid="5896817443635989056">"فیلتر"</string>
<string name="remote_action_failed" msgid="1383965239183576790">"ناموفق بود: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
<string name="private_space_label" msgid="2359721649407947001">"فضای خصوصی"</string>
- <string name="private_space_secondary_label" msgid="9203933341714508907">"برای راهاندازی یا باز کردن، ضربه بزنید"</string>
+ <string name="private_space_secondary_label" msgid="9203933341714508907">"برای راهاندازی یا باز کردن، تکضرب بزنید"</string>
<string name="ps_container_title" msgid="4391796149519594205">"خصوصی"</string>
<string name="ps_container_settings" msgid="6059734123353320479">"تنظیمات «فضای خصوصی»"</string>
<string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"خصوصی، باز."</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index f3b08cb..f95f26d 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -21,9 +21,9 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" msgid="649227358658669779">"Lanceur3"</string>
<string name="work_folder_name" msgid="3753320833950115786">"Travail"</string>
- <string name="activity_not_found" msgid="8071924732094499514">"L\'application n\'est pas installée."</string>
- <string name="activity_not_available" msgid="7456344436509528827">"Application indisponible"</string>
- <string name="safemode_shortcut_error" msgid="9160126848219158407">"L\'application téléchargée est désactivée en mode sans échec."</string>
+ <string name="activity_not_found" msgid="8071924732094499514">"L\'appli n\'est pas installée."</string>
+ <string name="activity_not_available" msgid="7456344436509528827">"Appli indisponible"</string>
+ <string name="safemode_shortcut_error" msgid="9160126848219158407">"L\'appli téléchargée est désactivée en mode sans échec."</string>
<string name="safemode_widget_error" msgid="4863470563535682004">"Widgets désactivés en mode sans échec"</string>
<string name="shortcut_not_available" msgid="2536503539825726397">"Le raccourci n\'est pas disponible"</string>
<string name="home_screen" msgid="5629429142036709174">"Accueil"</string>
@@ -34,9 +34,9 @@
<string name="split_app_usage_settings" msgid="7214375263347964093">"Paramètres d\'utilisation pour %1$s"</string>
<string name="save_app_pair" msgid="5647523853662686243">"Enr. paire d\'applis"</string>
<string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
- <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applications n\'est pas prise en charge sur cet appareil"</string>
- <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Déplier l\'appareil pour utiliser cette paire d\'applications"</string>
- <string name="app_pair_not_available" msgid="3556767440808032031">"La Paire d\'applications n\'est pas offerte"</string>
+ <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applis n\'est pas prise en charge sur cet appareil"</string>
+ <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Déplier l\'appareil pour utiliser cette paire d\'applis"</string>
+ <string name="app_pair_not_available" msgid="3556767440808032031">"La Paire d\'applis n\'est pas offerte"</string>
<string name="long_press_widget_to_add" msgid="3587712543577675817">"Maintenez le doigt sur un widget pour le déplacer."</string>
<string name="long_accessible_way_to_add" msgid="2733588281439571974">"Touchez 2x un widget et maintenez le doigt dessus pour le déplacer ou utiliser des actions personnalisées."</string>
<string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
@@ -69,20 +69,20 @@
<string name="widget_add_button_content_description" msgid="1810530016360039643">"Ajoutez le widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Touchez pour modifier les paramètres du widget"</string>
<string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifier les paramètres du widget"</string>
- <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Rechercher dans les applications"</string>
- <string name="all_apps_loading_message" msgid="5813968043155271636">"Chargement des applications en cours…"</string>
- <string name="all_apps_no_search_results" msgid="3200346862396363786">"Aucune application trouvée correspondant à « <xliff:g id="QUERY">%1$s</xliff:g> »"</string>
- <string name="label_application" msgid="8531721983832654978">"Application"</string>
- <string name="all_apps_label" msgid="5015784846527570951">"Toutes les applications"</string>
+ <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Rechercher dans les applis"</string>
+ <string name="all_apps_loading_message" msgid="5813968043155271636">"Chargement des applis en cours…"</string>
+ <string name="all_apps_no_search_results" msgid="3200346862396363786">"Aucune appli trouvée correspondant à « <xliff:g id="QUERY">%1$s</xliff:g> »"</string>
+ <string name="label_application" msgid="8531721983832654978">"Appli"</string>
+ <string name="all_apps_label" msgid="5015784846527570951">"Toutes les applis"</string>
<string name="notifications_header" msgid="1404149926117359025">"Notifications"</string>
<string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Maintenez le doigt sur un raccourci pour le déplacer."</string>
<string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"Touchez deux fois un raccourci et maintenez le doigt dessus pour le déplacer ou utiliser des actions personnalisées."</string>
<string name="out_of_space" msgid="6455557115204099579">"Pas d\'espace libre sur cet écran d\'accueil"</string>
<string name="hotseat_out_of_space" msgid="7448809638125333693">"Il n\'y a plus d\'espace dans la zone des favoris"</string>
- <string name="all_apps_button_label" msgid="8130441508702294465">"Liste des applications"</string>
+ <string name="all_apps_button_label" msgid="8130441508702294465">"Liste des applis"</string>
<string name="all_apps_search_results" msgid="5889367432531296759">"Résultats de recherche"</string>
- <string name="all_apps_button_personal_label" msgid="1315764287305224468">"Liste des applications personnelles"</string>
- <string name="all_apps_button_work_label" msgid="7270707118948892488">"Liste des applications professionnelles"</string>
+ <string name="all_apps_button_personal_label" msgid="1315764287305224468">"Liste des applis personnelles"</string>
+ <string name="all_apps_button_work_label" msgid="7270707118948892488">"Liste des applis professionnelles"</string>
<string name="remove_drop_target_label" msgid="7812859488053230776">"Supprimer"</string>
<string name="uninstall_drop_target_label" msgid="4722034217958379417">"Désinstaller"</string>
<string name="app_info_drop_target_label" msgid="692894985365717661">"Détails de l\'appli"</string>
@@ -92,17 +92,17 @@
<string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne pas suggérer d\'appli"</string>
<string name="pin_prediction" msgid="4196423321649756498">"Épingler la prédiction"</string>
<string name="permlab_install_shortcut" msgid="5632423390354674437">"installer des raccourcis"</string>
- <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permet à une application d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur."</string>
+ <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permet à une appli d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur."</string>
<string name="permlab_read_settings" msgid="5136500343007704955">"lire les paramètres et les raccourcis de la page d\'accueil"</string>
- <string name="permdesc_read_settings" msgid="4208061150510996676">"Permet à l\'application de lire les paramètres et les raccourcis de l\'écran d\'accueil."</string>
+ <string name="permdesc_read_settings" msgid="4208061150510996676">"Permet à l\'appli de lire les paramètres et les raccourcis de l\'écran d\'accueil."</string>
<string name="permlab_write_settings" msgid="4820028712156303762">"modifier les paramètres et les raccourcis de la page d\'accueil"</string>
- <string name="permdesc_write_settings" msgid="726859348127868466">"Permet à l\'application de modifier les paramètres et les raccourcis de l\'écran d\'accueil."</string>
+ <string name="permdesc_write_settings" msgid="726859348127868466">"Permet à l\'appli de modifier les paramètres et les raccourcis de l\'écran d\'accueil."</string>
<string name="gadget_error_text" msgid="740356548025791839">"Impossible de charger le widget"</string>
<string name="gadget_setup_text" msgid="8348374825537681407">"Paramètres du widget"</string>
<string name="gadget_complete_setup_text" msgid="309040266978007925">"Touchez pour terminer la configuration"</string>
- <string name="uninstall_system_app_text" msgid="4172046090762920660">"Impossible de désinstaller cette application, car il s\'agit d\'une application système."</string>
+ <string name="uninstall_system_app_text" msgid="4172046090762920660">"Impossible de désinstaller cette appli, car il s\'agit d\'une appli système."</string>
<string name="folder_hint_text" msgid="5174843001373488816">"Modifier le nom"</string>
- <string name="disabled_app_label" msgid="6673129024321402780">"L\'application <xliff:g id="APP_NAME">%1$s</xliff:g> est désactivée"</string>
+ <string name="disabled_app_label" msgid="6673129024321402780">"L\'appli <xliff:g id="APP_NAME">%1$s</xliff:g> est désactivée"</string>
<string name="dotted_app_label" msgid="1865617679843363410">"{count,plural, =1{{app_name} a # notification}one{{app_name} a # notification}other{{app_name} a # notifications}}"</string>
<string name="default_scroll_format" msgid="7475544710230993317">"Page %1$d sur %2$d"</string>
<string name="workspace_scroll_format" msgid="8458889198184077399">"Écran d\'accueil %1$d sur %2$d"</string>
@@ -114,7 +114,7 @@
<string name="folder_renamed" msgid="1794088362165669656">"Nouveau nom du dossier : <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="folder_name_format_exact" msgid="8626242716117004803">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> élément(s)"</string>
<string name="folder_name_format_overflow" msgid="4270108890534995199">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> éléments ou plus"</string>
- <string name="app_pair_name_format" msgid="8134106404716224054">"Paire d\'applications : <xliff:g id="APP1">%1$s</xliff:g> et <xliff:g id="APP2">%2$s</xliff:g>"</string>
+ <string name="app_pair_name_format" msgid="8134106404716224054">"Paire d\'applis : <xliff:g id="APP1">%1$s</xliff:g> et <xliff:g id="APP2">%2$s</xliff:g>"</string>
<string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fond d\'écran et style"</string>
<string name="edit_home_screen" msgid="8947858375782098427">"Modifier l\'écran d\'accueil"</string>
<string name="settings_button_text" msgid="8873672322605444408">"Paramètres d\'accueil"</string>
@@ -125,23 +125,23 @@
<string name="notification_dots_desc_on" msgid="1679848116452218908">"Activé"</string>
<string name="notification_dots_desc_off" msgid="1760796511504341095">"Désactivé"</string>
<string name="title_missing_notification_access" msgid="7503287056163941064">"L\'accès aux notifications est requis"</string>
- <string name="msg_missing_notification_access" msgid="281113995110910548">"Pour afficher les points de notification, activez les notifications d\'application pour <xliff:g id="NAME">%1$s</xliff:g>"</string>
+ <string name="msg_missing_notification_access" msgid="281113995110910548">"Pour afficher les points de notification, activez les notifications d\'appli pour <xliff:g id="NAME">%1$s</xliff:g>"</string>
<string name="title_change_settings" msgid="1376365968844349552">"Modifier les paramètres"</string>
<string name="notification_dots_service_title" msgid="4284221181793592871">"Afficher les points de notification"</string>
<string name="developer_options_title" msgid="700788437593726194">"Options pour les développeurs"</string>
- <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"Ajouter les icônes des applications à l\'écran d\'accueil"</string>
- <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"Pour les nouvelles applications"</string>
+ <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"Ajouter les icônes des applis à l\'écran d\'accueil"</string>
+ <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"Pour les nouvelles applis"</string>
<string name="package_state_unknown" msgid="7592128424511031410">"Inconnu"</string>
<string name="abandoned_clean_this" msgid="7610119707847920412">"Supprimer"</string>
<string name="abandoned_search" msgid="891119232568284442">"Rechercher"</string>
- <string name="abandoned_promises_title" msgid="7096178467971716750">"Cette application n\'est pas installée"</string>
- <string name="abandoned_promise_explanation" msgid="3990027586878167529">"L\'application liée à cette icône n\'est pas installée. Vous pouvez la supprimer ou rechercher l\'application et l\'installer manuellement."</string>
- <string name="app_installing_title" msgid="5864044122733792085">"Installation de l\'application <xliff:g id="NAME">%1$s</xliff:g> en cours, <xliff:g id="PROGRESS">%2$s</xliff:g> terminée"</string>
+ <string name="abandoned_promises_title" msgid="7096178467971716750">"Cette appli n\'est pas installée"</string>
+ <string name="abandoned_promise_explanation" msgid="3990027586878167529">"L\'appli liée à cette icône n\'est pas installée. Vous pouvez la supprimer ou rechercher l\'appli et l\'installer manuellement."</string>
+ <string name="app_installing_title" msgid="5864044122733792085">"Installation de l\'appli <xliff:g id="NAME">%1$s</xliff:g> en cours, <xliff:g id="PROGRESS">%2$s</xliff:g> terminée"</string>
<string name="app_downloading_title" msgid="8336702962104482644">"Téléchargement de <xliff:g id="NAME">%1$s</xliff:g> : <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
<string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> en attente d\'installation"</string>
<string name="app_archived_title" msgid="7717956158562544081">"L\'appli <xliff:g id="NAME">%1$s</xliff:g> est archivée. Touchez le bouton pour télécharger et restaurer l\'appli."</string>
- <string name="dialog_update_title" msgid="114234265740994042">"Mise à jour de l\'application requise"</string>
- <string name="dialog_update_message" msgid="4176784553982226114">"L\'application pour cette icône n\'est pas à jour. Vous pouvez soit la mettre à jour manuellement pour réactiver ce raccourci, soit retirer l\'icône."</string>
+ <string name="dialog_update_title" msgid="114234265740994042">"Mise à jour de l\'appli requise"</string>
+ <string name="dialog_update_message" msgid="4176784553982226114">"L\'appli pour cette icône n\'est pas à jour. Vous pouvez soit la mettre à jour manuellement pour réactiver ce raccourci, soit retirer l\'icône."</string>
<string name="dialog_update" msgid="2178028071796141234">"Mettre à jour"</string>
<string name="dialog_remove" msgid="6510806469849709407">"Retirer"</string>
<string name="widgets_list" msgid="796804551140113767">"Liste des widgets"</string>
@@ -174,15 +174,15 @@
<string name="all_apps_personal_tab" msgid="4190252696685155002">"Personnel"</string>
<string name="all_apps_work_tab" msgid="4884822796154055118">"Travail"</string>
<string name="work_profile_toggle_label" msgid="3081029915775481146">"Profil professionnel"</string>
- <string name="work_profile_edu_work_apps" msgid="7895468576497746520">"Les applications professionnelles sont indiquées par un badge et elles sont visibles pour votre administrateur informatique"</string>
+ <string name="work_profile_edu_work_apps" msgid="7895468576497746520">"Les applis professionnelles sont indiquées par un badge et elles sont visibles pour votre administrateur informatique"</string>
<string name="work_profile_edu_accept" msgid="6069788082535149071">"OK"</string>
- <string name="work_apps_paused_title" msgid="3040901117349444598">"Les applications professionnelles sont interrompues"</string>
- <string name="work_apps_paused_info_body" msgid="1687828929959237477">"Vous ne recevrez pas de notifications de vos applications professionnelles"</string>
- <string name="work_apps_paused_body" msgid="261634750995824906">"Les applications professionnelles ne peuvent ni vous envoyer de notifications, ni utiliser la pile, ni accéder à votre position"</string>
- <string name="work_apps_paused_telephony_unavailable_body" msgid="8358872357502756790">"Vous ne recevrez pas d\'appels téléphoniques, de messages texte ni de notifications de vos applications professionnelles"</string>
- <string name="work_apps_paused_edu_banner" msgid="8872412121608402058">"Les applications professionnelles sont indiquées par un badge et sont visibles pour votre administrateur informatique"</string>
+ <string name="work_apps_paused_title" msgid="3040901117349444598">"Les applis professionnelles sont interrompues"</string>
+ <string name="work_apps_paused_info_body" msgid="1687828929959237477">"Vous ne recevrez pas de notifications de vos applis professionnelles"</string>
+ <string name="work_apps_paused_body" msgid="261634750995824906">"Les applis professionnelles ne peuvent ni vous envoyer de notifications, ni utiliser la pile, ni accéder à votre position"</string>
+ <string name="work_apps_paused_telephony_unavailable_body" msgid="8358872357502756790">"Vous ne recevrez pas d\'appels téléphoniques, de messages texte ni de notifications de vos applis professionnelles"</string>
+ <string name="work_apps_paused_edu_banner" msgid="8872412121608402058">"Les applis professionnelles sont indiquées par un badge et sont visibles pour votre administrateur informatique"</string>
<string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
- <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Mettre en pause les applications professionnelles"</string>
+ <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Mettre en pause les applis professionnelles"</string>
<string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Réactiver"</string>
<string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrer"</string>
<string name="remote_action_failed" msgid="1383965239183576790">"Échec : <xliff:g id="WHAT">%1$s</xliff:g>"</string>
@@ -195,5 +195,5 @@
<string name="ps_container_lock_title" msgid="2640257399982364682">"Verrouiller"</string>
<string name="ps_container_transition" msgid="8667331812048014412">"Transition vers l\'Espace privé"</string>
<string name="ps_add_button_label" msgid="8127988716897128773">"Installer"</string>
- <string name="ps_add_button_content_description" msgid="3254274107740952556">"Installer des applications dans l\'Espace privé"</string>
+ <string name="ps_add_button_content_description" msgid="3254274107740952556">"Installer des applis dans l\'Espace privé"</string>
</resources>
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index d23f4d1..2688b83 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -26,6 +26,8 @@
<color name="home_settings_track_off_color">@android:color/system_neutral1_700</color>
<color name="widget_picker_title_color_dark">@android:color/system_neutral1_100</color>
+ <color name="widget_picker_description_color_dark">
+ @android:color/system_neutral2_200</color>
<color name="widget_picker_header_app_title_color_dark">
@android:color/system_neutral1_100</color>
<color name="widget_picker_header_app_subtitle_color_dark">
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index b287b2f..8cb5515 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -186,14 +186,14 @@
<string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ਰੋਕ ਹਟਾਓ"</string>
<string name="developer_options_filter_hint" msgid="5896817443635989056">"ਫਿਲਟਰ"</string>
<string name="remote_action_failed" msgid="1383965239183576790">"ਇਹ ਕਾਰਵਾਈ ਅਸਫਲ ਹੋਈ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
- <string name="private_space_label" msgid="2359721649407947001">"ਨਿੱਜੀ ਸਪੇਸ"</string>
+ <string name="private_space_label" msgid="2359721649407947001">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ"</string>
<string name="private_space_secondary_label" msgid="9203933341714508907">"ਸੈੱਟਅੱਪ ਕਰਨ ਜਾਂ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ"</string>
<string name="ps_container_title" msgid="4391796149519594205">"ਨਿੱਜੀ"</string>
- <string name="ps_container_settings" msgid="6059734123353320479">"ਨਿੱਜੀ ਸਪੇਸ ਸੰਬੰਧੀ ਸੈਟਿੰਗਾਂ"</string>
+ <string name="ps_container_settings" msgid="6059734123353320479">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ ਸੰਬੰਧੀ ਸੈਟਿੰਗਾਂ"</string>
<string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"ਨਿੱਜੀ, ਅਣਲਾਕ ਕੀਤਾ ਗਿਆ।"</string>
<string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"ਨਿੱਜੀ, ਲਾਕ ਕੀਤਾ ਗਿਆ।"</string>
<string name="ps_container_lock_title" msgid="2640257399982364682">"ਲਾਕ ਕਰੋ"</string>
- <string name="ps_container_transition" msgid="8667331812048014412">"ਨਿੱਜੀ ਸਪੇਸ ਨੂੰ ਤਬਦੀਲ ਕਰਨਾ"</string>
+ <string name="ps_container_transition" msgid="8667331812048014412">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ ਨੂੰ ਤਬਦੀਲ ਕਰਨਾ"</string>
<string name="ps_add_button_label" msgid="8127988716897128773">"ਸਥਾਪਤ ਕਰੋ"</string>
<string name="ps_add_button_content_description" msgid="3254274107740952556">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ ਵਿੱਚ ਐਪਾਂ ਸਥਾਪਤ ਕਰੋ"</string>
</resources>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index fa87221..7270366 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -75,6 +75,8 @@
<color name="widget_picker_title_color_light">
@android:color/system_neutral1_900</color>
+ <color name="widget_picker_description_color_light">
+ @android:color/system_neutral2_700</color>
<color name="widget_picker_header_app_title_color_light">
@android:color/system_neutral1_900</color>
<color name="widget_picker_header_app_subtitle_color_light">
diff --git a/res/values-v33/style.xml b/res/values-v33/style.xml
deleted file mode 100644
index 1261b23..0000000
--- a/res/values-v33/style.xml
+++ /dev/null
@@ -1,40 +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.
-*/
--->
-
-<resources>
- <style name="HomeSettings.Theme" parent="@android:style/Theme.DeviceDefault.Settings">
- <item name="android:listPreferredItemPaddingEnd">16dp</item>
- <item name="android:listPreferredItemPaddingStart">24dp</item>
- <item name="android:navigationBarColor">@android:color/transparent</item>
- <item name="android:statusBarColor">@android:color/transparent</item>
- <item name="android:switchStyle">@style/SwitchStyle</item>
- <item name="android:textAppearanceListItem">@style/HomeSettings.PreferenceTitle</item>
- <item name="android:windowActionBar">false</item>
- <item name="android:windowNoTitle">true</item>
- <item name="preferenceTheme">@style/HomeSettings.PreferenceTheme</item>
- <item name="android:windowAnimationStyle">@style/Animation.SharedBackground</item>
- </style>
-
- <style name="Animation.SharedBackground" parent="@android:style/Animation.Activity">
- <item name="android:activityOpenEnterAnimation">@anim/shared_x_axis_activity_open_enter</item>
- <item name="android:activityOpenExitAnimation">@anim/shared_x_axis_activity_open_exit</item>
- <item name="android:activityCloseEnterAnimation">@anim/shared_x_axis_activity_close_enter</item>
- <item name="android:activityCloseExitAnimation">@anim/shared_x_axis_activity_close_exit</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index be8b2e1..e4e047e 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -61,6 +61,7 @@
<attr name="preloadIconAccentColor" format="color" />
<attr name="preloadIconBackgroundColor" format="color" />
<attr name="widgetPickerTitleColor" format="color"/>
+ <attr name="widgetPickerDescriptionColor" format="color"/>
<attr name="widgetPickerPrimarySurfaceColor" format="color"/>
<attr name="widgetPickerSecondarySurfaceColor" format="color"/>
<attr name="widgetPickerHeaderAppTitleColor" format="color"/>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ce80964..8fa1992 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -104,6 +104,7 @@
<color name="widget_picker_primary_surface_color_light">#EFEDED</color>
<color name="widget_picker_secondary_surface_color_light">#FAF9F8</color>
<color name="widget_picker_title_color_light">#1F1F1F</color>
+ <color name="widget_picker_description_color_light">#4C4D50</color>
<color name="widget_picker_header_app_title_color_light">#1F1F1F</color>
<color name="widget_picker_header_app_subtitle_color_light">#444746</color>
<color name="widget_picker_header_background_color_light">#C2E7FF</color>
@@ -123,6 +124,7 @@
<color name="widget_picker_primary_surface_color_dark">#1F2020</color>
<color name="widget_picker_secondary_surface_color_dark">#393939</color>
<color name="widget_picker_title_color_dark">#E3E3E3</color>
+ <color name="widget_picker_description_color_dark">#CCCDCF</color>
<color name="widget_picker_header_app_title_color_dark">#E3E3E3</color>
<color name="widget_picker_header_app_subtitle_color_dark">#C4C7C5</color>
<color name="widget_picker_header_background_color_dark">#004A77</color>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ae3d3b3..f7273a0 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -243,6 +243,7 @@
<item name="widgetPickerSecondarySurfaceColor">
@color/widget_picker_secondary_surface_color_light</item>
<item name="widgetPickerTitleColor">@color/widget_picker_title_color_light</item>
+ <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_light</item>
<item name="widgetPickerHeaderAppTitleColor">
@color/widget_picker_header_app_title_color_light</item>
<item name="widgetPickerHeaderAppSubtitleColor">
@@ -282,6 +283,7 @@
@color/widget_picker_secondary_surface_color_dark</item>
<item name="widgetPickerTitleColor">
@color/widget_picker_title_color_dark</item>
+ <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_dark</item>
<item name="widgetPickerHeaderAppTitleColor">
@color/widget_picker_header_app_title_color_dark</item>
<item name="widgetPickerHeaderAppSubtitleColor">
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index b51e850..ef56246 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -37,6 +37,8 @@
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.logging.InstanceId;
@@ -221,6 +223,9 @@
dl.addView(frame);
frame.mIsOpen = true;
frame.post(() -> frame.snapToWidget(false));
+ TestEventEmitter.INSTANCE.get(widget.getContext()).sendEvent(
+ TestEvent.RESIZE_FRAME_SHOWING
+ );
}
private void setCornerRadiusFromWidget() {
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index eff748a..960d77a 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -27,8 +28,8 @@
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
-import com.android.app.animation.Interpolators;
import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -188,6 +189,10 @@
* Scrolls this recycler view to the top.
*/
public void scrollToTop() {
+ if (TestProtocol.sDebugTracing) {
+ Log.d(TestProtocol.PRIVATE_SPACE_SCROLL_FAILURE, "FastScrollRecyclerView#scrollToTop",
+ new Exception());
+ }
if (mScrollbar != null) {
mScrollbar.reattachThumbToScroll();
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d905801..cb897dc 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -166,7 +166,6 @@
import androidx.core.os.BuildCompat;
import androidx.window.embedding.RuleController;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.ActivityAllAppsContainerView;
@@ -181,6 +180,8 @@
import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -595,6 +596,7 @@
RuleController.getInstance(this).setRules(
RuleController.parseRules(this, R.xml.split_configuration));
}
+ TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE);
}
protected ModelCallbacks createModelCallbacks() {
@@ -771,6 +773,7 @@
// initialized properly.
onSaveInstanceState(new Bundle());
mModel.rebindCallbacks();
+ updateDisallowBack();
} finally {
Trace.endSection();
}
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 239967d..85c8b57 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -24,6 +24,7 @@
import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
@@ -63,6 +64,9 @@
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.widget.custom.CustomWidgetManager;
+import java.util.Locale;
+import java.util.Objects;
+
public class LauncherAppState implements SafeCloseable {
public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
@@ -115,14 +119,25 @@
}
SimpleBroadcastReceiver modelChangeReceiver =
- new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
- modelChangeReceiver.registerAsync(mContext, Intent.ACTION_LOCALE_CHANGED,
+ new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
+ final Locale oldLocale = mContext.getResources().getConfiguration().locale;
+ modelChangeReceiver.register(
+ mContext,
+ () -> {
+ // if local has changed before receiver is registered on bg thread,
+ // mModel needs to reload.
+ Locale newLocale = mContext.getResources().getConfiguration().locale;
+ if (!Objects.equals(oldLocale, newLocale)) {
+ mModel.forceReload();
+ }
+ },
+ Intent.ACTION_LOCALE_CHANGED,
ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
if (BuildConfig.IS_STUDIO_BUILD) {
mContext.registerReceiver(modelChangeReceiver, new IntentFilter(ACTION_FORCE_ROLOAD),
RECEIVER_EXPORTED);
}
- mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafelyAsync(mContext));
+ mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely(mContext));
SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
.addUserEventListener(mModel::onUserEvent);
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 13062b6..83c34ce 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -11,6 +11,8 @@
import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
import com.android.launcher3.allapps.AllAppsStore
import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
import com.android.launcher3.model.BgDataModel
import com.android.launcher3.model.StringCache
import com.android.launcher3.model.data.AppInfo
@@ -156,6 +158,7 @@
/*pause=*/ false,
deviceProfile.isTwoPanels
)
+ TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
}
/**
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 19a3002..a296f46 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -381,6 +381,28 @@
}
/**
+ * Scales a {@code RectF} in place about a specified pivot point.
+ *
+ * <p>This method modifies the given {@code RectF} directly to scale it proportionally
+ * by the given {@code scale}, while preserving its center at the specified
+ * {@code (pivotX, pivotY)} coordinates.
+ *
+ * @param rectF the {@code RectF} to scale, modified directly.
+ * @param pivotX the x-coordinate of the pivot point about which to scale.
+ * @param pivotY the y-coordinate of the pivot point about which to scale.
+ * @param scale the factor by which to scale the rectangle. Values less than 1 will
+ * shrink the rectangle, while values greater than 1 will enlarge it.
+ */
+ public static void scaleRectFAboutPivot(RectF rectF, float pivotX, float pivotY, float scale) {
+ rectF.offset(-pivotX, -pivotY);
+ rectF.left *= scale;
+ rectF.top *= scale;
+ rectF.right *= scale;
+ rectF.bottom *= scale;
+ rectF.offset(pivotX, pivotY);
+ }
+
+ /**
* Maps t from one range to another range.
* @param t The value to map.
* @param fromMin The lower bound of the range that t is being mapped from.
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index e601a3e..2995e8a 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,6 +80,8 @@
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dot.FolderDotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -314,7 +316,6 @@
*/
public Workspace(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
-
mLauncher = Launcher.getLauncher(context);
mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
mWallpaperManager = WallpaperManager.getInstance(context);
@@ -2218,6 +2219,7 @@
if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
d.stateAnnouncer.completeAction(R.string.item_moved);
}
+ TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP);
}
@Nullable
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 9623709..89e6adc 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -42,6 +42,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
@@ -260,8 +261,12 @@
public void dump(String prefix, PrintWriter writer) {
writer.println(prefix + "\tAllAppsStore Apps[] size: " + mApps.length);
for (int i = 0; i < mApps.length; i++) {
- writer.println(String.format("%s\tPackage index and name: %d/%s", prefix, i,
- mApps[i].componentName.getPackageName()));
+ writer.println(String.format(Locale.getDefault(),
+ "%s\tPackage index, name, and class: " + "%d/%s:%s",
+ prefix,
+ i,
+ mApps[i].componentName.getPackageName(),
+ mApps[i].componentName.getClassName()));
}
}
}
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 98ca420..4b38df8 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -26,6 +26,7 @@
import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
import android.content.Context;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -278,6 +279,13 @@
privateProfileManager.getReadyToAnimate())
&& privateProfileManager.getCurrentState() == STATE_ENABLED
? 0 : 1);
+ Log.d(TAG, "onBindViewHolder: "
+ + "isPrivateSpaceItem: " + isPrivateSpaceItem
+ + " isStateTransitioning: " + privateProfileManager.isStateTransitioning()
+ + " isScrolling: " + privateProfileManager.isScrolling()
+ + " readyToAnimate: " + privateProfileManager.getReadyToAnimate()
+ + " currentState: " + privateProfileManager.getCurrentState()
+ + " currentAlpha: " + icon.getAlpha());
}
// Views can still be bounded before the app list is updated hence showing icons
// after collapsing.
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 6f021ea..0f4204f 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -48,6 +48,7 @@
import android.content.Intent;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -89,6 +90,8 @@
* logic in the Personal tab.
*/
public class PrivateProfileManager extends UserProfileManager {
+
+ private static final String TAG = "PrivateProfileManager";
private static final int EXPAND_COLLAPSE_DURATION = 800;
private static final int SETTINGS_OPACITY_DURATION = 400;
private static final int TEXT_UNLOCK_OPACITY_DURATION = 300;
@@ -362,6 +365,7 @@
} else {
// Ensure any unwanted animations to not happen.
settingAndLockGroup.setLayoutTransition(null);
+ Log.d(TAG, "bindPrivateSpaceHeaderViewElements: removing transitions ");
}
updateView();
}
@@ -597,6 +601,9 @@
}
attachFloatingMaskView(expand);
ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
+ TextView lockText = mPSHeader.findViewById(R.id.lock_text);
+ PrivateSpaceSettingsButton privateSpaceSettingsButton =
+ mPSHeader.findViewById(R.id.ps_settings_button);
if (settingsAndLockGroup.getLayoutTransition() == null) {
// Set a new transition if the current ViewGroup does not already contain one as each
// transition should only happen once when applied.
@@ -612,13 +619,15 @@
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
+ Log.d(TAG, "updatePrivateStateAnimator: Private space animation expanding: "
+ + expand);
mStatsLogManager.logger().sendToInteractionJankMonitor(
expand
? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN
: LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN,
mAllApps.getActiveRecyclerView());
// Animate the collapsing of the text at the same time while updating lock button.
- mPSHeader.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE);
+ lockText.setVisibility(expand ? VISIBLE : GONE);
setAnimationRunning(true);
}
@@ -636,6 +645,11 @@
? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END
: LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END,
mAllApps.getActiveRecyclerView());
+ Log.d(TAG, "updatePrivateStateAnimator: lockText visibility: "
+ + lockText.getVisibility() + " lockTextAlpha: " + lockText.getAlpha());
+ Log.d(TAG, "updatePrivateStateAnimator: settingsCog visibility: "
+ + privateSpaceSettingsButton.getVisibility()
+ + " settingsCogAlpha: " + privateSpaceSettingsButton.getAlpha());
if (!expand) {
mAllApps.mAH.get(MAIN).mRecyclerView.removeItemDecoration(
mPrivateAppsSectionDecorator);
@@ -717,15 +731,19 @@
@Override
public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
View view, int i) {
+ Log.d(TAG, "updatePrivateStateAnimator: transition started: " + transition);
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
View view, int i) {
settingsAndLockGroup.setLayoutTransition(null);
mReadyToAnimate = false;
+ Log.d(TAG, "updatePrivateStateAnimator: transition finished: " + transition);
}
});
settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition);
+ Log.d(TAG, "updatePrivateStateAnimator: setting transition: "
+ + settingsAndLockTransition);
}
/** Change the settings gear alpha when expanded or collapsed. */
diff --git a/src/com/android/launcher3/anim/AnimatedFloat.java b/src/com/android/launcher3/anim/AnimatedFloat.java
index b414ab6..4441164 100644
--- a/src/com/android/launcher3/anim/AnimatedFloat.java
+++ b/src/com/android/launcher3/anim/AnimatedFloat.java
@@ -20,6 +20,8 @@
import android.animation.ObjectAnimator;
import android.util.FloatProperty;
+import java.util.function.Consumer;
+
/**
* A mutable float which allows animating the value
*/
@@ -38,9 +40,9 @@
}
};
- private static final Runnable NO_OP = () -> { };
+ private static final Consumer<Float> NO_OP = t -> { };
- private final Runnable mUpdateCallback;
+ private final Consumer<Float> mUpdateCallback;
private ObjectAnimator mValueAnimator;
// Only non-null when an animation is playing to this value.
private Float mEndValue;
@@ -52,6 +54,10 @@
}
public AnimatedFloat(Runnable updateCallback) {
+ this(v -> updateCallback.run());
+ }
+
+ public AnimatedFloat(Consumer<Float> updateCallback) {
mUpdateCallback = updateCallback;
}
@@ -60,6 +66,11 @@
value = initialValue;
}
+ public AnimatedFloat(Consumer<Float> updateCallback, float initialValue) {
+ this(updateCallback);
+ value = initialValue;
+ }
+
/**
* Returns an animation from the current value to the given value.
*/
@@ -99,7 +110,7 @@
public void updateValue(float v) {
if (Float.compare(v, value) != 0) {
value = v;
- mUpdateCallback.run();
+ mUpdateCallback.accept(value);
}
}
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index e58890f..47a2bdd 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -59,6 +59,13 @@
add(anim, springProperty);
}
+ /**
+ * Utility method to sent an interpolator on an animation and add it to the list
+ */
+ public void add(Animator anim, TimeInterpolator interpolator) {
+ add(anim, interpolator, SpringProperty.DEFAULT);
+ }
+
@Override
public void add(Animator anim) {
add(anim, SpringProperty.DEFAULT);
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
index 32445ec..870c876 100644
--- a/src/com/android/launcher3/apppairs/AppPairIcon.java
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -18,10 +18,12 @@
import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
+import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
+import android.util.FloatProperty;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -54,6 +56,26 @@
public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
private static final String TAG = "AppPairIcon";
+ // The duration of the scaling animation on hover enter/exit.
+ private static final int HOVER_SCALE_DURATION = 150;
+ // The default scale of the icon when not hovered.
+ private static final Float HOVER_SCALE_DEFAULT = 1f;
+ // The max scale of the icon when hovered.
+ private static final Float HOVER_SCALE_MAX = 1.1f;
+ // Animates the scale of the icon background on hover.
+ private static final FloatProperty<AppPairIcon> HOVER_SCALE_PROPERTY =
+ new FloatProperty<>("hoverScale") {
+ @Override
+ public void setValue(AppPairIcon view, float scale) {
+ view.mIconGraphic.setHoverScale(scale);
+ }
+
+ @Override
+ public Float get(AppPairIcon view) {
+ return view.mIconGraphic.getHoverScale();
+ }
+ };
+
// A view that holds the app pair icon graphic.
private AppPairIconGraphic mIconGraphic;
// A view that holds the app pair's title.
@@ -250,4 +272,14 @@
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
+
+ @Override
+ public void onHoverChanged(boolean hovered) {
+ super.onHoverChanged(hovered);
+ ObjectAnimator
+ .ofFloat(this, HOVER_SCALE_PROPERTY,
+ hovered ? HOVER_SCALE_MAX : HOVER_SCALE_DEFAULT)
+ .setDuration(HOVER_SCALE_DURATION)
+ .start();
+ }
}
diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
index db83d91..114ed2e 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
+++ b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
@@ -26,6 +26,7 @@
import androidx.annotation.NonNull;
+import com.android.launcher3.Utilities;
import com.android.launcher3.icons.FastBitmapDrawable;
/**
@@ -128,6 +129,18 @@
height - (mP.getStandardIconPadding() + mP.getOuterPadding())
);
+ // Scale each background from its center edge closest to the center channel.
+ Utilities.scaleRectFAboutPivot(
+ leftSide,
+ leftSide.left + leftSide.width(),
+ leftSide.top + leftSide.centerY(),
+ mP.getHoverScale());
+ Utilities.scaleRectFAboutPivot(
+ rightSide,
+ rightSide.left,
+ rightSide.top + rightSide.centerY(),
+ mP.getHoverScale());
+
drawCustomRoundedRect(canvas, leftSide, new float[]{
mP.getBigRadius(), mP.getBigRadius(),
mP.getSmallRadius(), mP.getSmallRadius(),
@@ -163,6 +176,18 @@
height - (mP.getStandardIconPadding() + mP.getOuterPadding())
);
+ // Scale each background from its center edge closest to the center channel.
+ Utilities.scaleRectFAboutPivot(
+ topSide,
+ topSide.left + topSide.centerX(),
+ topSide.top + topSide.height(),
+ mP.getHoverScale());
+ Utilities.scaleRectFAboutPivot(
+ bottomSide,
+ bottomSide.left + bottomSide.centerX(),
+ bottomSide.top,
+ mP.getHoverScale());
+
drawCustomRoundedRect(canvas, topSide, new float[]{
mP.getBigRadius(), mP.getBigRadius(),
mP.getBigRadius(), mP.getBigRadius(),
diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
index 45dc013..5b546d6 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
@@ -64,6 +64,8 @@
var isLeftRightSplit: Boolean = true
// The background paint color (based on container).
var bgColor: Int = 0
+ // The scale of the icon background while hovered.
+ var hoverScale: Float = 1f
init {
val activity: ActivityContext = ActivityContext.lookupContext(context)
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
index dce97eb..034b686 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -139,4 +139,19 @@
super.dispatchDraw(canvas)
drawable.draw(canvas)
}
+
+ /**
+ * Sets the scale of the icon background while hovered.
+ */
+ fun setHoverScale(scale: Float) {
+ drawParams.hoverScale = scale
+ redraw()
+ }
+
+ /**
+ * Gets the scale of the icon background while hovered.
+ */
+ fun getHoverScale(): Float {
+ return drawParams.hoverScale
+ }
}
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
new file mode 100644
index 0000000..650df5a
--- /dev/null
+++ b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.debug
+
+import android.content.Context
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.SafeCloseable
+
+/** Events fired by the launcher. */
+enum class TestEvent(val event: String) {
+ LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
+ WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
+ RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
+ WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
+}
+
+/** Interface to create TestEventEmitters. */
+interface TestEventEmitter : SafeCloseable {
+
+ companion object {
+ @JvmField
+ val INSTANCE =
+ MainThreadInitializedObject<TestEventEmitter> { _: Context? ->
+ TestEventsEmitterProduction()
+ }
+ }
+
+ fun sendEvent(event: TestEvent)
+}
+
+/**
+ * TestEventsEmitterProduction shouldn't do anything since it runs on the launcher code and not on
+ * tests. This is just a placeholder and test should override this class.
+ */
+class TestEventsEmitterProduction : TestEventEmitter {
+
+ override fun close() {}
+
+ override fun sendEvent(event: TestEvent) {}
+}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index bc5a164..c50c008 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -27,6 +27,7 @@
import android.view.View;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.app.animation.Interpolators;
import com.android.launcher3.DragSource;
@@ -69,8 +70,9 @@
*/
protected DragDriver mDragDriver = null;
+ @VisibleForTesting
/** Options controlling the drag behavior. */
- protected DragOptions mOptions;
+ public DragOptions mOptions;
/** Coordinate for motion down event */
protected final Point mMotionDown = new Point();
@@ -79,7 +81,8 @@
protected final Point mTmpPoint = new Point();
- protected DropTarget.DragObject mDragObject;
+ @VisibleForTesting
+ public DropTarget.DragObject mDragObject;
/** Who can receive drop events */
private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
diff --git a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
index fbe9e33..bebef70 100644
--- a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
+++ b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
@@ -26,7 +26,7 @@
public class SpringLoadedDragController implements OnAlarmListener {
// how long the user must hover over a mini-screen before it unshrinks
private static final long ENTER_SPRING_LOAD_HOVER_TIME = 500;
- private static final long ENTER_SPRING_LOAD_HOVER_TIME_IN_TEST = 2000;
+ private static final long ENTER_SPRING_LOAD_HOVER_TIME_IN_TEST = 3000;
private static final long ENTER_SPRING_LOAD_CANCEL_HOVER_TIME = 950;
Alarm mAlarm;
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index dcc55e6..d3c1a02 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -65,6 +65,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
import com.android.launcher3.AbstractFloatingView;
@@ -1693,6 +1694,11 @@
return windowBottomPx - folderBottomPx;
}
+ @VisibleForTesting
+ public boolean getDeleteFolderOnDropCompleted() {
+ return mDeleteFolderOnDropCompleted;
+ }
+
/**
* Save this listener for the special case of when we update the state and concurrently
* add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 9824992..37a8d9b 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -60,6 +60,7 @@
*/
public class FolderAnimationManager {
+ private static final float EXTRA_FOLDER_REVEAL_RADIUS_PERCENTAGE = 0.125F;
private static final int FOLDER_NAME_ALPHA_DURATION = 32;
private static final int LARGE_FOLDER_FOOTER_DURATION = 128;
@@ -158,12 +159,9 @@
mFolder.mFooter.setPivotX(0);
mFolder.mFooter.setPivotY(0);
- // We want to create a small X offset for the preview items, so that they follow their
- // expected path to their final locations. ie. an icon should not move right, if it's final
- // location is to its left. This value is arbitrarily defined.
- int previewItemOffsetX = (int) (previewSize / 2);
+ int previewItemOffsetX = 0;
if (Utilities.isRtl(mContext.getResources())) {
- previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX);
+ previewItemOffsetX = (int) (lp.width * initialScale - initialSize);
}
final int paddingOffsetX = (int) (mContent.getPaddingLeft() * initialScale);
@@ -239,29 +237,19 @@
play(a, shapeDelegate.createRevealAnimator(
mFolder, startRect, endRect, finalRadius, !mIsOpening));
- // Create reveal animator for the folder content (capture the top 4 icons 2x2)
- int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x
- + mDeviceProfile.folderCellWidthPx * 2;
- int rtlExtraWidth = 0;
- int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y
- + mDeviceProfile.folderCellHeightPx * 2;
int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage();
- // In RTL we want to move to the last 2 columns of icons in the folder.
if (Utilities.isRtl(mContext.getResources())) {
page = (mContent.getPageCount() - 1) - page;
- CellLayout clAtPage = mContent.getPageAt(page);
- if (clAtPage != null) {
- int numExtraRows = clAtPage.getCountX() - 2;
- rtlExtraWidth = (int) Math.max(numExtraRows * (mDeviceProfile.folderCellWidthPx
- + mDeviceProfile.folderCellLayoutBorderSpacePx.x), rtlExtraWidth);
- }
}
- int left = mContent.getPaddingLeft() + page * lp.width;
+ int left = page * lp.width;
+
+ int extraRadius = (int) ((mDeviceProfile.folderIconSizePx / initialScale)
+ * EXTRA_FOLDER_REVEAL_RADIUS_PERCENTAGE);
Rect contentStart = new Rect(
- left + rtlExtraWidth,
- 0,
- left + width + mContent.getPaddingRight() + rtlExtraWidth,
- height);
+ (int) (left + (startRect.left / initialScale)) - extraRadius,
+ (int) (startRect.top / initialScale) - extraRadius,
+ (int) (left + (startRect.right / initialScale)) + extraRadius,
+ (int) (startRect.bottom / initialScale) + extraRadius);
Rect contentEnd = new Rect(left, 0, left + lp.width, lp.height);
play(a, shapeDelegate.createRevealAnimator(
mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index ae8f1d5..6088941 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -508,6 +508,7 @@
&& !SHOULD_SHOW_FIRST_PAGE_WIDGET) {
CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID);
View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false);
+ // TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API changes.
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(
0, 0, firstScreen.getCountX(), 1);
lp.canReorder = false;
diff --git a/src/com/android/launcher3/model/data/TaskItemInfo.kt b/src/com/android/launcher3/model/data/TaskItemInfo.kt
new file mode 100644
index 0000000..fc1cd4d
--- /dev/null
+++ b/src/com/android/launcher3/model/data/TaskItemInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model.data
+
+/**
+ * Temporary class holding a Task ID to allow us to reference a Task when clicking a hotseat item.
+ *
+ * TODO(b/315344726): Remove this class when we have proper Taskbar support for multi-instance apps
+ */
+class TaskItemInfo(val taskId: Int, itemInfo: WorkspaceItemInfo) : WorkspaceItemInfo(itemInfo)
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index cf03462..7339111 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -75,7 +75,7 @@
private final List<BiConsumer<UserHandle, String>> mUserEventListeners = new ArrayList<>();
private final SimpleBroadcastReceiver mUserChangeReceiver =
- new SimpleBroadcastReceiver(this::onUsersChanged);
+ new SimpleBroadcastReceiver(MODEL_EXECUTOR, this::onUsersChanged);
private final Context mContext;
@@ -93,12 +93,12 @@
@Override
public void close() {
- MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafelySync(mContext));
+ MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafely(mContext));
}
@WorkerThread
private void initAsync() {
- mUserChangeReceiver.registerSync(mContext,
+ mUserChangeReceiver.register(mContext,
Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
Intent.ACTION_MANAGED_PROFILE_REMOVED,
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index f46dcd3..78709b8 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -46,7 +46,6 @@
import com.android.launcher3.Launcher;
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;
@@ -67,10 +66,7 @@
import com.android.launcher3.util.ApiWrapper;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.views.FloatingIconView;
-import com.android.launcher3.views.Snackbar;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.PendingAddShortcutInfo;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.PendingAppWidgetHostView;
import com.android.launcher3.widget.WidgetAddFlowHandler;
import com.android.launcher3.widget.WidgetManagerHelper;
@@ -127,20 +123,6 @@
}
} else if (tag instanceof ItemClickProxy) {
((ItemClickProxy) tag).onItemClicked(v);
- } else if (tag instanceof PendingAddShortcutInfo) {
- CharSequence msg = Utilities.wrapForTts(
- launcher.getText(R.string.long_press_shortcut_to_add),
- launcher.getString(R.string.long_accessible_way_to_add_shortcut));
- Snackbar.show(launcher, msg, null);
- } else if (tag instanceof PendingAddWidgetInfo) {
- if (DEBUG) {
- String targetPackage = ((PendingAddWidgetInfo) tag).getTargetPackage();
- Log.d(TAG, "onClick: PendingAddWidgetInfo clicked for package=" + targetPackage);
- }
- CharSequence msg = Utilities.wrapForTts(
- launcher.getText(R.string.long_press_widget_to_add),
- launcher.getString(R.string.long_accessible_way_to_add));
- Snackbar.show(launcher, msg, null);
}
}
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 3dcc663..860f852 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -49,6 +49,7 @@
import android.view.Display;
import androidx.annotation.AnyThread;
+import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
@@ -109,7 +110,10 @@
private DisplayInfoChangeListener mPriorityListener;
private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
- private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onIntent);
+ // We will register broadcast receiver on main thread to ensure not missing changes on
+ // TARGET_OVERLAY_PACKAGE and ACTION_OVERLAY_CHANGED.
+ private final SimpleBroadcastReceiver mReceiver =
+ new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::onIntent);
private Info mInfo;
private boolean mDestroyed = false;
@@ -132,11 +136,11 @@
mWindowContext.registerComponentCallbacks(this);
} else {
mWindowContext = null;
- mReceiver.registerAsync(mContext, ACTION_CONFIGURATION_CHANGED);
+ mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED);
}
// Initialize navigation mode change listener
- mReceiver.registerPkgActionsAsync(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
+ mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context);
Context displayInfoContext = getDisplayInfoContext(display);
@@ -223,7 +227,7 @@
} else {
// TODO: unregister broadcast receiver
}
- mReceiver.unregisterReceiverSafelyAsync(mContext);
+ mReceiver.unregisterReceiverSafely(mContext);
}
/**
@@ -513,9 +517,8 @@
return Collections.unmodifiableSet(mPerDisplayBounds.keySet());
}
- /**
- * Returns all {@link WindowBounds}s for the current display.
- */
+ /** Returns all {@link WindowBounds}s for the current display. */
+ @Nullable
public List<WindowBounds> getCurrentBounds() {
return mPerDisplayBounds.get(normalizedDisplayInfo);
}
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index 2737249..10559f3 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -20,21 +20,28 @@
import android.os.Process
import android.os.UserManager
import androidx.annotation.VisibleForTesting
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
class LockedUserState(private val mContext: Context) : SafeCloseable {
val isUserUnlockedAtLauncherStartup: Boolean
- var isUserUnlocked: Boolean
- private set
+ var isUserUnlocked = false
+ private set(value) {
+ field = value
+ if (value) {
+ notifyUserUnlocked()
+ }
+ }
private val mUserUnlockedActions: RunnableList = RunnableList()
@VisibleForTesting
- val mUserUnlockedReceiver = SimpleBroadcastReceiver {
- if (Intent.ACTION_USER_UNLOCKED == it.action) {
- isUserUnlocked = true
- notifyUserUnlocked()
+ val mUserUnlockedReceiver =
+ SimpleBroadcastReceiver(UI_HELPER_EXECUTOR) {
+ if (Intent.ACTION_USER_UNLOCKED == it.action) {
+ isUserUnlocked = true
+ }
}
- }
init {
// 1) when user reboots devices, launcher process starts at lock screen and both
@@ -43,26 +50,34 @@
// yet isUserUnlockedAtLauncherStartup will remains as false.
// 2) when launcher process restarts after user has unlocked screen, both variable are
// init as true and will not change.
- isUserUnlocked =
- mContext
- .getSystemService(UserManager::class.java)!!
- .isUserUnlocked(Process.myUserHandle())
+ isUserUnlocked = checkIsUserUnlocked()
isUserUnlockedAtLauncherStartup = isUserUnlocked
- if (isUserUnlocked) {
- notifyUserUnlocked()
- } else {
- mUserUnlockedReceiver.registerAsync(mContext, Intent.ACTION_USER_UNLOCKED)
+ if (!isUserUnlocked) {
+ mUserUnlockedReceiver.register(
+ mContext,
+ {
+ // If user is unlocked while registering broadcast receiver, we should update
+ // [isUserUnlocked], which will call [notifyUserUnlocked] in setter
+ if (checkIsUserUnlocked()) {
+ MAIN_EXECUTOR.execute { isUserUnlocked = true }
+ }
+ },
+ Intent.ACTION_USER_UNLOCKED
+ )
}
}
+ private fun checkIsUserUnlocked() =
+ mContext.getSystemService(UserManager::class.java)!!.isUserUnlocked(Process.myUserHandle())
+
private fun notifyUserUnlocked() {
mUserUnlockedActions.executeAllAndDestroy()
- mUserUnlockedReceiver.unregisterReceiverSafelyAsync(mContext)
+ mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
}
/** Stops the receiver from listening for ACTION_USER_UNLOCK broadcasts. */
override fun close() {
- mUserUnlockedReceiver.unregisterReceiverSafelyAsync(mContext)
+ mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
}
/**
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index c1d192c..12eff61 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -19,6 +19,8 @@
import static android.content.Intent.ACTION_SCREEN_ON;
import static android.content.Intent.ACTION_USER_PRESENT;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
import android.content.Context;
import android.content.Intent;
@@ -32,7 +34,8 @@
public static final MainThreadInitializedObject<ScreenOnTracker> INSTANCE =
new MainThreadInitializedObject<>(ScreenOnTracker::new);
- private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onReceive);
+ private final SimpleBroadcastReceiver mReceiver =
+ new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
private final CopyOnWriteArrayList<ScreenOnListener> mListeners = new CopyOnWriteArrayList<>();
private final Context mContext;
@@ -42,12 +45,12 @@
// Assume that the screen is on to begin with
mContext = context;
mIsScreenOn = true;
- mReceiver.registerAsync(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+ mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
}
@Override
public void close() {
- mReceiver.unregisterReceiverSafelyAsync(mContext);
+ mReceiver.unregisterReceiverSafely(mContext);
}
private void onReceive(Intent intent) {
diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
index 5f39cce..539a7cb 100644
--- a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
+++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
@@ -15,21 +15,17 @@
*/
package com.android.launcher3.util;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Handler;
import android.os.Looper;
import android.os.PatternMatcher;
import android.text.TextUtils;
+import androidx.annotation.AnyThread;
import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.BuildConfig;
import java.util.function.Consumer;
@@ -37,8 +33,16 @@
private final Consumer<Intent> mIntentConsumer;
- public SimpleBroadcastReceiver(Consumer<Intent> intentConsumer) {
+ // Handler to register/unregister broadcast receiver
+ private final Handler mHandler;
+
+ public SimpleBroadcastReceiver(LooperExecutor looperExecutor, Consumer<Intent> intentConsumer) {
+ this(looperExecutor.getHandler(), intentConsumer);
+ }
+
+ public SimpleBroadcastReceiver(Handler handler, Consumer<Intent> intentConsumer) {
mIntentConsumer = intentConsumer;
+ mHandler = handler;
}
@Override
@@ -46,55 +50,104 @@
mIntentConsumer.accept(intent);
}
- /** Helper method to register multiple actions. Caller should be on main thread. */
- @UiThread
- public void registerAsync(Context context, String... actions) {
- assertOnMainThread();
- UI_HELPER_EXECUTOR.execute(() -> registerSync(context, actions));
+ /** Calls {@link #register(Context, Runnable, String...)} with null completionCallback. */
+ @AnyThread
+ public void register(Context context, String... actions) {
+ register(context, null, actions);
}
- /** Helper method to register multiple actions. Caller should be on main thread. */
- @WorkerThread
- public void registerSync(Context context, String... actions) {
- assertOnBgThread();
+ /**
+ * Calls {@link #register(Context, Runnable, int, String...)} with null completionCallback.
+ */
+ @AnyThread
+ public void register(Context context, int flags, String... actions) {
+ register(context, null, flags, actions);
+ }
+
+ /**
+ * Register broadcast receiver. If this method is called on the same looper with mHandler's
+ * looper, then register will be called synchronously. Otherwise asynchronously. This ensures
+ * register happens on {@link #mHandler}'s looper.
+ *
+ * @param completionCallback callback that will be triggered after registration is completed,
+ * caller usually pass this callback to check if states has changed
+ * while registerReceiver() is executed on a binder call.
+ */
+ @AnyThread
+ public void register(
+ Context context, @Nullable Runnable completionCallback, String... actions) {
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ registerInternal(context, completionCallback, actions);
+ } else {
+ mHandler.post(() -> registerInternal(context, completionCallback, actions));
+ }
+ }
+
+ /** Register broadcast receiver and run completion callback if passed. */
+ @AnyThread
+ private void registerInternal(
+ Context context, @Nullable Runnable completionCallback, String... actions) {
context.registerReceiver(this, getFilter(actions));
+ if (completionCallback != null) {
+ completionCallback.run();
+ }
}
/**
- * Helper method to register multiple actions associated with a action. Caller should be from
- * main thread.
+ * Same as {@link #register(Context, Runnable, String...)} above but with additional flags
+ * params.
*/
- @UiThread
- public void registerPkgActionsAsync(Context context, @Nullable String pkg, String... actions) {
- assertOnMainThread();
- UI_HELPER_EXECUTOR.execute(() -> registerPkgActionsSync(context, pkg, actions));
+ @AnyThread
+ public void register(
+ Context context, @Nullable Runnable completionCallback, int flags, String... actions) {
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ registerInternal(context, completionCallback, flags, actions);
+ } else {
+ mHandler.post(() -> registerInternal(context, completionCallback, flags, actions));
+ }
+ }
+
+ /** Register broadcast receiver and run completion callback if passed. */
+ @AnyThread
+ private void registerInternal(
+ Context context, @Nullable Runnable completionCallback, int flags, String... actions) {
+ context.registerReceiver(this, getFilter(actions), flags);
+ if (completionCallback != null) {
+ completionCallback.run();
+ }
+ }
+
+ /** Same as {@link #register(Context, Runnable, String...)} above but with pkg name. */
+ @AnyThread
+ public void registerPkgActions(Context context, @Nullable String pkg, String... actions) {
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ context.registerReceiver(this, getPackageFilter(pkg, actions));
+ } else {
+ mHandler.post(() -> {
+ context.registerReceiver(this, getPackageFilter(pkg, actions));
+ });
+ }
}
/**
- * Helper method to register multiple actions associated with a action. Caller should be from
- * bg thread.
+ * Unregister broadcast receiver. If this method is called on the same looper with mHandler's
+ * looper, then unregister will be called synchronously. Otherwise asynchronously. This ensures
+ * unregister happens on {@link #mHandler}'s looper.
*/
- @WorkerThread
- public void registerPkgActionsSync(Context context, @Nullable String pkg, String... actions) {
- assertOnBgThread();
- context.registerReceiver(this, getPackageFilter(pkg, actions));
+ @AnyThread
+ public void unregisterReceiverSafely(Context context) {
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ unregisterReceiverSafelyInternal(context);
+ } else {
+ mHandler.post(() -> {
+ unregisterReceiverSafelyInternal(context);
+ });
+ }
}
- /**
- * Unregisters the receiver ignoring any errors on bg thread. Caller should be on main thread.
- */
- @UiThread
- public void unregisterReceiverSafelyAsync(Context context) {
- assertOnMainThread();
- UI_HELPER_EXECUTOR.execute(() -> unregisterReceiverSafelySync(context));
- }
-
- /**
- * Unregisters the receiver ignoring any errors on bg thread. Caller should be on bg thread.
- */
- @WorkerThread
- public void unregisterReceiverSafelySync(Context context) {
- assertOnBgThread();
+ /** Unregister broadcast receiver ignoring any errors. */
+ @AnyThread
+ private void unregisterReceiverSafelyInternal(Context context) {
try {
context.unregisterReceiver(this);
} catch (IllegalArgumentException e) {
@@ -121,20 +174,4 @@
}
return filter;
}
-
- private static void assertOnBgThread() {
- if (BuildConfig.IS_STUDIO_BUILD && isMainThread()) {
- throw new IllegalStateException("Should not be called from main thread!");
- }
- }
-
- private static void assertOnMainThread() {
- if (BuildConfig.IS_STUDIO_BUILD && !isMainThread()) {
- throw new IllegalStateException("Should not be called from bg thread!");
- }
- }
-
- private static boolean isMainThread() {
- return Thread.currentThread() == Looper.getMainLooper().getThread();
- }
}
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index a2277a0..f8cbe0d 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -32,7 +32,7 @@
private static final int MIN_PARALLAX_PAGE_SPAN = 4;
private final SimpleBroadcastReceiver mWallpaperChangeReceiver =
- new SimpleBroadcastReceiver(i -> onWallpaperChanged());
+ new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> onWallpaperChanged());
private final Workspace<?> mWorkspace;
private final boolean mIsRtl;
private final Handler mHandler;
@@ -198,10 +198,10 @@
public void setWindowToken(IBinder token) {
mWindowToken = token;
if (mWindowToken == null && mRegistered) {
- mWallpaperChangeReceiver.unregisterReceiverSafelyAsync(mWorkspace.getContext());
+ mWallpaperChangeReceiver.unregisterReceiverSafely(mWorkspace.getContext());
mRegistered = false;
} else if (mWindowToken != null && !mRegistered) {
- mWallpaperChangeReceiver.registerAsync(
+ mWallpaperChangeReceiver.register(
mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED);
onWallpaperChanged();
mRegistered = true;
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 1368084..c59e295 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -331,8 +331,21 @@
* status bar, into account.
*/
protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthUsed = getInsetsWidth();
+
DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+ measureChildWithMargins(mContent, widthMeasureSpec,
+ widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding);
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec));
+ }
+
+ /**
+ * Returns the width used on left and right by the insets / padding.
+ */
+ protected int getInsetsWidth() {
int widthUsed;
+ DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
if (deviceProfile.isTablet) {
widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile),
2 * (mInsets.left + mInsets.right));
@@ -343,11 +356,7 @@
widthUsed = Math.max(padding.left + padding.right,
2 * (mInsets.left + mInsets.right));
}
-
- measureChildWithMargins(mContent, widthMeasureSpec,
- widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding);
- setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
- MeasureSpec.getSize(heightMeasureSpec));
+ return widthUsed;
}
/** Returns the horizontal margins to be applied to the widget sheet. **/
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 35372d3..b7ad95e 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -40,7 +40,6 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
-import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
@@ -500,12 +499,6 @@
}
@Override
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
- info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
- }
-
- @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams();
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 9260af9..d84a219 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -16,7 +16,7 @@
package com.android.launcher3.widget.picker;
-import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering;
+import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering;
import android.content.ComponentName;
import android.content.Context;
@@ -38,6 +38,7 @@
import com.android.launcher3.pageindicators.PageIndicatorDots;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -57,6 +58,12 @@
private static final String INITIALLY_DISPLAYED_WIDGETS_STATE_KEY =
"widgetRecommendationsView:mDisplayedWidgets";
private static final int MAX_CATEGORIES = 3;
+
+ // Whether to show all widgets in a full page without any limitation on height
+ private boolean mShowFullPageViewIfLowDensity = false;
+ // Number of items below which a category is considered low density.
+ private static final int IDEAL_ITEMS_PER_CATEGORY = 2;
+
private TextView mRecommendationPageTitle;
private final List<String> mCategoryTitles = new ArrayList<>();
@@ -88,6 +95,14 @@
}
/**
+ * When there are less than 3 categories or when at least one category has less than 2 widgets,
+ * all widgets will be shown in a single page without being limited by the available height.
+ */
+ public void enableFullPageViewIfLowDensity() {
+ mShowFullPageViewIfLowDensity = true;
+ }
+
+ /**
* Saves the necessary state in the provided bundle. To be called in case of orientation /
* other config changes.
*/
@@ -170,6 +185,22 @@
return displayedWidgets.size();
}
+ private boolean shouldShowFullPageView(
+ Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations) {
+ if (mShowFullPageViewIfLowDensity) {
+ boolean hasLessCategories = recommendations.size() < MAX_CATEGORIES;
+ long lowDensityCategoriesCount = recommendations.values()
+ .stream()
+ .limit(MAX_CATEGORIES)
+ .filter(items -> items.size() < IDEAL_ITEMS_PER_CATEGORY).count();
+
+ // If there less number of categories or if there are at least 2 categorizes with less
+ // widgets, prefer showing single page view.
+ return hasLessCategories || lowDensityCategoriesCount > 1;
+ }
+ return false;
+ }
+
/**
* Displays the recommendations grouped by categories as pages.
* <p>In case of a single category, no title is displayed for it.</p>
@@ -188,6 +219,14 @@
Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
DeviceProfile deviceProfile, final @Px float availableHeight,
final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) {
+ if (shouldShowFullPageView(recommendations)) {
+ // Show all widgets in single page with unlimited available height.
+ return setRecommendations(
+ recommendations.values().stream().flatMap(Collection::stream).toList(),
+ deviceProfile, /*availableHeight=*/ Float.MAX_VALUE, availableWidth,
+ cellPadding);
+
+ }
this.mAvailableHeight = availableHeight;
this.mAvailableWidth = availableWidth;
Context context = getContext();
@@ -325,7 +364,7 @@
// Since we are limited by space, we don't sort recommendations - to show most relevant
// (if possible).
- List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
+ List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithReordering(
filteredRecommendedWidgets,
context,
deviceProfile,
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 9929892..2e36583 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -55,7 +55,6 @@
import com.android.launcher3.BaseActivity;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -416,19 +415,18 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
+ updateMaxSpansPerRow(availableWidth);
doMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (updateMaxSpansPerRow()) {
- doMeasure(widthMeasureSpec, heightMeasureSpec);
- }
}
- /** Returns {@code true} if the max spans have been updated. */
- private boolean updateMaxSpansPerRow() {
- if (getMeasuredWidth() == 0) return false;
-
- @Px int maxHorizontalSpan = getContentView().getMeasuredWidth()
- - (2 * mContentHorizontalMargin);
+ /** Returns {@code true} if the max spans have been updated.
+ *
+ * @param availableWidth Total width available within parent (includes insets).
+ */
+ private void updateMaxSpansPerRow(int availableWidth) {
+ @Px int maxHorizontalSpan = getAvailableWidthForSuggestions(
+ availableWidth - getInsetsWidth());
if (mMaxSpanPerRow != maxHorizontalSpan) {
mMaxSpanPerRow = maxHorizontalSpan;
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
@@ -439,16 +437,15 @@
mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
maxHorizontalSpan);
}
- onRecommendedWidgetsBound();
- return true;
+ post(this::onRecommendedWidgetsBound);
}
- return false;
}
- protected View getContentView() {
- return mHasWorkProfile
- ? mViewPager
- : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
+ /**
+ * Returns the width available to display suggestions.
+ */
+ protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) {
+ return pickerAvailableWidth - (2 * mContentHorizontalMargin);
}
@Override
@@ -493,7 +490,7 @@
.mWidgetsListAdapter.hasVisibleEntries());
if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) {
mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded;
- onRecommendedWidgetsBound();
+ post(this::onRecommendedWidgetsBound);
}
}
@@ -549,7 +546,7 @@
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
// Visibility of recommended widgets, recycler views and headers are handled in methods
// below.
- onRecommendedWidgetsBound();
+ post(this::onRecommendedWidgetsBound);
onWidgetsBound();
}
}
@@ -682,6 +679,18 @@
return sheet;
}
+ /**
+ * Updates the widget picker's title and description in the header to the provided values (if
+ * present).
+ */
+ public void mayUpdateTitleAndDescription(@Nullable String title,
+ @Nullable String descriptionRes) {
+ if (title != null) {
+ mHeaderTitle.setText(title);
+ }
+ // Full sheet doesn't support a description.
+ }
+
@Override
public void saveHierarchyState(SparseArray<Parcelable> sparseArray) {
Bundle bundle = new Bundle();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java
index 854700f..6a1921e 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java
@@ -16,6 +16,8 @@
package com.android.launcher3.widget.picker;
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_LIST;
import androidx.recyclerview.widget.DefaultItemAnimator;
@@ -26,6 +28,14 @@
public static final int MOVE_DURATION_MS = 90;
public static final int ADD_DURATION_MS = 120;
+ // DefaultItemAnimator runs change and move animations before running add animations (i.e.
+ // before expanded list item's content start animating to become visible on screen).
+ public static final int WIDGET_LIST_ITEM_APPEARANCE_START_DELAY =
+ areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0;
+ // Delay after which all item animations are ran and list item's content is visible.
+ public static final int WIDGET_LIST_ITEM_APPEARANCE_DELAY =
+ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY + ADD_DURATION_MS;
+
public WidgetsListItemAnimator() {
super();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 45d733a..679b0f5 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -15,10 +15,7 @@
*/
package com.android.launcher3.widget.picker;
-import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.CHANGE_DURATION_MS;
-import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.MOVE_DURATION_MS;
-
-import static android.animation.ValueAnimator.areAnimatorsEnabled;
+import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_START_DELAY;
import android.content.Context;
import android.graphics.Bitmap;
@@ -157,8 +154,7 @@
// Pass resize delay to let the "move" and "change" animations run before resizing the
// row.
tableRow.setupRow(widgetItems.size(),
- /*resizeDelayMs=*/
- areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0);
+ /*resizeDelayMs=*/ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY);
if (tableRow.getChildCount() > widgetItems.size()) {
for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
tableRow.getChildAt(j).setVisibility(View.GONE);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 1ed3d88..0bcab60 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -18,7 +18,7 @@
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx;
-import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_SIZE_COMPARATOR;
+import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_COUNT_COMPARATOR;
import static java.lang.Math.max;
@@ -163,6 +163,6 @@
}
// Perform re-ordering once we have filtered out recommendations that fit.
- return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
+ return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_COUNT_COMPARATOR).toList();
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 5d71db6..c84680d 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER;
import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
+import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY;
import android.content.Context;
import android.graphics.Rect;
@@ -31,9 +32,11 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ScrollView;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -58,9 +61,6 @@
* Popup for showing the full list of available widgets with a two-pane layout.
*/
public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
-
- private static final int PERSONAL_TAB = 0;
- private static final int WORK_TAB = 1;
private static final int MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 268;
private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
@@ -81,6 +81,7 @@
private int mActivePage = -1;
@Nullable
private PackageUserKey mSelectedHeader;
+ private TextView mHeaderDescription;
public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
@@ -118,12 +119,17 @@
mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+ if (!mDeviceProfile.isTwoPanels) {
+ mWidgetRecommendationsView.enableFullPageViewIfLowDensity();
+ }
// To save the currently displayed page, so that, it can be requested when rebinding
// recommendations with different size constraints.
mWidgetRecommendationsView.addPageSwitchListener(
newPage -> mRecommendationsCurrentPage = newPage);
mHeaderTitle = mContent.findViewById(R.id.title);
+ mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
+
mRightPane = mContent.findViewById(R.id.right_pane);
mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
@@ -139,6 +145,17 @@
}
@Override
+ public void mayUpdateTitleAndDescription(@Nullable String title, @Nullable String description) {
+ if (title != null) {
+ mHeaderTitle.setText(title);
+ }
+ if (description != null) {
+ mHeaderDescription.setText(description);
+ mHeaderDescription.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
if (enableCategorizedWidgetSuggestions()) {
// two pane picker is full width for fold as well as tablet.
@@ -281,10 +298,19 @@
mRightPane.removeAllViews();
mRightPane.addView(mWidgetRecommendationsContainer);
mRightPaneScrollView.setScrollY(0);
- mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo);
final boolean isChangingHeaders = mSelectedHeader == null
|| !mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey);
+ // If the initial focus view is still focused, it is likely a programmatic header
+ // click.
+ if (mSelectedHeader != null
+ && !getAccessibilityInitialFocusView().isAccessibilityFocused()) {
+ post(() -> {
+ mRightPaneScrollView.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
+ mRightPaneScrollView.performAccessibilityAction(
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+ });
+ }
if (isChangingHeaders) {
// If switching from another header, unselect any WidgetCells. This is necessary
// because we do not clear/recycle the WidgetCells in the recommendations container
@@ -296,7 +322,6 @@
mSelectedHeader = mSuggestedWidgetsPackageUserKey;
});
mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader);
- mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
}
@Override
@@ -313,6 +338,30 @@
}
@Override
+ @Px
+ protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) {
+ int rightPaneWidth = (int) Math.ceil(0.67 * pickerAvailableWidth);
+
+ if (mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
+ // See onLayout
+ int leftPaneWidth = (int) (0.33 * pickerAvailableWidth);
+ @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
+ @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
+ if (leftPaneWidth < minLeftPaneWidthPx) {
+ leftPaneWidth = minLeftPaneWidthPx;
+ } else if (leftPaneWidth > maxLeftPaneWidthPx) {
+ leftPaneWidth = maxLeftPaneWidthPx;
+ }
+ rightPaneWidth = pickerAvailableWidth - leftPaneWidth;
+ }
+
+ // Since suggestions are shown in right pane, the available width is 2/3 of total width of
+ // bottom sheet.
+ return rightPaneWidth - getResources().getDimensionPixelSize(
+ R.dimen.widget_list_horizontal_margin_two_pane); // right pane end margin.
+ }
+
+ @Override
public void onActivePageChanged(int currentActivePage) {
super.onActivePageChanged(currentActivePage);
@@ -323,21 +372,24 @@
mActivePage = currentActivePage;
- if (mSuggestedWidgetsHeader == null) {
- mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
- mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop();
- } else if (currentActivePage == PERSONAL_TAB || currentActivePage == WORK_TAB) {
- mSuggestedWidgetsHeader.callOnClick();
- }
+ // When using talkback, swiping left while on right pane, should navigate to the widgets
+ // list on left.
+ mAdapters.get(mActivePage).mWidgetsRecyclerView.setAccessibilityTraversalBefore(
+ mRightPaneScrollView.getId());
+
+ // On page change, select the first item in the list to show in the right pane.
+ mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
+ mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop();
}
@Override
protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
// The first item is always an empty space entry. Look for any more items.
boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
-
- mRightPane.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
-
+ if (!isWidgetAvailable) {
+ mRightPane.removeAllViews();
+ mRightPane.addView(mNoWidgetsView);
+ }
super.updateRecyclerViewVisibility(adapterHolder);
}
@@ -372,17 +424,16 @@
}
- @Override
- protected View getContentView() {
- return mRightPane;
- }
-
private HeaderChangeListener getHeaderChangeListener() {
return new HeaderChangeListener() {
@Override
public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) {
final boolean isSameHeader = mSelectedHeader != null
&& mSelectedHeader.equals(selectedHeader);
+ // If the initial focus view is still focused, it is likely a programmatic header
+ // click.
+ final boolean isUserClick = mSelectedHeader != null
+ && !getAccessibilityInitialFocusView().isAccessibilityFocused();
mSelectedHeader = selectedHeader;
WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider()
.getSelectedAppWidgets(selectedHeader);
@@ -427,11 +478,14 @@
};
mRightPane.removeAllViews();
mRightPane.addView(widgetsRowViewHolder.itemView);
+ if (isUserClick) {
+ mRightPaneScrollView.setAccessibilityPaneTitle(getContext().getString(
+ R.string.widget_picker_right_pane_accessibility_title,
+ contentEntry.mPkgItem.title));
+ postDelayed(() -> focusOnFirstWidgetCell(widgetsRowViewHolder.tableContainer),
+ WIDGET_LIST_ITEM_APPEARANCE_DELAY);
+ }
mRightPaneScrollView.setScrollY(0);
- mRightPane.setAccessibilityPaneTitle(
- getContext().getString(
- R.string.widget_picker_right_pane_accessibility_title,
- contentEntry.mPkgItem.title));
}
};
}
@@ -445,6 +499,18 @@
}
}
+ /**
+ * Requests focus on the first widget cell in the given widget section.
+ */
+ private static void focusOnFirstWidgetCell(ViewGroup parent) {
+ if (parent == null) return;
+ WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell);
+ if (cell != null) {
+ cell.performAccessibilityAction(
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+ }
+ }
+
private static void unselectWidgetCell(ViewGroup parent, WidgetItem item) {
if (parent == null || item == null) return;
WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc
diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
index edaf474..df72f07 100644
--- a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
+++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
@@ -69,6 +69,21 @@
});
/**
+ * Comparator that enables displaying rows with more number of items at the top, and then
+ * rest of widgets shown in increasing order of their size (totalW * H).
+ */
+ public static final Comparator<ArrayList<WidgetItem>> WIDGETS_TABLE_ROW_COUNT_COMPARATOR =
+ Comparator.comparingInt(row -> {
+ if (row.size() > 1) {
+ return -row.size();
+ } else {
+ int rowWidth = row.stream().mapToInt(w -> w.spanX).sum();
+ int rowHeight = row.get(0).spanY;
+ return (rowWidth * rowHeight);
+ }
+ });
+
+ /**
* Groups {@code widgetItems} items into a 2D array which matches their appearance in a UI
* table. This takes liberty to rearrange widgets to make the table visually appealing.
*/
diff --git a/tests/assets/ReorderWidgets/full_reorder_case b/tests/assets/ReorderWidgets/full_reorder_case
index 850e4fd..2890b79 100644
--- a/tests/assets/ReorderWidgets/full_reorder_case
+++ b/tests/assets/ReorderWidgets/full_reorder_case
@@ -17,12 +17,12 @@
# Test 4x4
board: 4x4
xxxx
-bbmm
+bbaa
iimm
-iiaa
+iimm
arguments: 0 2
board: 4x4
xxxx
-bbii
+bbaa
mmii
-mmaa
\ No newline at end of file
+mmii
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/push_reorder_case b/tests/assets/ReorderWidgets/push_reorder_case
index 8e845a2..1eacfae 100644
--- a/tests/assets/ReorderWidgets/push_reorder_case
+++ b/tests/assets/ReorderWidgets/push_reorder_case
@@ -17,28 +17,28 @@
#Test 5x5
board: 5x5
xxxxx
-bbbm-
+bbb--
--ccc
--ddd
------
-arguments: 2 1
+----m
+arguments: 2 2
board: 5x5
xxxxx
---m--
bbb--
+--m--
--ccc
--ddd
#6x5 Test
board: 6x5
xxxxxx
-bbbbm-
+bbbb--
--aaa-
--ddd-
-------
-arguments: 2 1
+-----m
+arguments: 2 2
board: 6x5
xxxxxx
---m---
bbbb--
+--m---
--aaa-
--ddd-
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/simple_reorder_case b/tests/assets/ReorderWidgets/simple_reorder_case
index 2c50ce4..991ccb5 100644
--- a/tests/assets/ReorderWidgets/simple_reorder_case
+++ b/tests/assets/ReorderWidgets/simple_reorder_case
@@ -21,7 +21,7 @@
--mm-
-----
-----
-arguments: 0 4
+arguments: 0 3
board: 5x5
xxxxx
-----
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 59d0de6..fab3015 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -174,6 +174,7 @@
public static final String TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE = "b/326908466";
public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890";
public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
+ public static final String PRIVATE_SPACE_SCROLL_FAILURE = "b/339737008";
public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
index 419cb3d..f1403e5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
@@ -54,7 +54,7 @@
}
public static class Arguments extends TestSection {
- String[] arguments;
+ public String[] arguments;
public Arguments(String[] arguments) {
super(State.ARGUMENTS);
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
index b239aed..ec83b8b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -8,7 +8,6 @@
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
-import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
@@ -34,6 +33,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
+@RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
class GeneratedPreviewTest {
@get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
private val providerName =
@@ -104,7 +104,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
fun widgetItem_hasGeneratedPreview() {
assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isTrue()
assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse()
@@ -112,7 +111,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
fun widgetItem_hasGeneratedPreview_noPreview() {
appWidgetProviderInfo.generatedPreviewCategories = 0
createWidgetItem()
@@ -122,7 +120,6 @@
}
@Test
- @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
fun widgetItem_hasGeneratedPreview_nullPreview() {
appWidgetProviderInfo.generatedPreviewCategories =
WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD
@@ -134,22 +131,12 @@
}
@Test
- @RequiresFlagsDisabled(FLAG_ENABLE_GENERATED_PREVIEWS)
- fun widgetItem_hasGeneratedPreview_flagDisabled() {
- assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_HOME_SCREEN)).isFalse()
- assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_KEYGUARD)).isFalse()
- assertThat(widgetItem.hasGeneratedPreview(WIDGET_CATEGORY_SEARCHBOX)).isFalse()
- }
-
- @Test
- @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
fun widgetItem_getGeneratedPreview() {
val preview = widgetItem.generatedPreviews.get(WIDGET_CATEGORY_HOME_SCREEN)
assertThat(preview).isEqualTo(generatedPreview)
}
@Test
- @RequiresFlagsEnabled(FLAG_ENABLE_GENERATED_PREVIEWS)
fun widgetCell_showGeneratedPreview() {
widgetCell.applyFromCellItem(widgetItem)
DatabaseWidgetPreviewLoader.getLoaderExecutor().submit {}.get()
@@ -157,12 +144,4 @@
assertThat(widgetCell.appWidgetHostViewPreview?.appWidgetInfo)
.isEqualTo(appWidgetProviderInfo)
}
-
- @Test
- @RequiresFlagsDisabled(FLAG_ENABLE_GENERATED_PREVIEWS)
- fun widgetCell_showGeneratedPreview_flagDisabled() {
- widgetCell.applyFromCellItem(widgetItem)
- DatabaseWidgetPreviewLoader.getLoaderExecutor().submit {}.get()
- assertThat(widgetCell.appWidgetHostViewPreview).isNull()
- }
}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
index 57117cb..430e496 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
@@ -22,8 +22,6 @@
import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
@@ -202,7 +200,6 @@
}
@Test
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/339109319
public void openPrivateSpaceSettings_triggersCorrectIntent() {
Intent expectedIntent = ApiWrapper.INSTANCE.get(mContext).getPrivateSpaceSettingsIntent();
ArgumentCaptor<Intent> acIntent = ArgumentCaptor.forClass(Intent.class);
diff --git a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
deleted file mode 100644
index 28a1325..0000000
--- a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
+++ /dev/null
@@ -1,312 +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;
-
-import static android.platform.uiautomator_helpers.DeviceHelpers.getContext;
-
-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.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MultipageCellLayout;
-import com.android.launcher3.celllayout.board.CellLayoutBoard;
-import com.android.launcher3.celllayout.board.TestWorkspaceBuilder;
-import com.android.launcher3.celllayout.board.WidgetRect;
-import com.android.launcher3.tapl.Widget;
-import com.android.launcher3.tapl.WidgetResizeFrame;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.ModelTestExtensions;
-import com.android.launcher3.util.rule.ShellCommandRule;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TaplReorderWidgetsTest extends AbstractLauncherUiTest<Launcher> {
-
- @Rule
- public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
-
- private static final String TAG = TaplReorderWidgetsTest.class.getSimpleName();
-
- private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
-
- TestWorkspaceBuilder mWorkspaceBuilder;
-
- @Before
- public void setup() throws Throwable {
- mWorkspaceBuilder = new TestWorkspaceBuilder(mTargetContext);
- super.setUp();
- }
-
- @After
- public void tearDown() {
- ModelTestExtensions.INSTANCE.clearModelDb(
- LauncherAppState.getInstance(getContext()).getModel()
- );
- }
-
- /**
- * Validate if the given board represent the current CellLayout
- **/
- private boolean validateBoard(List<CellLayoutBoard> testBoards) {
- ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
- if (workspaceBoards.size() < testBoards.size()) {
- return false;
- }
- for (int i = 0; i < testBoards.size(); i++) {
- if (testBoards.get(i).compareTo(workspaceBoards.get(i)) != 0) {
- return false;
- }
- }
- return true;
- }
-
- private FavoriteItemsTransaction buildWorkspaceFromBoards(List<CellLayoutBoard> boards,
- FavoriteItemsTransaction transaction) {
- for (int i = 0; i < boards.size(); i++) {
- CellLayoutBoard board = boards.get(i);
- mWorkspaceBuilder.buildFromBoard(board, transaction, i);
- }
- return transaction;
- }
-
- private void printCurrentWorkspace() {
- InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
- ArrayList<CellLayoutBoard> boards = workspaceToBoards();
- for (int i = 0; i < boards.size(); i++) {
- Log.d(TAG, "Screen number " + i);
- Log.d(TAG, ".\n" + boards.get(i).toString(idp.numColumns, idp.numRows));
- }
- }
-
- private ArrayList<CellLayoutBoard> workspaceToBoards() {
- return getFromLauncher(CellLayoutTestUtils::workspaceToBoards);
- }
-
- private WidgetRect getWidgetClosestTo(Point point) {
- ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
- int maxDistance = 9999;
- WidgetRect bestRect = null;
- for (int i = 0; i < workspaceBoards.get(0).getWidgets().size(); i++) {
- 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) {
- 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) {
- WidgetRect mainWidgetCellPos = CellLayoutBoard.getMainFromList(
- testCase.mStart);
-
- FavoriteItemsTransaction transaction =
- new FavoriteItemsTransaction(mTargetContext);
- transaction = buildWorkspaceFromBoards(testCase.mStart, transaction);
- transaction.commit();
- mLauncher.waitForLauncherInitialized();
- // resetLoaderState triggers the launcher to start loading the workspace which allows
- // 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);
- WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(testCase.moveMainTo.x,
- testCase.moveMainTo.y, mainWidgetCellPos.getSpanX(), mainWidgetCellPos.getSpanY());
- resizeFrame.dismiss();
-
- boolean isValid = false;
- for (List<CellLayoutBoard> boards : testCase.mEnd) {
- isValid |= validateBoard(boards);
- if (isValid) break;
- }
- printCurrentWorkspace();
- assertTrue("Non of the valid boards match with the current state", isValid);
- }
-
- /**
- * Run only the test define for the current grid size if such test exist
- *
- * @param testCaseMap map containing all the tests per grid size (Point)
- */
- private boolean runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName) {
- Point iconGridDimensions = mLauncher.getWorkspace().getIconGridDimensions();
- Log.d(TAG, "Running test " + testName + " for grid " + 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");
- }
-
- @Test
- public void pushTest() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"),
- "push_reorder_case");
- }
-
- @Test
- public void fullReorder() throws Exception {
- 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");
- }
-
- @Test
- 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,
- Map<Point, ReorderTestCase> testCaseMap) {
- CellLayoutTestCaseReader.Board startBoard =
- ((CellLayoutTestCaseReader.Board) sections.next());
- CellLayoutTestCaseReader.Arguments point =
- ((CellLayoutTestCaseReader.Arguments) sections.next());
- CellLayoutTestCaseReader.Board endBoard =
- ((CellLayoutTestCaseReader.Board) sections.next());
- Point moveTo = new Point(Integer.parseInt(point.arguments[0]),
- Integer.parseInt(point.arguments[1]));
- testCaseMap.put(endBoard.gridSize,
- new ReorderTestCase(startBoard.board, moveTo, endBoard.board));
- }
-
- private Map<Point, ReorderTestCase> getTestMap(String testPath) throws IOException {
- Map<Point, ReorderTestCase> testCaseMap = new HashMap<>();
- Iterator<CellLayoutTestCaseReader.TestSection> iterableSection =
- CellLayoutTestCaseReader.readFromFile(testPath).parse().iterator();
- while (iterableSection.hasNext()) {
- addTestCase(iterableSection, testCaseMap);
- }
- return testCaseMap;
- }
-}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
new file mode 100644
index 0000000..4cecb5a
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.integrationtest
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Workspace
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.widget.LauncherAppWidgetHostView
+
+object TestUtils {
+ fun <T> searchChildren(viewGroup: ViewGroup, type: Class<T>): T? where T : View {
+ for (i in 0..<viewGroup.childCount) {
+ val child = viewGroup.getChildAt(i)
+ if (type.isInstance(child)) {
+ return type.cast(child)
+ }
+ if (child is ViewGroup) {
+ val result = searchChildren(child, type)
+ if (result != null) {
+ return result
+ }
+ }
+ }
+ return null
+ }
+
+ fun getWidgetAtCell(
+ workspace: Workspace<*>,
+ cellX: Int,
+ cellY: Int
+ ): LauncherAppWidgetHostView {
+ val view =
+ (workspace.getPageAt(workspace.currentPage) as CellLayout).getChildAt(cellX, cellY)
+ assert(view != null) { "There is no view at $cellX , $cellY" }
+ assert(view is LauncherAppWidgetHostView) { "The view at $cellX , $cellY is not a widget" }
+ return view as LauncherAppWidgetHostView
+ }
+
+ fun getCellTopLeftRelativeToCellLayout(
+ workspace: Workspace<*>,
+ cellAndSpan: CellAndSpan
+ ): Point {
+ val target = Rect()
+ val cellLayout = workspace.getPageAt(workspace.currentPage) as CellLayout
+ cellLayout.cellToRect(
+ cellAndSpan.cellX,
+ cellAndSpan.cellY,
+ cellAndSpan.spanX,
+ cellAndSpan.spanY,
+ target
+ )
+ return Point(target.left, target.top)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
new file mode 100644
index 0000000..fb61ced
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.integrationtest.events
+
+import android.content.Context
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule to create EventWaiters to wait for events that happens on the Launcher. For reference look
+ * at [TestEvent] for existing events.
+ *
+ * Waiting for event should be used to prevent race conditions, it provides a more precise way of
+ * waiting for events compared to [AbstractLauncherUiTest#waitForLauncherCondition].
+ *
+ * This class overrides the [TestEventEmitter] with [TestEventsEmitterImplementation] and makes sure
+ * to return the [TestEventEmitter] to the previous value when finished.
+ */
+class EventsRule(val context: Context) : TestRule {
+
+ private var prevEventEmitter: TestEventEmitter? = null
+
+ private val eventEmitter = TestEventsEmitterImplementation()
+
+ override fun apply(base: Statement, description: Description?): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ beforeTest()
+ base.evaluate()
+ afterTest()
+ }
+ }
+ }
+
+ fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+ return eventEmitter.createEventWaiter(expectedEvent)
+ }
+
+ private fun beforeTest() {
+ prevEventEmitter = TestEventEmitter.INSTANCE.get(context)
+ TestEventEmitter.INSTANCE.initializeForTesting(eventEmitter)
+ }
+
+ private fun afterTest() {
+ TestEventEmitter.INSTANCE.initializeForTesting(prevEventEmitter)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
new file mode 100644
index 0000000..365ad4b
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.integrationtest.events
+
+import android.util.Log
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeoutOrNull
+
+enum class EventStatus() {
+ SUCCESS,
+ FAILURE,
+ TIMEOUT,
+}
+
+class EventWaiter(val eventToWait: TestEvent) {
+ private val deferrable = CompletableDeferred<EventStatus>()
+
+ companion object {
+ private const val TAG = "EventWaiter"
+ }
+
+ fun waitForSignal(timeout: Long = TimeUnit.SECONDS.toMillis(10)) = runBlocking {
+ var status = withTimeoutOrNull(timeout) { deferrable.await() }
+ if (status == null) {
+ status = EventStatus.TIMEOUT
+ }
+ if (status != EventStatus.SUCCESS) {
+ throw Exception("Failure waiting for event $eventToWait, failure = $status")
+ }
+ }
+
+ fun terminate() {
+ deferrable.complete(EventStatus.SUCCESS)
+ }
+}
+
+class TestEventsEmitterImplementation() : TestEventEmitter {
+ companion object {
+ private const val TAG = "TestEvents"
+ }
+
+ private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
+
+ fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+ val eventWaiter = EventWaiter(expectedEvent)
+ expectedEvents.add(eventWaiter)
+ return eventWaiter
+ }
+
+ private fun clearQueue() {
+ expectedEvents.clear()
+ }
+
+ override fun sendEvent(event: TestEvent) {
+ Log.d(TAG, "Signal received $event")
+ Log.d(TAG, "Total expected events ${expectedEvents.size}")
+ if (expectedEvents.isEmpty()) return
+ val eventWaiter = expectedEvents.last()
+ if (eventWaiter.eventToWait == event) {
+ Log.d(TAG, "Removing $event")
+ expectedEvents.removeLast()
+ eventWaiter.terminate()
+ } else {
+ Log.d(TAG, "Not matching $event")
+ }
+ }
+
+ override fun close() {
+ clearQueue()
+ }
+}
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 405dae7..46cafa7 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -177,8 +177,6 @@
*/
@Test
@PortraitLandscape
- @ScreenRecordRule.ScreenRecord // b/338869019
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/338869019
public void testAddDeleteShortcutOnHotseat() {
mLauncher.getWorkspace()
.deleteAppIcon(mLauncher.getWorkspace().getHotseatAppIcon(0))
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
new file mode 100644
index 0000000..e1daa74
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.folder
+
+import android.content.Context
+import android.graphics.Point
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.celllayout.board.FolderPoint
+import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import junit.framework.TestCase.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+/** Tests for [Folder] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderTest {
+
+ private val context: Context =
+ ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ private val workspaceBuilder = TestWorkspaceBuilder(context)
+ private val folder: Folder = Mockito.spy(Folder(context, null))
+
+ @After
+ fun tearDown() {
+ LauncherAppState.getInstance(context).model.clearModelDb()
+ }
+
+ @Test
+ fun `Undo a folder with 1 icon when onDropCompleted is called`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+ folder.mInfo.getContents().removeAt(0)
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val dragLayout = Mockito.mock(View::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.onDropCompleted(dragLayout, dragObject, true)
+ verify(folder, times(1)).replaceFolderWithFinalItem()
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ }
+
+ @Test
+ fun `Do not undo a folder with 2 icons when onDropCompleted is called`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val dragLayout = Mockito.mock(View::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.onDropCompleted(dragLayout, dragObject, true)
+ verify(folder, times(0)).replaceFolderWithFinalItem()
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ }
+
+ companion object {
+ const val TWO_ICON_FOLDER_TYPE = 'A'
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 6e01f9e..3d253b4 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -22,6 +22,7 @@
import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
import static com.android.launcher3.testing.shared.TestProtocol.WIDGET_CONFIG_NULL_EXTRA_INTENT;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -238,9 +239,9 @@
protected void clearPackageData(String pkg) throws IOException, InterruptedException {
final CountDownLatch count = new CountDownLatch(2);
final SimpleBroadcastReceiver broadcastReceiver =
- new SimpleBroadcastReceiver(i -> count.countDown());
+ new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> count.countDown());
// We OK to make binder calls on main thread in test.
- broadcastReceiver.registerPkgActionsSync(mTargetContext, pkg,
+ broadcastReceiver.registerPkgActions(mTargetContext, pkg,
Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED);
mDevice.executeShellCommand("pm clear " + pkg);
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
index 28d1faa..d40d3bc 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
@@ -23,8 +23,6 @@
import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -56,7 +54,6 @@
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TestViewHelpers;
import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
@@ -143,7 +140,6 @@
}
@Test
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894
public void testPendingWidget_withConfigScreen() {
// A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
// Do not bind the widget
@@ -193,7 +189,6 @@
}
@Test
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894
public void testPendingWidget_notRestored_brokenInstall() {
// A widget which is was being installed once, even if its not being
// installed at the moment is not removed.
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
index d653317..a148744 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
@@ -16,6 +16,8 @@
package com.android.launcher3.ui.workspace;
import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -38,6 +40,8 @@
import com.android.launcher3.tapl.HomeAppIconMenuItem;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.rule.ScreenRecordRule;
+import com.android.launcher3.util.rule.TestStabilityRule;
import org.junit.Test;
@@ -111,6 +115,8 @@
}
@Test
+ @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/350557998
+ @ScreenRecordRule.ScreenRecord // b/350557998
public void testShortcutIconWithTheme() throws Exception {
setThemeEnabled(true);
initialize(this);
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index e92d641..eb05000 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -113,8 +113,6 @@
@Test
@PortraitLandscape
- @ScreenRecordRule.ScreenRecord // b/329935119
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/329935119
public void testSinglePageDragIconWhenMultiplePageScrollingIsPossible() {
Workspace workspace = mLauncher.getWorkspace();
@@ -169,6 +167,7 @@
@Test
@PortraitLandscape
+ @ScreenRecordRule.ScreenRecord // b/352130094
public void testDragIconToPage2() {
Workspace workspace = mLauncher.getWorkspace();
@@ -247,7 +246,6 @@
}
@ScreenRecordRule.ScreenRecord // b/329935119
- @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/329935119
@Test
@PortraitLandscape
public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() {
diff --git a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
new file mode 100644
index 0000000..1de99c5
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import android.os.Looper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.same
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SimpleBroadcastReceiverTest {
+
+ private lateinit var underTest: SimpleBroadcastReceiver
+
+ @Mock private lateinit var intentConsumer: Consumer<Intent>
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var completionRunnable: Runnable
+ @Captor private lateinit var intentFilterCaptor: ArgumentCaptor<IntentFilter>
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, intentConsumer)
+ if (Looper.getMainLooper() == null) {
+ Looper.prepareMainLooper()
+ }
+ }
+
+ @Test
+ fun async_register() {
+ underTest.register(context, "test_action_1", "test_action_2")
+ awaitTasksCompleted()
+
+ verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
+ val intentFilter = intentFilterCaptor.value
+ assertThat(intentFilter.countActions()).isEqualTo(2)
+ assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+ assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+ }
+
+ @Test
+ fun async_register_withCompletionRunnable() {
+ underTest.register(context, completionRunnable, "test_action_1", "test_action_2")
+ awaitTasksCompleted()
+
+ verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
+ verify(completionRunnable).run()
+ val intentFilter = intentFilterCaptor.value
+ assertThat(intentFilter.countActions()).isEqualTo(2)
+ assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+ assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+ }
+
+ @Test
+ fun async_register_withCompletionRunnable_and_flag() {
+ underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+ awaitTasksCompleted()
+
+ verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
+ verify(completionRunnable).run()
+ val intentFilter = intentFilterCaptor.value
+ assertThat(intentFilter.countActions()).isEqualTo(2)
+ assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+ assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+ }
+
+ @Test
+ fun async_register_with_package() {
+ underTest.registerPkgActions(context, "pkg", "test_action_1", "test_action_2")
+
+ awaitTasksCompleted()
+ verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
+ val intentFilter = intentFilterCaptor.value
+ assertThat(intentFilter.getDataScheme(0)).isEqualTo("package")
+ assertThat(intentFilter.getDataSchemeSpecificPart(0).path).isEqualTo("pkg")
+ assertThat(intentFilter.countActions()).isEqualTo(2)
+ assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+ assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+ }
+
+ @Test
+ fun sync_register_withCompletionRunnable_and_flag() {
+ underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
+
+ underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+
+ verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
+ verify(completionRunnable).run()
+ val intentFilter = intentFilterCaptor.value
+ assertThat(intentFilter.countActions()).isEqualTo(2)
+ assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+ assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+ }
+
+ @Test
+ fun async_unregister() {
+ underTest.unregisterReceiverSafely(context)
+
+ awaitTasksCompleted()
+ verify(context).unregisterReceiver(same(underTest))
+ }
+
+ @Test
+ fun sync_unregister() {
+ underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
+
+ underTest.unregisterReceiverSafely(context)
+
+ verify(context).unregisterReceiver(same(underTest))
+ }
+
+ @Test
+ fun getPackageFilter() {
+ val intentFilter =
+ SimpleBroadcastReceiver.getPackageFilter("pkg", "test_action_1", "test_action_2")
+
+ assertThat(intentFilter.getDataScheme(0)).isEqualTo("package")
+ assertThat(intentFilter.getDataSchemeSpecificPart(0).path).isEqualTo("pkg")
+ assertThat(intentFilter.countActions()).isEqualTo(2)
+ assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+ assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+ }
+
+ private fun awaitTasksCompleted() {
+ UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+ }
+}