Merge "Don't modify invariant device profile" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index d4cea8d..4f5b1a0 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -622,3 +622,10 @@
description: "Enables custom theme manager in Launcher"
bug: "381897614"
}
+
+flag {
+ name: "enable_alt_tab_kqs_flatenning"
+ namespace: "lse_desktop_experience"
+ description: "Enable Alt + Tab KQS view to show apps in flattened structure"
+ bug: "382769617"
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index f379e22..5749c51 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -96,3 +96,17 @@
description: "Enable overview on connected displays."
bug: "363251602"
}
+
+flag {
+ name: "enable_overview_background_wallpaper_blur"
+ namespace: "launcher_overview"
+ description: "Enable wallpaper blur in overview."
+ bug: "369975912"
+}
+
+flag {
+ name: "enable_overview_desktop_tile_wallpaper_background"
+ namespace: "launcher_overview"
+ description: "Enable wallpaper background for desktop tasks in overview."
+ bug: "363257721"
+}
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index 00b5392..0972be1 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -51,7 +51,7 @@
<TextView
android:id="@+id/icon_text_collapsed"
android:layout_width="@dimen/task_thumbnail_icon_menu_text_collapsed_max_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+ android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:maxLines="1"
android:ellipsize="end"
@@ -62,7 +62,7 @@
<TextView
android:id="@+id/icon_text_expanded"
android:layout_width="@dimen/task_thumbnail_icon_menu_text_expanded_max_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+ android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:maxLines="1"
android:ellipsize="end"
diff --git a/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
index 71c782d..db47ff0 100644
--- a/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
@@ -48,13 +48,13 @@
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@id/text"
+ app:layout_constraintBottom_toTopOf="@id/small_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
style="@style/KeyboardQuickSwitchText.OnTaskView"
- android:id="@+id/text"
+ android:id="@+id/small_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
diff --git a/quickstep/res/layout/taskbar_edu_features.xml b/quickstep/res/layout/taskbar_edu_features.xml
index a7bd184..aa1312e 100644
--- a/quickstep/res/layout/taskbar_edu_features.xml
+++ b/quickstep/res/layout/taskbar_edu_features.xml
@@ -20,7 +20,7 @@
android:layout_height="wrap_content">
<TextView
- android:id="@+id/title"
+ android:id="@+id/taskbar_edu_title"
style="@style/TextAppearance.TaskbarEduTooltip.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -42,7 +42,7 @@
app:layout_constraintEnd_toEndOf="@id/splitscreen_text"
app:layout_constraintStart_toStartOf="@id/splitscreen_text"
- app:layout_constraintTop_toBottomOf="@id/title" />
+ app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title" />
<TextView
android:id="@+id/splitscreen_text"
@@ -72,7 +72,7 @@
app:layout_constraintEnd_toEndOf="@id/pinning_text"
app:layout_constraintStart_toStartOf="@id/pinning_text"
- app:layout_constraintTop_toBottomOf="@id/title" />
+ app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title" />
<TextView
android:id="@+id/pinning_text"
@@ -96,7 +96,7 @@
app:layout_constraintEnd_toEndOf="@id/suggestions_text"
app:layout_constraintStart_toStartOf="@id/suggestions_text"
- app:layout_constraintTop_toBottomOf="@id/title" />
+ app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title" />
<TextView
android:id="@+id/suggestions_text"
diff --git a/quickstep/res/layout/taskbar_edu_pinning.xml b/quickstep/res/layout/taskbar_edu_pinning.xml
index 27a7b23..5937d62 100644
--- a/quickstep/res/layout/taskbar_edu_pinning.xml
+++ b/quickstep/res/layout/taskbar_edu_pinning.xml
@@ -19,7 +19,7 @@
android:layout_height="wrap_content">
<TextView
- android:id="@+id/title"
+ android:id="@+id/taskbar_edu_title"
style="@style/TextAppearance.TaskbarEduTooltip.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -37,7 +37,7 @@
app:layout_constraintBottom_toTopOf="@id/pinning_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title"
app:lottie_rawRes="@raw/taskbar_edu_pinning"
app:lottie_autoPlay="true"
app:lottie_loop="true" />
diff --git a/quickstep/res/layout/taskbar_edu_search.xml b/quickstep/res/layout/taskbar_edu_search.xml
index ca84f35..ec4d4b4 100644
--- a/quickstep/res/layout/taskbar_edu_search.xml
+++ b/quickstep/res/layout/taskbar_edu_search.xml
@@ -19,7 +19,7 @@
android:layout_height="wrap_content">
<TextView
- android:id="@+id/title"
+ android:id="@+id/taskbar_edu_title"
style="@style/TextAppearance.TaskbarEduTooltip.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -37,7 +37,7 @@
app:layout_constraintBottom_toTopOf="@id/search_edu_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title"
app:lottie_rawRes="@raw/taskbar_edu_search"
app:lottie_autoPlay="true"
app:lottie_loop="true" />
diff --git a/quickstep/res/layout/taskbar_edu_swipe.xml b/quickstep/res/layout/taskbar_edu_swipe.xml
index 3f5e819..9b4809e 100644
--- a/quickstep/res/layout/taskbar_edu_swipe.xml
+++ b/quickstep/res/layout/taskbar_edu_swipe.xml
@@ -19,7 +19,7 @@
android:layout_height="wrap_content">
<TextView
- android:id="@+id/title"
+ android:id="@+id/taskbar_edu_title"
style="@style/TextAppearance.TaskbarEduTooltip.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -36,7 +36,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/taskbar_edu_stashing" />
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index e937410..7986a55 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -118,7 +118,7 @@
<string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Taskleiste immer anzeigen"</string>
<string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Damit die Taskleiste immer unten angezeigt wird, halte den Teiler gedrückt"</string>
<string name="taskbar_search_edu_title" msgid="5569194922234364530">"Aktionstaste gedrückt halten, um auf dem Bildschirm zu suchen"</string>
- <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Dieses Produkt verwendet den ausgewählten Teil deines Bildschirms für die Suche. Es gelten die <xliff:g id="BEGIN_PRIVACY_LINK"><a href="%1$s"></xliff:g>Datenschutzerklärung<xliff:g id="END_PRIVACY_LINK"></a></xliff:g> und die <xliff:g id="BEGIN_TOS_LINK"><a href="%2$s"></xliff:g>Nutzungsbedingungen<xliff:g id="END_TOS_LINK"></a></xliff:g> von Google."</string>
+ <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Dieses Produkt sucht dann anhand des ausgewählten Teils deines Displays nach weiteren Informationen. Es gelten die <xliff:g id="BEGIN_PRIVACY_LINK"><a href="%1$s"></xliff:g>Datenschutzerklärung<xliff:g id="END_PRIVACY_LINK"></a></xliff:g> und die <xliff:g id="BEGIN_TOS_LINK"><a href="%2$s"></xliff:g>Nutzungsbedingungen<xliff:g id="END_TOS_LINK"></a></xliff:g> von Google."</string>
<string name="taskbar_edu_close" msgid="887022990168191073">"Schließen"</string>
<string name="taskbar_edu_done" msgid="6880178093977704569">"Fertig"</string>
<string name="taskbar_button_home" msgid="2151398979630664652">"Startbildschirm"</string>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index 5ba3212..6cd544c 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -118,7 +118,7 @@
<string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Mostrar siempre la Barra de tareas"</string>
<string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Mantén presionado el divisor para mostrar siempre la Barra de tareas en la parte inferior de la pantalla"</string>
<string name="taskbar_search_edu_title" msgid="5569194922234364530">"Mantén presionada la tecla de acción para buscar qué hay en la pantalla"</string>
- <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Este producto usa la parte seleccionada de la pantalla para buscar. Se aplican la <xliff:g id="BEGIN_PRIVACY_LINK"><a href="%1$s"></xliff:g>Política de Privacidad<xliff:g id="END_PRIVACY_LINK"></a></xliff:g> y las <xliff:g id="BEGIN_TOS_LINK"><a href="%2$s"></xliff:g>Condiciones del Servicio<xliff:g id="END_TOS_LINK"></a></xliff:g> de Google."</string>
+ <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Este producto usa la parte seleccionada de la pantalla para hacer búsquedas. Se aplican la <xliff:g id="BEGIN_PRIVACY_LINK"><a href="%1$s"></xliff:g>Política de Privacidad<xliff:g id="END_PRIVACY_LINK"></a></xliff:g> y las <xliff:g id="BEGIN_TOS_LINK"><a href="%2$s"></xliff:g>Condiciones del Servicio<xliff:g id="END_TOS_LINK"></a></xliff:g> de Google."</string>
<string name="taskbar_edu_close" msgid="887022990168191073">"Cerrar"</string>
<string name="taskbar_edu_done" msgid="6880178093977704569">"Listo"</string>
<string name="taskbar_button_home" msgid="2151398979630664652">"Botón de inicio"</string>
diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml
index edd55d5..df665ed 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -96,7 +96,7 @@
<string name="action_share" msgid="2648470652637092375">"Compartir"</string>
<string name="action_screenshot" msgid="8171125848358142917">"Facer captura"</string>
<string name="action_split" msgid="2098009717623550676">"Dividir"</string>
- <string name="action_save_app_pair" msgid="5974823919237645229">"Gardar parella apps"</string>
+ <string name="action_save_app_pair" msgid="5974823919237645229">"Gardar parella de apps"</string>
<string name="toast_split_select_app" msgid="8464310533320556058">"Para usar a pantalla dividida, toca outra app"</string>
<string name="toast_contextual_split_select_app" msgid="433510957123687090">"Escolle outra aplicación para usar a pantalla dividida."</string>
<string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancelar"</string>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index ede50cc..fad3164 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -118,7 +118,7 @@
<string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Sempre mostrar a Barra de tarefas"</string>
<string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Toque e pressione o divisor para sempre mostrar a Barra de tarefas na parte de baixo da tela"</string>
<string name="taskbar_search_edu_title" msgid="5569194922234364530">"Toque na tecla de ação e pressione para pesquisar o que está na tela"</string>
- <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"O produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <xliff:g id="BEGIN_PRIVACY_LINK"><a href="%1$s"></xliff:g>Política de Privacidade<xliff:g id="END_PRIVACY_LINK"></a></xliff:g> e aos <xliff:g id="BEGIN_TOS_LINK"><a href="%2$s"></xliff:g>Termos de Serviço<xliff:g id="END_TOS_LINK"></a></xliff:g> do Google."</string>
+ <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Este produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <xliff:g id="BEGIN_PRIVACY_LINK"><a href="%1$s"></xliff:g>Política de Privacidade<xliff:g id="END_PRIVACY_LINK"></a></xliff:g> e aos <xliff:g id="BEGIN_TOS_LINK"><a href="%2$s"></xliff:g>Termos de Serviço<xliff:g id="END_TOS_LINK"></a></xliff:g> do Google."</string>
<string name="taskbar_edu_close" msgid="887022990168191073">"Fechar"</string>
<string name="taskbar_edu_done" msgid="6880178093977704569">"Concluído"</string>
<string name="taskbar_button_home" msgid="2151398979630664652">"Início"</string>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 5f2a63d..f8ca8d9 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -321,6 +321,7 @@
<item name="android:fontFamily">google-sans-text-medium</item>
<item name="android:textSize">@dimen/task_thumbnail_icon_menu_text_size</item>
<item name="android:textColor">@color/materialColorOnSurface</item>
+ <item name="android:includeFontPadding">false</item>
<item name="android:letterSpacing">0.025</item>
<item name="android:lineHeight">20sp</item>
</style>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index c748385..67feb6a 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -226,7 +226,6 @@
private static final int TASKBAR_TO_HOME_DURATION_FAST = 300;
private static final int TASKBAR_TO_HOME_DURATION_SLOW = 1000;
protected static final int CONTENT_SCALE_DURATION = 350;
- protected static final int CONTENT_SCRIM_DURATION = 350;
private static final int MAX_NUM_TASKS = 5;
@@ -244,7 +243,13 @@
private final StartingWindowListener mStartingWindowListener =
new StartingWindowListener(this);
- private ContentObserver mAnimationRemovalObserver = new ContentObserver(
+
+ // TODO(b/397690719): Investigate the memory leak from TaskStackChangeListeners#mImpl
+ // This is a temporary fix of memory leak b/397690719. We track registered
+ // {@link TaskRestartedDuringLaunchListener}, and remove them on activity destroy.
+ private final List<TaskRestartedDuringLaunchListener> mRegisteredTaskStackChangeListener =
+ new ArrayList<>();
+ private final ContentObserver mAnimationRemovalObserver = new ContentObserver(
ORDERED_BG_EXECUTOR.getHandler()) {
@Override
public void onChange(boolean selfChange) {
@@ -338,7 +343,14 @@
TaskRestartedDuringLaunchListener restartedListener =
new TaskRestartedDuringLaunchListener();
restartedListener.register(onEndCallback::executeAllAndDestroy);
- onEndCallback.add(restartedListener::unregister);
+ mRegisteredTaskStackChangeListener.add(restartedListener);
+ onEndCallback.add(new Runnable() {
+ @Override
+ public void run() {
+ restartedListener.unregister();
+ mRegisteredTaskStackChangeListener.remove(restartedListener);
+ }
+ });
RemoteAnimationRunnerCompat runner = createAppLaunchRunner(v, onEndCallback);
@@ -1210,6 +1222,12 @@
SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null);
ORDERED_BG_EXECUTOR.execute(() -> mLauncher.getContentResolver()
.unregisterContentObserver(mAnimationRemovalObserver));
+ if (BuildConfig.IS_STUDIO_BUILD && !mRegisteredTaskStackChangeListener.isEmpty()) {
+ throw new IllegalStateException("Failed to run onEndCallback created from"
+ + " getActivityLaunchOptions()");
+ }
+ mRegisteredTaskStackChangeListener.forEach(TaskRestartedDuringLaunchListener::unregister);
+ mRegisteredTaskStackChangeListener.clear();
}
/**
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
new file mode 100644
index 0000000..533a86a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.util.Log
+import android.view.Choreographer
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.DesktopModeFlags
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import androidx.core.animation.addListener
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.Companion.LAUNCH_CHANGE_MODES
+import com.android.wm.shell.shared.animation.MinimizeAnimator
+import com.android.wm.shell.shared.animation.WindowAnimator
+
+/**
+ * Helper class responsible for creating and managing animators for desktop app launch and related
+ * transitions.
+ *
+ * <p>This class handles the complex logic of creating various animators, including launch,
+ * minimize, and trampoline close animations, based on the provided transition information and
+ * launch type. It also utilizes {@link InteractionJankMonitor} to monitor animation jank.
+ *
+ * @param context The application context.
+ * @param launchType The type of app launch, containing animation parameters.
+ * @param cujType The CUJ (Critical User Journey) type for jank monitoring.
+ */
+class DesktopAppLaunchAnimatorHelper(
+ private val context: Context,
+ private val launchType: AppLaunchType,
+ @Cuj.CujType private val cujType: Int,
+ private val transactionSupplier: Supplier<Transaction>,
+) {
+
+ private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+
+ fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List<Animator> {
+ val launchChange = getLaunchChange(info)
+ requireNotNull(launchChange) {
+ val changesString =
+ info.changes.joinToString(", ") { change ->
+ "Change: mode=${change.mode}, " +
+ "taskId=${change.taskInfo?.id}, " +
+ "isFreeform=${change.taskInfo?.isFreeform}"
+ }
+ Log.e(
+ TAG,
+ "No launch change found: Transition type=${info.type}, changes=$changesString",
+ )
+ "expected an app launch Change"
+ }
+
+ val transaction = transactionSupplier.get()
+
+ val minimizeChange = getMinimizeChange(info)
+ val trampolineCloseChange = getTrampolineCloseChange(info)
+
+ val launchAnimator =
+ createLaunchAnimator(
+ launchChange,
+ transaction,
+ finishCallback,
+ isTrampoline = trampolineCloseChange != null,
+ )
+ val animatorsList = mutableListOf(launchAnimator)
+ if (minimizeChange != null) {
+ val minimizeAnimator =
+ createMinimizeAnimator(minimizeChange, transaction, finishCallback)
+ animatorsList.add(minimizeAnimator)
+ }
+ if (trampolineCloseChange != null) {
+ val trampolineCloseAnimator =
+ createTrampolineCloseAnimator(trampolineCloseChange, transaction)
+ animatorsList.add(trampolineCloseAnimator)
+ }
+ return animatorsList
+ }
+
+ private fun getLaunchChange(info: TransitionInfo): Change? =
+ info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }
+
+ private fun getMinimizeChange(info: TransitionInfo): Change? =
+ info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
+
+ private fun getTrampolineCloseChange(info: TransitionInfo): Change? {
+ if (
+ info.changes.size < 2 ||
+ !DesktopModeFlags.ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX.isTrue
+ ) {
+ return null
+ }
+ val openChange =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_OPEN && change.taskInfo?.isFreeform == true
+ }
+ val closeChange =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_CLOSE && change.taskInfo?.isFreeform == true
+ }
+ val openPackage = openChange?.taskInfo?.baseIntent?.component?.packageName
+ val closePackage = closeChange?.taskInfo?.baseIntent?.component?.packageName
+ return if (openPackage != null && closePackage != null && openPackage == closePackage) {
+ closeChange
+ } else {
+ null
+ }
+ }
+
+ private fun createLaunchAnimator(
+ change: Change,
+ transaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ isTrampoline: Boolean,
+ ): Animator {
+ val boundsAnimator =
+ WindowAnimator.createBoundsAnimator(
+ context.resources.displayMetrics,
+ launchType.boundsAnimationParams,
+ change,
+ transaction,
+ )
+ val alphaAnimator =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = launchType.alphaDurationMs
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transaction
+ .setAlpha(change.leash, animation.animatedValue as Float)
+ .setFrameTimeline(Choreographer.getInstance().vsyncId)
+ .apply()
+ }
+ }
+ val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
+ transaction.setCrop(change.leash, clipRect)
+ transaction.setCornerRadius(
+ change.leash,
+ ScreenDecorationsUtils.getWindowCornerRadius(context),
+ )
+ return AnimatorSet().apply {
+ interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
+ if (isTrampoline) {
+ play(alphaAnimator)
+ } else {
+ playTogether(boundsAnimator, alphaAnimator)
+ }
+ addListener(
+ onEnd = { animation ->
+ onAnimFinish(animation)
+ interactionJankMonitor.end(cujType)
+ }
+ )
+ }
+ }
+
+ private fun createMinimizeAnimator(
+ change: Change,
+ transaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ ): Animator {
+ return MinimizeAnimator.create(
+ context.resources.displayMetrics,
+ change,
+ transaction,
+ onAnimFinish,
+ )
+ }
+
+ private fun createTrampolineCloseAnimator(change: Change, transaction: Transaction): Animator {
+ return ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 100L
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
+ }
+ }
+ }
+
+ private companion object {
+ const val TAG = "DesktopAppLaunchAnimatorHelper"
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 2406fb6..79072a6 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -17,27 +17,19 @@
package com.android.launcher3.desktop
import android.animation.Animator
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
import android.content.Context
-import android.graphics.Rect
import android.os.IBinder
-import android.view.Choreographer
+import android.util.Log
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IRemoteTransitionFinishedCallback
import android.window.RemoteTransitionStub
import android.window.TransitionInfo
-import android.window.TransitionInfo.Change
-import androidx.core.animation.addListener
+import androidx.core.util.Supplier
import com.android.app.animation.Interpolators
import com.android.internal.jank.Cuj
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.policy.ScreenDecorationsUtils
import com.android.quickstep.RemoteRunnable
-import com.android.wm.shell.shared.animation.MinimizeAnimator
import com.android.wm.shell.shared.animation.WindowAnimator
import java.util.concurrent.Executor
@@ -48,14 +40,18 @@
* ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to
* that window.
*/
-class DesktopAppLaunchTransition(
- private val context: Context,
- private val mainExecutor: Executor,
+class DesktopAppLaunchTransition
+@JvmOverloads
+constructor(
+ context: Context,
private val launchType: AppLaunchType,
@Cuj.CujType private val cujType: Int,
+ private val mainExecutor: Executor,
+ transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
) : RemoteTransitionStub() {
- private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+ private val animatorHelper: DesktopAppLaunchAnimatorHelper =
+ DesktopAppLaunchAnimatorHelper(context, launchType, cujType, transactionSupplier)
enum class AppLaunchType(
val boundsAnimationParams: WindowAnimator.BoundsAnimationParams,
@@ -68,15 +64,16 @@
override fun startAnimation(
token: IBinder,
info: TransitionInfo,
- t: Transaction,
+ transaction: Transaction,
transitionFinishedCallback: IRemoteTransitionFinishedCallback,
) {
+ Log.v(TAG, "startAnimation: launchType=$launchType, cujType=$cujType")
val safeTransitionFinishedCallback = RemoteRunnable {
transitionFinishedCallback.onTransitionFinished(/* wct= */ null, /* sct= */ null)
}
mainExecutor.execute {
runAnimators(info, safeTransitionFinishedCallback)
- t.apply()
+ transaction.apply()
}
}
@@ -86,78 +83,12 @@
animators -= animator
if (animators.isEmpty()) finishedCallback.run()
}
- animators += createAnimators(info, animatorFinishedCallback)
+ animators += animatorHelper.createAnimators(info, animatorFinishedCallback)
animators.forEach { it.start() }
}
- private fun createAnimators(
- info: TransitionInfo,
- finishCallback: (Animator) -> Unit,
- ): List<Animator> {
- val transaction = Transaction()
- val launchAnimator =
- createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
- val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
- val minimizeAnimator =
- MinimizeAnimator.create(
- context.resources.displayMetrics,
- minimizeChange,
- transaction,
- finishCallback,
- )
- return listOf(launchAnimator, minimizeAnimator)
- }
-
- private fun getLaunchChange(info: TransitionInfo): Change =
- requireNotNull(info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }) {
- "expected an app launch Change"
- }
-
- private fun getMinimizeChange(info: TransitionInfo): Change? =
- info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
-
- private fun createLaunchAnimator(
- change: Change,
- transaction: Transaction,
- onAnimFinish: (Animator) -> Unit,
- ): Animator {
- val boundsAnimator =
- WindowAnimator.createBoundsAnimator(
- context.resources.displayMetrics,
- launchType.boundsAnimationParams,
- change,
- transaction,
- )
- val alphaAnimator =
- ValueAnimator.ofFloat(0f, 1f).apply {
- duration = launchType.alphaDurationMs
- interpolator = Interpolators.LINEAR
- addUpdateListener { animation ->
- transaction
- .setAlpha(change.leash, animation.animatedValue as Float)
- .setFrameTimeline(Choreographer.getInstance().vsyncId)
- .apply()
- }
- }
- val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
- transaction.setCrop(change.leash, clipRect)
- transaction.setCornerRadius(
- change.leash,
- ScreenDecorationsUtils.getWindowCornerRadius(context),
- )
- return AnimatorSet().apply {
- interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
- playTogether(boundsAnimator, alphaAnimator)
- addListener(
- onEnd = { animation ->
- onAnimFinish(animation)
- interactionJankMonitor.end(cujType)
- }
- )
- }
- }
-
companion object {
+ const val TAG = "DesktopAppLaunchTransition"
/** Change modes that represent a task becoming visible / launching in Desktop mode. */
val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
index 36c5fba..a72b5c4 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -48,9 +48,9 @@
RemoteTransition(
DesktopAppLaunchTransition(
context,
- MAIN_EXECUTOR,
AppLaunchType.UNMINIMIZE,
Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+ MAIN_EXECUTOR,
),
"DesktopWindowLimitUnminimize",
)
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 3773d02..6ee43ff 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.os.Debug
import android.util.Log
+import android.util.Slog
import android.util.SparseArray
import android.view.Display.DEFAULT_DISPLAY
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
@@ -61,8 +62,6 @@
* (Used only when multiple desks are enabled).
*
* @property displayId The ID of the display this object represents.
- * @property canCreateDesks true if it's possible to create new desks on the display represented
- * by this object.
* @property activeDeskId The ID of the active desk on the associated display (if any). It has a
* value of `INACTIVE_DESK_ID` (-1) if there are no active desks. Note that there can only be
* at most one active desk on each display.
@@ -70,11 +69,18 @@
*/
private data class DisplayDeskConfig(
val displayId: Int,
- var canCreateDesks: Boolean,
var activeDeskId: Int = INACTIVE_DESK_ID,
val deskIds: MutableSet<Int>,
)
+ /** True if it is possible to create new desks on current setup. */
+ var canCreateDesks: Boolean = false
+ private set(value) {
+ if (field == value) return
+ field = value
+ desktopVisibilityListeners.forEach { it.onCanCreateDesksChanged(field) }
+ }
+
/** Maps each display by its ID to its desks configuration. */
private val displaysDesksConfigsMap = SparseArray<DisplayDeskConfig>()
@@ -152,7 +158,8 @@
return areDesktopTasksVisible()
}
- val isInDesktopMode = displaysDesksConfigsMap[displayId].activeDeskId != INACTIVE_DESK_ID
+ val activeDeskId = getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID
+ val isInDesktopMode = activeDeskId != INACTIVE_DESK_ID
if (DEBUG) {
Log.d(TAG, "isInDesktopMode: $isInDesktopMode")
}
@@ -391,7 +398,10 @@
}
}
- private fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
+ private fun onListenerConnected(
+ displayDeskStates: Array<DisplayDeskState>,
+ canCreateDesks: Boolean,
+ ) {
if (!DesktopModeStatus.enableMultipleDesktops(context)) {
return
}
@@ -402,25 +412,24 @@
displaysDesksConfigsMap[displayDeskState.displayId] =
DisplayDeskConfig(
displayId = displayDeskState.displayId,
- canCreateDesks = displayDeskState.canCreateDesk,
activeDeskId = displayDeskState.activeDeskId,
deskIds = displayDeskState.deskIds.toMutableSet(),
)
}
+
+ this.canCreateDesks = canCreateDesks
}
- private fun getDisplayDeskConfig(displayId: Int): DisplayDeskConfig {
- return checkNotNull(displaysDesksConfigsMap[displayId]) {
- "Expected non-null desk config for display: $displayId"
- }
- }
+ private fun getDisplayDeskConfig(displayId: Int) =
+ displaysDesksConfigsMap[displayId]
+ ?: null.also { Slog.e(TAG, "Expected non-null desk config for display: $displayId") }
- private fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
+ private fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
if (!DesktopModeStatus.enableMultipleDesktops(context)) {
return
}
- getDisplayDeskConfig(displayId).canCreateDesks = canCreateDesks
+ this.canCreateDesks = canCreateDesks
}
private fun onDeskAdded(displayId: Int, deskId: Int) {
@@ -428,7 +437,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.add(deskId)) {
"Found a duplicate desk Id: $deskId on display: $displayId"
}
@@ -440,7 +449,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.remove(deskId)) {
"Removing non-existing desk Id: $deskId on display: $displayId"
}
@@ -457,7 +466,7 @@
val wasInDesktopMode = isInDesktopModeAndNotInOverview(displayId)
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(oldActiveDesk == it.activeDeskId) {
"Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
}
@@ -525,9 +534,12 @@
) : Stub() {
private val controller = WeakReference(controller)
- override fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
+ override fun onListenerConnected(
+ displayDeskStates: Array<DisplayDeskState>,
+ canCreateDesks: Boolean,
+ ) {
Executors.MAIN_EXECUTOR.execute {
- controller.get()?.onListenerConnected(displayDeskStates)
+ controller.get()?.onListenerConnected(displayDeskStates, canCreateDesks)
}
}
@@ -565,9 +577,9 @@
override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) {}
- override fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
+ override fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
Executors.MAIN_EXECUTOR.execute {
- controller.get()?.onCanCreateDesksChanged(displayId, canCreateDesks)
+ controller.get()?.onCanCreateDesksChanged(canCreateDesks)
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index ce96556..bf5c0c8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -104,9 +104,18 @@
mIcon2 = findViewById(R.id.icon_2);
mContent = findViewById(R.id.content);
- Resources resources = mContext.getResources();
-
Preconditions.assertNotNull(mContent);
+
+ TypefaceUtils.setTypeface(
+ mContent.findViewById(R.id.large_text),
+ TypefaceUtils.FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED
+ );
+ TypefaceUtils.setTypeface(
+ mContent.findViewById(R.id.small_text),
+ TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE
+ );
+
+ Resources resources = mContext.getResources();
mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
/* borderRadiusPx= */ mBorderRadius != INVALID_BORDER_RADIUS
? mBorderRadius
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 4581119..4b4d68d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -163,6 +163,10 @@
R.dimen.keyboard_quick_switch_view_small_spacing);
mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
mIsRtl = Utilities.isRtl(resources);
+
+ TypefaceUtils.setTypeface(
+ mNoRecentItemsPane.findViewById(R.id.no_recent_items_text),
+ TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE);
}
private void registerOnBackInvokedCallback() {
@@ -310,7 +314,7 @@
layoutInflater,
previousTaskView);
- desktopButton.<TextView>findViewById(R.id.text).setText(
+ desktopButton.<TextView>findViewById(R.id.small_text).setText(
resources.getString(R.string.quick_switch_desktop));
}
mDisplayingRecentTasks = !groupTasks.isEmpty() || useDesktopTaskView;
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 5af7ff8..5f7a026 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -290,9 +290,9 @@
remoteTransition = new RemoteTransition(
new DesktopAppLaunchTransition(
context,
- MAIN_EXECUTOR,
UNMINIMIZE,
- Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH
+ Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
+ MAIN_EXECUTOR
),
"DesktopKeyboardQuickSwitchUnminimize");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index b6b090c..2e5bebc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -119,7 +119,8 @@
mControllers = controllers;
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Resources resources = mActivity.getResources();
- if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()) {
+ if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()
+ || mActivity.isBubbleBarOnPhone()) {
mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size);
mStashedHandleWidth =
resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b9d1275..2eb4412 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -18,6 +18,7 @@
import static android.os.Trace.TRACE_TAG_APP;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
@@ -47,7 +48,8 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
import static com.android.window.flags.Flags.enableStartLaunchTransitionFromTaskbarBugfix;
-import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
+import static com.android.wm.shell.Flags.enableBubbleBar;
+import static com.android.wm.shell.Flags.enableBubbleBarOnPhones;
import static com.android.wm.shell.Flags.enableTinyTaskbar;
import static java.lang.invoke.MethodHandles.Lookup.PROTECTED;
@@ -77,6 +79,7 @@
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toast;
+import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
import android.window.RemoteTransition;
@@ -298,9 +301,10 @@
// If Bubble bar is present, TaskbarControllers depends on it so build it first.
Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
BubbleBarController.onTaskbarRecreated();
+ final boolean deviceBubbleBarEnabled = enableBubbleBarOnPhones()
+ || (!mDeviceProfile.isPhone && !mDeviceProfile.isVerticalBarLayout());
if (BubbleBarController.isBubbleBarEnabled()
- && !mDeviceProfile.isPhone
- && !mDeviceProfile.isVerticalBarLayout()
+ && deviceBubbleBarEnabled
&& bubbleBarView != null
) {
Optional<BubbleStashedHandleViewController> bubbleHandleController = Optional.empty();
@@ -434,8 +438,9 @@
.setIsTransientTaskbar(true)
.build();
}
- mNavMode = (enableTaskbarConnectedDisplays() && !mIsPrimaryDisplay)
- ? NavigationMode.THREE_BUTTONS : DisplayController.getNavigationMode(this);
+ mNavMode = (DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+ && !mIsPrimaryDisplay) ? NavigationMode.THREE_BUTTONS
+ : DisplayController.getNavigationMode(this);
}
@@ -511,6 +516,10 @@
return enableTinyTaskbar() && mDeviceProfile.isPhone && mDeviceProfile.isTaskbarPresent;
}
+ public boolean isBubbleBarOnPhone() {
+ return enableBubbleBarOnPhones() && enableBubbleBar() && mDeviceProfile.isPhone;
+ }
+
/**
* Returns {@code true} iff bubble bar is enabled (but not necessarily visible /
* containing bubbles).
@@ -868,7 +877,7 @@
@Override
public void onPopupVisibilityChanged(boolean isVisible) {
- setTaskbarWindowFocusable(isVisible);
+ setTaskbarWindowFocusable(isVisible /* focusable */, false /* imeFocusable */);
}
@Override
@@ -1227,17 +1236,29 @@
}
/**
- * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar
- * window.
+ * Sets whether the taskbar window should be focusable and IME focusable. This won't be IME
+ * focusable unless it is also focusable.
+ *
+ * @param focusable whether it should be focusable.
+ * @param imeFocusable whether it should be IME focusable.
+ *
+ * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE
+ * @see WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM
*/
- public void setTaskbarWindowFocusable(boolean focusable) {
+ public void setTaskbarWindowFocusable(boolean focusable, boolean imeFocusable) {
if (isPhoneMode()) {
return;
}
if (focusable) {
mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE;
+ if (imeFocusable) {
+ mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM;
+ } else {
+ mWindowLayoutParams.flags |= FLAG_ALT_FOCUSABLE_IM;
+ }
} else {
mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE;
+ mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM;
}
notifyUpdateLayoutParams();
}
@@ -1258,8 +1279,12 @@
}
/**
- * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar
- * window. If we're now focusable, also move nav buttons to a separate window above IME.
+ * Sets whether the taskbar window should be focusable, as well as IME focusable. If we're now
+ * focusable, also move nav buttons to a separate window above IME.
+ *
+ * @param focusable whether it should be focusable.
+ *
+ * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE
*/
public void setTaskbarWindowFocusableForIme(boolean focusable) {
if (focusable) {
@@ -1267,7 +1292,7 @@
} else {
mControllers.navbarButtonsViewController.moveNavButtonsBackToTaskbarWindow();
}
- setTaskbarWindowFocusable(focusable);
+ setTaskbarWindowFocusable(focusable, true /* imeFocusable */);
}
/** Adds the given view to WindowManager with the provided LayoutParams (creates new window). */
@@ -1533,9 +1558,9 @@
return new RemoteTransition(
new DesktopAppLaunchTransition(
this,
- getMainExecutor(),
appLaunchType,
- cujType
+ cujType,
+ getMainExecutor()
),
"TaskbarDesktopAppLaunch");
}
@@ -1558,7 +1583,10 @@
*/
private void launchFromInAppTaskbar(@Nullable RecentsView recents,
@Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
- if (recents == null) {
+ boolean launchedFromExternalDisplay =
+ DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+ && !mIsPrimaryDisplay;
+ if (recents == null && !launchedFromExternalDisplay) {
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index e44bce1..6d23853 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -108,7 +108,7 @@
fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) {
stashedHandleWidth =
res.getDimensionPixelSize(
- if (context.isPhoneMode || context.isTinyTaskbar) {
+ if (context.isPhoneMode || context.isTinyTaskbar || context.isBubbleBarOnPhone) {
R.dimen.taskbar_stashed_small_screen
} else {
R.dimen.taskbar_stashed_handle_width
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index b91f512..d531e2c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -52,6 +52,7 @@
import android.window.SurfaceSyncGroup;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.app.animation.Interpolators;
import com.android.internal.logging.InstanceId;
@@ -75,6 +76,7 @@
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.DisplayController;
@@ -517,6 +519,7 @@
return mIsSystemDragInProgress;
}
+ @VisibleForTesting
private void maybeOnDragEnd() {
if (!isDragging()) {
((BubbleTextView) mDragObject.originalView).setIconDisabled(false);
@@ -524,17 +527,38 @@
TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false);
mActivity.onDragEnd();
if (mReturnAnimator == null) {
+ // If an item is dropped on the bubble bar, the bubble bar handles the drop,
+ // so it should not collapse along with the taskbar.
+ boolean droppedOnBubbleBar = notifyBubbleBarItemDropped();
// Upon successful drag, immediately stash taskbar.
// Note, this must be done last to ensure no AutohideSuspendFlags are active, as
// that will prevent us from stashing until the timeout.
- mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
-
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
+ /* stash = */ true,
+ /* shouldBubblesFollow = */ !droppedOnBubbleBar
+ );
mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo)
.log(LAUNCHER_APP_LAUNCH_DRAGDROP);
}
}
}
+ /**
+ * Exits the Bubble Bar drop target mode if applicable.
+ *
+ * @return {@code true} if drop target mode was active.
+ */
+ private boolean notifyBubbleBarItemDropped() {
+ return mControllers.bubbleControllers.map(bc -> {
+ BubbleBarViewController bubbleBarViewController = bc.bubbleBarViewController;
+ boolean showingDropTarget = bubbleBarViewController.isShowingDropTarget();
+ if (showingDropTarget) {
+ bubbleBarViewController.onItemDroppedInBubbleBarDragZone();
+ }
+ return showingDropTarget;
+ }).orElse(false);
+ }
+
@Override
protected void endDrag() {
if (mDisallowGlobalDrag) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 26a552e..b4ffb74 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -141,6 +141,10 @@
tooltipStep = TOOLTIP_STEP_FEATURES
inflateTooltip(R.layout.taskbar_edu_swipe)
tooltip?.run {
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.taskbar_edu_title),
+ TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ )
requireViewById<LottieAnimationView>(R.id.swipe_animation).supportLightTheme()
show()
}
@@ -180,6 +184,23 @@
pinningEdu.visibility = GONE
}
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.taskbar_edu_title),
+ TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ )
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.splitscreen_text),
+ TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ )
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.suggestions_text),
+ TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ )
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.pinning_text),
+ TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ )
+
// Set up layout parameters.
content.updateLayoutParams { width = MATCH_PARENT }
updateLayoutParams<MarginLayoutParams> {
@@ -231,6 +252,15 @@
requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
.supportLightTheme()
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.taskbar_edu_title),
+ TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ )
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.pinning_text),
+ TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+ )
+
updateLayoutParams<BaseDragLayer.LayoutParams> {
if (DisplayController.isTransientTaskbar(activityContext)) {
bottomMargin += activityContext.deviceProfile.taskbarHeight
@@ -276,6 +306,13 @@
allowTouchDismissal = true
requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)
+
+ TypefaceUtils.setTypeface(
+ requireViewById(R.id.taskbar_edu_title),
+ TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+ )
+ TypefaceUtils.setTypeface(eduSubtitle, TypefaceUtils.FONT_FAMILY_BODY_SMALL_BASELINE)
+
showDisclosureText(eduSubtitle)
updateLayoutParams<BaseDragLayer.LayoutParams> {
if (DisplayController.isTransientTaskbar(activityContext)) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 96bcffd..34bb6e0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -32,7 +32,6 @@
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
-import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
@@ -46,6 +45,7 @@
import android.os.Handler;
import android.os.Trace;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -53,6 +53,7 @@
import android.view.MotionEvent;
import android.view.WindowManager;
import android.widget.FrameLayout;
+import android.window.DesktopExperienceFlags;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -75,14 +76,20 @@
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.statusbar.phone.BarTransitions;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
import java.io.PrintWriter;
+import java.util.Set;
import java.util.StringJoiner;
/**
@@ -118,11 +125,12 @@
Settings.Secure.NAV_BAR_KIDS_MODE);
private final Context mBaseContext;
+ private TaskbarNavButtonCallbacks mNavCallbacks;
// TODO: Remove this during the connected displays lifecycle refactor.
private final Context mPrimaryWindowContext;
private WindowManager mPrimaryWindowManager;
- private final TaskbarNavButtonController mDefaultNavButtonController;
- private final ComponentCallbacks mDefaultComponentCallbacks;
+ private TaskbarNavButtonController mPrimaryNavButtonController;
+ private ComponentCallbacks mPrimaryComponentCallbacks;
private final SimpleBroadcastReceiver mShutdownReceiver;
@@ -140,6 +148,11 @@
private final SparseArray<FrameLayout> mRootLayouts = new SparseArray<>();
/** DisplayId - {@link Boolean} map indicating if RootLayout was added to window. */
private final SparseBooleanArray mAddedRootLayouts = new SparseBooleanArray();
+ /** DisplayId - {@link TaskbarNavButtonController} map for Connected Display. */
+ private final SparseArray<TaskbarNavButtonController> mNavButtonControllers =
+ new SparseArray<>();
+ /** DisplayId - {@link ComponentCallbacks} map for Connected Display. */
+ private final SparseArray<ComponentCallbacks> mComponentCallbacks = new SparseArray<>();
private StatefulActivity mActivity;
private RecentsViewContainer mRecentsViewContainer;
@@ -158,27 +171,94 @@
private class RecreationListener implements DisplayController.DisplayInfoChangeListener {
@Override
public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
-
if ((flags & CHANGE_DENSITY) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Display density changed");
+ debugTaskbarManager("onDisplayInfoChanged - Display density changed",
+ context.getDisplayId());
}
if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Navigation mode changed");
+ debugTaskbarManager("onDisplayInfoChanged - Navigation mode changed",
+ context.getDisplayId());
}
if ((flags & CHANGE_DESKTOP_MODE) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Desktop mode changed");
+ debugTaskbarManager("onDisplayInfoChanged - Desktop mode changed",
+ context.getDisplayId());
}
if ((flags & CHANGE_TASKBAR_PINNING) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Taskbar pinning changed");
+ debugTaskbarManager("onDisplayInfoChanged - Taskbar pinning changed",
+ context.getDisplayId());
}
if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE | CHANGE_DESKTOP_MODE
| CHANGE_TASKBAR_PINNING)) != 0) {
+ debugTaskbarManager("onDisplayInfoChanged - Recreating Taskbar!",
+ context.getDisplayId());
recreateTaskbar();
}
}
}
- private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> recreateTaskbar();
+ private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> {
+ debugTaskbarManager("Settings changed! Recreating Taskbar!");
+ recreateTaskbar();
+ };
+
+ private final PerceptibleTaskListener mTaskStackListener;
+
+ private class PerceptibleTaskListener implements TaskStackChangeListener {
+ private ArraySet<Integer> mPerceptibleTasks = new ArraySet<Integer>();
+
+ @Override
+ public void onTaskMovedToFront(int taskId) {
+ if (mPerceptibleTasks.contains(taskId)) {
+ return;
+ }
+
+ // This listens to any Task, so we filter them by the ones shown in the launcher.
+ // For Tasks restored after startup, they will by default not be Perceptible, and no
+ // need to until user interacts with it by bringing it to the foreground.
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ // get pinned tasks
+ Set<Integer> taskbarPinnedTasks =
+ mTaskbars.valueAt(i).getControllers().taskbarViewController
+ .getTaskIdsForPinnedApps();
+
+ // mark as perceptible if the foregrounded task is in the list of apps shown in
+ // the launcher.
+ if (taskbarPinnedTasks.contains(taskId)
+ && ActivityManagerWrapper.getInstance()
+ .setTaskIsPerceptible(taskId, true)
+ ) {
+ mPerceptibleTasks.add(taskId);
+ }
+ }
+ }
+
+ /**
+ * Launcher also can display recently launched tasks that are not pinned. Also add
+ * these as perceptible
+ */
+ @Override
+ public void onRecentTaskListUpdated() {
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ for (GroupTask gTask : mTaskbars.valueAt(i).getControllers()
+ .taskbarRecentAppsController.getShownTasks()) {
+ for (Task task : gTask.getTasks()) {
+ int taskId = task.key.id;
+
+ if (!mPerceptibleTasks.contains(taskId)) {
+ ActivityManagerWrapper.getInstance()
+ .setTaskIsPerceptible(taskId, true);
+ mPerceptibleTasks.add(taskId);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ mPerceptibleTasks.remove(taskId);
+ }
+ };
private boolean mUserUnlocked = false;
@@ -191,6 +271,7 @@
@Override
public void run() {
int displayId = getDefaultDisplayId();
+ debugTaskbarManager("mActivityOnDestroyCallback running!", displayId);
if (mActivity != null) {
displayId = mActivity.getDisplayId();
mActivity.removeOnDeviceProfileChangeListener(
@@ -204,7 +285,6 @@
mRecentsViewContainer = null;
}
mActivity = null;
- debugWhyTaskbarNotDestroyed("clearActivity");
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (taskbar != null) {
taskbar.setUIController(TaskbarUIController.DEFAULT);
@@ -217,28 +297,27 @@
new UnfoldTransitionProgressProvider.TransitionProgressListener() {
@Override
public void onTransitionStarted() {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
- "fold/unfold transition started getting called.");
+ debugTaskbarManager("fold/unfold transition started getting called.");
}
@Override
public void onTransitionProgress(float progress) {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
- "fold/unfold transition progress : " + progress);
+ debugTaskbarManager(
+ "fold/unfold transition progress getting called. | progress="
+ + progress);
}
@Override
public void onTransitionFinishing() {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
+ debugTaskbarManager(
"fold/unfold transition finishing getting called.");
}
@Override
public void onTransitionFinished() {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
+ debugTaskbarManager(
"fold/unfold transition finished getting called.");
-
}
};
@@ -250,20 +329,22 @@
RecentsDisplayModel recentsDisplayModel) {
mBaseContext = context;
mAllAppsActionManager = allAppsActionManager;
+ mNavCallbacks = navCallbacks;
mRecentsDisplayModel = recentsDisplayModel;
- mPrimaryWindowContext = createWindowContext(getDefaultDisplayId());
- if (enableTaskbarNoRecreate()) {
- mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
- createTaskbarRootLayout(getDefaultDisplayId());
- }
- mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
- mDefaultComponentCallbacks = createDefaultComponentCallbacks();
+
+ // Set up primary display.
+ int primaryDisplayId = getDefaultDisplayId();
+ debugTaskbarManager("TaskbarManager constructor", primaryDisplayId);
+ mPrimaryWindowContext = createWindowContext(primaryDisplayId);
+ mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
+ createTaskbarRootLayout(primaryDisplayId);
+ createNavButtonController(primaryDisplayId);
+ createAndRegisterComponentCallbacks(primaryDisplayId);
+
SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
- Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
- mPrimaryWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
mShutdownReceiver =
new SimpleBroadcastReceiver(
mPrimaryWindowContext, UI_HELPER_EXECUTOR, i -> destroyAllTaskbars());
@@ -281,82 +362,16 @@
mTaskbarBroadcastReceiver.register(RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
});
- debugWhyTaskbarNotDestroyed("TaskbarManager created");
+ if (ActivityManagerWrapper.usePerceptibleTasks(getPrimaryWindowContext())) {
+ mTaskStackListener = new PerceptibleTaskListener();
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
+ } else {
+ mTaskStackListener = null;
+ }
+ debugTaskbarManager("TaskbarManager created");
recreateTaskbar();
}
- @NonNull
- private TaskbarNavButtonController createDefaultNavButtonController(Context context,
- TaskbarNavButtonCallbacks navCallbacks) {
- return new TaskbarNavButtonController(
- context,
- navCallbacks,
- SystemUiProxy.INSTANCE.get(mPrimaryWindowContext),
- new Handler(),
- new ContextualSearchInvoker(mPrimaryWindowContext));
- }
-
- private ComponentCallbacks createDefaultComponentCallbacks() {
- return new ComponentCallbacks() {
- private Configuration mOldConfig =
- mPrimaryWindowContext.getResources().getConfiguration();
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
- "onConfigurationChanged: " + newConfig);
- debugWhyTaskbarNotDestroyed(
- "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
- // TODO: adapt this logic to be specific to different displays.
- DeviceProfile dp = mUserUnlocked
- ? LauncherAppState.getIDP(mPrimaryWindowContext).getDeviceProfile(
- mPrimaryWindowContext)
- : null;
- int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
-
- if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
- Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "onConfigurationChanged: theme changed");
- // Only recreate for theme changes, not other UI mode changes such as docking.
- int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
- int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
- if (oldUiNightMode == newUiNightMode) {
- configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
- }
- }
-
- debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() "
- + "configDiff=" + Configuration.configurationDiffToString(configDiff));
- if (configDiff != 0 || getCurrentActivityContext() == null) {
- recreateTaskbar();
- } else {
- // Config change might be handled without re-creating the taskbar
- if (dp != null && !isTaskbarEnabled(dp)) {
- destroyDefaultTaskbar();
- } else {
- if (dp != null && isTaskbarEnabled(dp)) {
- if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
- // Re-initialize for screen size change? Should this be done
- // by looking at screen-size change flag in configDiff in the
- // block above?
- recreateTaskbar();
- } else {
- getCurrentActivityContext().updateDeviceProfile(dp);
- }
- }
- getCurrentActivityContext().onConfigurationChanged(configDiff);
- }
- }
- mOldConfig = new Configuration(newConfig);
- // reset taskbar was pinned value, so we don't automatically unstash taskbar upon
- // user unfolding the device.
- mSharedState.setTaskbarWasPinned(false);
- }
-
- @Override
- public void onLowMemory() { }
- };
- }
-
private void destroyAllTaskbars() {
for (int i = 0; i < mTaskbars.size(); i++) {
int displayId = mTaskbars.keyAt(i);
@@ -365,23 +380,18 @@
}
}
- private void destroyDefaultTaskbar() {
- destroyTaskbarForDisplay(getDefaultDisplayId());
- }
-
private void destroyTaskbarForDisplay(int displayId) {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "destroyTaskbarForDisplay: " + displayId);
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
- debugWhyTaskbarNotDestroyed("destroyTaskbarForDisplay: " + taskbar, displayId);
+ debugTaskbarManager("destroyTaskbarForDisplay: " + taskbar, displayId);
if (taskbar != null) {
taskbar.onDestroy();
// remove all defaults that we store
removeTaskbarFromMap(displayId);
}
- // make this display-specific
- DeviceProfile dp = mUserUnlocked ?
- LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
- getWindowContext(displayId)) : null;
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+ mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
if (dp == null || !isTaskbarEnabled(dp)) {
removeTaskbarRootViewFromWindow(displayId);
}
@@ -431,7 +441,10 @@
DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListener(
mRecreationListener);
recreateTaskbar();
- addTaskbarRootViewToWindow(getDefaultDisplayId());
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ int displayId = mTaskbars.keyAt(i);
+ addTaskbarRootViewToWindow(displayId);
+ }
}
/**
@@ -443,7 +456,7 @@
}
removeActivityCallbacksAndListeners();
mActivity = activity;
- debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity);
+ debugTaskbarManager("Set mActivity=" + mActivity);
mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"registering activity lifecycle callbacks from setActivity().");
@@ -492,7 +505,7 @@
return ql.getUnfoldTransitionProgressProvider();
}
} else {
- return SystemUiProxy.INSTANCE.get(mPrimaryWindowContext).getUnfoldTransitionProvider();
+ return SystemUiProxy.INSTANCE.get(mBaseContext).getUnfoldTransitionProvider();
}
return null;
}
@@ -530,13 +543,21 @@
/**
* This method is called multiple times (ex. initial init, then when user unlocks) in which case
- * we fully want to destroy the existing default display's taskbar and create a new one.
+ * we fully want to destroy existing taskbars and create all desired new ones.
* In other case (folding/unfolding) we don't need to remove and add window.
*/
@VisibleForTesting
public synchronized void recreateTaskbar() {
- // TODO: make this recreate all taskbars in map.
- recreateTaskbarForDisplay(getDefaultDisplayId());
+ // Handles initial creation case.
+ if (mTaskbars.size() == 0) {
+ recreateTaskbarForDisplay(getDefaultDisplayId());
+ return;
+ }
+
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ int displayId = mTaskbars.keyAt(i);
+ recreateTaskbarForDisplay(displayId);
+ }
}
/**
@@ -545,13 +566,12 @@
* In other case (folding/unfolding) we don't need to remove and add window.
*/
private void recreateTaskbarForDisplay(int displayId) {
- Trace.beginSection("recreateTaskbar");
+ Trace.beginSection("recreateTaskbarForDisplay");
try {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "recreateTaskbarForDisplay: " + displayId);
- // TODO: make this code display specific
- DeviceProfile dp = mUserUnlocked ?
- LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
- getWindowContext(displayId)) : null;
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+ mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
// All Apps action is unrelated to navbar unification, so we only need to check DP.
final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
@@ -559,15 +579,17 @@
destroyTaskbarForDisplay(displayId);
+ boolean displayExists = getDisplay(displayId) != null;
boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
- debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ debugTaskbarManager("recreateTaskbarForDisplay: isTaskbarEnabled=" + isTaskbarEnabled
+ " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
+ " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
- + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
- if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
- SystemUiProxy.INSTANCE.get(mPrimaryWindowContext)
+ + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent)
+ + " displayExists=" + displayExists);
+ if (!isTaskbarEnabled || !isLargeScreenTaskbar || !displayExists) {
+ SystemUiProxy.INSTANCE.get(mBaseContext)
.notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
- if (!isTaskbarEnabled) {
+ if (!isTaskbarEnabled || !displayExists) {
return;
}
}
@@ -575,6 +597,11 @@
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (enableTaskbarNoRecreate() || taskbar == null) {
taskbar = createTaskbarActivityContext(dp, displayId);
+ if (taskbar == null) {
+ debugTaskbarManager(
+ "recreateTaskbarForDisplay: new taskbar instance is null!", displayId);
+ return;
+ }
} else {
taskbar.updateDeviceProfile(dp);
}
@@ -585,7 +612,8 @@
// Non default displays should not use LauncherTaskbarUIController as they shouldn't
// have access to the Launcher activity.
- if (enableTaskbarConnectedDisplays() && !isDefaultDisplay(displayId)) {
+ if (!isDefaultDisplay(displayId)
+ && DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
taskbar.setUIController(createTaskbarUIControllerForNonDefaultDisplay(displayId));
} else if (mRecentsViewContainer != null) {
taskbar.setUIController(
@@ -622,8 +650,8 @@
}
public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
- if (mDefaultNavButtonController != null) {
- mDefaultNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
+ if (mPrimaryNavButtonController != null) {
+ mPrimaryNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
}
}
@@ -746,13 +774,25 @@
* primary device or a previously mirroring display is switched to extended mode.
*/
public void onDisplayAddSystemDecorations(int displayId) {
- if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ Display display = getDisplay(displayId);
+ if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+ displayId) || display == null) {
+ debugTaskbarManager("onDisplayAddSystemDecorations: not adding display");
return;
}
Context newWindowContext = createWindowContext(displayId);
if (newWindowContext != null) {
addWindowContextToMap(displayId, newWindowContext);
+ // TODO (b/391965805): remove once onDisplayAddSystemDecorations is working.
+ WindowManager wm = getWindowManager(displayId);
+ if (wm == null || !wm.shouldShowSystemDecors(displayId)) {
+ return;
+ }
+ createTaskbarRootLayout(displayId);
+ createNavButtonController(displayId);
+ createAndRegisterComponentCallbacks(displayId);
+ recreateTaskbarForDisplay(displayId);
}
}
@@ -761,12 +801,16 @@
* removed from the primary device.
*/
public void onDisplayRemoved(int displayId) {
- if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+ displayId)) {
return;
}
Context windowContext = getWindowContext(displayId);
if (windowContext != null) {
+ removeNavButtonController(displayId);
+ removeAndUnregisterComponentCallbacks(displayId);
+ destroyTaskbarForDisplay(displayId);
removeWindowContextFromMap(displayId);
}
}
@@ -774,7 +818,11 @@
/**
* Signal from SysUI indicating that system decorations should be removed from the display.
*/
- public void onDisplayRemoveSystemDecorations(int displayId) {}
+ public void onDisplayRemoveSystemDecorations(int displayId) {
+ // The display mirroring starts. The handling logic is the same as when removing a
+ // display.
+ onDisplayRemoved(displayId);
+ }
private void removeActivityCallbacksAndListeners() {
if (mActivity != null) {
@@ -796,7 +844,7 @@
*/
public void destroy() {
mRecentsViewContainer = null;
- debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
+ debugTaskbarManager("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
mTaskbarBroadcastReceiver.unregisterReceiverSafely();
@@ -809,8 +857,14 @@
SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
- mPrimaryWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
+ removeAndUnregisterComponentCallbacks(getDefaultDisplayId());
mShutdownReceiver.unregisterReceiverSafely();
+ if (ActivityManagerWrapper.usePerceptibleTasks(getPrimaryWindowContext())) {
+ for (Integer taskId: mTaskStackListener.mPerceptibleTasks) {
+ ActivityManagerWrapper.getInstance().setTaskIsPerceptible(taskId, false);
+ }
+ }
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
destroyAllTaskbars();
}
@@ -836,15 +890,20 @@
private void addTaskbarRootViewToWindow(int displayId) {
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (!enableTaskbarNoRecreate() || taskbar == null) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
- "addTaskbarRootViewToWindow - taskbar null | displayId=" + displayId);
+ debugTaskbarManager("addTaskbarRootViewToWindow - taskbar null", displayId);
+ return;
+ }
+
+ if (getDisplay(displayId) == null) {
+ debugTaskbarManager("addTaskbarRootViewToWindow - display null", displayId);
return;
}
if (!isTaskbarRootLayoutAddedForDisplay(displayId)) {
FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
- if (rootLayout != null) {
- getWindowManager(displayId).addView(rootLayout, taskbar.getWindowLayoutParams());
+ WindowManager windowManager = getWindowManager(displayId);
+ if (rootLayout != null && windowManager != null) {
+ windowManager.addView(rootLayout, taskbar.getWindowLayoutParams());
mAddedRootLayouts.put(displayId, true);
} else {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW,
@@ -864,10 +923,14 @@
return;
}
- if (isTaskbarRootLayoutAddedForDisplay(displayId)) {
- getWindowManager(displayId).removeViewImmediate(rootLayout);
+ WindowManager windowManager = getWindowManager(displayId);
+ if (isTaskbarRootLayoutAddedForDisplay(displayId) && windowManager != null) {
+ windowManager.removeViewImmediate(rootLayout);
mAddedRootLayouts.put(displayId, false);
removeTaskbarRootLayoutFromMap(displayId);
+ } else {
+ debugTaskbarManager("removeTaskbarRootViewFromWindow - WindowManager is null",
+ displayId);
}
}
@@ -913,24 +976,164 @@
/**
* Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
+ * @param dp The {@link DeviceProfile} for the display.
+ * @param displayId The ID of the display.
*/
- private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
- Display display = mBaseContext.getSystemService(DisplayManager.class).getDisplay(
- displayId);
- Context navigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
- ? mBaseContext.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
- : null;
+ private @Nullable TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp,
+ int displayId) {
+ Display display = getDisplay(displayId);
+ if (display == null) {
+ debugTaskbarManager("createTaskbarActivityContext: display null", displayId);
+ return null;
+ }
+
+ Context navigationBarPanelContext = null;
+ if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
+ navigationBarPanelContext = mBaseContext.createWindowContext(display,
+ TYPE_NAVIGATION_BAR_PANEL, null);
+ }
+
+ boolean isPrimaryDisplay = isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue();
TaskbarActivityContext newTaskbar = new TaskbarActivityContext(getWindowContext(displayId),
- navigationBarPanelContext, dp, mDefaultNavButtonController,
- mUnfoldProgressProvider, isDefaultDisplay(displayId),
- SystemUiProxy.INSTANCE.get(mPrimaryWindowContext));
+ navigationBarPanelContext, dp, getNavButtonController(displayId),
+ mUnfoldProgressProvider, isPrimaryDisplay,
+ SystemUiProxy.INSTANCE.get(mBaseContext));
addTaskbarToMap(displayId, newTaskbar);
return newTaskbar;
}
/**
+ * Create {@link ComponentCallbacks} for the given display and register it to the relevant
+ * WindowContext. For external displays, populate maps.
+ * @param displayId The ID of the display.
+ */
+ private void createAndRegisterComponentCallbacks(int displayId) {
+ ComponentCallbacks callbacks = new ComponentCallbacks() {
+ private Configuration mOldConfig =
+ getWindowContext(displayId).getResources().getConfiguration();
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
+ "onConfigurationChanged: " + newConfig);
+ debugTaskbarManager(
+ "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+ mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
+ int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
+
+ if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
+ Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "onConfigurationChanged: theme changed");
+ // Only recreate for theme changes, not other UI mode changes such as docking.
+ int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+ int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+ if (oldUiNightMode == newUiNightMode) {
+ configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
+ }
+ }
+
+ debugTaskbarManager("ComponentCallbacks#onConfigurationChanged() "
+ + "configDiff=" + Configuration.configurationDiffToString(configDiff));
+ if (configDiff != 0 || getCurrentActivityContext() == null) {
+ recreateTaskbar();
+ } else {
+ // Config change might be handled without re-creating the taskbar
+ if (dp != null && !isTaskbarEnabled(dp)) {
+ destroyTaskbarForDisplay(getDefaultDisplayId());
+ } else {
+ if (dp != null && isTaskbarEnabled(dp)) {
+ if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
+ // Re-initialize for screen size change? Should this be done
+ // by looking at screen-size change flag in configDiff in the
+ // block above?
+ recreateTaskbar();
+ } else {
+ getCurrentActivityContext().updateDeviceProfile(dp);
+ }
+ }
+ getCurrentActivityContext().onConfigurationChanged(configDiff);
+ }
+ }
+ mOldConfig = new Configuration(newConfig);
+ // reset taskbar was pinned value, so we don't automatically unstash taskbar upon
+ // user unfolding the device.
+ mSharedState.setTaskbarWasPinned(false);
+ }
+
+ @Override
+ public void onLowMemory() { }
+ };
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryComponentCallbacks = callbacks;
+ mPrimaryWindowContext.registerComponentCallbacks(callbacks);
+ } else {
+ mComponentCallbacks.put(displayId, callbacks);
+ getWindowContext(displayId).registerComponentCallbacks(callbacks);
+ }
+ }
+
+ /**
+ * Unregister {@link ComponentCallbacks} for the given display from its WindowContext. For
+ * external displays, remove from the map.
+ * @param displayId The ID of the display.
+ */
+ private void removeAndUnregisterComponentCallbacks(int displayId) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryWindowContext.unregisterComponentCallbacks(mPrimaryComponentCallbacks);
+ } else {
+ ComponentCallbacks callbacks = mComponentCallbacks.get(displayId);
+ getWindowContext(displayId).unregisterComponentCallbacks(callbacks);
+ mComponentCallbacks.delete(displayId);
+ }
+ }
+
+ /**
+ * Creates a {@link TaskbarNavButtonController} for the given display and adds it to the map
+ * if it doesn't already exist.
+ * @param displayId The ID of the display
+ */
+ private void createNavButtonController(int displayId) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryNavButtonController = new TaskbarNavButtonController(
+ mPrimaryWindowContext,
+ mNavCallbacks,
+ SystemUiProxy.INSTANCE.get(mBaseContext),
+ new Handler(),
+ new ContextualSearchInvoker(mBaseContext));
+ } else {
+ TaskbarNavButtonController navButtonController = new TaskbarNavButtonController(
+ getWindowContext(displayId),
+ mNavCallbacks,
+ SystemUiProxy.INSTANCE.get(mBaseContext),
+ new Handler(),
+ new ContextualSearchInvoker(mBaseContext));
+ mNavButtonControllers.put(displayId, navButtonController);
+ }
+ }
+
+ private TaskbarNavButtonController getNavButtonController(int displayId) {
+ return (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue())
+ ? mPrimaryNavButtonController : mNavButtonControllers.get(displayId);
+ }
+
+ private void removeNavButtonController(int displayId) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryNavButtonController = null;
+ } else {
+ mNavButtonControllers.delete(displayId);
+ }
+ }
+
+ /**
* Adds the {@link TaskbarActivityContext} associated with the given display ID to taskbar
* map if there is not already a taskbar mapped to that displayId.
*
@@ -958,12 +1161,16 @@
*/
private void createTaskbarRootLayout(int displayId) {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "createTaskbarRootLayout: " + displayId);
+ if (!enableTaskbarNoRecreate()) {
+ return;
+ }
+
FrameLayout newTaskbarRootLayout = new FrameLayout(getWindowContext(displayId)) {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// The motion events can be outside the view bounds of task bar, and hence
// manually dispatching them to the drag layer here.
- TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+ TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) {
return taskbar.getDragLayer().dispatchTouchEvent(ev);
}
@@ -1029,13 +1236,10 @@
* @param displayId The ID of the display for which to create the window context.
*/
private @Nullable Context createWindowContext(int displayId) {
- DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
- if (displayManager == null) {
- return null;
- }
-
- Display display = displayManager.getDisplay(displayId);
+ debugTaskbarManager("createWindowContext: " + displayId);
+ Display display = getDisplay(displayId);
if (display == null) {
+ debugTaskbarManager("createWindowContext: display null", displayId);
return null;
}
@@ -1043,10 +1247,29 @@
if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId)) {
windowType = TYPE_NAVIGATION_BAR;
}
+ debugTaskbarManager(
+ "createWindowContext: windowType=" + ((windowType == TYPE_NAVIGATION_BAR)
+ ? "TYPE_NAVIGATION_BAR" : "TYPE_NAVIGATION_BAR_PANEL"), displayId);
return mBaseContext.createWindowContext(display, windowType, null);
}
+ private @Nullable Display getDisplay(int displayId) {
+ DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
+ if (displayManager == null) {
+ debugTaskbarManager("cannot get DisplayManager", displayId);
+ return null;
+ }
+
+ Display display = displayManager.getDisplay(displayId);
+ if (display == null) {
+ debugTaskbarManager("Cannot get display!", displayId);
+ return null;
+ }
+
+ return displayManager.getDisplay(displayId);
+ }
+
/**
* Retrieves the window context of the taskbar for the specified display.
*
@@ -1054,7 +1277,8 @@
* @return The Window Context {@link Context} for a given display or {@code null}.
*/
private Context getWindowContext(int displayId) {
- return (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays())
+ return (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue())
? mPrimaryWindowContext : mWindowContexts.get(displayId);
}
@@ -1070,12 +1294,15 @@
* @return The window manager {@link WindowManager} for a given display or {@code null}.
*/
private @Nullable WindowManager getWindowManager(int displayId) {
- if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ debugTaskbarManager("cannot get mPrimaryWindowManager", displayId);
return mPrimaryWindowManager;
}
Context externalDisplayContext = getWindowContext(displayId);
if (externalDisplayContext == null) {
+ debugTaskbarManager("cannot get externalDisplayContext", displayId);
return null;
}
@@ -1110,18 +1337,20 @@
}
/** Temp logs for b/254119092. */
- public void debugWhyTaskbarNotDestroyed(String debugReason) {
- debugWhyTaskbarNotDestroyed(debugReason, getDefaultDisplayId());
+ public void debugTaskbarManager(String debugReason) {
+ debugTaskbarManager(debugReason, getDefaultDisplayId());
}
/** Temp logs for b/254119092. */
- public void debugWhyTaskbarNotDestroyed(String debugReason, int displayId) {
+ public void debugTaskbarManager(String debugReason, int displayId) {
StringJoiner log = new StringJoiner("\n");
- log.add(debugReason + " displayId=" + displayId);
+ log.add(debugReason + " displayId=" + displayId + " isDefaultDisplay=" + isDefaultDisplay(
+ displayId));
boolean activityTaskbarPresent = mActivity != null
&& mActivity.getDeviceProfile().isTaskbarPresent;
- Context windowContext = getWindowContext(displayId);
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ Context windowContext = mPrimaryWindowContext;
if (windowContext == null) {
log.add("window context for displayId" + displayId);
return;
@@ -1160,6 +1389,6 @@
}
private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
- dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
+ dp -> debugTaskbarManager("mActivity onDeviceProfileChanged");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 6016394..95724ad 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -104,6 +104,8 @@
private static final int FLAG_DELAY_TASKBAR_BG_TAG = 1 << 12;
public static final int FLAG_STASHED_FOR_BUBBLES = 1 << 13; // show handle for stashed hotseat
public static final int FLAG_TASKBAR_HIDDEN = 1 << 14; // taskbar hidden during dream, etc...
+ // taskbar should always be stashed for bubble bar on phone
+ public static final int FLAG_STASHED_BUBBLE_BAR_ON_PHONE = 1 << 15;
// If any of these flags are enabled, isInApp should return true.
private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -126,7 +128,7 @@
// If any of these flags are enabled, the taskbar must be stashed.
private static final int FLAGS_FORCE_STASHED = FLAG_STASHED_SYSUI | FLAG_STASHED_DEVICE_LOCKED
| FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN
- | FLAG_STASHED_FOR_BUBBLES;
+ | FLAG_STASHED_FOR_BUBBLES | FLAG_STASHED_BUBBLE_BAR_ON_PHONE;
/**
* How long to stash/unstash when manually invoked via long press.
@@ -359,6 +361,7 @@
// For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell
// us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
updateStateForFlag(FLAG_IN_APP, true);
+ updateStateForFlag(FLAG_STASHED_BUBBLE_BAR_ON_PHONE, mActivity.isBubbleBarOnPhone());
applyState(/* duration = */ 0);
@@ -574,7 +577,8 @@
*/
public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow,
boolean delayTaskbarBackground) {
- if (!DisplayController.isTransientTaskbar(mActivity)) {
+ if (!DisplayController.isTransientTaskbar(mActivity)
+ || mActivity.isBubbleBarOnPhone()) {
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index e5d642d..89bcb41 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -283,7 +283,7 @@
foundTask,
taskContainer.getIconView().getDrawable(),
taskContainer.getSnapshotView(),
- taskContainer.getSplitAnimationThumbnail(),
+ taskContainer.getThumbnail(),
null /* intent */,
null /* user */,
info);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
new file mode 100644
index 0000000..8b53ff1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.graphics.Typeface
+import android.widget.TextView
+import com.android.launcher3.Flags
+
+/**
+ * Helper util class to set pre-defined typefaces to textviews
+ *
+ * If the typeface font family is already defined here, you can just reuse it directly. Otherwise,
+ * please define it here for future use. You do not need to define the font style. If you need
+ * anything other than [Typeface.NORMAL], pass it inline when calling [setTypeface]
+ */
+class TypefaceUtils {
+
+ companion object {
+ const val FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED = "variable-headline-small-emphasized"
+ const val FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED = "variable-headline-large-emphasized"
+ const val FONT_FAMILY_BODY_SMALL_BASELINE = "variable-body-small"
+ const val FONT_FAMILY_BODY_MEDIUM_BASELINE = "variable-body-medium"
+ const val FONT_FAMILY_LABEL_LARGE_BASELINE = "variable-label-large"
+
+ @JvmStatic
+ @JvmOverloads
+ fun setTypeface(
+ textView: TextView?,
+ fontFamilyName: String,
+ fontStyle: Int = Typeface.NORMAL,
+ ) {
+ if (!Flags.expressiveThemeInTaskbarAndNavigation()) return
+ textView?.typeface = Typeface.create(fontFamilyName, fontStyle)
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 249773d..97be2e8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -44,10 +44,13 @@
private val arrowVisibleHeight: Float
private val strokeAlpha: Int
+ private val strokeColor: Int
+ private val strokeColorDropTarget: Int
private val shadowAlpha: Int
private val shadowBlur: Float
private val keyShadowDistance: Float
private var arrowHeightFraction = 1f
+ private var isShowingDropTarget: Boolean = false
var arrowPositionX: Float = 0f
private set
@@ -100,7 +103,9 @@
fillPaint.flags = Paint.ANTI_ALIAS_FLAG
fillPaint.style = Paint.Style.FILL
// configure stroke paint
- strokePaint.color = context.getColor(R.color.taskbar_stroke)
+ strokeColor = context.getColor(R.color.taskbar_stroke)
+ strokeColorDropTarget = context.getColor(com.android.internal.R.color.system_primary_fixed)
+ strokePaint.color = strokeColor
strokePaint.flags = Paint.ANTI_ALIAS_FLAG
strokePaint.style = Paint.Style.STROKE
strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width)
@@ -235,9 +240,25 @@
return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight))
}
+ /** Set whether the background should show the drop target */
+ fun showDropTarget(isDropTarget: Boolean) {
+ if (isShowingDropTarget == isDropTarget) {
+ return
+ }
+ isShowingDropTarget = isDropTarget
+ val strokeColor = if (isDropTarget) strokeColorDropTarget else strokeColor
+ val alpha = if (isDropTarget) DRAG_STROKE_ALPHA else strokeAlpha
+ strokePaint.color = strokeColor
+ strokePaint.alpha = alpha
+ invalidateSelf()
+ }
+
+ fun isShowingDropTarget() = isShowingDropTarget
+
companion object {
private const val DARK_THEME_STROKE_ALPHA = 51
private const val LIGHT_THEME_STROKE_ALPHA = 41
+ private const val DRAG_STROKE_ALPHA = 255
private const val DARK_THEME_SHADOW_ALPHA = 51
private const val LIGHT_THEME_SHADOW_ALPHA = 25
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 4e029e3..5ddbe03 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -33,7 +33,8 @@
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.Log;
-import android.widget.Toast;
+
+import androidx.annotation.NonNull;
import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
@@ -589,15 +590,24 @@
}
@Override
- public void onDragItemOverBubbleBarDragZone(BubbleBarLocation location) {
- //TODO(b/388894910): add meaningful implementation
- MAIN_EXECUTOR.execute(() ->
- Toast.makeText(mContext, "onDragItemOver " + location, Toast.LENGTH_SHORT).show());
+ public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
+ MAIN_EXECUTOR.execute(() -> {
+ mBubbleBarViewController.onDragItemOverBubbleBarDragZone(bubbleBarLocation);
+ if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
+ mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation);
+ }
+ });
}
@Override
public void onItemDraggedOutsideBubbleBarDropZone() {
-
+ MAIN_EXECUTOR.execute(() -> {
+ if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
+ BubbleBarLocation original = mBubbleBarViewController.getBubbleBarLocation();
+ mBubbleBarLocationListener.onBubbleBarLocationAnimated(original);
+ }
+ mBubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone();
+ });
}
/** Notifies WMShell to show the expanded view. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c001123..d43ebe2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -536,6 +536,16 @@
return (float) (displayWidth - getWidth() - margin);
}
+ /** Set whether the background should show the drop target */
+ public void showDropTarget(boolean isDropTarget) {
+ mBubbleBarBackground.showDropTarget(isDropTarget);
+ }
+
+ /** Returns whether the Bubble Bar is currently displaying a drop target. */
+ public boolean isShowingDropTarget() {
+ return mBubbleBarBackground.isShowingDropTarget();
+ }
+
/**
* Animate bubble bar to the given location transiently. Does not modify the layout or the value
* returned by {@link #getBubbleBarLocation()}.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 026f239..b90a5b0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -131,12 +131,13 @@
// Whether the bar is hidden when stashed
private boolean mHiddenForStashed;
private boolean mShouldShowEducation;
-
public boolean mOverflowAdded;
+ private boolean mIsLocationUpdatedForDropTarget = false;
private BubbleBarViewAnimator mBubbleBarViewAnimator;
private final FrameLayout mBubbleBarContainer;
private BubbleBarFlyoutController mBubbleBarFlyoutController;
+ private BubbleBarPinController mBubbleBarPinController;
private TaskbarSharedState mTaskbarSharedState;
private final TimeSource mTimeSource = System::currentTimeMillis;
private final int mTaskbarTranslationDelta;
@@ -166,6 +167,7 @@
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleBarController = bubbleControllers.bubbleBarController;
mBubbleDragController = bubbleControllers.bubbleDragController;
+ mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
mBubbleBarFlyoutController = new BubbleBarFlyoutController(
@@ -274,7 +276,10 @@
@Override
public boolean isOnLeft() {
- return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+ boolean shouldRevertLocation =
+ mBarView.isShowingDropTarget() && mIsLocationUpdatedForDropTarget;
+ boolean isOnLeft = mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+ return shouldRevertLocation != isOnLeft;
}
@Override
@@ -524,6 +529,61 @@
mBarView.animateToBubbleBarLocation(bubbleBarLocation);
}
+ /** Returns whether the Bubble Bar is currently displaying a drop target. */
+ public boolean isShowingDropTarget() {
+ return mBarView.isShowingDropTarget();
+ }
+
+ /**
+ * Notifies the controller that a drag event is over the Bubble Bar drop zone. The controller
+ * will display the appropriate drop target and enter drop target mode. The controller will also
+ * update the return value of {@link #isLocationUpdatedForDropTarget()} to true if location was
+ * updated.
+ */
+ public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
+ mBarView.showDropTarget(/* isDropTarget = */ true);
+ mIsLocationUpdatedForDropTarget = getBubbleBarLocation() != bubbleBarLocation;
+ if (mIsLocationUpdatedForDropTarget) {
+ animateBubbleBarLocation(bubbleBarLocation);
+ }
+ if (!hasBubbles()) {
+ mBubbleBarPinController.showDropTarget(bubbleBarLocation);
+ }
+ }
+
+ /**
+ * Returns {@code true} if location was updated after most recent
+ * {@link #onDragItemOverBubbleBarDragZone}}.
+ */
+ public boolean isLocationUpdatedForDropTarget() {
+ return mIsLocationUpdatedForDropTarget;
+ }
+
+ /**
+ * Notifies the controller that the drag event is outside the Bubble Bar drop zone.
+ * This will hide the drop target zone if there are no bubbles or return the
+ * Bubble Bar to its original location. The controller will also exit drop target
+ * mode and reset the value returned from {@link #isLocationUpdatedForDropTarget()} to false.
+ */
+ public void onItemDraggedOutsideBubbleBarDropZone() {
+ mBarView.showDropTarget(/* isDropTarget = */ false);
+ if (mIsLocationUpdatedForDropTarget) {
+ animateBubbleBarLocation(getBubbleBarLocation());
+ }
+ mBubbleBarPinController.hideDropTarget();
+ mIsLocationUpdatedForDropTarget = false;
+ }
+
+ /**
+ * Notifies the controller that the drag has completed over the Bubble Bar drop zone.
+ * The controller will hide the drop target if there are no bubbles and exit drop target mode.
+ */
+ public void onItemDroppedInBubbleBarDragZone() {
+ mBarView.showDropTarget(/* isDropTarget = */ false);
+ mBubbleBarPinController.hideDropTarget();
+ mIsLocationUpdatedForDropTarget = false;
+ }
+
/**
* The bounds of the bubble bar.
*/
@@ -996,7 +1056,12 @@
boolean isInApp = mTaskbarStashController.isInApp();
// if this is the first bubble, animate to the initial state.
if (mBarView.getBubbleChildCount() == 1 && !isUpdate) {
- mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
+ // If a drop target is visible and the first bubble is added, hide the empty drop target
+ if (mBarView.isShowingDropTarget()) {
+ mBubbleBarPinController.hideDropTarget();
+ }
+ mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding,
+ mBarView.isShowingDropTarget());
return;
}
// if we're not stashed or we're in persistent taskbar, animate for collapsed state.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 745c689..30cfafe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -365,7 +365,12 @@
}
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
- fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
+ fun animateToInitialState(
+ b: BubbleBarBubble,
+ isInApp: Boolean,
+ isExpanding: Boolean,
+ isDragging: Boolean = false,
+ ) {
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
@@ -374,15 +379,12 @@
// bubble bar to the handle if we're in an app.
val showAnimation = buildBubbleBarSpringInAnimation()
val hideAnimation =
- if (isInApp && !isExpanding) {
+ if (isInApp && !isExpanding && !isDragging) {
buildBubbleBarToHandleAnimation()
} else {
Runnable {
- moveToState(AnimatingBubble.State.ANIMATING_OUT)
- bubbleBarFlyoutController.collapseFlyout {
- onFlyoutRemoved()
- clearAnimatingBubble()
- }
+ collapseFlyoutAndUpdateState()
+ if (isDragging) return@Runnable
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -440,11 +442,7 @@
// first bounce the bubble bar and show the flyout. Then hide the flyout.
val showAnimation = buildBubbleBarBounceAnimation()
val hideAnimation = Runnable {
- moveToState(AnimatingBubble.State.ANIMATING_OUT)
- bubbleBarFlyoutController.collapseFlyout {
- onFlyoutRemoved()
- clearAnimatingBubble()
- }
+ collapseFlyoutAndUpdateState()
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -454,6 +452,14 @@
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
+ private fun collapseFlyoutAndUpdateState() {
+ moveToState(AnimatingBubble.State.ANIMATING_OUT)
+ bubbleBarFlyoutController.collapseFlyout {
+ onFlyoutRemoved()
+ clearAnimatingBubble()
+ }
+ }
+
/**
* The bubble bar animation when it is collapsed is divided into 2 chained animations. The first
* animation is a regular accelerate animation that moves the bubble bar upwards. When it ends
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index b25f999..15a27d1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -17,6 +17,7 @@
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -51,9 +52,8 @@
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -138,11 +138,11 @@
public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
- mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize());
+ mNormalizedIconSize = Math.round(getIconSize() * ICON_VISIBLE_AREA_FACTOR);
int shadowSize = context.getResources().getDimensionPixelSize(
R.dimen.blur_size_thin_outline);
mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
- mShapePath = IconShape.INSTANCE.get(context).getShape().getPath(mNormalizedIconSize);
+ mShapePath = ThemeManager.INSTANCE.get(context).getIconShape().getPath(mNormalizedIconSize);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index bb57d6e..019e746 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1380,7 +1380,7 @@
SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
if (taskbarManager != null) {
- taskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged");
+ taskbarManager.debugTaskbarManager("QuickstepLauncher#onDeviceProfileChanged");
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 6e901ee..1f34969 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -144,7 +144,7 @@
override fun isNonResizeableActivity(lai: LauncherActivityInfo) =
lai.activityInfo.resizeMode == ActivityInfo.RESIZE_MODE_UNRESIZEABLE
- override fun supportsMultiInstance(lai: LauncherActivityInfo) : Boolean {
+ override fun supportsMultiInstance(lai: LauncherActivityInfo): Boolean {
return try {
super.supportsMultiInstance(lai) || lai.supportsMultiInstance()
} catch (e: Exception) {
@@ -202,4 +202,7 @@
(appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode
override fun getRoundIconRes(appInfo: ApplicationInfo) = appInfo.roundIconRes
+
+ override fun isFileDrawable(shortcutInfo: ShortcutInfo) =
+ shortcutInfo.hasIconFile() || shortcutInfo.hasIconUri()
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 10513c0..b27c6e8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -153,7 +153,7 @@
return new PageAlphaProvider(DECELERATE_2) {
@Override
public float getPageAlpha(int pageIndex) {
- return launcher.getDeviceProfile().shouldShowAllAppsOnSheet()
+ return launcher.getDeviceProfile().isTablet
? superPageAlphaProvider.getPageAlpha(pageIndex)
: 0;
}
@@ -164,7 +164,7 @@
public int getVisibleElements(Launcher launcher) {
int elements = ALL_APPS_CONTENT | FLOATING_SEARCH_BAR;
// When All Apps is presented on a bottom sheet, HOTSEAT_ICONS are visible.
- if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) {
+ if (launcher.getDeviceProfile().isTablet) {
elements |= HOTSEAT_ICONS;
}
return elements;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 98737a5..88b7155 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -16,9 +16,11 @@
package com.android.launcher3.uioverrides.touchcontrollers
import android.content.Context
+import android.graphics.Rect
import android.view.MotionEvent
import androidx.dynamicanimation.animation.SpringAnimation
import com.android.app.animation.Interpolators.DECELERATE
+import com.android.app.animation.Interpolators.LINEAR
import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.R
import com.android.launcher3.Utilities.EDGE_NAV_BAR
@@ -29,6 +31,7 @@
import com.android.launcher3.util.MSDLPlayerWrapper
import com.android.launcher3.util.TouchController
import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
import com.android.quickstep.views.RecentsViewContainer
import com.android.quickstep.views.TaskView
import com.google.android.msdl.data.model.MSDLToken
@@ -50,6 +53,7 @@
recentsView.pagedOrientationHandler.upDownSwipeDirection,
)
private val isRtl = isRtl(container.resources)
+ private val tempTaskThumbnailBounds = Rect()
private var taskBeingDragged: TaskView? = null
private var springAnimation: SpringAnimation? = null
@@ -57,6 +61,7 @@
private var verticalFactor: Int = 0
private var hasDismissThresholdHapticRun = false
private var initialDisplacement: Float = 0f
+ private var recentsScaleAnimation: SpringAnimation? = null
private fun canInterceptTouch(ev: MotionEvent): Boolean =
when {
@@ -98,6 +103,7 @@
private fun onActionDown(ev: MotionEvent): Boolean {
springAnimation?.cancel()
+ recentsScaleAnimation?.cancel()
if (!canInterceptTouch(ev)) {
return false
}
@@ -108,7 +114,9 @@
recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev)
}
?.also {
- dismissLength = recentsView.pagedOrientationHandler.getSecondaryDimension(it)
+ // Dismiss length as bottom of task so it is fully off screen when dismissed.
+ it.getThumbnailBounds(tempTaskThumbnailBounds, relativeToDragLayer = true)
+ dismissLength = tempTaskThumbnailBounds.bottom
verticalFactor =
recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor
}
@@ -162,6 +170,8 @@
}
recentsView.redrawLiveTile()
}
+ val dismissFraction = displacement / (dismissLength * verticalFactor).toFloat()
+ RECENTS_SCALE_PROPERTY.setValue(recentsView, getRecentsScale(dismissFraction))
playDismissThresholdHaptic(displacement)
return true
}
@@ -216,6 +226,10 @@
if (isDismissing) (dismissLength * verticalFactor).toFloat() else 0f
)
}
+ recentsScaleAnimation =
+ recentsView.animateRecentsScale(RECENTS_SCALE_DEFAULT).addEndListener { _, _, _, _ ->
+ recentsScaleAnimation = null
+ }
}
// Returns if the current task being dragged is towards "positive" (e.g. dismissal).
@@ -230,8 +244,54 @@
springAnimation = null
}
+ private fun getRecentsScale(dismissFraction: Float): Float {
+ return when {
+ // Do not scale recents when dragging below origin.
+ dismissFraction <= 0 -> {
+ RECENTS_SCALE_DEFAULT
+ }
+ // Initially scale recents as the drag begins, up to the first threshold.
+ dismissFraction < RECENTS_SCALE_FIRST_THRESHOLD_FRACTION -> {
+ mapToRange(
+ dismissFraction,
+ 0f,
+ RECENTS_SCALE_FIRST_THRESHOLD_FRACTION,
+ RECENTS_SCALE_DEFAULT,
+ RECENTS_SCALE_ON_DISMISS_CANCEL,
+ LINEAR,
+ )
+ }
+ // Keep scale consistent until dragging to the dismiss threshold.
+ dismissFraction < RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION -> {
+ RECENTS_SCALE_ON_DISMISS_CANCEL
+ }
+ // Scale beyond the dismiss threshold again, to indicate dismiss will occur on release.
+ dismissFraction < RECENTS_SCALE_SECOND_THRESHOLD_FRACTION -> {
+ mapToRange(
+ dismissFraction,
+ RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION,
+ RECENTS_SCALE_SECOND_THRESHOLD_FRACTION,
+ RECENTS_SCALE_ON_DISMISS_CANCEL,
+ RECENTS_SCALE_ON_DISMISS_SUCCESS,
+ LINEAR,
+ )
+ }
+ // Keep scale beyond the dismiss threshold scaling consistent.
+ else -> {
+ RECENTS_SCALE_ON_DISMISS_SUCCESS
+ }
+ }
+ }
+
companion object {
private const val DISMISS_THRESHOLD_FRACTION = 0.5f
private const val DISMISS_THRESHOLD_HAPTIC_RANGE = 10f
+
+ private const val RECENTS_SCALE_ON_DISMISS_CANCEL = 0.9875f
+ private const val RECENTS_SCALE_ON_DISMISS_SUCCESS = 0.975f
+ private const val RECENTS_SCALE_DEFAULT = 1f
+ private const val RECENTS_SCALE_FIRST_THRESHOLD_FRACTION = 0.2f
+ private const val RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION = 0.5f
+ private const val RECENTS_SCALE_SECOND_THRESHOLD_FRACTION = 0.575f
}
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index b43c3ac..3640e1f 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1112,6 +1112,9 @@
public void onGestureEnded(float endVelocityPxPerMs, PointF velocityPxPerMs) {
float flingThreshold = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_speed);
+ Log.d(TAG, "onGestureEnded: mGestureStarted=" + mGestureStarted
+ + ", mIsMotionPaused=" + mIsMotionPaused
+ + ", flingThresholdPassed=" + (Math.abs(endVelocityPxPerMs) > flingThreshold));
boolean isFling = mGestureStarted && !mIsMotionPaused
&& Math.abs(endVelocityPxPerMs) > flingThreshold;
mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
@@ -1260,12 +1263,12 @@
dpiFromPx(velocityPxPerMs.x),
dpiFromPx(velocityPxPerMs.y),
Math.toDegrees(Math.atan2(-velocityPxPerMs.y, velocityPxPerMs.x)));
-
if (mGestureState.isHandlingAtomicEvent()) {
// Button mode, this is only used to go to recents.
return RECENTS;
}
+ Log.d(TAG, "calculateEndTarget: isCancel=" + isCancel + ", isFlingY=" + isFlingY);
GestureEndTarget endTarget;
if (isCancel) {
endTarget = LAST_TASK;
@@ -1275,6 +1278,7 @@
endTarget = calculateEndTargetForNonFling(velocityPxPerMs);
}
+ Log.d(TAG, "calculateEndTarget: endTarget(1)=" + endTarget);
if (mDeviceState.isOverviewDisabled() && endTarget == RECENTS) {
return LAST_TASK;
}
@@ -1293,6 +1297,7 @@
return LAST_TASK;
}
}
+ Log.d(TAG, "calculateEndTarget: endTarget(2)=" + endTarget);
return endTarget;
}
@@ -1301,9 +1306,12 @@
final boolean willGoToNewTask =
isScrollingToNewTask() && Math.abs(velocity.x) > Math.abs(endVelocity);
final boolean isSwipeUp = endVelocity < 0;
+ Log.d(TAG, "calculateEndTargetForFlingY: willGoToNewTask=" + willGoToNewTask
+ + ", isSwipeUp=" + isSwipeUp);
if (!isSwipeUp) {
final boolean isCenteredOnNewTask = mRecentsView != null
&& mRecentsView.getDestinationPage() != mRecentsView.getRunningTaskIndex();
+ Log.d(TAG, "calculateEndTargetForFlingY: isCenteredOnNewTask=" + isCenteredOnNewTask);
return willGoToNewTask || isCenteredOnNewTask ? NEW_TASK : LAST_TASK;
}
@@ -1316,6 +1324,9 @@
// Fully gestural mode.
final boolean isFlingX = Math.abs(velocity.x) > mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_speed);
+ Log.d(TAG, "calculateEndTargetForNonFling: isScrollingToNewTask=" + isScrollingToNewTask
+ + ", isFlingX=" + isFlingX
+ + ", mIsMotionPaused=" + mIsMotionPaused);
if (isScrollingToNewTask && isFlingX) {
// Flinging towards new task takes precedence over mIsMotionPaused (which only
// checks y-velocity).
@@ -1325,6 +1336,7 @@
} else if (isScrollingToNewTask) {
return NEW_TASK;
}
+ Log.d(TAG, "calculateEndTargetForNonFling: mCanSlowSwipeGoHome=" + mCanSlowSwipeGoHome);
return velocity.y < 0 && mCanSlowSwipeGoHome ? HOME : LAST_TASK;
}
diff --git a/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
index bafb0b2..444e77d 100644
--- a/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
+++ b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
@@ -17,7 +17,7 @@
package com.android.quickstep
import android.content.Context
-import com.android.systemui.shared.system.QuickStepContract
+import com.android.launcher3.R
// DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
open class DesktopFullscreenDrawParams
@@ -28,6 +28,6 @@
// computeCornerRadius is used as cornerRadiusProvider, so
// QuickStepContract::getWindowCornerRadius can be mocked properly.
private fun computeCornerRadius(context: Context): Float =
- QuickStepContract.getWindowCornerRadius(context)
+ context.resources.getDimension(R.dimen.desktop_windowing_freeform_rounded_corner_radius)
}
}
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index be0a339..783ec2c 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -514,7 +514,13 @@
private void finishAnimation() {
mLauncher.setPredictiveBackToHomeInProgress(false);
+ if (mBackTarget != null && mBackTarget.leash.isValid()) {
+ mBackTarget.leash.release();
+ }
mBackTarget = null;
+ if (mLauncherTarget != null && mLauncherTarget.leash.isValid()) {
+ mLauncherTarget.leash.release();
+ }
mLauncherTarget = null;
mBackInProgress = false;
mBackProgress = 0;
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 42aa86e..afdb403 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -293,10 +293,6 @@
val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
val deviceProfile = recentsViewContainer?.getDeviceProfile()
val uiController = containerInterface.getTaskbarController()
- val allowQuickSwitch =
- uiController != null &&
- deviceProfile != null &&
- (deviceProfile.isTablet || deviceProfile.isTwoPanels)
val focusedDisplayId = focusState.focusedDisplayId
val focusedDisplayUIController: TaskbarUIController? =
@@ -322,26 +318,26 @@
when (command.type) {
HIDE -> {
- if (!allowQuickSwitch) return true
+ if (uiController == null || deviceProfile?.isTablet == false) return true
keyboardTaskFocusIndex =
if (
enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
) {
focusedDisplayUIController.launchFocusedTask()
} else {
- uiController!!.launchFocusedTask()
+ uiController.launchFocusedTask()
}
if (keyboardTaskFocusIndex == -1) return true
}
KEYBOARD_INPUT ->
- if (allowQuickSwitch) {
+ if (uiController != null && deviceProfile?.isTablet == true) {
if (
enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
) {
focusedDisplayUIController.openQuickSwitchView()
} else {
- uiController!!.openQuickSwitchView()
+ uiController.openQuickSwitchView()
}
return true
} else {
@@ -365,7 +361,11 @@
TOGGLE -> {}
}
- recentsView?.setKeyboardTaskFocusIndex(keyboardTaskFocusIndex)
+ recentsView?.setKeyboardTaskFocusIndex(
+ recentsView.indexOfChild(recentsView.taskViews.elementAtOrNull(keyboardTaskFocusIndex))
+ ?: -1
+ )
+
// Handle recents view focus when launching from home
val animatorListener: Animator.AnimatorListener =
object : AnimatorListenerAdapter() {
@@ -526,7 +526,7 @@
// Stops requesting focused after first view gets focused.
recentsView.getTaskViewAt(keyboardTaskFocusIndex).requestFocus() ||
recentsView.nextTaskView.requestFocus() ||
- recentsView.getFirstTaskView().requestFocus() ||
+ recentsView.firstTaskView.requestFocus() ||
recentsView.requestFocus()
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index c276447..d7152b5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -21,6 +21,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.wm.shell.shared.TransitionUtil.TYPE_SPLIT_SCREEN_DIM_LAYER;
import android.annotation.Nullable;
import android.graphics.Rect;
@@ -190,7 +191,8 @@
ArrayList<RemoteAnimationTarget> apps, ArrayList<RemoteAnimationTarget> nonApps) {
for (int i = 0; i < appTargets.length; i++) {
RemoteAnimationTarget target = appTargets[i];
- if (target.windowType == TYPE_DOCK_DIVIDER) {
+ if (target.windowType == TYPE_DOCK_DIVIDER
+ || target.windowType == TYPE_SPLIT_SCREEN_DIM_LAYER) {
nonApps.add(target);
} else {
apps.add(target);
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 7990aae..a69d472 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -236,15 +236,14 @@
position[0] + width, position[1] + height);
// Take the thumbnail of the task without a scrim and apply it back after
- // TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval
- float alpha = 0f;
- if (!enableRefactorTaskThumbnail()) {
- alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha();
+ Bitmap thumbnail;
+ if (enableRefactorTaskThumbnail()) {
+ thumbnail = mTaskContainer.getThumbnail();
+ } else {
+ float alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha();
mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(0);
- }
- Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
- taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK);
- if (!enableRefactorTaskThumbnail()) {
+ thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
+ taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK);
mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(alpha);
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 37841e8..37c2d1c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -38,7 +38,9 @@
import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION;
import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
+import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
import static com.android.quickstep.util.AnimUtils.clampToDuration;
+import static com.android.wm.shell.shared.TransitionUtil.TYPE_SPLIT_SCREEN_DIM_LAYER;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -64,6 +66,7 @@
import com.android.app.animation.Interpolators;
import com.android.internal.jank.Cuj;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -101,6 +104,11 @@
private TaskViewUtils() {}
+ private static final Rect TEMP_THUMBNAIL_BOUNDS = new Rect();
+ private static final Rect TEMP_FULLSCREEN_BOUNDS = new Rect();
+ private static final PointF TEMP_TASK_DIMENSION = new PointF();
+ private static final PointF TEMP_PIVOT = new PointF();
+
/**
* Try to find a TaskView that corresponds with the component of the launched view.
*
@@ -163,7 +171,7 @@
public static <T extends Context & RecentsViewContainer & StatefulContainer<?>>
void createRecentsWindowAnimator(
@NonNull RecentsView<T, ?> recentsView,
- @NonNull TaskView v,
+ @NonNull TaskView taskView,
boolean skipViewChanges,
@NonNull RemoteAnimationTarget[] appTargets,
@NonNull RemoteAnimationTarget[] wallpaperTargets,
@@ -171,31 +179,31 @@
@Nullable DepthController depthController,
@Nullable TransitionInfo transitionInfo,
PendingAnimation out) {
- boolean isQuickSwitch = v.isEndQuickSwitchCuj();
- v.setEndQuickSwitchCuj(false);
+ boolean isQuickSwitch = taskView.isEndQuickSwitchCuj();
+ taskView.setEndQuickSwitchCuj(false);
final RemoteAnimationTargets targets =
new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets,
MODE_OPENING);
final RemoteAnimationTarget navBarTarget = targets.getNavBarRemoteAnimationTarget();
- SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
+ SurfaceTransactionApplier applier = new SurfaceTransactionApplier(taskView);
targets.addReleaseCheck(applier);
RemoteTargetHandle[] remoteTargetHandles;
RemoteTargetHandle[] recentsViewHandles = recentsView.getRemoteTargetHandles();
- if (v.isRunningTask() && recentsViewHandles != null) {
+ if (taskView.isRunningTask() && recentsViewHandles != null) {
// Re-use existing handles
remoteTargetHandles = recentsViewHandles;
} else {
- boolean forDesktop = v instanceof DesktopTaskView;
- RemoteTargetGluer gluer = new RemoteTargetGluer(v.getContext(),
+ boolean forDesktop = taskView instanceof DesktopTaskView;
+ RemoteTargetGluer gluer = new RemoteTargetGluer(taskView.getContext(),
recentsView.getSizeStrategy(), targets, forDesktop);
if (forDesktop) {
remoteTargetHandles = gluer.assignTargetsForDesktop(targets, transitionInfo);
- } else if (v.containsMultipleTasks()) {
+ } else if (taskView.containsMultipleTasks()) {
remoteTargetHandles = gluer.assignTargetsForSplitScreen(targets,
- ((GroupedTaskView) v).getSplitBoundsConfig());
+ ((GroupedTaskView) taskView).getSplitBoundsConfig());
} else {
remoteTargetHandles = gluer.assignTargets(targets);
}
@@ -209,8 +217,8 @@
remoteTargetHandle.getTransformParams().setSyncTransactionApplier(applier);
}
- int taskIndex = recentsView.indexOfChild(v);
- Context context = v.getContext();
+ int taskIndex = recentsView.indexOfChild(taskView);
+ Context context = taskView.getContext();
T container = RecentsViewContainer.containerFromContext(context);
DeviceProfile dp = container.getDeviceProfile();
@@ -218,11 +226,11 @@
boolean parallaxCenterAndAdjacentTask =
!showAsGrid && taskIndex != recentsView.getCurrentPage();
int taskRectTranslationPrimary = recentsView.getScrollOffset(taskIndex);
- int taskRectTranslationSecondary = showAsGrid ? (int) v.getGridTranslationY() : 0;
+ int taskRectTranslationSecondary = showAsGrid ? (int) taskView.getGridTranslationY() : 0;
RemoteTargetHandle[] topMostSimulators = null;
- if (!v.isRunningTask()) {
+ if (!taskView.isRunningTask()) {
// TVSs already initialized from the running task, no need to re-init
for (RemoteTargetHandle targetHandle : remoteTargetHandles) {
TaskViewSimulator tvsLocal = targetHandle.getTaskViewSimulator();
@@ -236,13 +244,13 @@
tvsLocal.fullScreenProgress.value = 0;
tvsLocal.recentsViewScale.value = 1;
if (!enableGridOnlyOverview()) {
- tvsLocal.setIsGridTask(v.isGridTask());
+ tvsLocal.setIsGridTask(taskView.isGridTask());
}
tvsLocal.getOrientationState().getOrientationHandler().set(tvsLocal,
TaskViewSimulator::setTaskRectTranslation, taskRectTranslationPrimary,
taskRectTranslationSecondary);
- if (v instanceof DesktopTaskView) {
+ if (taskView instanceof DesktopTaskView) {
targetHandle.getTransformParams().setTargetAlpha(1f);
} else {
// Fade in the task during the initial 20% of the animation
@@ -259,8 +267,11 @@
out.setFloat(tvsLocal.recentsViewScale,
AnimatedFloat.VALUE, tvsLocal.getFullScreenScale(),
TOUCH_RESPONSE);
- out.setFloat(tvsLocal.recentsViewScroll, AnimatedFloat.VALUE, 0,
- TOUCH_RESPONSE);
+ if (!enableGridOnlyOverview()) {
+ out.setFloat(tvsLocal.recentsViewScroll, AnimatedFloat.VALUE, 0,
+ TOUCH_RESPONSE);
+ }
+
out.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -270,6 +281,18 @@
showTransaction.getTransaction().show(targets.apps[i].leash);
}
applier.scheduleApply(showTransaction);
+
+ if (enableGridOnlyOverview()) {
+ taskView.getThumbnailBounds(TEMP_THUMBNAIL_BOUNDS, /*relativeToDragLayer=*/
+ true);
+ getTaskDimension(context, container.getDeviceProfile(),
+ TEMP_TASK_DIMENSION);
+ TEMP_FULLSCREEN_BOUNDS.set(0, 0, (int) TEMP_TASK_DIMENSION.x,
+ (int) TEMP_TASK_DIMENSION.y);
+ Utilities.getPivotsForScalingRectToRect(TEMP_THUMBNAIL_BOUNDS,
+ TEMP_FULLSCREEN_BOUNDS, TEMP_PIVOT);
+ tvsLocal.setPivotOverride(TEMP_PIVOT);
+ }
}
});
out.addOnFrameCallback(() -> {
@@ -320,7 +343,7 @@
if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulators != null
&& topMostSimulators.length > 0) {
- out.addFloat(v, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
+ out.addFloat(taskView, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
RemoteTargetHandle[] simulatorCopies = topMostSimulators;
for (RemoteTargetHandle handle : simulatorCopies) {
@@ -339,7 +362,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`
- View[] thumbnails = v.getSnapshotViews();
+ View[] thumbnails = taskView.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
@@ -732,7 +755,9 @@
List<SurfaceControl> auxiliarySurfaces = new ArrayList<>();
for (RemoteAnimationTarget target : nonApps) {
final SurfaceControl leash = target.leash;
- if (target.windowType == TYPE_DOCK_DIVIDER && leash != null && leash.isValid()) {
+ if ((target.windowType == TYPE_DOCK_DIVIDER
+ || target.windowType == TYPE_SPLIT_SCREEN_DIM_LAYER)
+ && leash != null && leash.isValid()) {
auxiliarySurfaces.add(leash);
}
}
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index c986b88..953b0c5 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -92,6 +92,8 @@
private static final String LOG_TAG = "AllSetActivity";
private static final String URI_SYSTEM_NAVIGATION_SETTING =
"#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S.:settings:fragment_args_key=gesture_system_navigation_input_summary;S.:settings:show_fragment=com.android.settings.gestures.SystemNavigationGestureSettings;end";
+ private static final String INTENT_ACTION_ACTIVITY_CLOSED =
+ "com.android.quickstep.interaction.ACTION_ALL_SET_ACTIVITY_CLOSED";
private static final String EXTRA_ACCENT_COLOR_DARK_MODE = "suwColorAccentDark";
private static final String EXTRA_ACCENT_COLOR_LIGHT_MODE = "suwColorAccentLight";
private static final String EXTRA_DEVICE_NAME = "suwDeviceName";
@@ -332,6 +334,7 @@
mLauncherStartAnim.dispatchOnEnd();
mLauncherStartAnim = null;
}
+ sendBroadcast(new Intent(INTENT_ACTION_ACTIVITY_CLOSED));
}
@Override
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
index 608fafd..12616a8 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.recents.data
import android.os.UserHandle
+import android.util.Log
import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
@@ -58,7 +59,7 @@
fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?)
/** Informs the listener that the default resolution for loading thumbnails has changed */
- fun onHighResLoadingStateChanged()
+ fun onHighResLoadingStateChanged(highResEnabled: Boolean)
}
}
@@ -91,8 +92,9 @@
}
override fun onHighResLoadingStateChanged(enabled: Boolean) {
+ Log.d(TAG, "onHighResLoadingStateChanged(enabled = $enabled)")
taskThumbnailChangedCallbacks.values.forEach { (_, callback) ->
- callback.onHighResLoadingStateChanged()
+ callback.onHighResLoadingStateChanged(enabled)
}
}
@@ -142,4 +144,8 @@
}
}
}
+
+ companion object {
+ const val TAG = "TaskVisualsChangedDelegateImpl"
+ }
}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index b1a5920..5274ef3 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -160,7 +160,17 @@
updateThumbnail(task.key.id, thumbnailData)
}
- override fun onHighResLoadingStateChanged() {
+ override fun onHighResLoadingStateChanged(highResEnabled: Boolean) {
+ val isTaskVisible = taskRequests.containsKey(task.key.id)
+ if (!isTaskVisible) return
+
+ val isCurrentThumbnailLowRes =
+ tasks.value[task.key.id]?.thumbnail?.reducedResolution
+ val isRequestedResHigherThanCurrent =
+ isCurrentThumbnailLowRes == null ||
+ (isCurrentThumbnailLowRes && highResEnabled)
+ if (!isRequestedResHigherThanCurrent) return
+
recentsCoroutineScope.launch(dispatcherProvider.background) {
updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index d2f10b6..c6b3d6a 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -28,22 +28,20 @@
import com.android.quickstep.recents.data.TasksRepository
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
internal typealias RecentsScopeId = String
-class RecentsDependencies private constructor(private val appContext: Context) {
+class RecentsDependencies private constructor(appContext: Context) {
private val scopes = mutableMapOf<RecentsScopeId, RecentsDependenciesScope>()
init {
@@ -173,8 +171,6 @@
val instance: Any =
when (modelClass) {
RecentsViewData::class.java -> RecentsViewData()
- TaskThumbnailViewModel::class.java ->
- TaskThumbnailViewModelImpl(getThumbnailPositionUseCase = inject())
TaskOverlayViewModel::class.java -> {
val task = extras["Task"] as Task
TaskOverlayViewModel(
@@ -188,13 +184,12 @@
IsThumbnailValidUseCase::class.java ->
IsThumbnailValidUseCase(rotationStateRepository = inject())
GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject())
- GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase()
GetThumbnailPositionUseCase::class.java ->
GetThumbnailPositionUseCase(
deviceProfileRepository = inject(),
rotationStateRepository = inject(),
- tasksRepository = inject(),
+ previewPositionHelper = PreviewPositionHelper(),
)
OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
else -> {
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
similarity index 62%
rename from quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
rename to quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
index bea1d07..8501382 100644
--- a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,28 +14,30 @@
* limitations under the License.
*/
-package com.android.quickstep.recents.usecase
+package com.android.quickstep.recents.domain.usecase
import android.graphics.Matrix
import android.graphics.Rect
-import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
import com.android.quickstep.recents.data.RecentsRotationStateRepository
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.model.ThumbnailData
import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
/** Use case for retrieving [Matrix] for positioning Thumbnail in a View */
class GetThumbnailPositionUseCase(
private val deviceProfileRepository: RecentsDeviceProfileRepository,
private val rotationStateRepository: RecentsRotationStateRepository,
- private val tasksRepository: RecentTasksRepository,
- private val previewPositionHelper: PreviewPositionHelper = PreviewPositionHelper(),
+ private val previewPositionHelper: PreviewPositionHelper,
) {
- fun run(taskId: Int, width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
- val thumbnailData =
- tasksRepository.getCurrentThumbnailById(taskId) ?: return MissingThumbnail
- val thumbnail = thumbnailData.thumbnail ?: return MissingThumbnail
+ operator fun invoke(
+ thumbnailData: ThumbnailData?,
+ width: Int,
+ height: Int,
+ isRtl: Boolean,
+ ): ThumbnailPosition {
+ val thumbnail =
+ thumbnailData?.thumbnail ?: return ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+
previewPositionHelper.updateThumbnailMatrix(
Rect(0, 0, thumbnail.width, thumbnail.height),
thumbnailData,
@@ -45,9 +47,11 @@
rotationStateRepository.getRecentsRotationState().activityRotation,
isRtl,
)
- return MatrixScaling(
- previewPositionHelper.matrix,
- previewPositionHelper.isOrientationChanged,
+ return ThumbnailPosition(
+ matrix = previewPositionHelper.matrix,
+ isRotated = previewPositionHelper.isOrientationChanged,
)
}
}
+
+data class ThumbnailPosition(val matrix: Matrix, val isRotated: Boolean)
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
index 0b299ee..3c4a384 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -24,7 +24,9 @@
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
+import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -48,6 +50,7 @@
private val getTaskUseCase: GetTaskUseCase,
private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase,
private val isThumbnailValidUseCase: IsThumbnailValidUseCase,
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
dispatcherProvider: DispatcherProvider,
) {
private var taskIds = MutableStateFlow(emptySet<Int>())
@@ -83,6 +86,19 @@
fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean =
isThumbnailValidUseCase(thumbnail, width, height)
+ fun getThumbnailPosition(
+ thumbnail: ThumbnailData?,
+ width: Int,
+ height: Int,
+ isRtl: Boolean,
+ ): ThumbnailPosition =
+ getThumbnailPositionUseCase(
+ thumbnailData = thumbnail,
+ width = width,
+ height = height,
+ isRtl = isRtl,
+ )
+
private fun mapToTaskTile(tasks: List<TaskData>, isLiveTile: Boolean): TaskTileUiState {
val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData
return TaskTileUiState(
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
deleted file mode 100644
index b9e9e02..0000000
--- a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
+++ /dev/null
@@ -1,26 +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.quickstep.recents.usecase
-
-import android.graphics.Bitmap
-import com.android.quickstep.recents.data.RecentTasksRepository
-
-/** 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? = taskRepository.getCurrentThumbnailById(taskId)?.thumbnail
-}
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
deleted file mode 100644
index 1a1bef7..0000000
--- a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
+++ /dev/null
@@ -1,26 +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.quickstep.recents.usecase
-
-import android.graphics.Matrix
-
-/** State on how a task Thumbnail can fit on given canvas */
-sealed class ThumbnailPositionState {
- data object MissingThumbnail : ThumbnailPositionState()
-
- data class MatrixScaling(val matrix: Matrix, val isRotated: Boolean) : ThumbnailPositionState()
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 63e93ba..e91073a 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -18,7 +18,9 @@
import android.content.Context
import android.graphics.Color
+import android.graphics.Matrix
import android.graphics.Outline
+import android.graphics.Path
import android.graphics.Rect
import android.graphics.drawable.ShapeDrawable
import android.util.AttributeSet
@@ -34,34 +36,15 @@
import com.android.launcher3.R
import com.android.launcher3.util.MultiPropertyFactory
import com.android.launcher3.util.ViewPool
-import com.android.launcher3.util.coroutines.DispatcherProvider
-import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.views.FixedSizeImageView
import com.android.quickstep.views.TaskThumbnailViewHeader
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
- private val recentsCoroutineScope: CoroutineScope = RecentsDependencies.get()
- private val dispatcherProvider: DispatcherProvider = RecentsDependencies.get()
-
- // This is initialised here and set in onAttachedToWindow because onLayout can be called before
- // onAttachedToWindow so this property needs to be initialised as it is used below.
- private lateinit var viewModel: TaskThumbnailViewModel
-
- private lateinit var viewAttachedScope: CoroutineScope
-
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 thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) }
@@ -70,10 +53,22 @@
private val dimAlpha: MultiPropertyFactory<View> by lazy {
MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf)
}
+ private val outlinePath = Path()
+ private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null
private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
private var uiState: TaskThumbnailUiState = Uninitialized
+
+ /**
+ * Sets the outline bounds of the view. Default to use view's bound as outline when set to null.
+ */
+ var outlineBounds: Rect? = null
+ set(value) {
+ field = value
+ invalidateOutline()
+ }
+
private val bounds = Rect()
var cornerRadius: Float = 0f
@@ -94,36 +89,37 @@
override fun onFinishInflate() {
super.onFinishInflate()
-
maybeCreateHeader()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
- viewAttachedScope =
- CoroutineScope(
- SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskThumbnailView")
- )
- viewModel = RecentsDependencies.get(this)
clipToOutline = true
outlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
- outline.setRoundRect(bounds, cornerRadius)
+ val outlineRect = outlineBounds ?: bounds
+ outlinePath.apply {
+ rewind()
+ addRoundRect(
+ outlineRect.left.toFloat(),
+ outlineRect.top.toFloat(),
+ outlineRect.right.toFloat(),
+ outlineRect.bottom.toFloat(),
+ cornerRadius / scaleX,
+ cornerRadius / scaleY,
+ Path.Direction.CW,
+ )
+ }
+ outline.setPath(outlinePath)
}
}
}
- // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
- fun destroyScopes() {
- val scopeToCancel = viewAttachedScope
- recentsCoroutineScope.launch(dispatcherProvider.background) {
- scopeToCancel.cancel("TaskThumbnailView detaching from window")
- }
- }
-
override fun onRecycle() {
uiState = Uninitialized
+ onSizeChanged = null
+ outlineBounds = null
resetViews()
}
@@ -159,11 +155,13 @@
splashIcon.alpha = value
}
+ fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) {
+ onSizeChanged = action
+ }
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
- if (uiState is SnapshotSplash) {
- setImageMatrix()
- }
+ onSizeChanged?.invoke(width, height)
bounds.set(0, 0, w, h)
invalidateOutline()
}
@@ -220,11 +218,12 @@
drawBackground(snapshot.backgroundColor)
thumbnailView.setImageBitmap(snapshot.bitmap)
thumbnailView.isInvisible = false
- setImageMatrix()
}
- private fun setImageMatrix() {
- thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
+ fun setImageMatrix(matrix: Matrix) {
+ if (uiState is SnapshotSplash) {
+ thumbnailView.imageMatrix = matrix
+ }
}
private fun logDebug(message: String) {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 81a904b..9bff3ac 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -19,9 +19,7 @@
import android.graphics.Matrix
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
@@ -36,7 +34,7 @@
private val task: Task,
recentsViewData: RecentsViewData,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
- recentTasksRepository: RecentTasksRepository,
+ private val recentTasksRepository: RecentTasksRepository,
dispatcherProvider: DispatcherProvider,
) {
val overlayState =
@@ -60,22 +58,17 @@
.flowOn(dispatcherProvider.background)
fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
- val matrix: Matrix
- val isRotated: Boolean
- when (
- val thumbnailPositionState =
- getThumbnailPositionUseCase.run(task.key.id, width, height, isRtl)
- ) {
- is MatrixScaling -> {
- matrix = thumbnailPositionState.matrix
- isRotated = thumbnailPositionState.isRotated
- }
- is MissingThumbnail -> {
- matrix = Matrix.IDENTITY_MATRIX
- isRotated = false
- }
- }
- return ThumbnailPositionState(matrix, isRotated)
+ val thumbnailPositionState =
+ getThumbnailPositionUseCase(
+ thumbnailData = recentTasksRepository.getCurrentThumbnailById(task.key.id),
+ width = width,
+ height = height,
+ isRtl = isRtl,
+ )
+ return ThumbnailPositionState(
+ thumbnailPositionState.matrix,
+ thumbnailPositionState.isRotated,
+ )
}
data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
deleted file mode 100644
index e641737..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,28 +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.quickstep.task.viewmodel
-
-import android.graphics.Matrix
-
-/** ViewModel for representing TaskThumbnails */
-interface TaskThumbnailViewModel {
- /** Attaches this ViewModel to a specific task id for it to provide data from. */
- fun bind(taskId: Int)
-
- /** Returns a Matrix which can be applied to the snapshot */
- fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
deleted file mode 100644
index 94c40d1..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ /dev/null
@@ -1,47 +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 goveryning permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.viewmodel
-
-import android.app.ActivityTaskManager.INVALID_TASK_ID
-import android.graphics.Matrix
-import android.util.Log
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState
-
-class TaskThumbnailViewModelImpl(
- private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
-) : TaskThumbnailViewModel {
- private var taskId: Int = INVALID_TASK_ID
-
- override fun bind(taskId: Int) {
- Log.d(TAG, "bind taskId: $taskId")
- this.taskId = taskId
- }
-
- override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix =
- when (
- val thumbnailPositionState =
- getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
- ) {
- is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
- is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
- }
-
- private companion object {
- const val TAG = "TaskThumbnailViewModel"
- }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 88d14b7..a9dbbf2 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -127,7 +127,7 @@
val drawable = getDrawable(container.iconView, splitSelectSource)
return SplitAnimInitProps(
container.snapshotView,
- container.splitAnimationThumbnail,
+ container.thumbnail,
drawable,
fadeWithThumbnail = true,
isStagedTask = true,
@@ -147,7 +147,7 @@
val drawable = getDrawable(it.iconView, splitSelectSource)
return SplitAnimInitProps(
it.snapshotView,
- it.splitAnimationThumbnail,
+ it.thumbnail,
drawable,
fadeWithThumbnail = true,
isStagedTask = true,
@@ -215,13 +215,13 @@
if (enableOverviewIconMenu()) {
builder.add(
ObjectAnimator.ofFloat(
- (iconView as IconAppChipView).splitTranslationX,
+ (iconView as IconAppChipView).getSplitTranslationX(),
MULTI_PROPERTY_VALUE,
0f,
)
)
builder.add(
- ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f)
+ ObjectAnimator.ofFloat(iconView.getSplitTranslationY(), MULTI_PROPERTY_VALUE, 0f)
)
}
@@ -985,12 +985,11 @@
val splitTree: Pair<Change, List<Change>>? = extractTopParentAndChildren(transitionInfo)
check(splitTree != null) { "Could not find a split root candidate" }
val rootCandidate = splitTree.first
- val stageRootTaskIds: Set<Int> = splitTree.second
- .map { it.taskInfo!!.taskId }
- .toSet()
- val leafTasks: List<Change> = transitionInfo.changes
- .filter { it.taskInfo != null && it.taskInfo!!.parentTaskId in stageRootTaskIds}
- .toList()
+ val stageRootTaskIds: Set<Int> = splitTree.second.map { it.taskInfo!!.taskId }.toSet()
+ val leafTasks: List<Change> =
+ transitionInfo.changes
+ .filter { it.taskInfo != null && it.taskInfo!!.parentTaskId in stageRootTaskIds }
+ .toList()
// Starting position is a 34% size tile centered in the middle of the screen.
// Ending position is the full device screen.
@@ -1031,8 +1030,13 @@
val endAbsBounds = leaf.endAbsBounds
t.setAlpha(leaf.leash, 1f)
- t.setCrop(leaf.leash, 0f, 0f,
- endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
+ t.setCrop(
+ leaf.leash,
+ 0f,
+ 0f,
+ endAbsBounds.width().toFloat(),
+ endAbsBounds.height().toFloat(),
+ )
t.setPosition(leaf.leash, 0f, 0f)
}
@@ -1040,10 +1044,18 @@
val endAbsBounds = stageRoot.endAbsBounds
t.setAlpha(stageRoot.leash, 1f)
- t.setCrop(stageRoot.leash, 0f, 0f,
- endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
- t.setPosition(stageRoot.leash, endAbsBounds.left.toFloat(),
- endAbsBounds.top.toFloat())
+ t.setCrop(
+ stageRoot.leash,
+ 0f,
+ 0f,
+ endAbsBounds.width().toFloat(),
+ endAbsBounds.height().toFloat(),
+ )
+ t.setPosition(
+ stageRoot.leash,
+ endAbsBounds.left.toFloat(),
+ endAbsBounds.top.toFloat(),
+ )
}
t.apply()
}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
deleted file mode 100644
index 35e90f2..0000000
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
-
-import com.android.launcher3.util.IntArray;
-
-import java.lang.annotation.Retention;
-import java.util.List;
-
-/**
- * Helper class for navigating RecentsView grid tasks via arrow keys and tab.
- */
-public class TaskGridNavHelper {
- public static final int CLEAR_ALL_PLACEHOLDER_ID = -1;
- public static final int ADD_DESK_PLACEHOLDER_ID = -2;
-
- public static final int DIRECTION_UP = 0;
- public static final int DIRECTION_DOWN = 1;
- public static final int DIRECTION_LEFT = 2;
- public static final int DIRECTION_RIGHT = 3;
- public static final int DIRECTION_TAB = 4;
-
- @Retention(SOURCE)
- @IntDef({DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TAB})
- public @interface TASK_NAV_DIRECTION {}
-
- private final IntArray mOriginalTopRowIds;
- private final IntArray mTopRowIds = new IntArray();
- private final IntArray mBottomRowIds = new IntArray();
-
- public TaskGridNavHelper(IntArray topIds, IntArray bottomIds,
- List<Integer> largeTileIds, boolean hasAddDesktopButton) {
- mOriginalTopRowIds = topIds.clone();
- generateTaskViewIdGrid(topIds, bottomIds, largeTileIds, hasAddDesktopButton);
- }
-
- private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray,
- List<Integer> largeTileIds, boolean hasAddDesktopButton) {
- // Add AddDesktopButton and lage tiles to both rows.
- if (hasAddDesktopButton) {
- mTopRowIds.add(ADD_DESK_PLACEHOLDER_ID);
- mBottomRowIds.add(ADD_DESK_PLACEHOLDER_ID);
- }
- for (Integer tileId : largeTileIds) {
- mTopRowIds.add(tileId);
- mBottomRowIds.add(tileId);
- }
-
- // Add row ids to their respective rows.
- mTopRowIds.addAll(topRowIdArray);
- mBottomRowIds.addAll(bottomRowIdArray);
-
- // Fill in the shorter array with the ids from the longer one.
- while (mTopRowIds.size() > mBottomRowIds.size()) {
- mBottomRowIds.add(mTopRowIds.get(mBottomRowIds.size()));
- }
- while (mBottomRowIds.size() > mTopRowIds.size()) {
- mTopRowIds.add(mBottomRowIds.get(mTopRowIds.size()));
- }
-
- // Add the clear all button to the end of both arrays.
- mTopRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
- mBottomRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
- }
-
- /**
- * Returns the id of the next page in the grid or -1 for the clear all button.
- */
- public int getNextGridPage(int currentPageTaskViewId, int delta,
- @TASK_NAV_DIRECTION int direction, boolean cycle) {
- boolean inTop = mTopRowIds.contains(currentPageTaskViewId);
- int index = inTop ? mTopRowIds.indexOf(currentPageTaskViewId)
- : mBottomRowIds.indexOf(currentPageTaskViewId);
- int maxSize = Math.max(mTopRowIds.size(), mBottomRowIds.size());
- int nextIndex = index + delta;
-
- switch (direction) {
- case DIRECTION_UP:
- case DIRECTION_DOWN: {
- return inTop ? mBottomRowIds.get(index) : mTopRowIds.get(index);
- }
- case DIRECTION_LEFT: {
- int boundedIndex = cycle ? nextIndex % maxSize : Math.min(nextIndex, maxSize - 1);
- return inTop ? mTopRowIds.get(boundedIndex)
- : mBottomRowIds.get(boundedIndex);
- }
- case DIRECTION_RIGHT: {
- int boundedIndex =
- cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex)
- : Math.max(nextIndex, 0);
- boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
- return inOriginalTop ? mTopRowIds.get(boundedIndex)
- : mBottomRowIds.get(boundedIndex);
- }
- case DIRECTION_TAB: {
- int boundedIndex =
- cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize)
- : Math.min(nextIndex, maxSize - 1);
- if (delta >= 0) {
- return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
- ? mBottomRowIds.get(index)
- : mTopRowIds.get(boundedIndex);
- } else {
- if (mTopRowIds.contains(currentPageTaskViewId)) {
- if (boundedIndex < 0) {
- // If no cycling, always return the first task.
- return mTopRowIds.get(0);
- } else {
- return mBottomRowIds.get(boundedIndex);
- }
- } else {
- // Go up to top if there is task above
- return mTopRowIds.get(index) != mBottomRowIds.get(index)
- ? mTopRowIds.get(index)
- : mBottomRowIds.get(boundedIndex);
- }
- }
- }
- default:
- return currentPageTaskViewId;
- }
- }
-
- /**
- * Returns the column of a task's id in the grid.
- */
- public int getColumn(int taskViewId) {
- return mTopRowIds.contains(taskViewId) ? mTopRowIds.indexOf(taskViewId)
- : mBottomRowIds.indexOf(taskViewId);
- }
-}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
new file mode 100644
index 0000000..0e78801
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util
+
+import com.android.launcher3.util.IntArray
+import kotlin.math.abs
+import kotlin.math.max
+
+/** Helper class for navigating RecentsView grid tasks via arrow keys and tab. */
+class TaskGridNavHelper(
+ private val topIds: IntArray,
+ bottomIds: IntArray,
+ largeTileIds: List<Int>,
+ hasAddDesktopButton: Boolean,
+) {
+ private val topRowIds = mutableListOf<Int>()
+ private val bottomRowIds = mutableListOf<Int>()
+
+ init {
+ // Add AddDesktopButton and lage tiles to both rows.
+ if (hasAddDesktopButton) {
+ topRowIds += ADD_DESK_PLACEHOLDER_ID
+ bottomRowIds += ADD_DESK_PLACEHOLDER_ID
+ }
+ topRowIds += largeTileIds
+ bottomRowIds += largeTileIds
+
+ // Add row ids to their respective rows.
+ topRowIds += topIds
+ bottomRowIds += bottomIds
+
+ // Fill in the shorter array with the ids from the longer one.
+ topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0))
+ bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0))
+
+ // Add the clear all button to the end of both arrays.
+ topRowIds += CLEAR_ALL_PLACEHOLDER_ID
+ bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID
+ }
+
+ /** Returns the id of the next page in the grid or -1 for the clear all button. */
+ fun getNextGridPage(
+ currentPageTaskViewId: Int,
+ delta: Int,
+ direction: TaskNavDirection,
+ cycle: Boolean,
+ ): Int {
+ val inTop = topRowIds.contains(currentPageTaskViewId)
+ val index =
+ if (inTop) topRowIds.indexOf(currentPageTaskViewId)
+ else bottomRowIds.indexOf(currentPageTaskViewId)
+ val maxSize = max(topRowIds.size, bottomRowIds.size)
+ val nextIndex = index + delta
+
+ return when (direction) {
+ TaskNavDirection.UP,
+ TaskNavDirection.DOWN -> {
+ if (inTop) bottomRowIds[index] else topRowIds[index]
+ }
+ TaskNavDirection.LEFT -> {
+ val boundedIndex =
+ if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1)
+ if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+ }
+ TaskNavDirection.RIGHT -> {
+ val boundedIndex =
+ if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex)
+ else nextIndex.coerceAtLeast(0)
+ val inOriginalTop = topIds.contains(currentPageTaskViewId)
+ if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+ }
+ TaskNavDirection.TAB -> {
+ val boundedIndex =
+ if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize)
+ else nextIndex.coerceAtMost(maxSize - 1)
+ if (delta >= 0) {
+ if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index]
+ else topRowIds[boundedIndex]
+ } else {
+ if (topRowIds.contains(currentPageTaskViewId)) {
+ if (boundedIndex < 0) {
+ // If no cycling, always return the first task.
+ topRowIds[0]
+ } else {
+ bottomRowIds[boundedIndex]
+ }
+ } else {
+ // Go up to top if there is task above
+ if (topRowIds[index] != bottomRowIds[index]) topRowIds[index]
+ else bottomRowIds[boundedIndex]
+ }
+ }
+ }
+ else -> currentPageTaskViewId
+ }
+ }
+
+ /**
+ * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab
+ * navigation, starting from the initial TaskView ID, towards the start or end of the grid.
+ *
+ * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
+ * negative value moves backward towards the beginning. The offset is the distance between
+ * columns the tasks are in.
+ */
+ fun gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId: Int,
+ towardsStart: Boolean,
+ ): Sequence<Pair<Int, Int>> = sequence {
+ val draggedTaskViewColumn = getColumn(initialTaskViewId)
+ var nextTaskViewId: Int = initialTaskViewId
+ var previousTaskViewId: Int = Int.MIN_VALUE
+ while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) {
+ previousTaskViewId = nextTaskViewId
+ nextTaskViewId =
+ getNextGridPage(
+ nextTaskViewId,
+ if (towardsStart) -1 else 1,
+ TaskNavDirection.TAB,
+ cycle = false,
+ )
+ if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) {
+ val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn)
+ yield(Pair(nextTaskViewId, columnOffset))
+ }
+ }
+ }
+
+ /** Returns the column of a task's id in the grid. */
+ private fun getColumn(taskViewId: Int): Int =
+ if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId)
+ else bottomRowIds.indexOf(taskViewId)
+
+ enum class TaskNavDirection {
+ UP,
+ DOWN,
+ LEFT,
+ RIGHT,
+ TAB,
+ }
+
+ companion object {
+ const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1
+ const val ADD_DESK_PLACEHOLDER_ID: Int = -2
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
index 91e8376..6e2d469 100644
--- a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
@@ -16,16 +16,11 @@
package com.android.quickstep.util;
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-
-import android.app.Activity;
import android.app.ActivityManager;
import android.util.Log;
import androidx.annotation.NonNull;
-import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
-import com.android.quickstep.RecentsModel;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 09e9c8b..b844079 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -97,6 +97,8 @@
private final FullscreenDrawParams mCurrentFullscreenParams;
public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
+ public final AnimatedFloat taskGridTranslationX = new AnimatedFloat();
+ public final AnimatedFloat taskGridTranslationY = new AnimatedFloat();
// Carousel properties
public final AnimatedFloat carouselScale = new AnimatedFloat();
@@ -445,6 +447,7 @@
taskPrimaryTranslation.value);
mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
taskSecondaryTranslation.value);
+ mMatrix.postTranslate(taskGridTranslationX.value, taskGridTranslationY.value);
mMatrix.postScale(carouselScale.value, carouselScale.value,
mIsRecentsRtl ? mCarouselTaskSize.right : mCarouselTaskSize.left,
@@ -484,6 +487,8 @@
+ " taskRect: " + mTaskRect
+ " taskPrimaryT: " + taskPrimaryTranslation.value
+ " taskSecondaryT: " + taskSecondaryTranslation.value
+ + " taskGridTranslationX: " + taskGridTranslationX.value
+ + " taskGridTranslationY: " + taskGridTranslationY.value
+ " recentsPrimaryT: " + recentsViewPrimaryTranslation.value
+ " recentsSecondaryT: " + recentsViewSecondaryTranslation.value
+ " recentsScroll: " + recentsViewScroll.value
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index 1c1fbd8..716803a 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -222,7 +222,7 @@
return null;
}
- // Pubic getters so outside packages can read the values.
+ // Public getters so outside packages can read the values.
public float getProgress() {
return mProgress;
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index da160f1..fbda3b3 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -21,6 +21,7 @@
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
+import android.graphics.Rect.intersects
import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
@@ -30,6 +31,7 @@
import android.view.ViewStub
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
+import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix
import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
@@ -57,6 +59,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.RecentsOrientedState
+import kotlin.math.roundToInt
/** TaskView that contains all tasks that are part of the desktop. */
class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
@@ -102,7 +105,7 @@
* Holds the default (user placed) positions of task windows. This can be moved into the
* viewModel once RefactorTaskThumbnail has been launched.
*/
- private var defaultTaskPositions: List<DesktopTaskBoundsData> = emptyList()
+ private var fullscreenTaskPositions: List<DesktopTaskBoundsData> = emptyList()
/**
* When enableDesktopExplodedView is enabled, this controls the gradual transition from the
@@ -172,42 +175,44 @@
val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
- val containerWidth = layoutParams.width
- val containerHeight = layoutParams.height - thumbnailTopMarginPx
+ val taskViewWidth = layoutParams.width
+ val taskViewHeight = layoutParams.height - thumbnailTopMarginPx
BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
- val windowWidth = tempPointF.x.toInt()
- val windowHeight = tempPointF.y.toInt()
- val scaleWidth = containerWidth / windowWidth.toFloat()
- val scaleHeight = containerHeight / windowHeight.toFloat()
+ val screenWidth = tempPointF.x.toInt()
+ val screenHeight = tempPointF.y.toInt()
+ val screenRect = Rect(0, 0, screenWidth, screenHeight)
+ val scaleWidth = taskViewWidth / screenWidth.toFloat()
+ val scaleHeight = taskViewHeight / screenHeight.toFloat()
taskContainers.forEach {
val taskId = it.task.key.id
- val defaultPosition = defaultTaskPositions.firstOrNull { it.taskId == taskId } ?: return
- val position =
+ val fullscreenTaskPosition =
+ fullscreenTaskPositions.firstOrNull { it.taskId == taskId } ?: return
+ val overviewTaskPosition =
if (enableDesktopExplodedView()) {
viewModel!!
.organizedDesktopTaskPositions
.firstOrNull { it.taskId == taskId }
?.let { organizedPosition ->
- TEMP_RECT.apply {
+ TEMP_OVERVIEW_TASK_POSITION.apply {
lerpRect(
- defaultPosition.bounds,
+ fullscreenTaskPosition.bounds,
organizedPosition.bounds,
explodeProgress,
)
}
- } ?: defaultPosition.bounds
+ } ?: fullscreenTaskPosition.bounds
} else {
- defaultPosition.bounds
+ fullscreenTaskPosition.bounds
}
if (enableDesktopExplodedView()) {
getRemoteTargetHandle(taskId)?.let { remoteTargetHandle ->
val fromRect =
- TEMP_RECTF1.apply {
- set(defaultPosition.bounds)
+ TEMP_FROM_RECTF.apply {
+ set(fullscreenTaskPosition.bounds)
scale(scaleWidth)
offset(
lastComputedTaskSize.left.toFloat(),
@@ -215,8 +220,8 @@
)
}
val toRect =
- TEMP_RECTF2.apply {
- set(position)
+ TEMP_TO_RECTF.apply {
+ set(overviewTaskPosition)
scale(scaleWidth)
offset(
lastComputedTaskSize.left.toFloat(),
@@ -230,10 +235,10 @@
}
}
- val taskLeft = position.left * scaleWidth
- val taskTop = position.top * scaleHeight
- val taskWidth = position.width() * scaleWidth
- val taskHeight = position.height() * scaleHeight
+ val taskLeft = overviewTaskPosition.left * scaleWidth
+ val taskTop = overviewTaskPosition.top * scaleHeight
+ val taskWidth = overviewTaskPosition.width() * scaleWidth
+ val taskHeight = overviewTaskPosition.height() * scaleHeight
// TODO(b/394660950): Revisit the choice to update the layout when explodeProgress == 1.
// To run the explode animation in reverse, it may be simpler to use translation/scale
// for all cases where the progress is non-zero.
@@ -254,6 +259,24 @@
leftMargin = taskLeft.toInt()
topMargin = taskTop.toInt()
}
+
+ if (
+ enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail()
+ ) {
+ it.thumbnailView.outlineBounds =
+ if (intersects(overviewTaskPosition, screenRect))
+ Rect(overviewTaskPosition).apply {
+ intersectUnchecked(screenRect)
+ // Offset to 0,0 to transform into TaskThumbnailView's coordinate
+ // system.
+ offset(-overviewTaskPosition.left, -overviewTaskPosition.top)
+ left = (left * scaleWidth).roundToInt()
+ top = (top * scaleHeight).roundToInt()
+ right = (right * scaleWidth).roundToInt()
+ bottom = (bottom * scaleHeight).roundToInt()
+ }
+ else null
+ }
} else {
// During the animation, apply translation and scale such that the view is
// transformed to where we want, without triggering layout.
@@ -346,13 +369,13 @@
val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt())
DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4)
- defaultTaskPositions =
+ fullscreenTaskPositions =
taskContainers.map {
DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS)
}
if (enableDesktopExplodedView()) {
- viewModel?.organizeDesktopTasks(desktopSize, defaultTaskPositions)
+ viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions)
}
positionTaskWindows()
}
@@ -449,8 +472,8 @@
private const val VIEW_POOL_INITIAL_SIZE = 0
private val DEFAULT_BOUNDS = Rect()
// Temporaries used for various purposes to avoid allocations.
- private val TEMP_RECT = Rect()
- private val TEMP_RECTF1 = RectF()
- private val TEMP_RECTF2 = RectF()
+ private val TEMP_OVERVIEW_TASK_POSITION = Rect()
+ private val TEMP_FROM_RECTF = RectF()
+ private val TEMP_TO_RECTF = RectF()
}
}
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 229c8f5..2abfb13 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -171,12 +171,10 @@
val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2
// setMaxWidth() needs to be called before mIconView.setIconOrientation which is
// called in the super below.
- (leftTopTaskContainer.iconView as IconAppChipView).setMaxWidth(
+ (leftTopTaskContainer.iconView as IconAppChipView).maxWidth =
groupedTaskViewSizes.first.x - iconMargins
- )
- (rightBottomTaskContainer.iconView as IconAppChipView).setMaxWidth(
+ (rightBottomTaskContainer.iconView as IconAppChipView).maxWidth =
groupedTaskViewSizes.second.x - iconMargins
- )
}
}
super.setOrientationState(orientationState)
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
deleted file mode 100644
index 5270477..0000000
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ /dev/null
@@ -1,471 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import static com.android.app.animation.Interpolators.EMPHASIZED;
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.RectEvaluator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewAnimationUtils;
-import android.view.ViewOutlineProvider;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiPropertyFactory;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * An icon app menu view which can be used in place of an IconView in overview TaskViews.
- */
-public class IconAppChipView extends FrameLayout implements TaskViewIcon {
-
- private static final int MENU_BACKGROUND_REVEAL_DURATION = 417;
- private static final int MENU_BACKGROUND_HIDE_DURATION = 333;
-
- private static final int NUM_ALPHA_CHANNELS = 4;
- private static final int INDEX_CONTENT_ALPHA = 0;
- private static final int INDEX_COLOR_FILTER_ALPHA = 1;
- private static final int INDEX_MODAL_ALPHA = 2;
- /** Used to hide the app chip for 90:10 flex split. */
- private static final int INDEX_MINIMUM_RATIO_ALPHA = 3;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- private View mMenuAnchorView;
- private IconView mIconView;
- // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
- private TextView mIconTextCollapsedView;
- private TextView mIconTextExpandedView;
- private ImageView mIconArrowView;
- private final Rect mBackgroundRelativeLtrLocation = new Rect();
- final RectEvaluator mBackgroundAnimationRectEvaluator =
- new RectEvaluator(mBackgroundRelativeLtrLocation);
- private final int mCollapsedMenuDefaultWidth;
- private final int mExpandedMenuDefaultWidth;
- private final int mCollapsedMenuDefaultHeight;
- private final int mExpandedMenuDefaultHeight;
- private final int mIconMenuMarginTopStart;
- private final int mMenuToChipGap;
- private final int mBackgroundMarginTopStart;
- private final int mAppNameHorizontalMargin;
- private final int mIconViewMarginStart;
- private final int mAppIconSize;
- private final int mArrowSize;
- private final int mIconViewDrawableExpandedSize;
- private final int mArrowMarginEnd;
- private AnimatorSet mAnimator;
-
- private int mMaxWidth = Integer.MAX_VALUE;
-
- private static final int INDEX_SPLIT_TRANSLATION = 0;
- private static final int INDEX_MENU_TRANSLATION = 1;
- private static final int INDEX_COUNT_TRANSLATION = 2;
-
- private final MultiPropertyFactory<View> mViewTranslationX;
- private final MultiPropertyFactory<View> mViewTranslationY;
-
- /**
- * Gets the view split x-axis translation
- */
- public MultiPropertyFactory<View>.MultiProperty getSplitTranslationX() {
- return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION);
- }
-
- /**
- * Sets the view split x-axis translation
- * @param translationX x-axis translation
- */
- public void setSplitTranslationX(float translationX) {
- getSplitTranslationX().setValue(translationX);
- }
-
- /**
- * Gets the view split y-axis translation
- */
- public MultiPropertyFactory<View>.MultiProperty getSplitTranslationY() {
- return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION);
- }
-
- /**
- * Sets the view split y-axis translation
- * @param translationY y-axis translation
- */
- public void setSplitTranslationY(float translationY) {
- getSplitTranslationY().setValue(translationY);
- }
-
- /**
- * Gets the menu x-axis translation for split task
- */
- public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() {
- return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
- }
-
- /**
- * Gets the menu y-axis translation for split task
- */
- public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
- return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
- }
-
- public IconAppChipView(Context context) {
- this(context, null);
- }
-
- public IconAppChipView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- Resources res = getResources();
- mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
- mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
-
- // Menu dimensions
- mCollapsedMenuDefaultWidth =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width);
- mExpandedMenuDefaultWidth =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width);
- mCollapsedMenuDefaultHeight =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height);
- mExpandedMenuDefaultHeight =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height);
- mIconMenuMarginTopStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
- mMenuToChipGap = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_expanded_gap);
-
- // Background dimensions
- mBackgroundMarginTopStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
-
- // Contents dimensions
- mAppNameHorizontalMargin = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed);
- mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin);
- mIconViewMarginStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_view_start_margin);
- mAppIconSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size);
- mArrowSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_arrow_size);
- mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
-
- mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
- INDEX_COUNT_TRANSLATION,
- Float::sum);
- mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
- INDEX_COUNT_TRANSLATION,
- Float::sum);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mIconView = findViewById(R.id.icon_view);
- mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed);
- mIconTextExpandedView = findViewById(R.id.icon_text_expanded);
- mIconArrowView = findViewById(R.id.icon_arrow);
- mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor);
- }
-
- protected IconView getIconView() {
- return mIconView;
- }
-
- @Override
- public void setText(CharSequence text) {
- if (mIconTextCollapsedView != null) {
- mIconTextCollapsedView.setText(text);
- }
- if (mIconTextExpandedView != null) {
- mIconTextExpandedView.setText(text);
- }
- }
-
- @Override
- public Drawable getDrawable() {
- return mIconView == null ? null : mIconView.getDrawable();
- }
-
- @Override
- public void setDrawable(Drawable icon) {
- if (mIconView != null) {
- mIconView.setDrawable(icon);
- }
- }
-
- @Override
- public void setDrawableSize(int iconWidth, int iconHeight) {
- if (mIconView != null) {
- mIconView.setDrawableSize(iconWidth, iconHeight);
- }
- }
-
- /**
- * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
- * split screen.
- */
- public void setMaxWidth(int maxWidth) {
- // Width showing only the app icon and arrow. Max width should not be set to less than this.
- int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd;
- mMaxWidth = Math.max(maxWidth, minimumMaxWidth);
- }
-
- @Override
- public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
- RecentsPagedOrientationHandler orientationHandler =
- orientationState.getOrientationHandler();
- // Layout params for anchor view
- LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams();
- anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap;
- mMenuAnchorView.setLayoutParams(anchorLayoutParams);
-
- // Layout Params for the Menu View (this)
- LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
- iconMenuParams.width = mExpandedMenuDefaultWidth;
- iconMenuParams.height = mExpandedMenuDefaultHeight;
- orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart,
- mIconMenuMarginTopStart);
- setLayoutParams(iconMenuParams);
-
- // Layout params for the background
- Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
- mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds);
- setOutlineProvider(new ViewOutlineProvider() {
- final Rect mRtlAppliedOutlineBounds = new Rect();
- @Override
- public void getOutline(View view, Outline outline) {
- mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation);
- if (isLayoutRtl()) {
- int width = getWidth();
- mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right;
- mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left;
- }
- outline.setRoundRect(
- mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f);
- }
- });
-
- // Layout Params for the Icon View
- LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
- int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart;
- orientationHandler.setIconAppChipChildrenParams(
- iconParams, iconMarginStartRelativeToParent);
-
- mIconView.setLayoutParams(iconParams);
- mIconView.setDrawableSize(mAppIconSize, mAppIconSize);
-
- // Layout Params for the collapsed Icon Text View
- int textMarginStart =
- iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin;
- LayoutParams iconTextCollapsedParams =
- (LayoutParams) mIconTextCollapsedView.getLayoutParams();
- orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart);
- int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart
- - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd;
- iconTextCollapsedParams.width = collapsedTextWidth;
- mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams);
- mIconTextCollapsedView.setAlpha(1f);
-
- // Layout Params for the expanded Icon Text View
- LayoutParams iconTextExpandedParams =
- (LayoutParams) mIconTextExpandedView.getLayoutParams();
- orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart);
- mIconTextExpandedView.setLayoutParams(iconTextExpandedParams);
- mIconTextExpandedView.setAlpha(0f);
- mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth);
-
- // Layout Params for the Icon Arrow View
- LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
- int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
- orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
- mIconArrowView.setPivotY(iconArrowParams.height / 2f);
- mIconArrowView.setLayoutParams(iconArrowParams);
-
- // This method is called twice sometimes (like when rotating split tasks). It is called
- // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
- // a new width. This happens because we update widths on rotation and on measure of
- // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
- // it has just measured, so we explicitly call it here.
- measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY));
- }
-
- @Override
- public void setIconColorTint(int color, float amount) {
- // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
- float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR);
- mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha);
- }
-
- @Override
- public void setContentAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setModalAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setFlexSplitAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MINIMUM_RATIO_ALPHA).setValue(alpha);
- }
-
- @Override
- public int getDrawableWidth() {
- return mIconView == null ? 0 : mIconView.getDrawableWidth();
- }
-
- @Override
- public int getDrawableHeight() {
- return mIconView == null ? 0 : mIconView.getDrawableHeight();
- }
-
- protected void revealAnim(boolean isRevealing) {
- cancelInProgressAnimations();
- final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
- final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds();
- final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation);
- mAnimator = new AnimatorSet();
-
- if (isRevealing) {
- boolean isRtl = isLayoutRtl();
- bringToFront();
- // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
- Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
- mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
- mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth());
- // Animate background clipping
- ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
- mBackgroundAnimationRectEvaluator,
- initialBackground,
- expandedBackgroundBounds);
- backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
- float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize;
- float arrowTranslationX =
- expandedBackgroundBounds.right - collapsedBackgroundBounds.right;
- float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin;
- float iconCenterToTextExpanded =
- mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin;
- float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed;
-
- float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX;
- float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX;
-
- mAnimator.playTogether(
- expandedTextRevealAnim,
- backgroundAnimator,
- ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling),
- ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X,
- textTranslationXWithRtl),
- ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X,
- textTranslationXWithRtl),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
- ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
- ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
- ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1));
- mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
- } else {
- // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
- Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
- mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
- mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth());
-
- // Animate background clipping
- ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
- mBackgroundAnimationRectEvaluator,
- initialBackground,
- collapsedBackgroundBounds);
- backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
- mAnimator.playTogether(
- expandedTextClipAnim,
- backgroundAnimator,
- ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
- ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
- ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1));
- mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
- }
-
- mAnimator.setInterpolator(EMPHASIZED);
- mAnimator.start();
- }
-
- private Rect getCollapsedBackgroundLtrBounds() {
- Rect bounds = new Rect(
- 0,
- 0,
- Math.min(mMaxWidth, mCollapsedMenuDefaultWidth),
- mCollapsedMenuDefaultHeight);
- bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart);
- return bounds;
- }
-
- private Rect getExpandedBackgroundLtrBounds() {
- return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
- }
-
- private void cancelInProgressAnimations() {
- // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
- // expecting to be mutable and will cause a crash if they are re-used.
- if (mAnimator != null && mAnimator.isStarted()) {
- mAnimator.cancel();
- mAnimator = null;
- }
- }
-
- @Override
- public View asView() {
- return this;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
new file mode 100644
index 0000000..85d14cc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewAnimationUtils
+import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.app.animation.Interpolators
+import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.quickstep.util.RecentsOrientedState
+import kotlin.math.max
+import kotlin.math.min
+
+/** An icon app menu view which can be used in place of an IconView in overview TaskViews. */
+class IconAppChipView
+@JvmOverloads
+constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon {
+
+ private var iconView: IconView? = null
+ private var iconArrowView: ImageView? = null
+ private var menuAnchorView: View? = null
+ // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
+ private var iconTextCollapsedView: TextView? = null
+ private var iconTextExpandedView: TextView? = null
+
+ private val backgroundRelativeLtrLocation = Rect()
+ private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation)
+
+ // Menu dimensions
+ private val collapsedMenuDefaultWidth: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width)
+ private val expandedMenuDefaultWidth: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width)
+ private val collapsedMenuDefaultHeight =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height)
+ private val expandedMenuDefaultHeight =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height)
+ private val iconMenuMarginTopStart =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin)
+ private val menuToChipGap: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap)
+
+ // Background dimensions
+ private val backgroundMarginTopStart: Int =
+ resources.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_background_margin_top_start
+ )
+
+ // Contents dimensions
+ private val appNameHorizontalMargin =
+ resources.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed
+ )
+ private val arrowMarginEnd =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin)
+ private val iconViewMarginStart =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin)
+ private val appIconSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size)
+ private val arrowSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size)
+ private val iconViewDrawableExpandedSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size)
+
+ private var animator: AnimatorSet? = null
+
+ private val multiValueAlpha: MultiValueAlpha =
+ MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) }
+
+ private val viewTranslationX: MultiPropertyFactory<View> =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+ private val viewTranslationY: MultiPropertyFactory<View> =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+ var maxWidth = Int.MAX_VALUE
+ /**
+ * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
+ * split screen.
+ */
+ set(value) {
+ // Width showing only the app icon and arrow. Max width should not be set to less than
+ // this.
+ val minMaxWidth = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd
+ field = max(value, minMaxWidth)
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ iconView = findViewById(R.id.icon_view)
+ iconTextCollapsedView = findViewById(R.id.icon_text_collapsed)
+ iconTextExpandedView = findViewById(R.id.icon_text_expanded)
+ iconArrowView = findViewById(R.id.icon_arrow)
+ menuAnchorView = findViewById(R.id.icon_view_menu_anchor)
+ }
+
+ override fun setText(text: CharSequence?) {
+ iconTextCollapsedView?.text = text
+ iconTextExpandedView?.text = text
+ }
+
+ override fun getDrawable(): Drawable? = iconView?.drawable
+
+ override fun setDrawable(icon: Drawable?) {
+ iconView?.drawable = icon
+ }
+
+ override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+ iconView?.setDrawableSize(iconWidth, iconHeight)
+ }
+
+ override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+ val orientationHandler = orientationState.orientationHandler
+ // Layout params for anchor view
+ val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams
+ anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap
+ menuAnchorView!!.layoutParams = anchorLayoutParams
+
+ // Layout Params for the Menu View (this)
+ val iconMenuParams = layoutParams as LayoutParams
+ iconMenuParams.width = expandedMenuDefaultWidth
+ iconMenuParams.height = expandedMenuDefaultHeight
+ orientationHandler.setIconAppChipMenuParams(
+ this,
+ iconMenuParams,
+ iconMenuMarginTopStart,
+ iconMenuMarginTopStart,
+ )
+ layoutParams = iconMenuParams
+
+ // Layout params for the background
+ val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+ backgroundRelativeLtrLocation.set(collapsedBackgroundBounds)
+ outlineProvider =
+ object : ViewOutlineProvider() {
+ val mRtlAppliedOutlineBounds: Rect = Rect()
+
+ override fun getOutline(view: View, outline: Outline) {
+ mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation)
+ if (isLayoutRtl) {
+ val width = width
+ mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right
+ mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left
+ }
+ outline.setRoundRect(
+ mRtlAppliedOutlineBounds,
+ mRtlAppliedOutlineBounds.height() / 2f,
+ )
+ }
+ }
+
+ // Layout Params for the Icon View
+ val iconParams = iconView!!.layoutParams as LayoutParams
+ val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart
+ orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent)
+
+ iconView!!.layoutParams = iconParams
+ iconView!!.setDrawableSize(appIconSize, appIconSize)
+
+ // Layout Params for the collapsed Icon Text View
+ val textMarginStart =
+ iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMargin
+ val iconTextCollapsedParams = iconTextCollapsedView!!.layoutParams as LayoutParams
+ orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart)
+ val collapsedTextWidth =
+ (collapsedBackgroundBounds.width() -
+ iconViewMarginStart -
+ appIconSize -
+ arrowSize -
+ appNameHorizontalMargin -
+ arrowMarginEnd)
+ iconTextCollapsedParams.width = collapsedTextWidth
+ iconTextCollapsedView!!.layoutParams = iconTextCollapsedParams
+ iconTextCollapsedView!!.alpha = 1f
+
+ // Layout Params for the expanded Icon Text View
+ val iconTextExpandedParams = iconTextExpandedView!!.layoutParams as LayoutParams
+ orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart)
+ iconTextExpandedView!!.layoutParams = iconTextExpandedParams
+ iconTextExpandedView!!.alpha = 0f
+ iconTextExpandedView!!.setRevealClip(
+ true,
+ 0f,
+ appIconSize / 2f,
+ collapsedTextWidth.toFloat(),
+ )
+
+ // Layout Params for the Icon Arrow View
+ val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams
+ val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize
+ orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart)
+ iconArrowView!!.pivotY = iconArrowParams.height / 2f
+ iconArrowView!!.layoutParams = iconArrowParams
+
+ // This method is called twice sometimes (like when rotating split tasks). It is called
+ // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
+ // a new width. This happens because we update widths on rotation and on measure of
+ // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
+ // it has just measured, so we explicitly call it here.
+ measure(
+ MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY),
+ )
+ }
+
+ override fun setIconColorTint(color: Int, amount: Float) {
+ // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
+ val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR)
+ multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha
+ }
+
+ override fun setContentAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha
+ }
+
+ override fun setModalAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha
+ }
+
+ override fun setFlexSplitAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha
+ }
+
+ override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0
+
+ override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0
+
+ /** Gets the view split x-axis translation */
+ fun getSplitTranslationX(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationX.get(INDEX_SPLIT_TRANSLATION)
+
+ /**
+ * Sets the view split x-axis translation
+ *
+ * @param value x-axis translation
+ */
+ fun setSplitTranslationX(value: Float) {
+ getSplitTranslationX().value = value
+ }
+
+ /** Gets the view split y-axis translation */
+ fun getSplitTranslationY(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationY[INDEX_SPLIT_TRANSLATION]
+
+ /**
+ * Sets the view split y-axis translation
+ *
+ * @param value y-axis translation
+ */
+ fun setSplitTranslationY(value: Float) {
+ getSplitTranslationY().value = value
+ }
+
+ /** Gets the menu x-axis translation for split task */
+ fun getMenuTranslationX(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationX[INDEX_MENU_TRANSLATION]
+
+ /** Gets the menu y-axis translation for split task */
+ fun getMenuTranslationY(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationY[INDEX_MENU_TRANSLATION]
+
+ internal fun revealAnim(isRevealing: Boolean) {
+ cancelInProgressAnimations()
+ val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+ val expandedBackgroundBounds = getExpandedBackgroundLtrBounds()
+ val initialBackground = Rect(backgroundRelativeLtrLocation)
+ animator = AnimatorSet()
+
+ if (isRevealing) {
+ val isRtl = isLayoutRtl
+ bringToFront()
+ // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+ val expandedTextRevealAnim =
+ ViewAnimationUtils.createCircularReveal(
+ iconTextExpandedView,
+ 0,
+ iconTextExpandedView!!.height / 2,
+ iconTextCollapsedView!!.width.toFloat(),
+ iconTextExpandedView!!.width.toFloat(),
+ )
+ // Animate background clipping
+ val backgroundAnimator =
+ ValueAnimator.ofObject(
+ backgroundAnimationRectEvaluator,
+ initialBackground,
+ expandedBackgroundBounds,
+ )
+ backgroundAnimator.addUpdateListener { invalidateOutline() }
+
+ val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat()
+ val arrowTranslationX =
+ (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat()
+ val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMargin
+ val iconCenterToTextExpanded =
+ iconViewDrawableExpandedSize / 2f + appNameHorizontalMargin
+ val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed
+
+ val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX
+ val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX
+
+ animator!!.playTogether(
+ expandedTextRevealAnim,
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling),
+ ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling),
+ ObjectAnimator.ofFloat(
+ iconTextCollapsedView,
+ TRANSLATION_X,
+ textTranslationXWithRtl,
+ ),
+ ObjectAnimator.ofFloat(
+ iconTextExpandedView,
+ TRANSLATION_X,
+ textTranslationXWithRtl,
+ ),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 0f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 1f),
+ ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
+ ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f),
+ )
+ animator!!.setDuration(MENU_BACKGROUND_REVEAL_DURATION.toLong())
+ } else {
+ // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+ val expandedTextClipAnim =
+ ViewAnimationUtils.createCircularReveal(
+ iconTextExpandedView,
+ 0,
+ iconTextExpandedView!!.height / 2,
+ iconTextExpandedView!!.width.toFloat(),
+ iconTextCollapsedView!!.width.toFloat(),
+ )
+
+ // Animate background clipping
+ val backgroundAnimator =
+ ValueAnimator.ofObject(
+ backgroundAnimationRectEvaluator,
+ initialBackground,
+ collapsedBackgroundBounds,
+ )
+ backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? ->
+ invalidateOutline()
+ }
+
+ animator!!.playTogether(
+ expandedTextClipAnim,
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 1f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 0f),
+ ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f),
+ )
+ animator!!.setDuration(MENU_BACKGROUND_HIDE_DURATION.toLong())
+ }
+
+ animator!!.interpolator = Interpolators.EMPHASIZED
+ animator!!.start()
+ }
+
+ private fun getCollapsedBackgroundLtrBounds(): Rect {
+ val bounds =
+ Rect(0, 0, min(maxWidth, collapsedMenuDefaultWidth), collapsedMenuDefaultHeight)
+ bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart)
+ return bounds
+ }
+
+ private fun getExpandedBackgroundLtrBounds() =
+ Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight)
+
+ private fun cancelInProgressAnimations() {
+ // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
+ // expecting to be mutable and will cause a crash if they are re-used.
+ if (animator != null && animator!!.isStarted) {
+ animator!!.cancel()
+ animator = null
+ }
+ }
+
+ override fun asView(): View = this
+
+ private companion object {
+ private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b }
+
+ private const val MENU_BACKGROUND_REVEAL_DURATION = 417
+ private const val MENU_BACKGROUND_HIDE_DURATION = 333
+
+ private const val NUM_ALPHA_CHANNELS = 4
+ private const val INDEX_CONTENT_ALPHA = 0
+ private const val INDEX_COLOR_FILTER_ALPHA = 1
+ private const val INDEX_MODAL_ALPHA = 2
+
+ /** Used to hide the app chip for 90:10 flex split. */
+ private const val INDEX_MINIMUM_RATIO_ALPHA = 3
+
+ private const val INDEX_SPLIT_TRANSLATION = 0
+ private const val INDEX_MENU_TRANSLATION = 1
+ private const val INDEX_COUNT_TRANSLATION = 2
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
new file mode 100644
index 0000000..96eed87
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.os.VibrationAttributes
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.FloatValueHolder
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.launcher3.R
+import com.android.launcher3.Utilities.boundToRange
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.DynamicResource
+import com.android.launcher3.util.MSDLPlayerWrapper
+import com.android.quickstep.util.TaskGridNavHelper
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.InteractionProperties
+import kotlin.math.abs
+
+/**
+ * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
+ * RecentsView related to TaskView dismissal.
+ */
+class RecentsDismissUtils(private val recentsView: RecentsView<*, *>) {
+
+ /**
+ * Creates the spring animations which run when a dragged task view in overview is released.
+ *
+ * <p>When a task dismiss is cancelled, the task will return to its original position via a
+ * spring animation. As it passes the threshold of its settling state, its neighbors will spring
+ * in response to the perceived impact of the settling task.
+ */
+ fun createTaskDismissSettlingSpringAnimation(
+ draggedTaskView: TaskView?,
+ velocity: Float,
+ isDismissing: Boolean,
+ detector: SingleAxisSwipeDetector,
+ dismissLength: Int,
+ onEndRunnable: () -> Unit,
+ ): SpringAnimation? {
+ draggedTaskView ?: return null
+ val taskDismissFloatProperty =
+ FloatPropertyCompat.createFloatPropertyCompat(
+ draggedTaskView.secondaryDismissTranslationProperty
+ )
+ // Animate dragged task towards dismissal or rest state.
+ val draggedTaskViewSpringAnimation =
+ SpringAnimation(draggedTaskView, taskDismissFloatProperty)
+ .setSpring(createExpressiveDismissSpringForce())
+ .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
+ .addUpdateListener { animation, value, _ ->
+ if (isDismissing && abs(value) >= abs(dismissLength)) {
+ animation.cancel()
+ } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+ taskDismissFloatProperty.getValue(draggedTaskView)
+ }
+ recentsView.redrawLiveTile()
+ }
+ }
+ .addEndListener { _, _, _, _ ->
+ if (isDismissing) {
+ recentsView.dismissTask(
+ draggedTaskView,
+ /* animateTaskView = */ false,
+ /* removeTask = */ true,
+ )
+ } else {
+ recentsView.onDismissAnimationEnds()
+ }
+ onEndRunnable()
+ }
+ if (!isDismissing) {
+ addNeighboringSpringAnimationsForDismissCancel(
+ draggedTaskView,
+ draggedTaskViewSpringAnimation,
+ )
+ }
+ return draggedTaskViewSpringAnimation
+ }
+
+ private fun addNeighboringSpringAnimationsForDismissCancel(
+ draggedTaskView: TaskView,
+ draggedTaskViewSpringAnimation: SpringAnimation,
+ ) {
+ // Empty spring animation exists for conditional start, and to drive neighboring springs.
+ val neighborsToSettle =
+ SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
+ var lastPosition = 0f
+ var startSettling = false
+ draggedTaskViewSpringAnimation.addUpdateListener { _, value, velocity ->
+ // Start the settling animation the first time the dragged task passes the origin (from
+ // negative displacement to positive displacement). We do not check for an exact value
+ // to compare to, as the update listener does not necessarily hit every value (e.g. a
+ // value of zero). Do not check again once it has started settling, as a spring can
+ // bounce past the origin multiple times depending on the stiffness and damping ratio.
+ if (startSettling) return@addUpdateListener
+ if (lastPosition < 0 && value >= 0) {
+ startSettling = true
+ }
+ lastPosition = value
+ if (startSettling) {
+ neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
+ playDismissSettlingHaptic(velocity)
+ }
+ }
+
+ // Add tasks before dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ var previousNeighbor = neighborsToSettle
+ getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
+ (taskView, offset) ->
+ previousNeighbor =
+ createNeighboringTaskViewSpringAnimation(
+ taskView,
+ offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
+ previousNeighbor,
+ )
+ }
+ // Add tasks after dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ previousNeighbor = neighborsToSettle
+ getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
+ (taskView, offset) ->
+ previousNeighbor =
+ createNeighboringTaskViewSpringAnimation(
+ taskView,
+ offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
+ previousNeighbor,
+ )
+ }
+ }
+
+ /**
+ * Gets pairs of (TaskView, offset) adjacent the dragged task in visual order.
+ *
+ * <p>Gets tasks either before or after the dragged task along with their offset from it. The
+ * offset is the distance between indices for carousels, or distance between columns for grids.
+ */
+ private fun getTasksOffsetPairAdjacentToDraggedTask(
+ draggedTaskView: TaskView,
+ towardsStart: Boolean,
+ ): Sequence<Pair<TaskView, Int>> {
+ if (recentsView.showAsGrid()) {
+ val taskGridNavHelper =
+ TaskGridNavHelper(
+ recentsView.topRowIdArray,
+ recentsView.bottomRowIdArray,
+ recentsView.mUtils.getLargeTaskViewIds(),
+ hasAddDesktopButton = false,
+ )
+ return taskGridNavHelper
+ .gridTaskViewIdOffsetPairInTabOrderSequence(
+ draggedTaskView.taskViewId,
+ towardsStart,
+ )
+ .mapNotNull { (taskViewId, columnOffset) ->
+ recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
+ Pair(taskView, columnOffset)
+ }
+ }
+ } else {
+ val taskViewList = recentsView.mUtils.taskViews.toList()
+ val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
+
+ return if (towardsStart) {
+ taskViewList
+ .take(draggedTaskViewIndex)
+ .reversed()
+ .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
+ .asSequence()
+ } else {
+ taskViewList
+ .takeLast(taskViewList.size - draggedTaskViewIndex - 1)
+ .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
+ .asSequence()
+ }
+ }
+ }
+
+ /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
+ private fun createNeighboringTaskViewSpringAnimation(
+ taskView: TaskView,
+ dampingOffsetRatio: Float,
+ previousNeighborSpringAnimation: SpringAnimation,
+ ): SpringAnimation {
+ val neighboringTaskViewSpringAnimation =
+ SpringAnimation(
+ taskView,
+ FloatPropertyCompat.createFloatPropertyCompat(
+ taskView.secondaryDismissTranslationProperty
+ ),
+ )
+ .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio))
+ // Update live tile on spring animation.
+ if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+ taskView.secondaryDismissTranslationProperty.get(taskView)
+ }
+ recentsView.redrawLiveTile()
+ }
+ }
+ // Drive current neighbor's spring with the previous neighbor's.
+ previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
+ neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
+ }
+ return neighboringTaskViewSpringAnimation
+ }
+
+ private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ return SpringForce()
+ .setDampingRatio(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) +
+ dampingRatioOffset
+ )
+ .setStiffness(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
+ )
+ }
+
+ /**
+ * Plays a haptic as the dragged task view settles back into its rest state.
+ *
+ * <p>Haptic intensity is proportional to velocity.
+ */
+ private fun playDismissSettlingHaptic(velocity: Float) {
+ val maxDismissSettlingVelocity =
+ recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
+ MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
+ .playToken(
+ MSDLToken.CANCEL,
+ InteractionProperties.DynamicVibrationScale(
+ boundToRange(velocity / maxDismissSettlingVelocity, 0f, 1f),
+ VibrationAttributes.Builder()
+ .setUsage(VibrationAttributes.USAGE_TOUCH)
+ .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
+ .build(),
+ ),
+ )
+ }
+
+ /** Animates RecentsView's scale to the provided value, using spring animations. */
+ fun animateRecentsScale(scale: Float): SpringAnimation {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ val dampingRatio = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio)
+ val stiffness = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_stiffness)
+
+ // Spring which sets the Recents scale on update. This is needed, as the SpringAnimation
+ // struggles to animate small values like changing recents scale from 0.9 to 1. So
+ // we animate over a larger range (e.g. 900 to 1000) and convert back to the required value.
+ // (This is instead of converting RECENTS_SCALE_PROPERTY to a FloatPropertyCompat and
+ // animating it directly via springs.)
+ val initialRecentsScaleSpringValue =
+ RECENTS_SCALE_SPRING_MULTIPLIER * RECENTS_SCALE_PROPERTY.get(recentsView)
+ return SpringAnimation(FloatValueHolder(initialRecentsScaleSpringValue))
+ .setSpring(
+ SpringForce(initialRecentsScaleSpringValue)
+ .setDampingRatio(dampingRatio)
+ .setStiffness(stiffness)
+ )
+ .addUpdateListener { _, value, _ ->
+ RECENTS_SCALE_PROPERTY.setValue(
+ recentsView,
+ value / RECENTS_SCALE_SPRING_MULTIPLIER,
+ )
+ }
+ .apply { animateToFinalPosition(RECENTS_SCALE_SPRING_MULTIPLIER * scale) }
+ }
+
+ private companion object {
+ // The additional damping to apply to tasks further from the dismissed task.
+ private const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
+ private const val RECENTS_SCALE_SPRING_MULTIPLIER = 1000f
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 424271a..c5a76cb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -65,11 +65,6 @@
import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP;
import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
@@ -162,6 +157,7 @@
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StatefulContainer;
@@ -184,6 +180,7 @@
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.util.ViewPool;
import com.android.launcher3.util.coroutines.DispatcherProvider;
+import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener;
import com.android.quickstep.BaseContainerInterface;
import com.android.quickstep.GestureState;
import com.android.quickstep.HighResLoadingState;
@@ -270,7 +267,7 @@
CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
HighResLoadingState.HighResLoadingStateChangedCallback,
- TaskVisualsChangeListener {
+ TaskVisualsChangeListener, DesktopVisibilityListener {
private static final String TAG = "RecentsView";
private static final boolean DEBUG = false;
@@ -561,6 +558,10 @@
private final Rect mTaskViewDeadZoneRect = new Rect();
private final Rect mTopRowDeadZoneRect = new Rect();
private final Rect mBottomRowDeadZoneRect = new Rect();
+
+ @Nullable
+ private DesktopVisibilityController mDesktopVisibilityController = null;
+
/**
* Reflects if Recents is currently in the middle of a gesture, and if so, which tasks are
* running. If a gesture is not in progress, this will be null.
@@ -856,6 +857,7 @@
private final RecentsViewModel mRecentsViewModel;
private final RecentsViewModelHelper mHelper;
protected final RecentsViewUtils mUtils = new RecentsViewUtils(this);
+ protected final RecentsDismissUtils mDismissUtils = new RecentsDismissUtils(this);
private final Matrix mTmpMatrix = new Matrix();
@@ -917,14 +919,18 @@
mAddDesktopButton = (AddDesktopButton) LayoutInflater.from(context).inflate(
R.layout.overview_add_desktop_button, this, false);
mAddDesktopButton.setOnClickListener(this::createDesk);
+
+ mDesktopVisibilityController = DesktopVisibilityController.INSTANCE.get(mContext);
}
mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
10 /* initial size */);
+ int groupedViewPoolInitialSize = enableRefactorTaskThumbnail() ? 2 : 10;
mGroupedTaskViewPool = new ViewPool<>(context, this,
- R.layout.task_grouped, 20 /* max size */, 10 /* initial size */);
+ R.layout.task_grouped, 20 /* max size */, groupedViewPoolInitialSize);
+ int desktopViewPoolInitialSize = DesktopModeStatus.canEnterDesktopMode(mContext) ? 1 : 0;
mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop,
- 5 /* max size */, 1 /* initial size */);
+ 5 /* max size */, desktopViewPoolInitialSize);
setOrientationHandler(mOrientationState.getOrientationHandler());
mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources());
@@ -1229,13 +1235,16 @@
mSyncTransactionApplier = new SurfaceTransactionApplier(this);
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
.setSyncTransactionApplier(mSyncTransactionApplier));
- RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
+ RecentsModel.INSTANCE.get(mContext).addThumbnailChangeListener(this);
mIPipAnimationListener.setActivityAndRecentsView(mContainer, this);
- SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(
+ SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(
mIPipAnimationListener);
mOrientationState.initListeners();
mTaskOverlayFactory.initListeners();
mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
+ if (mDesktopVisibilityController != null) {
+ mDesktopVisibilityController.registerDesktopVisibilityListener(this);
+ }
}
@Override
@@ -1250,12 +1259,15 @@
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
.setSyncTransactionApplier(null));
executeSideTaskLaunchCallback();
- RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
- SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null);
+ RecentsModel.INSTANCE.get(mContext).removeThumbnailChangeListener(this);
+ SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(null);
mIPipAnimationListener.setActivityAndRecentsView(null, null);
mOrientationState.destroyListeners();
mTaskOverlayFactory.removeListeners();
mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
+ if (mDesktopVisibilityController != null) {
+ mDesktopVisibilityController.unregisterDesktopVisibilityListener(this);
+ }
reset();
}
@@ -2015,7 +2027,7 @@
}
// If the list changed, maybe the focused task doesn't exist anymore.
if (newFocusedTaskView == null) {
- newFocusedTaskView = mUtils.getExpectedFocusedTask();
+ newFocusedTaskView = mUtils.getFirstNonDesktopTaskView();
}
}
setFocusedTaskViewId(
@@ -2121,15 +2133,6 @@
return mTaskViewCount;
}
- /**
- * Transverse RecentsView children to calculate the amount of DesktopTaskViews.
- *
- * @return Number of children that are instances of DesktopTaskView
- */
- private int getDesktopTaskViewCount() {
- return mUtils.getDesktopTaskViewCount();
- }
-
/** Counts {@link TaskView}s that are not {@link DesktopTaskView} instances. */
public int getNonDesktopTaskViewCount() {
return mUtils.getNonDesktopTaskViewCount();
@@ -2696,8 +2699,6 @@
}
if (enableRefactorTaskThumbnail()) {
mRecentsViewModel.onReset();
- // TODO(b/391842220) Remove TaskViews rather than calling specific logic to cancel scope
- getTaskViews().forEach(TaskView::destroyScopes);
}
}
@@ -2924,28 +2925,40 @@
BaseState<?> endState = mSizeStrategy.stateFromGestureEndTarget(endTarget);
if (endState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
TaskView runningTaskView = getRunningTaskView();
- float runningTaskPrimaryGridTranslation = 0;
- float runningTaskSecondaryGridTranslation = 0;
+ float runningTaskGridTranslationX = 0;
+ float runningTaskGridTranslationY = 0;
if (runningTaskView != null) {
// Apply the grid translation to running task unless it's being snapped to
// and removes the current translation applied to the running task.
- runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX()
+ runningTaskGridTranslationX = runningTaskView.getGridTranslationX()
- runningTaskView.getNonGridTranslationX();
- runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY();
+ runningTaskGridTranslationY = runningTaskView.getGridTranslationY();
}
for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) {
TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator();
if (animatorSet == null) {
setGridProgress(1);
- tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
- tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
+ if (enableGridOnlyOverview()) {
+ tvs.taskGridTranslationX.value = runningTaskGridTranslationX;
+ tvs.taskGridTranslationY.value = runningTaskGridTranslationY;
+ } else {
+ tvs.taskPrimaryTranslation.value = runningTaskGridTranslationX;
+ tvs.taskSecondaryTranslation.value = runningTaskGridTranslationY;
+ }
} else {
animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
- animatorSet.play(tvs.carouselScale.animateToValue(1));
- animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
- runningTaskPrimaryGridTranslation));
- animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
- runningTaskSecondaryGridTranslation));
+ if (enableGridOnlyOverview()) {
+ animatorSet.play(tvs.carouselScale.animateToValue(1));
+ animatorSet.play(tvs.taskGridTranslationX.animateToValue(
+ runningTaskGridTranslationX));
+ animatorSet.play(tvs.taskGridTranslationY.animateToValue(
+ runningTaskGridTranslationY));
+ } else {
+ animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
+ runningTaskGridTranslationX));
+ animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
+ runningTaskGridTranslationY));
+ }
}
}
}
@@ -3071,7 +3084,7 @@
focusedTaskViewId = INVALID_TASK_ID;
} else if (enableLargeDesktopWindowingTile()
&& getRunningTaskView() instanceof DesktopTaskView) {
- TaskView focusedTaskView = getTaskViewAt(getDesktopTaskViewCount());
+ TaskView focusedTaskView = mUtils.getFirstNonDesktopTaskView();
focusedTaskViewId =
focusedTaskView != null ? focusedTaskView.getTaskViewId() : INVALID_TASK_ID;
} else {
@@ -3498,8 +3511,13 @@
final TaskView runningTask = getRunningTaskView();
if (showAsGrid() && enableGridOnlyOverview() && runningTask != null) {
runActionOnRemoteHandles(
- remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
- .taskSecondaryTranslation.value = runningTask.getGridTranslationY()
+ remoteTargetHandle -> {
+ remoteTargetHandle.getTaskViewSimulator().taskGridTranslationX.value =
+ runningTask.getGridTranslationX()
+ - runningTask.getNonGridTranslationX();
+ remoteTargetHandle.getTaskViewSimulator().taskGridTranslationY.value =
+ runningTask.getGridTranslationY();
+ }
);
}
@@ -3613,12 +3631,9 @@
if (taskView.isRunningTask()) {
anim.addOnFrameCallback(() -> {
if (!mEnableDrawingLiveTile) return;
- runActionOnRemoteHandles(
- remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
- .taskSecondaryTranslation.value = getPagedOrientationHandler()
- .getSecondaryValue(taskView.getTranslationX(),
- taskView.getTranslationY()
- ));
+ runActionOnRemoteHandles(remoteTargetHandle ->
+ remoteTargetHandle.getTaskViewSimulator().taskSecondaryTranslation.value =
+ taskView.getSecondaryDismissTranslationProperty().get(taskView));
redrawLiveTile();
});
}
@@ -3940,9 +3955,9 @@
int distanceFromDismissedTask = 1;
int slidingTranslation = 0;
if (isSlidingTasks) {
- int nextSnappedPage = isStagingFocusedTask
- ? indexOfChild(mUtils.getFirstSmallTaskView())
- : mUtils.getDesktopTaskViewCount();
+ int nextSnappedPage = indexOfChild(isStagingFocusedTask
+ ? mUtils.getFirstSmallTaskView()
+ : mUtils.getFirstNonDesktopTaskView());
slidingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
- getScrollForPage(nextSnappedPage);
slidingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
@@ -3968,7 +3983,7 @@
Math.abs(i - dismissedIndex),
scrollDiff,
anim,
- splitTimings, i);
+ splitTimings);
needsCurveUpdates = true;
}
} else if (child instanceof TaskView taskView) {
@@ -4344,8 +4359,12 @@
int indexDiff,
int scrollDiffPerPage,
PendingAnimation pendingAnimation,
- SplitAnimationTimings splitTimings,
- int index) {
+ SplitAnimationTimings splitTimings) {
+ // No need to translate the AddDesktopButton on dismissing a TaskView, which should be
+ // always at the right most position, even when dismissing the last TaskView.
+ if (view instanceof AddDesktopButton) {
+ return;
+ }
FloatProperty translationProperty = view instanceof TaskView
? ((TaskView) view).getPrimaryDismissTranslationProperty()
: getPagedOrientationHandler().getPrimaryViewTranslate();
@@ -4540,7 +4559,7 @@
}
private boolean snapToPageRelative(int delta, boolean cycle,
- @TaskGridNavHelper.TASK_NAV_DIRECTION int direction) {
+ TaskGridNavHelper.TaskNavDirection direction) {
// Set next page if scroll animation is still running, otherwise cannot snap to the
// next page on successive key presses. Setting the current page aborts the scroll.
if (!mScroller.isFinished()) {
@@ -4559,7 +4578,7 @@
return true;
}
- private int getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction,
+ private int getNextPageInternal(int delta, TaskGridNavHelper.TaskNavDirection direction,
boolean cycle) {
if (!showAsGrid()) {
return getNextPage() + delta;
@@ -4647,15 +4666,19 @@
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_TAB:
return snapToPageRelative(event.isShiftPressed() ? -1 : 1, true /* cycle */,
- DIRECTION_TAB);
+ TaskGridNavHelper.TaskNavDirection.TAB);
case KeyEvent.KEYCODE_DPAD_RIGHT:
- return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */, DIRECTION_RIGHT);
+ return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.RIGHT);
case KeyEvent.KEYCODE_DPAD_LEFT:
- return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */, DIRECTION_LEFT);
+ return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.LEFT);
case KeyEvent.KEYCODE_DPAD_UP:
- return snapToPageRelative(1, false /* cycle */, DIRECTION_UP);
+ return snapToPageRelative(1, false /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.UP);
case KeyEvent.KEYCODE_DPAD_DOWN:
- return snapToPageRelative(1, false /* cycle */, DIRECTION_DOWN);
+ return snapToPageRelative(1, false /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.DOWN);
case KeyEvent.KEYCODE_DEL:
case KeyEvent.KEYCODE_FORWARD_DEL:
dismissCurrentTask();
@@ -5658,7 +5681,7 @@
anim.play(ObjectAnimator.ofFloat(this, FULLSCREEN_PROGRESS, 1));
anim.addListener(new AnimatorListenerAdapter() {
@Override
- public void onAnimationStart(@NonNull Animator animation, boolean isReverse) {
+ public void onAnimationStart(@NonNull Animator animation) {
taskView.getThumbnailBounds(mTempRect, /*relativeToDragLayer=*/true);
getTaskDimension(mContext, mContainer.getDeviceProfile(), mTempPointF);
Rect fullscreenBounds = new Rect(0, 0, (int) mTempPointF.x,
@@ -5680,6 +5703,18 @@
});
}
}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // If live tile is not launching, reset the pivot applied above.
+ if (!taskView.isRunningTask()) {
+ runActionOnRemoteHandles(
+ remoteTargetHandle -> {
+ remoteTargetHandle.getTaskViewSimulator().setPivotOverride(
+ null);
+ });
+ }
+ }
});
} else if (!showAsGrid) {
// We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -5788,10 +5823,12 @@
mPendingAnimation = new PendingAnimation(duration);
mPendingAnimation.add(anim);
- runActionOnRemoteHandles(
- remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
- .addOverviewToAppAnim(mPendingAnimation, interpolator));
- mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+ if (taskView.isRunningTask()) {
+ runActionOnRemoteHandles(
+ remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
+ .addOverviewToAppAnim(mPendingAnimation, interpolator));
+ mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+ }
mPendingAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -6244,12 +6281,12 @@
}
@Override
- protected int getChildVisibleSize(int index) {
- final TaskView taskView = getTaskViewAt(index);
+ protected int getChildVisibleSize(int childIndex) {
+ final TaskView taskView = getTaskViewAt(childIndex);
if (taskView == null) {
- return super.getChildVisibleSize(index);
+ return super.getChildVisibleSize(childIndex);
}
- return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
+ return (int) (super.getChildVisibleSize(childIndex) * taskView.getSizeAdjustment(
showAsFullscreen()));
}
@@ -6308,7 +6345,7 @@
* Returns how many pixels the page is offset on the currently laid out dominant axis.
*/
private int getUnclampedScrollOffset(int pageIndex) {
- if (pageIndex == -1) {
+ if (pageIndex == INVALID_PAGE) {
return 0;
}
// Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
@@ -6836,6 +6873,11 @@
}
}
+ @Override
+ public void onCanCreateDesksChanged(boolean canCreateDesks) {
+ // TODO: b/389209338 - update the AddDesktopButton's visibility on this.
+ }
+
/** Get the color used for foreground scrimming the RecentsView for sharing. */
public static int getForegroundScrimDimColor(Context context) {
return context.getColor(R.color.overview_foreground_scrim_color);
@@ -6946,10 +6988,17 @@
public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
float velocity, boolean isDismissing, SingleAxisSwipeDetector detector,
int dismissLength, Function0<Unit> onEndRunnable) {
- return mUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity,
+ return mDismissUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity,
isDismissing, detector, dismissLength, onEndRunnable);
}
+ /**
+ * Animates RecentsView's scale to the provided value, using spring animations.
+ */
+ public SpringAnimation animateRecentsScale(float scale) {
+ return mDismissUtils.animateRecentsScale(scale);
+ }
+
public interface TaskLaunchListener {
void onTaskLaunched();
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index d18a5f7..f742ec3 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -17,30 +17,16 @@
package com.android.quickstep.views
import android.graphics.Rect
-import android.os.VibrationAttributes
import android.view.View
import androidx.core.view.children
-import androidx.dynamicanimation.animation.FloatPropertyCompat
-import androidx.dynamicanimation.animation.FloatValueHolder
-import androidx.dynamicanimation.animation.SpringAnimation
-import androidx.dynamicanimation.animation.SpringForce
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
-import com.android.launcher3.R
-import com.android.launcher3.Utilities.boundToRange
-import com.android.launcher3.touch.SingleAxisSwipeDetector
-import com.android.launcher3.util.DynamicResource
import com.android.launcher3.util.IntArray
-import com.android.launcher3.util.MSDLPlayerWrapper
import com.android.quickstep.util.GroupTask
-import com.android.quickstep.util.TaskGridNavHelper
import com.android.quickstep.util.isExternalDisplay
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
import com.android.systemui.shared.recents.model.ThumbnailData
-import com.google.android.msdl.data.model.MSDLToken
-import com.google.android.msdl.domain.InteractionProperties
import java.util.function.BiConsumer
-import kotlin.math.abs
/**
* Helper class for [RecentsView]. This util class contains refactored and extracted functions from
@@ -87,7 +73,7 @@
}
/** Counts [TaskView]s that are [DesktopTaskView] instances. */
- fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
+ private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
/** Counts [TaskView]s that are not [DesktopTaskView] instances. */
fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView }
@@ -105,7 +91,7 @@
}
/** Returns the expected focus task. */
- fun getExpectedFocusedTask(): TaskView? =
+ fun getFirstNonDesktopTaskView(): TaskView? =
if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
else taskViews.firstOrNull()
@@ -308,261 +294,7 @@
}
}
- /**
- * Creates the spring animations which run when a dragged task view in overview is released.
- *
- * <p>When a task dismiss is cancelled, the task will return to its original position via a
- * spring animation. As it passes the threshold of its settling state, its neighbors will spring
- * in response to the perceived impact of the settling task.
- */
- fun createTaskDismissSettlingSpringAnimation(
- draggedTaskView: TaskView?,
- velocity: Float,
- isDismissing: Boolean,
- detector: SingleAxisSwipeDetector,
- dismissLength: Int,
- onEndRunnable: () -> Unit,
- ): SpringAnimation? {
- draggedTaskView ?: return null
- val taskDismissFloatProperty =
- FloatPropertyCompat.createFloatPropertyCompat(
- draggedTaskView.secondaryDismissTranslationProperty
- )
- // Animate dragged task towards dismissal or rest state.
- val draggedTaskViewSpringAnimation =
- SpringAnimation(draggedTaskView, taskDismissFloatProperty)
- .setSpring(createExpressiveDismissSpringForce())
- .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
- .addUpdateListener { animation, value, _ ->
- if (isDismissing && abs(value) >= abs(dismissLength)) {
- // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
- draggedTaskView.alpha = 0f
- animation.cancel()
- } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
- recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
- remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
- taskDismissFloatProperty.getValue(draggedTaskView)
- }
- recentsView.redrawLiveTile()
- }
- }
- .addEndListener { _, _, _, _ ->
- if (isDismissing) {
- recentsView.dismissTask(
- draggedTaskView,
- /* animateTaskView = */ false,
- /* removeTask = */ true,
- )
- } else {
- recentsView.onDismissAnimationEnds()
- }
- onEndRunnable()
- }
- if (!isDismissing) {
- addNeighboringSpringAnimationsForDismissCancel(
- draggedTaskView,
- draggedTaskViewSpringAnimation,
- recentsView.pageCount,
- )
- }
- return draggedTaskViewSpringAnimation
- }
-
- private fun addNeighboringSpringAnimationsForDismissCancel(
- draggedTaskView: TaskView,
- draggedTaskViewSpringAnimation: SpringAnimation,
- taskCount: Int,
- ) {
- // Empty spring animation exists for conditional start, and to drive neighboring springs.
- val neighborsToSettle =
- SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
- var lastPosition = 0f
- var startSettling = false
- draggedTaskViewSpringAnimation.addUpdateListener { _, value, velocity ->
- // Start the settling animation the first time the dragged task passes the origin (from
- // negative displacement to positive displacement). We do not check for an exact value
- // to compare to, as the update listener does not necessarily hit every value (e.g. a
- // value of zero). Do not check again once it has started settling, as a spring can
- // bounce past the origin multiple times depending on the stiffness and damping ratio.
- if (startSettling) return@addUpdateListener
- if (lastPosition < 0 && value >= 0) {
- startSettling = true
- }
- lastPosition = value
- if (startSettling) {
- neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
- playDismissSettlingHaptic(velocity)
- }
- }
-
- // Add tasks before dragged index, fanning out from the dragged task.
- // The order they are added matters, as each spring drives the next.
- var previousNeighbor = neighborsToSettle
- getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
- (taskView, offset) ->
- previousNeighbor =
- createNeighboringTaskViewSpringAnimation(
- taskView,
- offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
- previousNeighbor,
- )
- }
- // Add tasks after dragged index, fanning out from the dragged task.
- // The order they are added matters, as each spring drives the next.
- previousNeighbor = neighborsToSettle
- getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
- (taskView, offset) ->
- previousNeighbor =
- createNeighboringTaskViewSpringAnimation(
- taskView,
- offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
- previousNeighbor,
- )
- }
- }
-
- /**
- * Gets pairs of (TaskView, offset) adjacent the dragged task in visual order.
- *
- * <p>Gets tasks either before or after the dragged task along with their offset from it. The
- * offset is the distance between indices for carousels, or distance between columns for grids.
- */
- private fun getTasksOffsetPairAdjacentToDraggedTask(
- draggedTaskView: TaskView,
- towardsStart: Boolean,
- ): Sequence<Pair<TaskView, Int>> {
- if (recentsView.showAsGrid()) {
- return gridTaskOffsetPairInTabOrderSequence(draggedTaskView, towardsStart)
- } else {
- val taskViewList = taskViews.toList()
- val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
-
- return if (towardsStart) {
- taskViewList
- .take(draggedTaskViewIndex)
- .reversed()
- .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
- .asSequence()
- } else {
- taskViewList
- .takeLast(taskViewList.size - draggedTaskViewIndex - 1)
- .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
- .asSequence()
- }
- }
- }
-
- /**
- * Returns a sequence of pairs of (TaskViews, offsets) in the grid, ordered according to tab
- * navigation, starting from the dragged TaskView, towards the start or end of the grid.
- *
- * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
- * negative value moves backward towards the beginning. The offset is the distance between
- * columns the tasks are in.
- */
- private fun gridTaskOffsetPairInTabOrderSequence(
- draggedTaskView: TaskView,
- towardsStart: Boolean,
- ): Sequence<Pair<TaskView, Int>> = sequence {
- val taskGridNavHelper =
- TaskGridNavHelper(
- recentsView.topRowIdArray,
- recentsView.bottomRowIdArray,
- getLargeTaskViewIds(),
- /* hasAddDesktopButton= */ false,
- )
- val draggedTaskViewColumn = taskGridNavHelper.getColumn(draggedTaskView.taskViewId)
- var nextTaskView: TaskView? = draggedTaskView
- var previousTaskView: TaskView? = null
- while (nextTaskView != previousTaskView && nextTaskView != null) {
- previousTaskView = nextTaskView
- nextTaskView =
- recentsView.getTaskViewFromTaskViewId(
- taskGridNavHelper.getNextGridPage(
- nextTaskView.taskViewId,
- if (towardsStart) -1 else 1,
- TaskGridNavHelper.DIRECTION_TAB,
- /* cycle = */ false,
- )
- )
- if (nextTaskView != null && nextTaskView != previousTaskView) {
- val columnOffset =
- abs(
- taskGridNavHelper.getColumn(nextTaskView.taskViewId) - draggedTaskViewColumn
- )
- yield(Pair(nextTaskView, columnOffset))
- }
- }
- }
-
- /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
- private fun createNeighboringTaskViewSpringAnimation(
- taskView: TaskView,
- dampingOffsetRatio: Float,
- previousNeighborSpringAnimation: SpringAnimation,
- ): SpringAnimation {
- val neighboringTaskViewSpringAnimation =
- SpringAnimation(
- taskView,
- FloatPropertyCompat.createFloatPropertyCompat(
- taskView.secondaryDismissTranslationProperty
- ),
- )
- .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio))
- // Update live tile on spring animation.
- if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
- neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
- recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
- remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
- taskView.secondaryDismissTranslationProperty.get(taskView)
- }
- recentsView.redrawLiveTile()
- }
- }
- // Drive current neighbor's spring with the previous neighbor's.
- previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
- neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
- }
- return neighboringTaskViewSpringAnimation
- }
-
- private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce {
- val resourceProvider = DynamicResource.provider(recentsView.mContainer)
- return SpringForce()
- .setDampingRatio(
- resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) +
- dampingRatioOffset
- )
- .setStiffness(
- resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
- )
- }
-
- /**
- * Plays a haptic as the dragged task view settles back into its rest state.
- *
- * <p>Haptic intensity is proportional to velocity.
- */
- private fun playDismissSettlingHaptic(velocity: Float) {
- val maxDismissSettlingVelocity =
- recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
- MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
- .playToken(
- MSDLToken.CANCEL,
- InteractionProperties.DynamicVibrationScale(
- boundToRange(velocity / maxDismissSettlingVelocity, 0f, 1f),
- VibrationAttributes.Builder()
- .setUsage(VibrationAttributes.USAGE_TOUCH)
- .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
- .build(),
- ),
- )
- }
-
companion object {
val TEMP_RECT = Rect()
-
- // The additional damping to apply to tasks further from the dismissed task.
- const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index bbe1af4..7301cfc 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.views
import android.graphics.Bitmap
+import android.graphics.Matrix
import android.view.View
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.model.data.TaskViewItemInfo
@@ -24,13 +25,9 @@
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.ViewUtils.addAccessibleChildToList
-import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.getScope
-import com.android.quickstep.recents.di.inject
import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -55,27 +52,19 @@
) {
val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
- // TODO(b/390581380): Remove this after this bug is fixed
- private val taskThumbnailViewModel: TaskThumbnailViewModel by
- RecentsDependencies.inject(snapshotView)
-
init {
if (enableRefactorTaskThumbnail()) {
require(snapshotView is TaskThumbnailView)
- RecentsDependencies.getScope(snapshotView).apply {
- val taskViewScope = RecentsDependencies.getScope(taskView)
- linkTo(taskViewScope)
-
- val taskContainerScope = RecentsDependencies.getScope(this@TaskContainer)
- linkTo(taskContainerScope)
- }
} else {
require(snapshotView is TaskThumbnailViewDeprecated)
}
}
internal var thumbnailData: ThumbnailData? = null
- val splitAnimationThumbnail: Bitmap?
+ private set
+
+ val thumbnail: Bitmap?
+ /** If possible don't use this. It should be replaced as part of b/331753115. */
get() =
if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail
else thumbnailViewDeprecated.thumbnail
@@ -106,9 +95,7 @@
fun bind() {
digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
- if (enableRefactorTaskThumbnail()) {
- taskThumbnailViewModel.bind(task.key.id)
- } else {
+ if (!enableRefactorTaskThumbnail()) {
thumbnailViewDeprecated.bind(task, overlay, taskView)
}
overlay.init()
@@ -120,19 +107,12 @@
snapshotView.scaleY = 1f
overlay.destroy()
if (enableRefactorTaskThumbnail()) {
- RecentsDependencies.getInstance().removeScope(snapshotView)
- RecentsDependencies.getInstance().removeScope(this)
isThumbnailValid = false
} else {
thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
}
}
- // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
- fun destroyScopes() {
- thumbnailView.destroyScopes()
- }
-
fun setOverlayEnabled(enabled: Boolean) {
if (!enableRefactorTaskThumbnail()) {
thumbnailViewDeprecated.setOverlayEnabled(enabled)
@@ -189,4 +169,8 @@
thumbnailViewDeprecated.setSplashAlpha(progress)
}
}
+
+ fun updateThumbnailMatrix(matrix: Matrix) {
+ thumbnailView.setImageMatrix(matrix)
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 56a35bd..49ec31d 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -41,7 +41,6 @@
import android.widget.Toast
import androidx.annotation.IntDef
import androidx.annotation.VisibleForTesting
-import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
import com.android.app.animation.Interpolators
import com.android.launcher3.Flags.enableCursorHoverStates
@@ -671,11 +670,6 @@
taskContainers.forEach { it.destroy() }
}
- fun destroyScopes() {
- // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
- taskContainers.forEach { it.destroyScopes() }
- }
-
// TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
override fun hasOverlappingRendering() = false
@@ -778,9 +772,14 @@
container.setState(
state = containerState,
liveTile = state.isLiveTile,
- hasHeader = type == TaskViewType.DESKTOP,
+ hasHeader = state.hasHeader,
)
updateThumbnailValidity(container)
+ updateThumbnailMatrix(
+ container = container,
+ width = container.thumbnailView.width,
+ height = container.thumbnailView.height,
+ )
if (enableOverviewIconMenu()) {
setIconState(container, containerState)
@@ -798,6 +797,23 @@
applyThumbnailSplashAlpha()
}
+ /**
+ * Updates the thumbnail's transformation matrix and rotation state within a TaskContainer.
+ *
+ * This function is called to reposition the thumbnail in the following scenarios:
+ * - When the TTV's size changes (onSizeChanged), and it's displaying a SnapshotSplash.
+ * - When drawing a snapshot (drawSnapshot).
+ *
+ * @param container The TaskContainer holding the thumbnail to be updated.
+ * @param width The desired width of the thumbnail's container.
+ * @param height The desired height of the thumbnail's container.
+ */
+ private fun updateThumbnailMatrix(container: TaskContainer, width: Int, height: Int) {
+ val thumbnailPosition =
+ viewModel!!.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
+ container.updateThumbnailMatrix(thumbnailPosition.matrix)
+ }
+
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (enableRefactorTaskThumbnail()) {
@@ -843,6 +859,7 @@
getTaskUseCase = RecentsDependencies.get(),
getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
isThumbnailValidUseCase = RecentsDependencies.get(),
+ getThumbnailPositionUseCase = RecentsDependencies.get(),
dispatcherProvider = RecentsDependencies.get(),
)
.apply { bind(*taskIds) }
@@ -852,7 +869,10 @@
container.bind()
if (enableRefactorTaskThumbnail()) {
container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
- container.thumbnailView.doOnLayout { updateThumbnailValidity(container) }
+ container.thumbnailView.doOnSizeChange { width, height ->
+ updateThumbnailValidity(container)
+ updateThumbnailMatrix(container, width, height)
+ }
}
}
setOrientationState(orientedState)
@@ -1397,7 +1417,7 @@
container.task,
container.iconView.drawable,
container.snapshotView,
- container.splitAnimationThumbnail,
+ container.thumbnail,
/* intent */ null,
/* user */ null,
container.itemInfo,
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
deleted file mode 100644
index 1a2b1c3..0000000
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,29 +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.quickstep.task.thumbnail
-
-import android.graphics.Matrix
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-
-class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
- override fun bind(taskId: Int) {
- // no-op
- }
-
- override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean) =
- Matrix.IDENTITY_MATRIX
-}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index c3b4d15..80b2c16 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -16,15 +16,21 @@
package com.android.quickstep.task.thumbnail
import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.drawable.BitmapDrawable
import android.view.LayoutInflater
+import android.view.Surface.ROTATION_0
+import androidx.core.graphics.set
import com.android.launcher3.R
-import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
-import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -46,13 +52,6 @@
ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
)
- private val taskThumbnailViewModel = FakeTaskThumbnailViewModel()
-
- @After
- fun tearDown() {
- RecentsDependencies.destroy()
- }
-
@Test
fun taskThumbnailView_uninitializedByDefault() {
screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
@@ -91,6 +90,96 @@
}
@Test
+ fun taskThumbnailView_liveTile_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_liveTile") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(TaskThumbnailUiState.LiveTile.WithoutHeader)
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_image_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_image") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ null,
+ )
+ )
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_image_withoutHeader_withImageMatrix() {
+ screenshotRule.screenshotTest("taskThumbnailView_image_withMatrix") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(
+ createBitmap(
+ width = VIEW_ENV_WIDTH / 2,
+ height = lessThanHeightMatchingAspectRatio,
+ ),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
+ null,
+ )
+ )
+ setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_splash_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_partial_splash") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ BitmapDrawable(activity.resources, createSplash()),
+ )
+ )
+ updateSplashAlpha(0.5f)
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_splash_withoutHeader_withImageMatrix() {
+ screenshotRule.screenshotTest("taskThumbnailView_partial_splash_withMatrix") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(
+ createBitmap(
+ width = VIEW_ENV_WIDTH / 2,
+ height = lessThanHeightMatchingAspectRatio,
+ ),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
+ BitmapDrawable(activity.resources, createSplash()),
+ )
+ )
+ setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+ updateSplashAlpha(0.5f)
+ }
+ }
+ }
+
+ @Test
fun taskThumbnailView_dimmed_tintAmount() {
screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity ->
activity.actionBar?.hide()
@@ -124,18 +213,47 @@
}
}
+ @Test
+ fun taskThumbnailView_scaled_roundRoundedCorners() {
+ screenshotRule.screenshotTest("taskThumbnailView_scaledRoundedCorners") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ scaleX = 0.75f
+ scaleY = 0.3f
+ setState(BackgroundOnly(Color.YELLOW))
+ }
+ }
+ }
+
private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
- val di = RecentsDependencies.initialize(context)
val taskThumbnailView =
LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
as TaskThumbnailView
taskThumbnailView.cornerRadius = CORNER_RADIUS
- val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
- di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
-
return taskThumbnailView
}
+ private fun createSplash() = createBitmap(width = 20, height = 20, rectColorRotation = 1)
+
+ private fun createBitmap(
+ width: Int = VIEW_ENV_WIDTH,
+ height: Int = VIEW_ENV_HEIGHT,
+ rectColorRotation: Int = 0,
+ ) =
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
+ Canvas(this).apply {
+ val paint = Paint()
+ paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4]
+ drawRect(0f, 0f, width / 2f, height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4]
+ drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4]
+ drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint)
+ paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4]
+ drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint)
+ }
+ }
+
companion object {
@Parameters(name = "{0}")
@JvmStatic
@@ -147,5 +265,8 @@
)
const val CORNER_RADIUS = 56f
+ val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN)
+ const val VIEW_ENV_WIDTH = 1440
+ const val VIEW_ENV_HEIGHT = 3120
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
similarity index 90%
rename from quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
index 9ca9fe4..7ebef45 100644
--- a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
@@ -27,12 +27,10 @@
import android.window.TransitionFilter
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.quickstep.SystemUiProxy
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.google.common.truth.Truth.assertThat
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -43,7 +41,6 @@
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-import org.mockito.quality.Strictness
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -51,12 +48,6 @@
@get:Rule val mSetFlagsRule = SetFlagsRule()
- private val mockitoSession =
- mockitoSession()
- .strictness(Strictness.LENIENT)
- .mockStatic(DesktopModeStatus::class.java)
- .startMocking()
-
private val context = mock<Context>()
private val systemUiProxy = mock<SystemUiProxy>()
private lateinit var transitionManager: DesktopAppLaunchTransitionManager
@@ -68,11 +59,6 @@
transitionManager = DesktopAppLaunchTransitionManager(context, systemUiProxy)
}
- @After
- fun tearDown() {
- mockitoSession.finishMocking()
- }
-
@Test
@EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
fun registerTransitions_appLaunchFlagEnabled_registersTransition() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 46b5659..a456fb9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -60,6 +60,7 @@
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -573,6 +574,71 @@
}
@Test
+ fun animateToInitialState_whileDragging_inApp() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+ var notifiedBubbleBarVisible = false
+ val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true }
+ val animator =
+ BubbleBarViewAnimator(
+ bubbleBarView,
+ bubbleStashController,
+ flyoutController,
+ bubbleBarParentViewController,
+ onExpanded = emptyRunnable,
+ onBubbleBarVisible = onBubbleBarVisible,
+ animatorScheduler,
+ )
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleBarView.visibility = INVISIBLE
+ animator.animateToInitialState(
+ bubble,
+ isInApp = true,
+ isExpanding = false,
+ isDragging = true,
+ )
+ }
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ barAnimator.assertIsNotRunning()
+ assertThat(animator.isAnimating).isTrue()
+ assertThat(bubbleBarView.alpha).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
+ waitForFlyoutToShow()
+
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
+ assertThat(animator.isAnimating).isFalse()
+ assertThat(bubbleBarView.alpha).isEqualTo(1)
+ assertThat(handle.translationY).isEqualTo(0)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(notifiedBubbleBarVisible).isTrue()
+
+ verify(bubbleStashController, never()).stashBubbleBarImmediate()
+ }
+
+ @Test
fun animateToInitialState_inApp_autoExpanding() {
setUpBubbleBar()
setUpBubbleStashController()
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 e10afc4..40d5e02 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
@@ -22,7 +22,6 @@
import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.yield
import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
@@ -31,6 +30,8 @@
private val completionPrevented: MutableSet<Int> = mutableSetOf()
private val getThumbnailCalls = mutableMapOf<Int, Int>()
+ var highResEnabled = true
+
/** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
override suspend fun getThumbnail(task: Task): ThumbnailData {
getThumbnailCalls[task.key.id] = (getThumbnailCalls[task.key.id] ?: 0) + 1
@@ -38,9 +39,10 @@
while (task.key.id in completionPrevented) {
yield()
}
- return mock<ThumbnailData>().also {
- whenever(it.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
- }
+ return ThumbnailData(
+ thumbnail = taskIdToBitmap[task.key.id],
+ reducedResolution = !highResEnabled,
+ )
}
fun getNumberOfGetThumbnailCalls(taskId: Int): Int = getThumbnailCalls[taskId] ?: 0
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
index 41f6bfd..b91f8bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
@@ -19,16 +19,19 @@
import android.content.ComponentName
import android.content.Intent
import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
import com.android.systemui.shared.recents.model.Task.TaskKey
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
+@RunWith(AndroidJUnit4::class)
class TaskVisualsChangedDelegateTest {
private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
@@ -83,21 +86,21 @@
// Correct match
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
- expectedListener
+ expectedListener,
)
// 1 out of 2 match
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1),
- listener
+ listener,
)
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2),
- listener
+ listener,
)
// 0 out of 2 match
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2),
- listener
+ listener,
)
systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
@@ -112,11 +115,11 @@
val newListener = mock<TaskIconChangedCallback>()
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
- replacedListener
+ replacedListener,
)
systemUnderTest.registerTaskIconChangedCallback(
createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
- newListener
+ newListener,
)
systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
@@ -132,11 +135,11 @@
val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 1),
- expectedListener
+ expectedListener,
)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 2),
- additionalListener
+ additionalListener,
)
systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
@@ -146,22 +149,41 @@
}
@Test
- fun onHighResLoadingStateChanged_notifiesAllListeners() {
+ fun onHighResLoadingStateChanged_toEnabled_notifiesAllListeners() {
val expectedListener = mock<TaskThumbnailChangedCallback>()
val additionalListener = mock<TaskThumbnailChangedCallback>()
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 1),
- expectedListener
+ expectedListener,
)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 2),
- additionalListener
+ additionalListener,
)
systemUnderTest.onHighResLoadingStateChanged(true)
- verify(expectedListener).onHighResLoadingStateChanged()
- verify(additionalListener).onHighResLoadingStateChanged()
+ verify(expectedListener).onHighResLoadingStateChanged(true)
+ verify(additionalListener).onHighResLoadingStateChanged(true)
+ }
+
+ @Test
+ fun onHighResLoadingStateChanged_toDisabled_notifiesAllListeners() {
+ val expectedListener = mock<TaskThumbnailChangedCallback>()
+ val additionalListener = mock<TaskThumbnailChangedCallback>()
+ systemUnderTest.registerTaskThumbnailChangedCallback(
+ createTaskKey(id = 1),
+ expectedListener,
+ )
+ systemUnderTest.registerTaskThumbnailChangedCallback(
+ createTaskKey(id = 2),
+ additionalListener,
+ )
+
+ systemUnderTest.onHighResLoadingStateChanged(false)
+
+ verify(expectedListener).onHighResLoadingStateChanged(false)
+ verify(additionalListener).onHighResLoadingStateChanged(false)
}
@Test
@@ -171,7 +193,7 @@
val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
systemUnderTest.registerTaskThumbnailChangedCallback(
createTaskKey(id = 1),
- replacedListener1
+ replacedListener1,
)
systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1)
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 823f808..10be6fd 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
@@ -326,8 +326,9 @@
}
@Test
- fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() =
+ fun onHighResLoadingStateChanged_highResReplacesLowResThumbnail() =
testScope.runTest {
+ taskThumbnailDataSource.highResEnabled = false
recentsModel.seedTasks(defaultTaskList)
systemUnderTest.getAllTaskData(forceRefresh = true)
@@ -337,16 +338,77 @@
val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
val taskDataFlow = systemUnderTest.getTaskDataById(1)
- val task1ThumbnailValues = mutableListOf<Bitmap?>()
+ val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
testScope.backgroundScope.launch {
- taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
+ taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
}
taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+ taskThumbnailDataSource.highResEnabled = true
taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
- assertThat(task1ThumbnailValues.first()).isEqualTo(expectedPreviousBitmap)
- assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
+ val firstThumbnailValue = task1ThumbnailValues.first()!!
+ assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+ assertThat(firstThumbnailValue.reducedResolution).isTrue()
+
+ val lastThumbnailValue = task1ThumbnailValues.last()!!
+ assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedBitmap)
+ assertThat(lastThumbnailValue.reducedResolution).isFalse()
+ }
+
+ @Test
+ fun onHighResLoadingStateChanged_invisibleTaskIgnored() =
+ testScope.runTest {
+ taskThumbnailDataSource.highResEnabled = false
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(setOf(1))
+
+ val invisibleTaskId = 2
+ val taskDataFlow = systemUnderTest.getTaskDataById(invisibleTaskId)
+
+ val task2ThumbnailValues = mutableListOf<ThumbnailData?>()
+ testScope.backgroundScope.launch {
+ taskDataFlow.map { it?.thumbnail }.toList(task2ThumbnailValues)
+ }
+
+ taskThumbnailDataSource.highResEnabled = true
+ taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
+
+ assertThat(task2ThumbnailValues.filterNotNull()).isEmpty()
+ assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(2)).isEqualTo(0)
+ }
+
+ @Test
+ fun onHighResLoadingStateChanged_lowResDoesNotReplaceHighResThumbnail() =
+ testScope.runTest {
+ taskThumbnailDataSource.highResEnabled = true
+ recentsModel.seedTasks(defaultTaskList)
+ systemUnderTest.getAllTaskData(forceRefresh = true)
+
+ systemUnderTest.setVisibleTasks(setOf(1))
+
+ val expectedBitmap = mock<Bitmap>()
+ val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+ val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+ val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
+ testScope.backgroundScope.launch {
+ taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
+ }
+
+ taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+ taskThumbnailDataSource.highResEnabled = false
+ taskVisualsChangedDelegate.onHighResLoadingStateChanged(false)
+
+ val firstThumbnailValue = task1ThumbnailValues.first()!!
+ assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+ assertThat(firstThumbnailValue.reducedResolution).isFalse()
+
+ val lastThumbnailValue = task1ThumbnailValues.last()!!
+ assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+ assertThat(lastThumbnailValue.reducedResolution).isFalse()
}
@Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
similarity index 61%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
index bd7d970..a253280 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,22 +14,15 @@
* limitations under the License.
*/
-package com.android.quickstep.recents.usecase
+package com.android.quickstep.recents.domain.usecase
-import android.content.ComponentName
-import android.content.Intent
import android.graphics.Bitmap
-import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Rect
import android.view.Surface.ROTATION_90
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
import com.google.common.truth.Truth.assertThat
@@ -43,55 +36,34 @@
/** Test for [GetThumbnailPositionUseCase] */
@RunWith(AndroidJUnit4::class)
class GetThumbnailPositionUseCaseTest {
- 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 deviceProfileRepository = FakeRecentsDeviceProfileRepository()
private val rotationStateRepository = FakeRecentsRotationStateRepository()
- private val tasksRepository = FakeTasksRepository()
private val previewPositionHelper = mock<PreviewPositionHelper>()
private val systemUnderTest =
GetThumbnailPositionUseCase(
deviceProfileRepository,
rotationStateRepository,
- tasksRepository,
previewPositionHelper,
)
@Test
- fun invisibleTask_returnsIdentityMatrix() = runTest {
- tasksRepository.seedTasks(listOf(task))
-
- assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
- .isInstanceOf(MissingThumbnail::class.java)
+ fun nullThumbnailData_returnsIdentityMatrix() = runTest {
+ val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+ val result = systemUnderTest.invoke(null, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)
+ assertThat(result).isEqualTo(expectedResult)
}
@Test
- fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
-
- assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
- .isInstanceOf(MissingThumbnail::class.java)
+ fun withoutThumbnail_returnsIdentityMatrix() = runTest {
+ val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+ val result =
+ systemUnderTest.invoke(ThumbnailData(), CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)
+ assertThat(result).isEqualTo(expectedResult)
}
@Test
fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
-
val isLargeScreen = true
deviceProfileRepository.setRecentsDeviceProfile(
deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen)
@@ -108,13 +80,14 @@
whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated)
- assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .isEqualTo(MatrixScaling(MATRIX, isRotated))
+ val result = systemUnderTest.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+ val expectedResult = ThumbnailPosition(MATRIX, isRotated)
+ assertThat(result).isEqualTo(expectedResult)
verify(previewPositionHelper)
.updateThumbnailMatrix(
Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT),
- thumbnailData,
+ THUMBNAIL_DATA,
CANVAS_WIDTH,
CANVAS_HEIGHT,
isLargeScreen,
@@ -123,8 +96,7 @@
)
}
- companion object {
- const val TASK_ID = 2
+ private companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
const val CANVAS_WIDTH = 300
@@ -133,5 +105,14 @@
Matrix().apply {
setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
}
+
+ val THUMBNAIL_DATA =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ }
+ )
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
index 08e459b..a97ef0c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
@@ -29,6 +29,7 @@
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
@@ -58,6 +59,7 @@
private val recentsViewData = RecentsViewData()
private val getTaskUseCase = mock<GetTaskUseCase>()
+ private val getThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val isThumbnailValidUseCase =
spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository()))
private lateinit var sut: TaskViewModel
@@ -71,6 +73,7 @@
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
isThumbnailValidUseCase = isThumbnailValidUseCase,
+ getThumbnailPositionUseCase = getThumbnailPositionUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
@@ -112,6 +115,7 @@
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
isThumbnailValidUseCase = isThumbnailValidUseCase,
+ getThumbnailPositionUseCase = getThumbnailPositionUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
sut.bind(TASK_MODEL_1.id)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
deleted file mode 100644
index 0044631..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ /dev/null
@@ -1,84 +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.quickstep.recents.usecase
-
-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(setOf(TaskOverlayViewModelTest.TASK_ID))
-
- assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
- }
-
- private 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/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
deleted file mode 100644
index 4b4e2eb..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ /dev/null
@@ -1,88 +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.quickstep.task.thumbnail
-
-import android.graphics.Matrix
-import android.platform.test.flag.junit.SetFlagsRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [TaskThumbnailView] */
-@RunWith(AndroidJUnit4::class)
-class TaskThumbnailViewModelImplTest {
- @get:Rule val setFlagsRule = SetFlagsRule()
-
- private val dispatcher = StandardTestDispatcher()
- private val testScope = TestScope(dispatcher)
-
- private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
-
- private val systemUnderTest by lazy { TaskThumbnailViewModelImpl(mGetThumbnailPositionUseCase) }
-
- @Test
- fun getSnapshotMatrix_MissingThumbnail() =
- testScope.runTest {
- val taskId = 2
- val isRtl = true
-
- whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MissingThumbnail)
-
- systemUnderTest.bind(taskId)
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(Matrix.IDENTITY_MATRIX)
- }
-
- @Test
- fun getSnapshotMatrix_MatrixScaling() =
- testScope.runTest {
- val taskId = 2
- val isRtl = true
-
- whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MatrixScaling(MATRIX, isRotated = false))
-
- systemUnderTest.bind(taskId)
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(MATRIX)
- }
-
- private companion object {
- const val CANVAS_WIDTH = 300
- const val CANVAS_HEIGHT = 600
- val MATRIX =
- Matrix().apply {
- setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
- }
- }
-}
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
deleted file mode 100644
index 95504af..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ /dev/null
@@ -1,195 +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.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.launcher3.util.TestDispatcherProvider
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-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.quickstep.task.viewmodel.TaskOverlayViewModel.ThumbnailPositionState
-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.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-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] */
-@OptIn(ExperimentalCoroutinesApi::class)
-@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 mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
- private val dispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(dispatcher)
- private val systemUnderTest =
- TaskOverlayViewModel(
- task,
- recentsViewData,
- mGetThumbnailPositionUseCase,
- tasksRepository,
- TestDispatcherProvider(dispatcher),
- )
-
- @Test
- fun initialStateIsDisabled() =
- testScope.runTest { assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) }
-
- @Test
- fun recentsViewOverlayDisabled_Disabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = false
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-
- assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
- }
-
- @Test
- fun taskNotFullyVisible_Disabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf()
-
- assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
- }
-
- @Test
- fun noThumbnail_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- task.isLocked = false
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
- }
-
- @Test
- fun withThumbnail_RealSnapshot_NotLocked_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = true
- task.isLocked = false
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
- }
-
- @Test
- fun withThumbnail_RealSnapshot_Locked_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = true
- task.isLocked = true
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
- }
-
- @Test
- fun withThumbnail_FakeSnapshot_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = false
- task.isLocked = false
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
- }
-
- @Test
- fun getThumbnailMatrix_MissingThumbnail() =
- testScope.runTest {
- val isRtl = true
-
- whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MissingThumbnail)
-
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
- }
-
- @Test
- fun getThumbnailMatrix_MatrixScaling() =
- testScope.runTest {
- val isRtl = true
- val isRotated = true
-
- whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MatrixScaling(MATRIX, isRotated))
-
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
- }
-
- companion object {
- const val TASK_ID = 0
- const val THUMBNAIL_WIDTH = 100
- const val THUMBNAIL_HEIGHT = 200
- const val CANVAS_WIDTH = 300
- const val CANVAS_HEIGHT = 600
- val MATRIX =
- Matrix().apply {
- setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
- }
- }
-}
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 5051251..9d000a4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -89,7 +89,7 @@
@Before
fun setup() {
whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView)
- whenever(mockTaskContainer.splitAnimationThumbnail).thenReturn(mockBitmap)
+ whenever(mockTaskContainer.thumbnail).thenReturn(mockBitmap)
whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
whenever(mockTaskContainer.task).thenReturn(mockTask)
whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable)
@@ -117,7 +117,7 @@
assertEquals(
"Did not fallback to use splitSource icon drawable",
mockSplitSourceDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -133,7 +133,7 @@
assertEquals(
"Did not use taskView icon drawable",
mockTaskViewDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -152,7 +152,7 @@
assertEquals(
"Did not use taskView icon drawable",
mockTaskViewDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -168,7 +168,7 @@
assertEquals(
"Did not use splitSource icon drawable",
mockSplitSourceDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -190,13 +190,13 @@
val splitAnimInitProps: SplitAnimationController.Companion.SplitAnimInitProps =
splitAnimationController.getFirstAnimInitViews(
{ mockGroupedTaskView },
- { splitSelectSource }
+ { splitSelectSource },
)
assertEquals(
"Did not use splitSource icon drawable",
mockSplitSourceDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -214,7 +214,7 @@
any(),
any(),
any(),
- any()
+ any(),
)
spySplitAnimationController.playSplitLaunchAnimation(
@@ -230,7 +230,7 @@
null /* info */,
null /* t */,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -243,7 +243,7 @@
any(),
any(),
any(),
- any()
+ any(),
)
}
@@ -267,7 +267,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -296,7 +296,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -325,7 +325,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -353,7 +353,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -381,7 +381,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -408,7 +408,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
index f2fa0c5..cb088fd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -16,13 +16,13 @@
package com.android.quickstep.util
import com.android.launcher3.util.IntArray
-import com.android.quickstep.util.TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID
-import com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP
+import com.android.quickstep.util.TaskGridNavHelper.Companion.ADD_DESK_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.Companion.CLEAR_ALL_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.DOWN
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.LEFT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.RIGHT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.TAB
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.UP
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -35,8 +35,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_DOWN, delta = 1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, DOWN, delta = 1)).isEqualTo(2)
}
/* ↑----→
@@ -46,7 +45,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_UP, delta = 1)).isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, UP, delta = 1)).isEqualTo(2)
}
/* ↓----↑
@@ -57,8 +56,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_DOWN, delta = 1))
- .isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, DOWN, delta = 1)).isEqualTo(1)
}
/*
@@ -68,7 +66,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_UP, delta = 1)).isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, UP, delta = 1)).isEqualTo(1)
}
/*
@@ -78,8 +76,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_LEFT, delta = 1))
- .isEqualTo(3)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, LEFT, delta = 1)).isEqualTo(3)
}
/*
@@ -89,8 +86,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_LEFT, delta = 1))
- .isEqualTo(4)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, LEFT, delta = 1)).isEqualTo(4)
}
/*
@@ -100,8 +96,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
- assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_RIGHT, delta = -1))
- .isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 3, RIGHT, delta = -1)).isEqualTo(1)
}
/*
@@ -111,8 +106,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
- assertThat(getNextGridPage(currentPageTaskViewId = 4, DIRECTION_RIGHT, delta = -1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 4, RIGHT, delta = -1)).isEqualTo(2)
}
/*
@@ -124,7 +118,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_RIGHT, delta = -1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, RIGHT, delta = -1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -137,7 +131,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_RIGHT, delta = -1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, RIGHT, delta = -1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -149,7 +143,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 5, DIRECTION_LEFT, delta = 1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 5, LEFT, delta = 1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -161,7 +155,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 6, DIRECTION_LEFT, delta = 1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 6, LEFT, delta = 1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -176,11 +170,7 @@
@Test
fun equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
assertThat(
- getNextGridPage(
- currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
- delta = 1,
- )
+ getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, LEFT, delta = 1)
)
.isEqualTo(1)
}
@@ -194,11 +184,7 @@
@Test
fun equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
assertThat(
- getNextGridPage(
- currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
- delta = -1,
- )
+ getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, RIGHT, delta = -1)
)
.isEqualTo(6)
}
@@ -214,7 +200,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -233,7 +219,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_UP,
+ UP,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -254,7 +240,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_DOWN,
+ DOWN,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -275,7 +261,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -297,7 +283,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -316,7 +302,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 7,
- DIRECTION_DOWN,
+ DOWN,
delta = 1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -335,7 +321,7 @@
assertThat(
getNextGridPage(
/* topIds = */ currentPageTaskViewId = 7,
- DIRECTION_UP,
+ UP,
delta = 1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -353,7 +339,7 @@
assertThat(
getNextGridPage(
/* topIds = */ currentPageTaskViewId = 6,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -372,7 +358,7 @@
assertThat(
getNextGridPage(
/* topIds = */ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -391,7 +377,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
bottomIds = IntArray.wrap(2, 4, 6, 7),
)
@@ -406,8 +392,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = 1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = 1)).isEqualTo(2)
}
/*
@@ -418,8 +403,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = 1))
- .isEqualTo(3)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = 1)).isEqualTo(3)
}
/*
@@ -431,8 +415,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_TAB, delta = -1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 3, TAB, delta = -1)).isEqualTo(2)
}
/*
@@ -442,8 +425,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = -1))
- .isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = -1)).isEqualTo(1)
}
/*
@@ -453,9 +435,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() {
- assertThat(
- getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = -1, cycle = false)
- )
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = -1, cycle = false))
.isEqualTo(1)
}
@@ -469,7 +449,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_TAB,
+ TAB,
delta = 1,
cycle = false,
)
@@ -487,7 +467,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -505,7 +485,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -525,7 +505,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -546,7 +526,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -565,7 +545,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 2,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -584,7 +564,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 1,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -603,7 +583,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_TAB,
+ TAB,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -621,7 +601,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
topIds = IntArray(),
bottomIds = IntArray.wrap(2),
@@ -643,7 +623,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_TAB,
+ TAB,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -662,7 +642,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 1,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
hasAddDesktopButton = true,
)
@@ -681,7 +661,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 2,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
hasAddDesktopButton = true,
)
@@ -701,7 +681,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
hasAddDesktopButton = true,
)
@@ -722,7 +702,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
hasAddDesktopButton = true,
)
@@ -741,7 +721,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_UP,
+ UP,
delta = 1,
hasAddDesktopButton = true,
)
@@ -760,7 +740,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_DOWN,
+ DOWN,
delta = 1,
hasAddDesktopButton = true,
)
@@ -778,7 +758,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID),
hasAddDesktopButton = true,
@@ -797,7 +777,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID),
hasAddDesktopButton = true,
@@ -806,9 +786,43 @@
.isEqualTo(ADD_DESK_PLACEHOLDER_ID)
}
+ // Col offset: 0 1 2
+ // -----------
+ // ID grid: 4 2 0 start
+ // end [5] 3 1
+ @Test
+ fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsStart() {
+ val expected = listOf(Pair(4, 0), Pair(3, 1), Pair(2, 1), Pair(1, 2), Pair(0, 2))
+ assertThat(
+ gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId = 5,
+ towardsStart = true,
+ )
+ .toList()
+ )
+ .isEqualTo(expected)
+ }
+
+ // Col offset: 2 1 0
+ // -----------
+ // ID grid: 4 2 [0] start
+ // end 5 3 1
+ @Test
+ fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsEnd() {
+ val expected = listOf(Pair(1, 0), Pair(2, 1), Pair(3, 1), Pair(4, 2), Pair(5, 2))
+ assertThat(
+ gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId = 0,
+ towardsStart = false,
+ )
+ .toList()
+ )
+ .isEqualTo(expected)
+ }
+
private fun getNextGridPage(
currentPageTaskViewId: Int,
- direction: Int,
+ direction: TaskGridNavHelper.TaskNavDirection,
delta: Int,
topIds: IntArray = IntArray.wrap(1, 3, 5),
bottomIds: IntArray = IntArray.wrap(2, 4, 6),
@@ -821,6 +835,22 @@
return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle)
}
+ private fun gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId: Int,
+ towardsStart: Boolean,
+ topIds: IntArray = IntArray.wrap(0, 2, 4),
+ bottomIds: IntArray = IntArray.wrap(1, 3, 5),
+ largeTileIds: List<Int> = emptyList(),
+ hasAddDesktopButton: Boolean = false,
+ ): Sequence<Pair<Int, Int>> {
+ val taskGridNavHelper =
+ TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
+ return taskGridNavHelper.gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId,
+ towardsStart,
+ )
+ }
+
private companion object {
const val FOCUSED_TASK_ID = 99
const val DESKTOP_TASK_ID = 100
diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
new file mode 100644
index 0000000..4b8f2a2
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.statehandlers
+
+import android.content.Context
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.quickstep.SystemUiProxy
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+/**
+ * Tests the behavior of [DesktopVisibilityController] in regards to multiple desktops and multiple
+ * displays.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopVisibilityControllerTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus::class.java)
+ .startMocking()
+
+ private val context = mock<Context>()
+ private val systemUiProxy = mock<SystemUiProxy>()
+ private val lifeCycleTracker = mock<DaggerSingletonTracker>()
+ private lateinit var desktopVisibilityController: DesktopVisibilityController
+
+ @Before
+ fun setUp() {
+ whenever(context.resources).thenReturn(mock())
+ whenever(DesktopModeStatus.enableMultipleDesktops(context)).thenReturn(true)
+ desktopVisibilityController =
+ DesktopVisibilityController(context, systemUiProxy, lifeCycleTracker)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND)
+ fun noCrashWhenCheckingNonExistentDisplay() {
+ assertFalse(desktopVisibilityController.isInDesktopMode(displayId = 500))
+ assertFalse(desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId = 300))
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 76aab39..a523e02 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.Display.DEFAULT_DISPLAY
import androidx.test.platform.app.InstrumentationRegistry
@@ -106,6 +107,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+ @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
fun createDesktopTaskShortcutFactory_transparentTask() {
val baseComponent = ComponentName("", /* class */ "")
val taskKey =
diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
index 818841a..2db94f6 100644
--- a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.view.Display.DEFAULT_DISPLAY
@@ -113,6 +114,7 @@
Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
)
+ @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
fun createExternalDisplayTaskShortcut_transparentTask() {
val baseComponent = ComponentName("", /* class */ "")
val taskKey =
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 79d3c19..1c7f51c 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -50,6 +50,7 @@
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
@@ -62,6 +63,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Comparator;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -541,6 +543,7 @@
@Test
@PortraitLandscape
+ @ScreenRecordRule.ScreenRecord // TODO(b/396447643): Remove screen record.
public void testDismissCancel() throws Exception {
startTestAppsWithCheck();
Overview overview = mLauncher.goHome().switchToOverview();
@@ -557,6 +560,27 @@
numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount()));
}
+ @Test
+ @PortraitLandscape
+ public void testDismissBottomRow() throws Exception {
+ assumeTrue(mLauncher.isTablet());
+ mLauncher.goHome().switchToOverview().dismissAllTasks();
+ startTestAppsWithCheck();
+ Overview overview = mLauncher.goHome().switchToOverview();
+ assertIsInState("Launcher internal state didn't switch to Overview",
+ ExpectedState.OVERVIEW);
+ final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+ OverviewTask bottomTask = overview.getCurrentTasksForTablet().stream().max(
+ Comparator.comparingInt(OverviewTask::getTaskCenterY)).get();
+ assertNotNull("bottomTask null", bottomTask);
+
+ bottomTask.dismiss();
+
+ runOnRecentsView(recentsView -> assertEquals(
+ "Dismissing a bottomTask didn't remove 1 bottomTask from Overview",
+ numTasks - 1, recentsView.getTaskViewCount()));
+ }
+
private void startTestAppsWithCheck() throws Exception {
startTestApps();
expectLaunchedAppState();
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
new file mode 100644
index 0000000..b4d9f5b
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.util.DisplayMetrics
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.launcher3.desktop.DesktopAppLaunchAnimatorHelper
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class DesktopAppLaunchAnimatorHelperTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val context = mock<Context>()
+ private val resources = mock<Resources>()
+ private val transaction = mock<SurfaceControl.Transaction>()
+ private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>()
+
+ private lateinit var helper: DesktopAppLaunchAnimatorHelper
+
+ @Before
+ fun setUp() {
+ helper =
+ DesktopAppLaunchAnimatorHelper(
+ context = context,
+ launchType = AppLaunchType.LAUNCH,
+ cujType = Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+ transactionSupplier = transactionSupplier,
+ )
+ whenever(transactionSupplier.get()).thenReturn(transaction)
+ whenever(transaction.setCrop(any(), any())).thenReturn(transaction)
+ whenever(transaction.setCornerRadius(any(), any())).thenReturn(transaction)
+
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.displayMetrics).thenReturn(DisplayMetrics())
+ whenever(context.mainThreadHandler).thenReturn(MAIN_EXECUTOR.handler)
+ }
+
+ @Test
+ fun launchTransition_returnsLaunchAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(1)
+ assertLaunchAnimator(actual[0])
+ }
+
+ @Test
+ fun minimizeTransition_returnsLaunchAndMinimizeAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagEnabled_returnsLaunchAndCloseAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertTrampolineLaunchAnimator(actual[0])
+ assertCloseAnimator(actual[1])
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagDisabled_returnsLaunchAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(1)
+ assertLaunchAnimator(actual[0])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagEnabled_hitDesktopWindowLimit_returnsLaunchMinimizeCloseAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(3)
+ assertTrampolineLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ assertCloseAnimator(actual[2])
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagDisabled_hitDesktopWindowLimit_returnsLaunchMinimizeAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ }
+
+ private fun assertLaunchAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator)
+ .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.interpolator)
+ assertThat(animator.childAnimations[0].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.durationMs)
+ assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[1].interpolator).isEqualTo(Interpolators.LINEAR)
+ assertThat(animator.childAnimations[1].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+ }
+
+ private fun assertTrampolineLaunchAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(1)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator).isEqualTo(Interpolators.LINEAR)
+ assertThat(animator.childAnimations[0].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+ }
+
+ private fun assertMinimizeAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator)
+ .isInstanceOf(Interpolators.STANDARD_ACCELERATE::class.java)
+ assertThat(animator.childAnimations[0].duration).isEqualTo(200)
+ assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[1].interpolator)
+ .isInstanceOf(Interpolators.LINEAR::class.java)
+ assertThat(animator.childAnimations[1].duration).isEqualTo(100)
+ }
+
+ private fun assertCloseAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.interpolator).isInstanceOf(Interpolators.LINEAR::class.java)
+ assertThat(animator.duration).isEqualTo(100)
+ }
+
+ private companion object {
+ val TASK_INFO_FREEFORM =
+ ActivityManager.RunningTaskInfo().apply {
+ baseIntent =
+ Intent().apply {
+ component = ComponentName("com.example.app", "com.example.app.MainActivity")
+ }
+ configuration.windowConfiguration.windowingMode =
+ WindowConfiguration.WINDOWING_MODE_FREEFORM
+ }
+ }
+}
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index bd792ac..1a4a3f2 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -28,7 +28,7 @@
<string name="shortcut_not_available" msgid="2536503539825726397">"Sparčiojo klavišo negalima naudoti"</string>
<string name="home_screen" msgid="5629429142036709174">"Pagrindinis"</string>
<string name="set_default_home_app" msgid="5808906607627586381">"Nustatykite „<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>“ kaip numatytąją pagrindinę programą skiltyje „Nustatymai“"</string>
- <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Išskaidyto ekrano režimas"</string>
+ <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Išskaidytas ekranas"</string>
<string name="split_app_info_accessibility" msgid="5475288491241414932">"Programos „%1$s“ informacija"</string>
<string name="split_app_usage_settings" msgid="7214375263347964093">"„%1$s“ naudojimo nustatymai"</string>
<string name="new_window_option_taskbar" msgid="6448780542727767211">"Naujas langas"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 3031259..46aadc0 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -63,7 +63,7 @@
<string name="widgets_full_sheet_search_bar_hint" msgid="8484659090860596457">"ସନ୍ଧାନ କରନ୍ତୁ"</string>
<string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"ସନ୍ଧାନ ବାକ୍ସରୁ ଟେକ୍ସଟ୍ ଖାଲି କରନ୍ତୁ"</string>
<string name="no_widgets_available" msgid="4337693382501046170">"ୱିଜେଟ୍ ଏବଂ ସର୍ଟକଟଗୁଡ଼ିକ ଉପଲବ୍ଧ ନାହିଁ"</string>
- <string name="no_search_results" msgid="3787956167293097509">"କୌଣସି ୱିଜେଟ୍ କିମ୍ବା ସର୍ଟକଟ୍ ମିଳିଲା ନାହିଁ"</string>
+ <string name="no_search_results" msgid="3787956167293097509">"କୌଣସି ୱିଜେଟ କିମ୍ବା ସର୍ଟକଟ ମିଳିଲା ନାହିଁ"</string>
<string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"ବ୍ୟକ୍ତିଗତ"</string>
<string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ୱାର୍କ"</string>
<string name="widget_category_conversations" msgid="8894438636213590446">"ବାର୍ତ୍ତାଳାପଗୁଡ଼ିକ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index c15e2e9..17ecb78 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -94,7 +94,7 @@
<string name="all_apps_button_work_label" msgid="7270707118948892488">"ਕਾਰਜ-ਸਥਾਨ ਸੰਬੰਧੀ ਐਪਾਂ ਦੀ ਸੂਚੀ"</string>
<string name="remove_drop_target_label" msgid="7812859488053230776">"ਹਟਾਓ"</string>
<string name="uninstall_drop_target_label" msgid="4722034217958379417">"ਅਣਸਥਾਪਤ ਕਰੋ"</string>
- <string name="app_info_drop_target_label" msgid="692894985365717661">"ਐਪ ਜਾਣਕਾਰੀ"</string>
+ <string name="app_info_drop_target_label" msgid="692894985365717661">"ਐਪ ਸੰਬੰਧੀ ਜਾਣਕਾਰੀ"</string>
<string name="install_private_system_shortcut_label" msgid="1616889277073184841">"ਪ੍ਰਾਈਵੇਟ ਵਜੋਂ ਸਥਾਪਤ ਕਰੋ"</string>
<string name="uninstall_private_system_shortcut_label" msgid="8423460530441627982">"ਐਪ ਅਣਸਥਾਪਤ ਕਰੋ"</string>
<string name="install_drop_target_label" msgid="2539096853673231757">"ਸਥਾਪਤ ਕਰੋ"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index b6563b0..4145fb6 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -46,14 +46,14 @@
<string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d వెడల్పు X %2$d ఎత్తు"</string>
<string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్"</string>
<string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్, %2$d వెడల్పు %3$d ఎత్తు ఉండాలి"</string>
- <string name="add_item_request_drag_hint" msgid="8730547755622776606">"విడ్జెట్ను మొదటి స్క్రీన్లో తిప్పడానికి దాన్ని తాకి, & నొక్కి పట్టుకోండి"</string>
+ <string name="add_item_request_drag_hint" msgid="8730547755622776606">"విడ్జెట్ను మొదటి స్క్రీన్లో అటు, ఇటు కదపడానికి దాన్ని తాకి, నొక్కి పట్టుకోండి"</string>
<string name="add_to_home_screen" msgid="9168649446635919791">"మొదటి స్క్రీన్కు జోడించండి"</string>
<string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"మొదటి స్క్రీన్కు <xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్ జోడించబడింది"</string>
<string name="suggested_widgets_header_title" msgid="1844314680798145222">"సూచనలు"</string>
<string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"నిత్యావసరాలు"</string>
<string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"వార్తలు & మ్యాగజైన్లు"</string>
<string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"వినోదం"</string>
- <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"సామాజికం"</string>
+ <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"సోషల్ మీడియా"</string>
<string name="others_widget_recommendation_category_label" msgid="5555987036267226245">"మీ కోసం సూచించినవి"</string>
<string name="widget_picker_right_pane_accessibility_title" msgid="1673313931455067502">"కుడి వైపున <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> విడ్జెట్లు, ఎడమ వైపున సెర్చ్, ఇతర ఆప్షన్లు"</string>
<string name="widgets_count" msgid="6467746476364652096">"{count,plural, =1{# విడ్జెట్}other{# విడ్జెట్లు}}"</string>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e6b3457..bd42b2b 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -27,6 +27,7 @@
import static com.android.launcher3.icons.BitmapInfo.FLAG_SKIP_USER_BADGE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
@@ -75,7 +76,6 @@
import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
@@ -723,8 +723,7 @@
protected void drawDotIfNecessary(Canvas canvas) {
if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
getIconBounds(mDotParams.iconBounds);
- Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ Utilities.scaleRectAboutCenter(mDotParams.iconBounds, ICON_VISIBLE_AREA_FACTOR);
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
@@ -773,9 +772,7 @@
return;
}
getIconBounds(mRunningAppIconBounds);
- Utilities.scaleRectAboutCenter(
- mRunningAppIconBounds,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ Utilities.scaleRectAboutCenter(mRunningAppIconBounds, ICON_VISIBLE_AREA_FACTOR);
final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 3b283c3..c85ca49 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -31,6 +31,7 @@
import static com.android.launcher3.testing.shared.ResourceUtils.roundPxValueFromFloat;
import static com.android.wm.shell.Flags.enableBubbleBar;
import static com.android.wm.shell.Flags.enableTinyTaskbar;
+import static com.android.wm.shell.Flags.enableBubbleBarOnPhones;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -52,9 +53,8 @@
import com.android.launcher3.CellLayout.ContainerType;
import com.android.launcher3.DevicePaddings.DevicePadding;
import com.android.launcher3.folder.ClippedFolderIconLayoutRule;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.responsive.CalculatedCellSpec;
import com.android.launcher3.responsive.CalculatedHotseatSpec;
@@ -388,7 +388,7 @@
/** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */
DeviceProfile(Context context, InvariantDeviceProfile inv, Info info,
- WindowManagerProxy wmProxy, IconShape iconShape, WindowBounds windowBounds,
+ WindowManagerProxy wmProxy, ThemeManager themeManager, WindowBounds windowBounds,
SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
boolean transposeLayoutWithOrientation, boolean isMultiDisplay, boolean isGestureMode,
@NonNull final ViewScaleProvider viewScaleProvider,
@@ -420,7 +420,9 @@
isTablet = info.isTablet(windowBounds);
isPhone = !isTablet;
isTwoPanels = isTablet && isMultiDisplay;
- isTaskbarPresent = (isTablet || (enableTinyTaskbar() && isGestureMode))
+ boolean taskbarOrBubbleBarOnPhones = enableTinyTaskbar()
+ || (enableBubbleBar() && enableBubbleBarOnPhones());
+ isTaskbarPresent = (isTablet || (taskbarOrBubbleBarOnPhones && isGestureMode))
&& wmProxy.isTaskbarDrawnInProcess();
// Some more constants.
@@ -846,8 +848,8 @@
dimensionOverrideProvider.accept(this);
// This is done last, after iconSizePx is calculated above.
- mDotRendererWorkSpace = createDotRenderer(iconShape, iconSizePx, dotRendererCache);
- mDotRendererAllApps = createDotRenderer(iconShape, allAppsIconSizePx, dotRendererCache);
+ mDotRendererWorkSpace = createDotRenderer(themeManager, iconSizePx, dotRendererCache);
+ mDotRendererAllApps = createDotRenderer(themeManager, allAppsIconSizePx, dotRendererCache);
}
/**
@@ -869,12 +871,12 @@
}
private static DotRenderer createDotRenderer(
- @NonNull IconShape iconShape, int size, @NonNull SparseArray<DotRenderer> cache) {
+ @NonNull ThemeManager themeManager, int size, @NonNull SparseArray<DotRenderer> cache) {
DotRenderer renderer = cache.get(size);
if (renderer == null) {
renderer = new DotRenderer(
size,
- iconShape.getShape().getPath(DEFAULT_DOT_SIZE),
+ themeManager.getIconShape().getPath(DEFAULT_DOT_SIZE),
DEFAULT_DOT_SIZE);
cache.put(size, renderer);
}
@@ -1371,7 +1373,7 @@
updateHotseatSizes(iconSizePx);
// Folder icon
- folderIconSizePx = IconNormalizer.getNormalizedCircleSize(iconSizePx);
+ folderIconSizePx = Math.round(iconSizePx * ICON_VISIBLE_AREA_FACTOR);
folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2;
// Update widget padding:
@@ -2478,7 +2480,7 @@
private final InvariantDeviceProfile mInv;
private final Info mInfo;
private final WindowManagerProxy mWMProxy;
- private final IconShape mIconShape;
+ private final ThemeManager mThemeManager;
private WindowBounds mWindowBounds;
private boolean mIsMultiDisplay;
@@ -2495,12 +2497,12 @@
private boolean mIsTransientTaskbar;
public Builder(Context context, InvariantDeviceProfile inv, Info info,
- WindowManagerProxy wmProxy, IconShape iconShape) {
+ WindowManagerProxy wmProxy, ThemeManager themeManager) {
mContext = context;
mInv = inv;
mInfo = info;
mWMProxy = wmProxy;
- mIconShape = iconShape;
+ mThemeManager = themeManager;
mIsTransientTaskbar = info.isTransientTaskbar();
}
@@ -2581,7 +2583,7 @@
if (mOverrideProvider == null) {
mOverrideProvider = DEFAULT_DIMENSION_PROVIDER;
}
- return new DeviceProfile(mContext, mInv, mInfo, mWMProxy, mIconShape,
+ return new DeviceProfile(mContext, mInv, mInfo, mWMProxy, mThemeManager,
mWindowBounds, mDotRendererCache,
mIsMultiWindowMode, mTransposeLayoutWithOrientation, mIsMultiDisplay,
mIsGestureMode, mViewScaleProvider, mOverrideProvider, mIsTransientTaskbar);
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index fc071d7..f189549 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -57,7 +57,7 @@
import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.dagger.LauncherAppComponent;
import com.android.launcher3.dagger.LauncherAppSingleton;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.DeviceGridState;
@@ -85,6 +85,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import javax.inject.Inject;
@@ -136,7 +137,7 @@
private final DisplayController mDisplayController;
private final WindowManagerProxy mWMProxy;
private final LauncherPrefs mPrefs;
- private final IconShape mIconShape;
+ private final ThemeManager mThemeManager;
/**
* Number of icons per row and column in the workspace.
@@ -252,7 +253,7 @@
public Point defaultWallpaperSize;
- private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+ private final List<OnIDPChangeListener> mChangeListeners = new CopyOnWriteArrayList<>();
@Inject
InvariantDeviceProfile(
@@ -260,12 +261,12 @@
LauncherPrefs prefs,
DisplayController dc,
WindowManagerProxy wmProxy,
- IconShape iconShape,
+ ThemeManager themeManager,
DaggerSingletonTracker lifeCycle) {
mDisplayController = dc;
mWMProxy = wmProxy;
mPrefs = prefs;
- mIconShape = iconShape;
+ mThemeManager = themeManager;
String gridName = prefs.get(GRID_NAME);
initGrid(context, gridName);
@@ -490,7 +491,7 @@
}
DeviceProfile.Builder newDPBuilder(Context context, Info info) {
- return new DeviceProfile.Builder(context, this, info, mWMProxy, mIconShape);
+ return new DeviceProfile.Builder(context, this, info, mWMProxy, mThemeManager);
}
public void addOnChangeListener(OnIDPChangeListener listener) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 315301a..3edba99 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -71,6 +71,7 @@
import static com.android.launcher3.LauncherState.NO_SCALE;
import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
@@ -182,8 +183,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.debug.TestEventEmitter.TestEvent;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -610,7 +611,7 @@
RuleController.getInstance(this).setRules(
RuleController.parseRules(this, R.xml.split_configuration));
}
- TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE);
+ TestEventEmitter.sendEvent(TestEvent.LAUNCHER_ON_CREATE);
}
protected ModelCallbacks createModelCallbacks() {
@@ -1466,7 +1467,10 @@
}
getModelWriter().addItemToDatabase(info, container, screenId, cellXY[0], cellXY[1]);
- mWorkspace.addInScreen(view, info);
+ AnimatorSet anim = new AnimatorSet();
+ anim.addListener(forEndCallback(() ->
+ view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)));
+ bindInflatedItems(Collections.singletonList(Pair.create(info, view)), anim);
} else {
// Adding a shortcut to a Folder.
FolderIcon folderIcon = findFolderIcon(container);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 71013c3..bf2ad92 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,54 +16,32 @@
package com.android.launcher3;
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
-import static android.content.Context.RECEIVER_EXPORTED;
-
-import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-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;
-
-import android.content.ComponentName;
import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
import android.util.Log;
import androidx.annotation.Nullable;
-import androidx.core.os.BuildCompat;
+import com.android.launcher3.dagger.LauncherComponentProvider;
import com.android.launcher3.graphics.ThemeManager;
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.icons.LauncherIconProvider;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.ModelLauncherCallbacks;
+import com.android.launcher3.model.ModelInitializer;
import com.android.launcher3.model.WidgetsFilterDataProvider;
-import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.pm.InstallSessionTracker;
import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.LockedUserState;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SettingsCache;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.widget.custom.CustomWidgetManager;
public class LauncherAppState implements SafeCloseable {
public static final String TAG = "LauncherAppState";
- public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
// We do not need any synchronization for this variable as its only written on UI thread.
public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -94,82 +72,21 @@
mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> context.getPackageManager().isSafeMode());
- mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> {
- if (modelPropertiesChanged) {
- refreshAndReloadLauncher();
- }
- });
- ThemeChangeListener themeChangeListener = this::refreshAndReloadLauncher;
- ThemeManager.INSTANCE.get(context).addChangeListener(themeChangeListener);
- mOnTerminateCallback.add(() ->
- ThemeManager.INSTANCE.get(context).removeChangeListener(themeChangeListener));
-
- ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
- LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
- launcherApps.registerCallback(callbacks);
- mOnTerminateCallback.add(() ->
- mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
-
- if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
- ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
- params.setEnableUnarchivalConfirmation(false);
- params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps());
- launcherApps.setArchiveCompatibility(params);
- }
-
- SimpleBroadcastReceiver modelChangeReceiver =
- new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
- modelChangeReceiver.register(
- ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
- if (BuildConfig.IS_STUDIO_BUILD) {
- modelChangeReceiver.register(RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD);
- }
- mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely());
-
- SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
- .addUserEventListener(mModel::onUserEvent);
- mOnTerminateCallback.add(userChangeListener::close);
-
- if (enableSmartspaceRemovalToggle()) {
- OnSharedPreferenceChangeListener firstPagePinnedItemListener =
- new OnSharedPreferenceChangeListener() {
- @Override
- public void onSharedPreferenceChanged(
- SharedPreferences sharedPreferences, String key) {
- if (SMARTSPACE_ON_HOME_SCREEN.equals(key)) {
- mModel.forceReload();
- }
- }
- };
- LauncherPrefs.getPrefs(mContext).registerOnSharedPreferenceChangeListener(
- firstPagePinnedItemListener);
- mOnTerminateCallback.add(() -> LauncherPrefs.getPrefs(mContext)
- .unregisterOnSharedPreferenceChangeListener(firstPagePinnedItemListener));
- }
-
- LockedUserState.get(context).runOnUserUnlocked(() -> {
- CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
- mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
-
- SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
- mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
- mOnTerminateCallback.add(iconChangeTracker::close);
-
- InstallSessionTracker installSessionTracker =
- InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(callbacks);
- mOnTerminateCallback.add(installSessionTracker::unregister);
- });
-
- // Register an observer to rebind the notification listener when dots are re-enabled.
- SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
- SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged;
- settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister);
- onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
- mOnTerminateCallback.add(() ->
- settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
- // Register an observer to notify Launcher about Private Space settings toggle.
- registerPrivateSpaceHideWhenLockListener(settingsCache);
+ ModelInitializer initializer = new ModelInitializer(
+ context,
+ LauncherComponentProvider.get(context).getIconPool(),
+ mIconCache,
+ mInvariantDeviceProfile,
+ ThemeManager.INSTANCE.get(context),
+ UserCache.INSTANCE.get(context),
+ SettingsCache.INSTANCE.get(context),
+ mIconProvider,
+ CustomWidgetManager.INSTANCE.get(context),
+ InstallSessionHelper.INSTANCE.get(context),
+ closeable -> mOnTerminateCallback.add(closeable::close)
+ );
+ initializer.initialize(mModel);
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -186,32 +103,6 @@
mOnTerminateCallback.add(mModel::destroy);
}
- private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
- if (areNotificationDotsEnabled) {
- NotificationListener.requestRebind(new ComponentName(
- mContext, NotificationListener.class));
- }
- }
-
- private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) {
- SettingsCache.OnChangeListener psHideWhenLockChangedListener =
- this::onPrivateSpaceHideWhenLockChanged;
- settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psHideWhenLockChangedListener);
- mOnTerminateCallback.add(() -> settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI,
- psHideWhenLockChangedListener));
- }
-
- private void onPrivateSpaceHideWhenLockChanged(boolean isPrivateSpaceHideOnLockEnabled) {
- mModel.forceReload();
- }
-
- private void refreshAndReloadLauncher() {
- LauncherIcons.clearPool(mContext);
- mIconCache.updateIconParams(
- mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
- mModel.forceReload();
- }
-
/**
* Call from Application.onTerminate(), which is not guaranteed to ever be called.
*/
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index 185629b..6e4276d 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -15,13 +15,11 @@
*/
package com.android.launcher3
-import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.os.UserHandle
import android.text.TextUtils
-import android.util.Log
import android.util.Pair
import androidx.annotation.WorkerThread
import com.android.launcher3.celllayout.CellPosMapper
@@ -47,7 +45,6 @@
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.pm.UserCache
import com.android.launcher3.shortcuts.ShortcutRequest
-import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.util.PackageManagerHelper
@@ -173,15 +170,8 @@
}
}
- fun onBroadcastIntent(intent: Intent) {
- if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
- when (intent.action) {
- LauncherAppState.ACTION_FORCE_ROLOAD ->
- // If we have changed locale we need to clear out the labels in all apps/workspace.
- forceReload()
- DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED ->
- enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
- }
+ fun reloadStringCache() {
+ enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
}
/**
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 5338fb4..d01f35d 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -11,8 +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.debug.TestEventEmitter.TestEvent
import com.android.launcher3.model.BgDataModel
import com.android.launcher3.model.StringCache
import com.android.launcher3.model.data.AppInfo
@@ -154,7 +154,7 @@
/*pause=*/ false,
deviceProfile.isTwoPanels,
)
- TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
+ TestEventEmitter.sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
}
/**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index b41a425..cfb1161 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,8 +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.debug.TestEventEmitter.TestEvent;
import com.android.launcher3.dot.FolderDotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -2255,7 +2255,7 @@
if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
d.stateAnnouncer.completeAction(R.string.item_moved);
}
- TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP);
+ TestEventEmitter.sendEvent(TestEvent.WORKSPACE_ON_DROP);
}
@Nullable
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 260ff9f..fafa60b 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -116,6 +116,7 @@
ScrimView.ScrimDrawingController {
+ private static final String TAG = "ActivityAllAppsContainerView";
public static final float PULL_MULTIPLIER = .02f;
public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
@@ -600,6 +601,7 @@
mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
findViewById(R.id.tab_personal)
.setOnClickListener((View view) -> {
+ Log.d(TAG, "rebindAdapters: " + "Clicked personal tab.");
if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
mActivityContext.getStatsLogManager().logger()
.log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
@@ -607,6 +609,7 @@
});
findViewById(R.id.tab_work)
.setOnClickListener((View view) -> {
+ Log.d(TAG, "rebindAdapters: " + "Clicked work tab.");
if (mViewPager.snapToPage(AdapterHolder.WORK)) {
mActivityContext.getStatsLogManager().logger()
.log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java
index a14ac98..864ede8 100644
--- a/src/com/android/launcher3/allapps/WorkPausedCard.java
+++ b/src/com/android/launcher3/allapps/WorkPausedCard.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.res.Configuration;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
@@ -34,6 +35,7 @@
*/
public class WorkPausedCard extends LinearLayout implements View.OnClickListener {
+ private static final String TAG = "WorkPausedCard";
private final ActivityContext mActivityContext;
private Button mBtn;
@@ -79,6 +81,7 @@
@Override
public void onClick(View view) {
+ Log.d(TAG, "WorkPausedCard clicked.");
mActivityContext.getAppsView().getWorkManager().setWorkProfileEnabled(true);
mActivityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP);
}
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 6d7d193..920efa4 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -200,6 +200,7 @@
private void onWorkFabClicked(View view) {
if (getCurrentState() == STATE_ENABLED && mWorkUtilityView.isEnabled()) {
+ Log.d(TAG, "Work FAB clicked.");
logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP);
setWorkProfileEnabled(false);
}
diff --git a/src/com/android/launcher3/allapps/WorkUtilityView.java b/src/com/android/launcher3/allapps/WorkUtilityView.java
index e42a6b9..20ceb15 100644
--- a/src/com/android/launcher3/allapps/WorkUtilityView.java
+++ b/src/com/android/launcher3/allapps/WorkUtilityView.java
@@ -30,6 +30,7 @@
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -63,6 +64,7 @@
public class WorkUtilityView extends LinearLayout implements Insettable,
KeyboardInsetAnimationCallback.KeyboardInsetListener {
+ private static final String TAG = "WorkUtilityView";
private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
private static final int EXPAND_COLLAPSE_DURATION = 300;
@@ -143,9 +145,11 @@
mSchedulerButton.setOnClickListener(null);
if (shouldUseScheduler()) {
mSchedulerButton.setVisibility(VISIBLE);
- mSchedulerButton.setOnClickListener(view ->
- mActivityContext.startActivitySafely(view,
- new Intent(mWorkSchedulerIntentAction), null /* itemInfo */));
+ mSchedulerButton.setOnClickListener(view -> {
+ Log.d(TAG, "WorkScheduler button clicked.");
+ mActivityContext.startActivitySafely(view,
+ new Intent(mWorkSchedulerIntentAction), null /* itemInfo */);
+ });
}
}
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 87b5459..31d0da0 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -20,8 +20,8 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.ThemeManager;
+import com.android.launcher3.icons.LauncherIcons.IconPool;
import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.UserCache;
@@ -56,7 +56,6 @@
ApiWrapper getApiWrapper();
CustomWidgetManager getCustomWidgetManager();
DynamicResource getDynamicResource();
- IconShape getIconShape();
InstallSessionHelper getInstallSessionHelper();
ItemInstallQueue getItemInstallQueue();
RefreshRateTracker getRefreshRateTracker();
@@ -74,6 +73,7 @@
WallpaperColorHints getWallpaperColorHints();
LockedUserState getLockedUserState();
InvariantDeviceProfile getIDP();
+ IconPool getIconPool();
/** Builder for LauncherBaseAppComponent. */
interface Builder {
diff --git a/src/com/android/launcher3/debug/TestEventEmitter.java b/src/com/android/launcher3/debug/TestEventEmitter.java
new file mode 100644
index 0000000..ed3b4bb
--- /dev/null
+++ b/src/com/android/launcher3/debug/TestEventEmitter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+/**
+ * TestEventsEmitter shouldn't do anything since it runs on the launcher code and not on
+ * tests. This is just a placeholder and test should mock the static sendEvent method.
+ * See "EventsRule.kt" in tests folder where sendEvent is statically mocked to change the
+ * behavior in tests.
+ */
+public class TestEventEmitter {
+ public static void sendEvent(TestEvent event) {
+ }
+
+ /** Events fired by the launcher. */
+ public enum TestEvent {
+
+ LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
+ WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
+ RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
+ WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
+ SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
+ SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED");
+
+ TestEvent(String event) {
+ }
+
+ }
+}
+
+
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
deleted file mode 100644
index 52b454f..0000000
--- a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
+++ /dev/null
@@ -1,59 +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.debug
-
-import android.content.Context
-import android.util.Log
-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"),
- SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
- SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED"),
-}
-
-/** 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) {
- Log.d("TestEventsEmitterProduction", "Event sent ${event.event}")
- }
-}
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 072673d..981b78b 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -60,9 +60,9 @@
import com.android.app.animation.Interpolators;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.views.ActivityContext;
@@ -246,10 +246,12 @@
public void setItemInfo(final ItemInfo info) {
// Load the adaptive icon on a background thread and add the view in ui thread.
MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
+ ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
int w = mWidth;
int h = mHeight;
Pair<AdaptiveIconDrawable, Drawable> fullDrawable = Utilities.getFullDrawable(
- mActivity, info, w, h, true /* shouldThemeIcon */);
+ mActivity, info, w, h,
+ themeManager.isIconThemeEnabled());
if (fullDrawable != null) {
AdaptiveIconDrawable adaptiveIcon = fullDrawable.first;
int blurMargin = (int) mActivity.getResources()
@@ -261,12 +263,7 @@
// be scaled down due to icon normalization.
mBadge = fullDrawable.second;
FastBitmapDrawable.setBadgeBounds(mBadge, bounds);
-
- try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
- // Since we just want the scale, avoid heavy drawing operations
- Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(
- new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null)));
- }
+ Utilities.scaleRectAboutCenter(bounds, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
// Shrink very tiny bit so that the clip path is smaller than the original bitmap
// that has anti aliased edges and shadows.
@@ -274,9 +271,8 @@
Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
adaptiveIcon.setBounds(shrunkBounds);
- IconShape iconShape = IconShape.INSTANCE.get(getContext());
final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon
- ? iconShape.getFolderShape() : iconShape.getShape())
+ ? themeManager.getFolderShape() : themeManager.getIconShape())
.getPath(shrunkBounds);
mTranslateX = new SpringFloatValue(DragView.this,
@@ -567,7 +563,7 @@
return mContentViewParent;
}
- /** Return true if {@link mContent} is a {@link AppWidgetHostView}. */
+ /** Return true if {@link #mContent} is a {@link AppWidgetHostView}. */
public boolean containsAppWidgetHostView() {
return mContent instanceof AppWidgetHostView;
}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 2157610..d2354c1 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -46,8 +46,8 @@
import com.android.launcher3.anim.PropertyResetListener;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.IconShape.ShapeDelegate;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
@@ -237,7 +237,7 @@
}
play(a, getAnimator(mFolder.mFooter, ALPHA, 0, 1f), footerStartDelay, footerAlphaDuration);
- ShapeDelegate shapeDelegate = IconShape.INSTANCE.get(mContext).getFolderShape();
+ ShapeDelegate shapeDelegate = ThemeManager.INSTANCE.get(mContext).getFolderShape();
// Create reveal animator for the folder background
play(a, shapeDelegate.createRevealAnimator(
mFolder, startRect, endRect, finalRadius, !mIsOpening));
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 77fa355..ba8a290 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -47,8 +47,8 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.IconShape.ShapeDelegate;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
@@ -260,7 +260,7 @@
}
private ShapeDelegate getShape() {
- return IconShape.INSTANCE.get(mContext).getFolderShape();
+ return ThemeManager.INSTANCE.get(mContext).getFolderShape();
}
public void drawShadow(Canvas canvas) {
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 50d6d1c..3bd9fb5 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -119,7 +119,7 @@
IconPalette.getPreloadProgressColor(context, info.bitmap.color),
getPreloadColors(context),
Utilities.isDarkTheme(context),
- IconShape.INSTANCE.get(context).getShape().getPath(DEFAULT_PATH_SIZE)
+ ThemeManager.INSTANCE.get(context).getIconShape().getPath(DEFAULT_PATH_SIZE)
);
}
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 686024d..f0d670e 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -40,6 +40,7 @@
import android.util.SparseIntArray;
import android.view.ContextThemeWrapper;
import android.view.Display;
+import android.view.Surface;
import android.view.SurfaceControlViewHost;
import android.view.SurfaceControlViewHost.SurfacePackage;
import android.view.View;
@@ -286,6 +287,20 @@
}
context = context.createConfigurationContext(configuration);
}
+ if (InvariantDeviceProfile.INSTANCE.get(context).isFixedLandscape) {
+ Configuration configuration = new Configuration(
+ context.getResources().getConfiguration()
+ );
+ int width = configuration.screenWidthDp;
+ int height = configuration.screenHeightDp;
+ if (configuration.screenHeightDp > configuration.screenWidthDp) {
+ configuration.screenWidthDp = height;
+ configuration.screenHeightDp = width;
+ configuration.orientation = Surface.ROTATION_90;
+ }
+ context = context.createConfigurationContext(configuration);
+ }
+
if (Flags.newCustomizationPickerUi()) {
if (mPreviewColorOverride != null) {
LocalColorExtractor.newInstance(context)
@@ -396,15 +411,28 @@
}
renderer.hideBottomRow(mHideQsb);
View view = renderer.getRenderedView(dataModel, widgetProviderInfoMap);
- // This aspect scales the view to fit in the surface and centers it
- final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
- mHeight / (float) view.getMeasuredHeight());
- view.setScaleX(scale);
- view.setScaleY(scale);
+
view.setPivotX(0);
view.setPivotY(0);
- view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
- view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+ if (idp.isFixedLandscape) {
+ final float scale = Math.min(mHeight / (float) view.getMeasuredWidth(),
+ mWidth / (float) view.getMeasuredHeight());
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ view.setRotation(90);
+ view.setTranslationX((mHeight - scale * view.getWidth()) / 2 + mWidth);
+ view.setTranslationY((mWidth - scale * view.getHeight()) / 2);
+ } else {
+ // This aspect scales the view to fit in the surface and centers it
+ final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
+ mHeight / (float) view.getMeasuredHeight());
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
+ view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+ }
+
+
if (!Flags.newCustomizationPickerUi()) {
view.setAlpha(0);
view.animate().alpha(1)
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
similarity index 80%
rename from src/com/android/launcher3/graphics/IconShape.kt
rename to src/com/android/launcher3/graphics/ShapeDelegate.kt
index eac3440..9033eac 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -42,78 +42,37 @@
import androidx.graphics.shapes.toPath
import androidx.graphics.shapes.transformed
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
-import com.android.launcher3.dagger.LauncherAppComponent
-import com.android.launcher3.dagger.LauncherAppSingleton
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
import com.android.launcher3.icons.GraphicsUtils
-import com.android.launcher3.icons.IconNormalizer.normalizeAdaptiveIcon
-import com.android.launcher3.util.DaggerSingletonObject
-import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.views.ClipPathView
-import javax.inject.Inject
/** Abstract representation of the shape of an icon shape */
-@LauncherAppSingleton
-class IconShape
-@Inject
-constructor(private val themeManager: ThemeManager, lifeCycle: DaggerSingletonTracker) {
+interface ShapeDelegate {
- val normalizationScale =
- normalizeAdaptiveIcon(
- AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)),
- AREA_CALC_SIZE,
- )
+ fun getPath(pathSize: Float = DEFAULT_PATH_SIZE) =
+ Path().apply { addToPath(this, 0f, 0f, pathSize / 2) }
- var shape: ShapeDelegate = pickBestShape(themeManager.iconState.iconMask)
- private set
-
- var folderShape: ShapeDelegate =
- themeManager.iconState.run {
- if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape
- else pickBestShape(folderShapeMask)
+ fun getPath(bounds: Rect) =
+ Path().apply {
+ addToPath(
+ this,
+ bounds.left.toFloat(),
+ bounds.top.toFloat(),
+ // Radius is half of the average size of the icon
+ (bounds.width() + bounds.height()) / 4f,
+ )
}
- private set
- init {
- val changeListener = ThemeChangeListener {
- shape = pickBestShape(themeManager.iconState.iconMask)
- folderShape =
- themeManager.iconState.run {
- if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape
- else pickBestShape(folderShapeMask)
- }
- }
- themeManager.addChangeListener(changeListener)
- lifeCycle.addCloseable { themeManager.removeChangeListener(changeListener) }
- }
+ fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
- interface ShapeDelegate {
- fun getPath(pathSize: Float = DEFAULT_PATH_SIZE) =
- Path().apply { addToPath(this, 0f, 0f, pathSize / 2) }
+ fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
- fun getPath(bounds: Rect) =
- Path().apply {
- addToPath(
- this,
- bounds.left.toFloat(),
- bounds.top.toFloat(),
- // Radius is half of the average size of the icon
- (bounds.width() + bounds.height()) / 4f,
- )
- }
-
- fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
-
- fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
-
- fun <T> createRevealAnimator(
- target: T,
- startRect: Rect,
- endRect: Rect,
- endRadius: Float,
- isReversed: Boolean,
- ): ValueAnimator where T : View, T : ClipPathView
- }
+ fun <T> createRevealAnimator(
+ target: T,
+ startRect: Rect,
+ endRect: Rect,
+ endRadius: Float,
+ isReversed: Boolean,
+ ): ValueAnimator where T : View, T : ClipPathView
class Circle : RoundedSquare(1f) {
@@ -179,10 +138,15 @@
}
.createRevealAnimator(target, isReversed)
}
+
+ override fun equals(other: Any?) =
+ other is RoundedSquare && other.radiusRatio == radiusRatio
+
+ override fun hashCode() = radiusRatio.hashCode()
}
/** Generic shape delegate with pathString in bounds [0, 0, 100, 100] */
- class GenericPathShape(pathString: String) : ShapeDelegate {
+ data class GenericPathShape(private val pathString: String) : ShapeDelegate {
private val poly =
RoundedPolygon(
features = SvgPathParser.parseFeatures(pathString),
@@ -287,7 +251,6 @@
}
companion object {
- @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
const val TAG = "IconShape"
const val DEFAULT_PATH_SIZE = 100f
@@ -312,7 +275,6 @@
}
}
- @VisibleForTesting
fun pickBestShape(shapeStr: String): ShapeDelegate {
val baseShape =
if (shapeStr.isNotEmpty()) {
@@ -332,7 +294,6 @@
return pickBestShape(baseShape, shapeStr)
}
- @VisibleForTesting
fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate {
val calcAreaDiff = areaDiffCalculator(baseShape)
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index 1636da8..de85460 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -19,12 +19,14 @@
import android.content.Context
import android.content.res.Resources
import com.android.launcher3.EncryptionType
+import com.android.launcher3.Item
import com.android.launcher3.LauncherPrefChangeListener
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape
import com.android.launcher3.icons.IconThemeController
import com.android.launcher3.icons.mono.MonoIconThemeController
import com.android.launcher3.shapes.ShapesProvider
@@ -37,50 +39,57 @@
/** Centralized class for managing Launcher icon theming */
@LauncherAppSingleton
-open class ThemeManager
+class ThemeManager
@Inject
constructor(
- @ApplicationContext protected val context: Context,
- protected val prefs: LauncherPrefs,
+ @ApplicationContext private val context: Context,
+ private val prefs: LauncherPrefs,
+ private val iconControllerFactory: IconControllerFactory,
lifecycle: DaggerSingletonTracker,
) {
/** Representation of the current icon state */
- var iconState = parseIconState()
+ var iconState = parseIconState(null)
private set
var isMonoThemeEnabled
set(value) = prefs.put(THEMED_ICONS, value)
get() = prefs.get(THEMED_ICONS)
- val themeController: IconThemeController?
+ val themeController
get() = iconState.themeController
- val isIconThemeEnabled: Boolean
+ val isIconThemeEnabled
get() = themeController != null
+ val iconShape
+ get() = iconState.iconShape
+
+ val folderShape
+ get() = iconState.folderShape
+
private val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
init {
val receiver = SimpleBroadcastReceiver(context, MAIN_EXECUTOR) { verifyIconState() }
receiver.registerPkgActions("android", ACTION_OVERLAY_CHANGED)
- val prefListener = LauncherPrefChangeListener { key ->
- when (key) {
- KEY_THEMED_ICONS,
- KEY_ICON_SHAPE -> verifyIconState()
- }
- }
- prefs.addListener(prefListener, THEMED_ICONS, PREF_ICON_SHAPE)
+ val keys = (iconControllerFactory.prefKeys + PREF_ICON_SHAPE)
+ val keysArray = keys.toTypedArray()
+ val prefKeySet = keys.map { it.sharedPrefKey }
+ val prefListener = LauncherPrefChangeListener { key ->
+ if (prefKeySet.contains(key)) verifyIconState()
+ }
+ prefs.addListener(prefListener, *keysArray)
lifecycle.addCloseable {
receiver.unregisterReceiverSafely()
- prefs.removeListener(prefListener)
+ prefs.removeListener(prefListener, *keysArray)
}
}
- protected fun verifyIconState() {
- val newState = parseIconState()
+ private fun verifyIconState() {
+ val newState = parseIconState(iconState)
if (newState == iconState) return
iconState = newState
@@ -91,7 +100,7 @@
fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener)
- private fun parseIconState(): IconState {
+ private fun parseIconState(oldState: IconState?): IconState {
val shapeModel =
prefs.get(PREF_ICON_SHAPE).let { shapeOverride ->
ShapesProvider.iconShapes.values.firstOrNull { it.key == shapeOverride }
@@ -102,22 +111,38 @@
CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL -> ""
else -> context.resources.getString(CONFIG_ICON_MASK_RES_ID)
}
+
+ val iconShape =
+ if (oldState != null && oldState.iconMask == iconMask) oldState.iconShape
+ else pickBestShape(iconMask)
+
+ val folderShapeMask = shapeModel?.folderPathString ?: iconMask
+ val folderShape =
+ when {
+ oldState != null && oldState.folderShapeMask == folderShapeMask ->
+ oldState.folderShape
+ folderShapeMask == iconMask || folderShapeMask.isEmpty() -> iconShape
+ else -> pickBestShape(folderShapeMask)
+ }
+
return IconState(
iconMask = iconMask,
- folderShapeMask = shapeModel?.folderPathString ?: iconMask,
- themeController = createThemeController(),
+ folderShapeMask = folderShapeMask,
+ themeController = iconControllerFactory.createThemeController(),
+ iconScale = shapeModel?.iconScale ?: 1f,
+ iconShape = iconShape,
+ folderShape = folderShape,
)
}
- protected open fun createThemeController(): IconThemeController? {
- return if (isMonoThemeEnabled) MONO_THEME_CONTROLLER else null
- }
-
data class IconState(
val iconMask: String,
val folderShapeMask: String,
val themeController: IconThemeController?,
val themeCode: String = themeController?.themeID ?: "no-theme",
+ val iconScale: Float = 1f,
+ val iconShape: ShapeDelegate,
+ val folderShape: ShapeDelegate,
) {
fun toUniqueId() = "${iconMask.hashCode()},$themeCode"
}
@@ -127,6 +152,15 @@
fun onThemeChanged()
}
+ open class IconControllerFactory @Inject constructor(protected val prefs: LauncherPrefs) {
+
+ open val prefKeys: List<Item> = listOf(THEMED_ICONS)
+
+ open fun createThemeController(): IconThemeController? {
+ return if (prefs.get(THEMED_ICONS)) MONO_THEME_CONTROLLER else null
+ }
+ }
+
companion object {
@JvmField val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getThemeManager)
diff --git a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
index 225e12f..50dd146 100644
--- a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
+++ b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
@@ -30,6 +30,7 @@
import com.android.launcher3.icons.cache.BaseIconCache
import com.android.launcher3.icons.cache.CachingLogic
import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ApiWrapper
import com.android.launcher3.util.ApplicationInfoWrapper
import com.android.launcher3.util.PackageUserKey
import com.android.launcher3.util.Themes
@@ -114,7 +115,15 @@
d,
IconOptions()
.setExtractedColor(Themes.getColorAccent(context))
- .setSourceHint(getSourceHint(info, cache)),
+ .setSourceHint(
+ getSourceHint(info, cache)
+ .copy(
+ isFileDrawable =
+ ApiWrapper.INSTANCE[context].isFileDrawable(
+ info.shortcutInfo
+ )
+ )
+ ),
)
} ?: BitmapInfo.LOW_RES_INFO
}
diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java
index 482360c..836b7d1 100644
--- a/src/com/android/launcher3/icons/LauncherIconProvider.java
+++ b/src/com/android/launcher3/icons/LauncherIconProvider.java
@@ -30,7 +30,7 @@
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ShapeDelegate;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.util.ApiWrapper;
@@ -54,13 +54,13 @@
private Map<String, ThemeData> mThemedIconMap;
private final ApiWrapper mApiWrapper;
- private final IconShape mIconShape;
+ private final ThemeManager mThemeManager;
public LauncherIconProvider(Context context) {
super(context);
- setIconThemeSupported(ThemeManager.INSTANCE.get(context).isMonoThemeEnabled());
+ mThemeManager = ThemeManager.INSTANCE.get(context);
mApiWrapper = ApiWrapper.INSTANCE.get(context);
- mIconShape = IconShape.INSTANCE.get(context);
+ setIconThemeSupported(mThemeManager.isMonoThemeEnabled());
}
/**
@@ -91,7 +91,7 @@
@Override
protected Drawable loadAppInfoIcon(ApplicationInfo info, Resources resources, int density) {
// Tries to load the round icon res, if the app defines it as an adaptive icon
- if (mIconShape.getShape() instanceof IconShape.Circle) {
+ if (mThemeManager.getIconShape() instanceof ShapeDelegate.Circle) {
int roundIconRes = mApiWrapper.getRoundIconRes(info);
if (roundIconRes != 0 && roundIconRes != info.icon) {
try {
diff --git a/src/com/android/launcher3/icons/LauncherIcons.kt b/src/com/android/launcher3/icons/LauncherIcons.kt
index 518f29d..6c018e8 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.kt
+++ b/src/com/android/launcher3/icons/LauncherIcons.kt
@@ -24,29 +24,36 @@
import android.os.UserHandle
import com.android.launcher3.Flags
import com.android.launcher3.InvariantDeviceProfile
-import com.android.launcher3.LauncherPrefs
-import com.android.launcher3.graphics.IconShape
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.pm.UserCache
-import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.SafeCloseable
import com.android.launcher3.util.UserIconInfo
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import java.util.concurrent.ConcurrentLinkedQueue
+import javax.inject.Inject
/**
* Wrapper class to provide access to [BaseIconFactory] and also to provide pool of this class that
* are threadsafe.
*/
class LauncherIcons
-protected constructor(
- context: Context,
- fillResIconDpi: Int,
- iconBitmapSize: Int,
- private val pool: ConcurrentLinkedQueue<LauncherIcons>,
-) : BaseIconFactory(context, fillResIconDpi, iconBitmapSize), AutoCloseable {
+@AssistedInject
+internal constructor(
+ @ApplicationContext context: Context,
+ idp: InvariantDeviceProfile,
+ private var themeManager: ThemeManager,
+ private var userCache: UserCache,
+ @Assisted private val pool: ConcurrentLinkedQueue<LauncherIcons>,
+) : BaseIconFactory(context, idp.fillResIconDpi, idp.iconBitmapSize), AutoCloseable {
+
+ private val iconScale = themeManager.iconState.iconScale
init {
- mThemeController = ThemeManager.INSTANCE[context].themeController
+ mThemeController = themeManager.themeController
}
/** Recycles a LauncherIcons that may be in-use. */
@@ -56,12 +63,12 @@
}
override fun getUserInfo(user: UserHandle): UserIconInfo {
- return UserCache.INSTANCE[mContext].getUserInfo(user)
+ return userCache.getUserInfo(user)
}
public override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
if (!Flags.enableLauncherIconShapes()) return drawable.iconMask
- return IconShape.INSTANCE[mContext].shape.getPath(iconBounds)
+ return themeManager.iconShape.getPath(iconBounds)
}
override fun drawAdaptiveIcon(
@@ -73,14 +80,6 @@
super.drawAdaptiveIcon(canvas, drawable, overridePath)
return
}
- val shapeKey = LauncherPrefs.get(mContext).get(ThemeManager.PREF_ICON_SHAPE)
- val iconScale =
- when (shapeKey) {
- "seven_sided_cookie" -> SEVEN_SIDED_COOKIE_SCALE
- "four_sided_cookie" -> FOUR_SIDED_COOKIE_SCALE
- "sunny" -> VERY_SUNNY_SCALE
- else -> DEFAULT_ICON_SCALE
- }
canvas.clipPath(overridePath)
canvas.drawColor(Color.BLACK)
canvas.save()
@@ -98,42 +97,31 @@
recycle()
}
- private class Pool(private val context: Context) : SafeCloseable {
+ @AssistedFactory
+ internal interface LauncherIconsFactory {
+ fun create(pool: ConcurrentLinkedQueue<LauncherIcons>): LauncherIcons
+ }
+
+ @LauncherAppSingleton
+ class IconPool @Inject internal constructor(private val factory: LauncherIconsFactory) {
private var pool = ConcurrentLinkedQueue<LauncherIcons>()
- fun obtain(): LauncherIcons {
- val pool = pool
- return pool.poll()
- ?: InvariantDeviceProfile.INSTANCE[context].let {
- LauncherIcons(context, it.fillResIconDpi, it.iconBitmapSize, pool)
- }
- }
+ fun obtain(): LauncherIcons = pool.let { it.poll() ?: factory.create(it) }
- override fun close() {
+ fun clear() {
pool = ConcurrentLinkedQueue()
}
}
companion object {
- private const val SEVEN_SIDED_COOKIE_SCALE = 72f / 80f
- private const val FOUR_SIDED_COOKIE_SCALE = 72f / 83.4f
- private const val VERY_SUNNY_SCALE = 72f / 92f
- private const val DEFAULT_ICON_SCALE = 1f
-
- private val POOL = MainThreadInitializedObject { Pool(it) }
/**
- * Return a new Message instance from the global pool. Allows us to avoid allocating new
- * objects in many cases.
+ * Return a new LauncherIcons instance from the global pool. Allows us to avoid allocating
+ * new objects in many cases.
*/
@JvmStatic
- fun obtain(context: Context): LauncherIcons {
- return POOL[context].obtain()
- }
+ fun obtain(context: Context): LauncherIcons = context.appComponent.iconPool.obtain()
- @JvmStatic
- fun clearPool(context: Context) {
- POOL[context].close()
- }
+ @JvmStatic fun clearPool(context: Context) = context.appComponent.iconPool.clear()
}
}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 47f13bd..5d0a7bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -238,16 +238,19 @@
Collections.sort(hotseatToBeAdded);
Collections.sort(workspaceToBeAdded);
- List<Integer> idsInUse = dstWorkspaceItems.stream()
+ List<DbEntry> remainingDstHotseatItems = destReader.loadHotseatEntries();
+ List<DbEntry> remainingDstWorkspaceItems = destReader.loadAllWorkspaceEntries();
+ List<Integer> idsInUse = remainingDstHotseatItems.stream()
.map(entry -> entry.id)
.collect(Collectors.toList());
- idsInUse.addAll(dstHotseatItems.stream()
+ idsInUse.addAll(remainingDstWorkspaceItems.stream()
.map(entry -> entry.id)
.collect(Collectors.toList()));
+
// Migrate hotseat
solveHotseatPlacement(helper, destHotseatSize,
- srcReader, destReader, dstHotseatItems, hotseatToBeAdded, idsInUse);
+ srcReader, destReader, remainingDstHotseatItems, hotseatToBeAdded, idsInUse);
// Migrate workspace.
// First we create a collection of the screens
@@ -467,7 +470,7 @@
final Context mContext;
int mLastScreenId = -1;
- final Map<Integer, List<DbEntry>> mWorkspaceEntriesByScreenId =
+ Map<Integer, List<DbEntry>> mWorkspaceEntriesByScreenId =
new ArrayMap<>();
public DbReader(SQLiteDatabase db, String tableName, Context context) {
@@ -539,6 +542,7 @@
}
protected List<DbEntry> loadAllWorkspaceEntries() {
+ mWorkspaceEntriesByScreenId.clear();
final List<DbEntry> workspaceEntries = new ArrayList<>();
Cursor c = queryWorkspace(
new String[]{
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 9586bf3..5df135a 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -183,9 +183,11 @@
)
}
+ val remainingDstHotseatItems = destReader.loadHotseatEntries()
+
placeHotseatItems(
itemsToBeAdded,
- dstHotseatItems,
+ remainingDstHotseatItems,
destHotseatSize,
helper,
srcReader,
@@ -265,9 +267,10 @@
)
}
+ val remainingDstWorkspaceItems = destReader.loadAllWorkspaceEntries()
placeWorkspaceItems(
workspaceToBeAdded,
- dstWorkspaceItems,
+ remainingDstWorkspaceItems,
targetSize.x,
targetSize.y,
helper,
diff --git a/src/com/android/launcher3/model/ModelInitializer.kt b/src/com/android/launcher3/model/ModelInitializer.kt
new file mode 100644
index 0000000..69a320a
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelInitializer.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model
+
+import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED
+import android.content.ComponentName
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.ArchiveCompatibilityParams
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener
+import com.android.launcher3.LauncherModel
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.graphics.ThemeManager
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.LauncherIconProvider
+import com.android.launcher3.icons.LauncherIcons.IconPool
+import com.android.launcher3.notification.NotificationListener
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI
+import com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI
+import com.android.launcher3.util.SimpleBroadcastReceiver
+import com.android.launcher3.widget.custom.CustomWidgetManager
+import java.util.function.Consumer
+
+/** Utility class for initializing all model callbacks */
+class ModelInitializer(
+ @ApplicationContext private val context: Context,
+ private val iconPool: IconPool,
+ private val iconCache: IconCache,
+ private val idp: InvariantDeviceProfile,
+ private val themeManager: ThemeManager,
+ private val userCache: UserCache,
+ private val settingsCache: SettingsCache,
+ private val iconProvider: LauncherIconProvider,
+ private val customWidgetManager: CustomWidgetManager,
+ private val installSessionHelper: InstallSessionHelper,
+ private val closeActions: Consumer<SafeCloseable>,
+) {
+
+ fun initialize(model: LauncherModel) {
+ fun refreshAndReloadLauncher() {
+ iconPool.clear()
+ iconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize)
+ model.forceReload()
+ }
+
+ // IDP changes
+ val idpChangeListener = OnIDPChangeListener { modelChanged ->
+ if (modelChanged) refreshAndReloadLauncher()
+ }
+ idp.addOnChangeListener(idpChangeListener)
+ closeActions.accept { idp.removeOnChangeListener(idpChangeListener) }
+
+ // Theme changes
+ val themeChangeListener = ThemeChangeListener { refreshAndReloadLauncher() }
+ themeManager.addChangeListener(themeChangeListener)
+ closeActions.accept { themeManager.removeChangeListener(themeChangeListener) }
+
+ // System changes
+ val modelCallbacks = model.newModelCallbacks()
+ val launcherApps = context.getSystemService(LauncherApps::class.java)!!
+ launcherApps.registerCallback(modelCallbacks)
+ closeActions.accept { launcherApps.unregisterCallback(modelCallbacks) }
+
+ if (Utilities.ATLEAST_V && Flags.enableSupportForArchiving()) {
+ launcherApps.setArchiveCompatibility(
+ ArchiveCompatibilityParams().apply {
+ setEnableUnarchivalConfirmation(false)
+ setEnableIconOverlay(!Flags.useNewIconForArchivedApps())
+ }
+ )
+ }
+
+ // Device profile policy changes
+ val dpUpdateReceiver =
+ SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.reloadStringCache() }
+ dpUpdateReceiver.register(ACTION_DEVICE_POLICY_RESOURCE_UPDATED)
+ closeActions.accept { dpUpdateReceiver.unregisterReceiverSafely() }
+
+ // Development helper
+ if (BuildConfig.IS_STUDIO_BUILD) {
+ val reloadReceiver =
+ SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.forceReload() }
+ reloadReceiver.register(Context.RECEIVER_EXPORTED, ACTION_FORCE_RELOAD)
+ closeActions.accept { reloadReceiver.unregisterReceiverSafely() }
+ }
+
+ // User changes
+ closeActions.accept(userCache.addUserEventListener(model::onUserEvent))
+
+ // Private space settings changes
+ val psSettingsListener = SettingsCache.OnChangeListener { model.forceReload() }
+ settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+ closeActions.accept {
+ settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+ }
+
+ // Notification dots changes
+ val notificationChanges =
+ SettingsCache.OnChangeListener { dotsEnabled ->
+ if (dotsEnabled)
+ NotificationListener.requestRebind(
+ ComponentName(context, NotificationListener::class.java)
+ )
+ }
+ settingsCache.register(NOTIFICATION_BADGING_URI, notificationChanges)
+ notificationChanges.onSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI))
+ closeActions.accept {
+ settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationChanges)
+ }
+
+ // removable smartspace
+ if (Flags.enableSmartspaceRemovalToggle()) {
+ val smartSpacePrefChanges =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (LoaderTask.SMARTSPACE_ON_HOME_SCREEN == key) model.forceReload()
+ }
+ getPrefs(context).registerOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+ closeActions.accept {
+ getPrefs(context).unregisterOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+ }
+ }
+
+ // Custom widgets
+ closeActions.accept(customWidgetManager.addWidgetRefreshCallback(model::rebindCallbacks))
+
+ // Icon changes
+ closeActions.accept(
+ iconProvider.registerIconChangeListener(model::onAppIconChanged, MODEL_EXECUTOR.handler)
+ )
+
+ // Install session changes
+ closeActions.accept(installSessionHelper.registerInstallTracker(modelCallbacks))
+ }
+
+ companion object {
+ private const val ACTION_FORCE_RELOAD = "force-reload-launcher"
+ }
+}
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index b9c928c..7451ce2 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -34,13 +34,15 @@
import com.android.launcher3.Flags;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SafeCloseable;
import java.lang.ref.WeakReference;
import java.util.Objects;
@SuppressWarnings("NewApi")
@WorkerThread
-public class InstallSessionTracker extends PackageInstaller.SessionCallback {
+public class InstallSessionTracker extends PackageInstaller.SessionCallback implements
+ SafeCloseable {
public static final String TAG = "InstallSessionTracker";
@@ -196,7 +198,8 @@
}
}
- public void unregister() {
+ @Override
+ public void close() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
mInstaller.unregisterSessionCallback(this);
} else {
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index b748011..bc5064d 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -110,13 +110,16 @@
public static <T extends Context & ActivityContext> Runnable createUpdateRunnable(
final T context,
final ItemInfo originalInfo,
- final Handler uiHandler, final PopupContainerWithArrow container,
- final List<DeepShortcutView> shortcutViews) {
+ final Handler uiHandler,
+ final PopupContainerWithArrow container,
+ final List<DeepShortcutView> shortcutViews
+ ) {
final ComponentName activity = originalInfo.getTargetComponent();
final UserHandle user = originalInfo.user;
+ final String targetPackage = originalInfo.getTargetPackage();
return () -> {
ApplicationInfoWrapper infoWrapper =
- new ApplicationInfoWrapper(context, originalInfo.getTargetPackage(), user);
+ new ApplicationInfoWrapper(context, targetPackage, user);
List<ShortcutInfo> shortcuts = new ShortcutRequest(context, user)
.withContainer(activity)
.query(ShortcutRequest.PUBLISHED);
diff --git a/src/com/android/launcher3/shapes/IconShapeModel.kt b/src/com/android/launcher3/shapes/IconShapeModel.kt
index dd6c432..fc49adc 100644
--- a/src/com/android/launcher3/shapes/IconShapeModel.kt
+++ b/src/com/android/launcher3/shapes/IconShapeModel.kt
@@ -21,4 +21,5 @@
val title: String,
val pathString: String,
val folderPathString: String = pathString,
+ val iconScale: Float = 1f,
)
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
index 7e1f640..5427c89 100644
--- a/src/com/android/launcher3/shapes/ShapesProvider.kt
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -174,6 +174,7 @@
pathString =
"M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
folderPathString = folderShapes["complexClover"]!!,
+ iconScale = 72f / 83.4f,
),
"seven_sided_cookie" to
IconShapeModel(
@@ -182,6 +183,7 @@
pathString =
"M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
folderPathString = folderShapes["clover"]!!,
+ iconScale = 72f / 80f,
),
"arch" to
IconShapeModel(
@@ -191,14 +193,6 @@
"M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
folderPathString = folderShapes["arch"]!!,
),
- "sunny" to
- IconShapeModel(
- key = "sunny",
- title = "sunny",
- pathString =
- "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
- folderPathString = folderShapes["clover"]!!,
- ),
)
} else {
mapOf(
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index 107bcc1..2cc4909 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -207,6 +207,11 @@
}
config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
+ if (launcher.getDeviceProfile().isPhone) {
+ config.setInterpolator(ANIM_WORKSPACE_FADE, INSTANT);
+ config.setInterpolator(ANIM_HOTSEAT_FADE, INSTANT);
+ config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+ }
} else {
if (config.isUserControlled()) {
config.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR_MANUAL));
@@ -248,6 +253,11 @@
}
config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
+ if (launcher.getDeviceProfile().isPhone) {
+ config.setInterpolator(ANIM_WORKSPACE_FADE, FINAL_FRAME);
+ config.setInterpolator(ANIM_HOTSEAT_FADE, FINAL_FRAME);
+ config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+ }
} else {
config.setInterpolator(ANIM_DEPTH,
config.isUserControlled() ? BLUR_MANUAL : BLUR_ATOMIC);
diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java
index 56337b0..0510d59 100644
--- a/src/com/android/launcher3/util/ApiWrapper.java
+++ b/src/com/android/launcher3/util/ApiWrapper.java
@@ -216,6 +216,13 @@
return 0;
}
+ /**
+ * Checks if the shortcut is using an icon with file or URI source
+ */
+ public boolean isFileDrawable(@NonNull ShortcutInfo shortcutInfo) {
+ return false;
+ }
+
private static class NoopDrawable extends ColorDrawable {
@Override
public int getIntrinsicHeight() {
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 647d170..11f0bc2 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -509,7 +509,17 @@
* @param isInDesktopModeAndNotInOverview True if a desktop is currently active on the given
* display, and Overview is currently inactive.
*/
- void onIsInDesktopModeChanged(int displayId, boolean isInDesktopModeAndNotInOverview);
+ default void onIsInDesktopModeChanged(int displayId,
+ boolean isInDesktopModeAndNotInOverview) {
+ }
+
+ /**
+ * Called whenever the conditions that allow the creation of desks change.
+ *
+ * @param canCreateDesks whether it is possible to create new desks.
+ */
+ default void onCanCreateDesksChanged(boolean canCreateDesks) {
+ }
}
}
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index f90a3e4..a295d6b 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.Utilities.boundToRange;
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import static java.lang.Math.max;
@@ -44,10 +45,11 @@
import androidx.core.util.Consumer;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
/**
* A view used to draw both layers of an {@link AdaptiveIconDrawable}.
@@ -67,6 +69,7 @@
private boolean mIsAdaptiveIcon = false;
private ValueAnimator mRevealAnimator;
+ private float mIconScale;
private final Rect mStartRevealRect = new Rect();
private final Rect mEndRevealRect = new Rect();
@@ -172,9 +175,12 @@
mTaskCornerRadius = cornerRadius / scale;
if (mIsAdaptiveIcon) {
- if (!isOpening && progress >= shapeProgressStart) {
+ final ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
+ mIconScale = themeManager.getIconState().getIconScale();
+ if ((!isOpening || Flags.enableLauncherIconShapes())
+ && progress >= shapeProgressStart) {
if (mRevealAnimator == null) {
- mRevealAnimator = IconShape.INSTANCE.get(getContext()).getShape()
+ mRevealAnimator = themeManager.getIconShape()
.createRevealAnimator(this, mStartRevealRect,
mOutline, mTaskCornerRadius, !isOpening);
mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
@@ -258,8 +264,7 @@
mStartRevealRect.set(0, 0, originalWidth, originalHeight);
if (!isFolderIcon) {
- Utilities.scaleRectAboutCenter(mStartRevealRect,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ Utilities.scaleRectAboutCenter(mStartRevealRect, ICON_VISIBLE_AREA_FACTOR);
}
if (dp.isLandscape) {
@@ -309,17 +314,24 @@
@Override
public void draw(Canvas canvas) {
- int count = canvas.save();
+ int count1 = canvas.save();
if (mClipPath != null) {
canvas.clipPath(mClipPath);
}
- super.draw(canvas);
+ int count2 = canvas.save();
+ float iconCenterX =
+ (mFinalDrawableBounds.right - mFinalDrawableBounds.left) / 2f * mIconScale;
+ float iconCenterY =
+ (mFinalDrawableBounds.bottom - mFinalDrawableBounds.top) / 2f * mIconScale;
+ canvas.scale(mIconScale, mIconScale, iconCenterX, iconCenterY);
if (mBackground != null) {
mBackground.draw(canvas);
}
if (mForeground != null) {
mForeground.draw(canvas);
}
+ canvas.restoreToCount(count2);
+ super.draw(canvas);
if (mTaskViewArtist != null) {
canvas.saveLayerAlpha(
0,
@@ -333,7 +345,7 @@
canvas.scale(drawScale, drawScale);
mTaskViewArtist.taskViewDrawCallback.accept(canvas);
}
- canvas.restoreToCount(count);
+ canvas.restoreToCount(count1);
}
void recycle() {
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 22857b1..5b3abc3 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -55,7 +55,7 @@
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.popup.SystemShortcut;
@@ -463,10 +463,7 @@
Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline,
(int) position.height() + blurSizeOutline);
bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2);
-
- try (LauncherIcons li = LauncherIcons.obtain(l)) {
- Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable));
- }
+ Utilities.scaleRectAboutCenter(bounds, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
bounds.inset(
(int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
diff --git a/src/com/android/launcher3/widget/picker/OWNERS b/src/com/android/launcher3/widget/picker/OWNERS
index 6aabbfa..991193f 100644
--- a/src/com/android/launcher3/widget/picker/OWNERS
+++ b/src/com/android/launcher3/widget/picker/OWNERS
@@ -6,7 +6,6 @@
#
# Widget Picker OWNERS
-zakcohen@google.com
shamalip@google.com
wvk@google.com
diff --git a/tests/Android.bp b/tests/Android.bp
index 4bc654c..fc08e86 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -168,6 +168,7 @@
"src/**/*Test.java",
"src/**/*Test.kt",
"src/**/RoboApiWrapper.kt",
+ "src/**/EventsRule.kt",
"multivalentTests/src/**/*Test.java",
"multivalentTests/src/**/*Test.kt",
],
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 9d42e1b..3658989 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -294,6 +294,8 @@
gridName: String? = GRID_NAME.defaultValue,
) {
setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_TWOLINE_TOGGLE)
+ // TODO: re-enable as part of b/396211437
+ setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES)
val windowsBounds = perDisplayBoundsCache[displayInfo]!!
val realBounds = windowsBounds[rotation]
whenever(windowManagerProxy.getDisplayInfo(any())).thenReturn(displayInfo)
@@ -362,7 +364,13 @@
context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText)
private fun writeToDevice(context: Context, fileName: String, content: String) {
- File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content)
+ val dir =
+ File(context.filesDir, "dumpTests").also {
+ if (!it.exists()) {
+ it.mkdirs()
+ }
+ }
+ File(dir, fileName).writeText(content)
}
protected fun Float.dpToPx(): Float {
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 060c28c..f855c51 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -71,7 +71,7 @@
inv,
info,
context.appComponent.wmProxy,
- context.appComponent.iconShape,
+ context.appComponent.themeManager,
windowBounds,
SparseArray(),
/*isMultiWindowMode=*/ false,
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
similarity index 93%
rename from tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
index 311676a..7e38f0e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
@@ -28,13 +28,13 @@
import androidx.core.graphics.PathParser
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.launcher3.graphics.IconShape.Circle
-import com.android.launcher3.graphics.IconShape.Companion.AREA_CALC_SIZE
-import com.android.launcher3.graphics.IconShape.Companion.AREA_DIFF_THRESHOLD
-import com.android.launcher3.graphics.IconShape.Companion.areaDiffCalculator
-import com.android.launcher3.graphics.IconShape.Companion.pickBestShape
-import com.android.launcher3.graphics.IconShape.GenericPathShape
-import com.android.launcher3.graphics.IconShape.RoundedSquare
+import com.android.launcher3.graphics.ShapeDelegate.Circle
+import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_CALC_SIZE
+import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_DIFF_THRESHOLD
+import com.android.launcher3.graphics.ShapeDelegate.Companion.areaDiffCalculator
+import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape
+import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape
+import com.android.launcher3.graphics.ShapeDelegate.RoundedSquare
import com.android.launcher3.icons.GraphicsUtils
import com.android.launcher3.views.ClipPathView
import com.google.common.truth.Truth.assertThat
@@ -43,7 +43,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class IconShapeTest {
+class ShapeDelegateTest {
@Test
fun `areaDiffCalculator increases with outwards shape`() {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index adf38fe..5607bb4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -275,36 +275,15 @@
val readerGridA = DbReader(db, TMP_TABLE, context)
val readerGridB = DbReader(db, TABLE_NAME, context)
// migrate from A -> B
- if (Flags.gridMigrationRefactor()) {
- var gridSizeMigrationLogic = GridSizeMigrationLogic()
- val idsInUse = mutableListOf<Int>()
- gridSizeMigrationLogic.migrateHotseat(
- 5,
- idp.numDatabaseHotseatIcons,
- readerGridA,
- readerGridB,
- dbHelper,
- idsInUse,
- )
- gridSizeMigrationLogic.migrateWorkspace(
- readerGridA,
- readerGridB,
- dbHelper,
- Point(idp.numColumns, idp.numRows),
- idsInUse,
- )
- } else {
- GridSizeMigrationDBController.migrate(
- dbHelper,
- readerGridA,
- readerGridB,
- 5,
- idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
- )
- }
+ migrateGrid(
+ dbHelper,
+ readerGridA,
+ readerGridB,
+ 5,
+ idp.numDatabaseHotseatIcons,
+ idp.numColumns,
+ idp.numRows,
+ )
// Check hotseat items in grid B
var c =
@@ -388,8 +367,8 @@
locMap = parseLocMap(c)
// Expected workspace items in grid A
// _ _ _ _ _
- // _ _ _ _ 5
- // 9 _ 6 _ 7
+ // 9 _ _ _ 5
+ // _ _ 6 _ 7
// _ _ 8 _ _
// _ _ _ _ _
assertThat(locMap.size.toLong()).isEqualTo(5)
@@ -399,7 +378,7 @@
assertThat(locMap[testPackage7]).isEqualTo(Triple(0, 4, 2))
assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 2, 3))
// Verify items that didn't exist in grid A are added in new screen
- assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
+ assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 1))
// remove item from B
db.delete(TMP_TABLE, "$_ID=7", null)
@@ -493,37 +472,15 @@
val readerGridA = DbReader(db, TMP_TABLE, context)
val readerGridB = DbReader(db, TABLE_NAME, context)
// migrate from A -> B
- if (Flags.gridMigrationRefactor()) {
- var gridSizeMigrationLogic = GridSizeMigrationLogic()
- val idsInUse = mutableListOf<Int>()
- gridSizeMigrationLogic.migrateHotseat(
- 5,
- idp.numDatabaseHotseatIcons,
- readerGridA,
- readerGridB,
- dbHelper,
- idsInUse,
- )
- gridSizeMigrationLogic.migrateWorkspace(
- readerGridA,
- readerGridB,
- dbHelper,
- Point(idp.numColumns, idp.numRows),
- idsInUse,
- )
- } else {
- GridSizeMigrationDBController.migrate(
- dbHelper,
- readerGridA,
- readerGridB,
- 5,
- idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
- )
- }
-
+ migrateGrid(
+ dbHelper,
+ readerGridA,
+ readerGridB,
+ 5,
+ idp.numDatabaseHotseatIcons,
+ idp.numColumns,
+ idp.numRows,
+ )
// Check hotseat items in grid B
var c =
db.query(
@@ -597,6 +554,112 @@
)
}
+ @Test
+ @Throws(Exception::class)
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun testMigrationToFullGridFlagOn() {
+ testMigrationToFullGrid()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun testHotseatMigrationToFullGridFlagOff() {
+ testMigrationToFullGrid()
+ }
+
+ @Throws(Exception::class)
+ fun testMigrationToFullGrid() {
+ // Hotseat items in grid A
+ // 1 2 3 4 5
+ addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
+ addItem(ITEM_TYPE_DEEP_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+ addItem(ITEM_TYPE_DEEP_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
+ addItem(ITEM_TYPE_APPLICATION, 3, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
+ addItem(ITEM_TYPE_APPLICATION, 4, CONTAINER_HOTSEAT, 0, 0, testPackage5, 5, TMP_TABLE)
+
+ // Hotseat items in grid B
+ // 6 7 8 9
+ addItem(ITEM_TYPE_DEEP_SHORTCUT, 0, CONTAINER_HOTSEAT, 0, 0, testPackage6)
+ addItem(ITEM_TYPE_DEEP_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage7)
+ addItem(ITEM_TYPE_APPLICATION, 2, CONTAINER_HOTSEAT, 0, 0, testPackage8)
+ addItem(ITEM_TYPE_APPLICATION, 3, CONTAINER_HOTSEAT, 0, 0, testPackage9)
+
+ // Workspace items in grid A
+ // _ _ _ _ _
+ // 6 _ _ _ _
+ // _ _ _ _ _
+ // _ _ _ _ _
+ // _ _ _ _ _
+ addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 1, testPackage6, 6, TMP_TABLE)
+
+ // Workspace items in grid B
+ // _ _ _ _
+ // 1 2 3 4
+ // _ _ _ _
+ // _ _ _ _
+ addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 1, testPackage1)
+ addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 1, 1, testPackage2)
+ addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 1, testPackage3)
+ addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 3, 1, testPackage4)
+
+ idp.numDatabaseHotseatIcons = 4
+ idp.numColumns = 4
+ idp.numRows = 4
+ val readerGridA = DbReader(db, TMP_TABLE, context)
+ val readerGridB = DbReader(db, TABLE_NAME, context)
+
+ // migrate from A -> B
+ migrateGrid(
+ dbHelper,
+ readerGridA,
+ readerGridB,
+ 5,
+ idp.numDatabaseHotseatIcons,
+ idp.numColumns,
+ idp.numRows,
+ )
+
+ // Check hotseat items in grid B
+ var c =
+ db.query(
+ TABLE_NAME,
+ arrayOf(SCREEN, INTENT),
+ "container=$CONTAINER_HOTSEAT",
+ null,
+ SCREEN,
+ null,
+ null,
+ ) ?: throw IllegalStateException()
+ // Expected hotseat items in grid B
+ // 1 2 3 4
+ verifyHotseat(
+ c,
+ mutableListOf(testPackage1, testPackage2, testPackage3, testPackage4).toList(),
+ 4,
+ )
+
+ // Check workspace items in grid B
+ c =
+ db.query(
+ TABLE_NAME,
+ arrayOf(SCREEN, CELLX, CELLY, INTENT),
+ "container=$CONTAINER_DESKTOP",
+ null,
+ null,
+ null,
+ null,
+ ) ?: throw IllegalStateException()
+ val locMap = parseLocMap(c)
+ // Expected workspace items in grid B
+ // _ _ _ _
+ // 6 _ _ _
+ // _ _ _ _
+ // _ _ _ _
+ assertThat(locMap.size.toLong()).isEqualTo(1)
+ assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 0, 1))
+ }
+
private fun migrateGrid(
dbHelper: DatabaseHelper,
srcReader: DbReader,
@@ -621,7 +684,7 @@
srcReader,
destReader,
dbHelper,
- Point(idp.numColumns, idp.numRows),
+ Point(pointX, pointY),
idsInUse,
)
} else {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index 7a403e1..a55d64b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -454,7 +454,7 @@
assertThat(mAllDeepShortcuts).isEmpty()
verify(mockCursor)
.markDeleted(
- "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
+ "Pinned shortcut not found from request. package=pkg, user=$mUserHandle",
"shortcut_not_found",
)
}
@@ -513,7 +513,7 @@
verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
verify(mockCursor)
.markDeleted(
- "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
+ "Pinned shortcut not found from request. package=pkg, user=$mUserHandle",
"shortcut_not_found",
)
}
@@ -533,7 +533,7 @@
whenever(disabledMessage).thenReturn("")
whenever(disabledReason).thenReturn(0)
whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
- whenever(userHandle).thenReturn(Process.myUserHandle())
+ whenever(userHandle).thenReturn(mUserHandle)
}
mIconRequestInfos = mutableListOf()
// Make sure shortcuts map has expected key from expected package
@@ -565,7 +565,7 @@
mockBgDataModel = mock<BgDataModel>()
mockCursor =
mock<LoaderCursor>().apply {
- user = UserHandle(0)
+ user = mUserHandle
itemType = ITEM_TYPE_FOLDER
id = 1
container = 100
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
index 15a9964..23c1da9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -219,7 +219,7 @@
.whenever(launcherApps)
.unregisterPackageInstallerSessionCallback(installSessionTracker)
// When
- installSessionTracker.unregister()
+ installSessionTracker.close()
// Then
verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
index 2b8896e..49d305b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
@@ -22,7 +22,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES
-import com.android.launcher3.graphics.IconShape.GenericPathShape
+import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape
import com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI
import org.junit.Rule
import org.junit.Test
@@ -63,15 +63,6 @@
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
- fun `verify valid path sunny`() {
- ShapesProvider.iconShapes["sunny"]?.apply {
- GenericPathShape(pathString)
- PathParser.createPathFromPathData(pathString)
- }
- }
-
- @Test
- @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path circle`() {
ShapesProvider.iconShapes["circle"]?.apply {
GenericPathShape(pathString)
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt
new file mode 100644
index 0000000..20ad60f
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt
@@ -0,0 +1,52 @@
+/*
+ * 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 com.android.launcher3.debug.TestEventEmitter.TestEvent
+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"
+ private val SIGNAL_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
+ }
+
+ fun waitForSignal(timeout: Long = SIGNAL_TIMEOUT) = 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)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
index fb61ced..45eb5e1 100644
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
@@ -17,11 +17,15 @@
package com.android.launcher3.celllayout.integrationtest.events
import android.content.Context
-import com.android.launcher3.debug.TestEvent
+import android.util.Log
+import com.android.dx.mockito.inline.extended.ExtendedMockito.*
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.launcher3.debug.TestEventEmitter
+import com.android.launcher3.debug.TestEventEmitter.TestEvent
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
+import org.mockito.quality.Strictness
/**
* Rule to create EventWaiters to wait for events that happens on the Launcher. For reference look
@@ -30,35 +34,65 @@
* 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.
+ * This class mocks the static method [TestEventEmitter.sendEvent]
*/
class EventsRule(val context: Context) : TestRule {
- private var prevEventEmitter: TestEventEmitter? = null
+ private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
- private val eventEmitter = TestEventsEmitterImplementation()
+ private lateinit var mockitoSession: StaticMockitoSession
override fun apply(base: Statement, description: Description?): Statement {
return object : Statement() {
override fun evaluate() {
- beforeTest()
- base.evaluate()
- afterTest()
+ try {
+ beforeTest()
+ base.evaluate()
+ } finally {
+ afterTest()
+ }
}
}
}
fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
- return eventEmitter.createEventWaiter(expectedEvent)
+ val eventWaiter = EventWaiter(expectedEvent)
+ expectedEvents.add(eventWaiter)
+ return eventWaiter
}
private fun beforeTest() {
- prevEventEmitter = TestEventEmitter.INSTANCE.get(context)
- TestEventEmitter.INSTANCE.initializeForTesting(eventEmitter)
+ mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(TestEventEmitter::class.java)
+ .startMocking()
+
+ doAnswer { invocation ->
+ val event = (invocation.arguments[0] as TestEvent)
+ Log.d(TAG, "Signal received $event")
+ Log.d(TAG, "Total expected events ${expectedEvents.size}")
+ if (!expectedEvents.isEmpty()) {
+ val eventWaiter = expectedEvents.last()
+ if (eventWaiter.eventToWait == event) {
+ Log.d(TAG, "Removing $event")
+ expectedEvents.removeLast()
+ eventWaiter.terminate()
+ } else {
+ Log.d(TAG, "Not matching $event")
+ }
+ }
+ null
+ }
+ .`when` { TestEventEmitter.sendEvent(any()) }
}
private fun afterTest() {
- TestEventEmitter.INSTANCE.initializeForTesting(prevEventEmitter)
+ mockitoSession.finishMocking()
+ expectedEvents.clear()
+ }
+
+ companion object {
+ private const val TAG = "TestEvents"
}
}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
deleted file mode 100644
index 5e062d0..0000000
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
+++ /dev/null
@@ -1,90 +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.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"
- private val SIGNAL_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
- }
-
- fun waitForSignal(timeout: Long = SIGNAL_TIMEOUT) = 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/widget/picker/OWNERS b/tests/src/com/android/launcher3/widget/picker/OWNERS
index 775b0c7..716ab90 100644
--- a/tests/src/com/android/launcher3/widget/picker/OWNERS
+++ b/tests/src/com/android/launcher3/widget/picker/OWNERS
@@ -5,7 +5,6 @@
#
# Widget Picker OWNERS
-zakcohen@google.com
shamalip@google.com
wvk@google.com
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 1158521..4e94e1e 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -129,7 +129,7 @@
return mTask.getVisibleCenter().x;
}
- int getTaskCenterY() {
+ public int getTaskCenterY() {
return mTask.getVisibleCenter().y;
}