Merge "Release predict_back animaton leash in Launcher." 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/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/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
new file mode 100644
index 0000000..adbcc75
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.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) { "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()
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 2406fb6..578bba5 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -17,27 +17,18 @@
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.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 +39,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,
- private val launchType: AppLaunchType,
+class DesktopAppLaunchTransition
+@JvmOverloads
+constructor(
+ context: Context,
+ 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,7 +63,7 @@
override fun startAnimation(
token: IBinder,
info: TransitionInfo,
- t: Transaction,
+ transaction: Transaction,
transitionFinishedCallback: IRemoteTransitionFinishedCallback,
) {
val safeTransitionFinishedCallback = RemoteRunnable {
@@ -76,7 +71,7 @@
}
mainExecutor.execute {
runAnimators(info, safeTransitionFinishedCallback)
- t.apply()
+ transaction.apply()
}
}
@@ -86,77 +81,10 @@
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 {
/** 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..37e8d4d 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
@@ -152,7 +153,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")
}
@@ -409,18 +411,16 @@
}
}
- 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) {
if (!DesktopModeStatus.enableMultipleDesktops(context)) {
return
}
- getDisplayDeskConfig(displayId).canCreateDesks = canCreateDesks
+ getDisplayDeskConfig(displayId)?.canCreateDesks = canCreateDesks
}
private fun onDeskAdded(displayId: Int, deskId: Int) {
@@ -428,7 +428,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.add(deskId)) {
"Found a duplicate desk Id: $deskId on display: $displayId"
}
@@ -440,7 +440,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.remove(deskId)) {
"Removing non-existing desk Id: $deskId on display: $displayId"
}
@@ -457,7 +457,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}"
}
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..d1e63f3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -47,7 +47,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 +78,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 +300,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 +437,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 +515,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).
@@ -1533,9 +1541,9 @@
return new RemoteTransition(
new DesktopAppLaunchTransition(
this,
- getMainExecutor(),
appLaunchType,
- cujType
+ cujType,
+ getMainExecutor()
),
"TaskbarDesktopAppLaunch");
}
@@ -1558,7 +1566,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..46802c3 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;
@@ -53,6 +52,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;
@@ -118,11 +118,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 +141,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 +164,35 @@
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 boolean mUserUnlocked = false;
@@ -191,6 +205,7 @@
@Override
public void run() {
int displayId = getDefaultDisplayId();
+ debugTaskbarManager("mActivityOnDestroyCallback running!", displayId);
if (mActivity != null) {
displayId = mActivity.getDisplayId();
mActivity.removeOnDeviceProfileChangeListener(
@@ -204,7 +219,6 @@
mRecentsViewContainer = null;
}
mActivity = null;
- debugWhyTaskbarNotDestroyed("clearActivity");
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (taskbar != null) {
taskbar.setUIController(TaskbarUIController.DEFAULT);
@@ -217,28 +231,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 +263,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 +296,10 @@
mTaskbarBroadcastReceiver.register(RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
});
- debugWhyTaskbarNotDestroyed("TaskbarManager created");
+ 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 +308,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 +369,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 +384,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 +433,7 @@
return ql.getUnfoldTransitionProgressProvider();
}
} else {
- return SystemUiProxy.INSTANCE.get(mPrimaryWindowContext).getUnfoldTransitionProvider();
+ return SystemUiProxy.INSTANCE.get(mBaseContext).getUnfoldTransitionProvider();
}
return null;
}
@@ -530,13 +471,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 +494,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 +507,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 +525,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 +540,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 +578,8 @@
}
public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
- if (mDefaultNavButtonController != null) {
- mDefaultNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
+ if (mPrimaryNavButtonController != null) {
+ mPrimaryNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
}
}
@@ -746,13 +702,26 @@
* 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;
+ }
+
+ // TODO (b/391965805): remove once onDisplayAddSystemDecorations is working.
+ WindowManager wm = getWindowManager(displayId);
+ if (wm == null || !wm.shouldShowSystemDecors(displayId)) {
return;
}
Context newWindowContext = createWindowContext(displayId);
if (newWindowContext != null) {
addWindowContextToMap(displayId, newWindowContext);
+ createTaskbarRootLayout(displayId);
+ createNavButtonController(displayId);
+ createAndRegisterComponentCallbacks(displayId);
+ recreateTaskbarForDisplay(displayId);
}
}
@@ -761,12 +730,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);
}
}
@@ -796,7 +769,7 @@
*/
public void destroy() {
mRecentsViewContainer = null;
- debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
+ debugTaskbarManager("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
mTaskbarBroadcastReceiver.unregisterReceiverSafely();
@@ -809,7 +782,7 @@
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();
destroyAllTaskbars();
}
@@ -836,15 +809,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 +842,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 +895,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 +1080,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 +1155,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 +1166,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 +1196,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 +1213,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 +1256,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 +1308,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/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..37b8dc7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -51,7 +51,7 @@
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;
@@ -142,7 +142,7 @@
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/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/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/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 35a6e72..37c2d1c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -38,6 +38,7 @@
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;
@@ -65,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;
@@ -102,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.
*
@@ -164,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,
@@ -172,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);
}
@@ -210,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();
@@ -219,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();
@@ -237,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
@@ -260,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) {
@@ -271,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(() -> {
@@ -321,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) {
@@ -340,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
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 4b3bef3..c6b3d6a 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -31,7 +31,6 @@
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.GetThumbnailUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
import com.android.systemui.shared.recents.model.Task
@@ -42,7 +41,7 @@
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 {
@@ -185,7 +184,6 @@
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(
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/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index a1963c9..a9dbbf2 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -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/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/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 5bfcf43..a29d302 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -851,6 +851,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();
@@ -916,10 +917,12 @@
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());
@@ -2010,7 +2013,7 @@
}
// If the list changed, maybe the focused task doesn't exist anymore.
if (newFocusedTaskView == null) {
- newFocusedTaskView = mUtils.getExpectedFocusedTask();
+ newFocusedTaskView = mUtils.getFirstNonDesktopTaskView();
}
}
setFocusedTaskViewId(
@@ -2116,15 +2119,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();
@@ -2917,28 +2911,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));
+ }
}
}
}
@@ -3064,7 +3070,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 {
@@ -3491,8 +3497,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();
+ }
);
}
@@ -3606,12 +3617,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();
});
}
@@ -3933,9 +3941,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
@@ -3961,7 +3969,7 @@
Math.abs(i - dismissedIndex),
scrollDiff,
anim,
- splitTimings, i);
+ splitTimings);
needsCurveUpdates = true;
}
} else if (child instanceof TaskView taskView) {
@@ -4337,8 +4345,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();
@@ -5655,7 +5667,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,
@@ -5677,6 +5689,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.
@@ -5785,10 +5809,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) {
@@ -6241,12 +6267,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()));
}
@@ -6305,7 +6331,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
@@ -6943,10 +6969,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 d3549fb..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,234 +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()) {
- val taskGridNavHelper =
- TaskGridNavHelper(
- recentsView.topRowIdArray,
- recentsView.bottomRowIdArray,
- getLargeTaskViewIds(),
- hasAddDesktopButton = false,
- )
- return taskGridNavHelper
- .gridTaskViewIdOffsetPairInTabOrderSequence(
- draggedTaskView.taskViewId,
- towardsStart,
- )
- .mapNotNull { (taskViewId, columnOffset) ->
- recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
- Pair(taskView, columnOffset)
- }
- }
- } 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()
- }
- }
- }
-
- /** 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 ce20bf9..7301cfc 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -25,8 +25,6 @@
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.ui.mapper.TaskUiStateMapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.task.thumbnail.TaskThumbnailView
@@ -57,13 +55,6 @@
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)
}
@@ -116,8 +107,6 @@
snapshotView.scaleY = 1f
overlay.destroy()
if (enableRefactorTaskThumbnail()) {
- RecentsDependencies.getInstance().removeScope(snapshotView)
- RecentsDependencies.getInstance().removeScope(this)
isThumbnailValid = false
} else {
thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
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/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
deleted file mode 100644
index 90ef61d..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ /dev/null
@@ -1,83 +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.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(TASK_ID to thumbnailData))
-
- assertThat(systemUnderTest.run(TASK_ID)).isNull()
- }
-
- @Test
- fun taskVisible_returnsThumbnail() {
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(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/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/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 79d3c19..aaaa0e4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -62,6 +62,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Comparator;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -557,6 +558,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..03dd943 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -75,8 +75,8 @@
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.graphics.ShapeDelegate;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
@@ -724,7 +724,7 @@
if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
getIconBounds(mDotParams.iconBounds);
Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ ShapeDelegate.getNormalizationScale());
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
@@ -775,7 +775,7 @@
getIconBounds(mRunningAppIconBounds);
Utilities.scaleRectAboutCenter(
mRunningAppIconBounds,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ ShapeDelegate.getNormalizationScale());
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..4561506 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,7 +53,7 @@
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;
@@ -388,7 +389,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 +421,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 +849,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 +872,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);
}
@@ -2478,7 +2481,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 +2498,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 +2584,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 4107c8f..870e6d6 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 38e2806..3edba99 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -183,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;
@@ -611,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() {
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 35ae68f..31d0da0 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -20,7 +20,6 @@
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;
@@ -57,7 +56,6 @@
ApiWrapper getApiWrapper();
CustomWidgetManager getCustomWidgetManager();
DynamicResource getDynamicResource();
- IconShape getIconShape();
InstallSessionHelper getInstallSessionHelper();
ItemInstallQueue getItemInstallQueue();
RefreshRateTracker getRefreshRateTracker();
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..bff323c 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -60,7 +60,7 @@
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.model.data.ItemInfo;
@@ -274,9 +274,9 @@
Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
adaptiveIcon.setBounds(shrunkBounds);
- IconShape iconShape = IconShape.INSTANCE.get(getContext());
+ ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon
- ? iconShape.getFolderShape() : iconShape.getShape())
+ ? themeManager.getFolderShape() : themeManager.getIconShape())
.getPath(shrunkBounds);
mTranslateX = new SpringFloatValue(DragView.this,
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/IconShape.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
similarity index 81%
rename from src/com/android/launcher3/graphics/IconShape.kt
rename to src/com/android/launcher3/graphics/ShapeDelegate.kt
index eac3440..df0c8f9 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -42,78 +42,38 @@
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 +139,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 +252,6 @@
}
companion object {
- @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
const val TAG = "IconShape"
const val DEFAULT_PATH_SIZE = 100f
@@ -312,7 +276,6 @@
}
}
- @VisibleForTesting
fun pickBestShape(shapeStr: String): ShapeDelegate {
val baseShape =
if (shapeStr.isNotEmpty()) {
@@ -332,7 +295,6 @@
return pickBestShape(baseShape, shapeStr)
}
- @VisibleForTesting
fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate {
val calcAreaDiff = areaDiffCalculator(baseShape)
@@ -380,5 +342,12 @@
centerY = (bottom - top) / 2,
rounding = CornerRounding(cornerR),
)
+
+ @JvmStatic
+ val normalizationScale =
+ normalizeAdaptiveIcon(
+ AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)),
+ AREA_CALC_SIZE,
+ )
}
}
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index e767290..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,24 +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"
}
@@ -129,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/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 d173fd6..6c018e8 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.kt
+++ b/src/com/android/launcher3/icons/LauncherIcons.kt
@@ -27,7 +27,6 @@
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
-import com.android.launcher3.graphics.IconShape
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.pm.UserCache
import com.android.launcher3.util.UserIconInfo
@@ -46,9 +45,8 @@
internal constructor(
@ApplicationContext context: Context,
idp: InvariantDeviceProfile,
- themeManager: ThemeManager,
+ private var themeManager: ThemeManager,
private var userCache: UserCache,
- private var iconShape: IconShape,
@Assisted private val pool: ConcurrentLinkedQueue<LauncherIcons>,
) : BaseIconFactory(context, idp.fillResIconDpi, idp.iconBitmapSize), AutoCloseable {
@@ -70,7 +68,7 @@
public override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
if (!Flags.enableLauncherIconShapes()) return drawable.iconMask
- return iconShape.shape.getPath(iconBounds)
+ return themeManager.iconShape.getPath(iconBounds)
}
override fun drawAdaptiveIcon(
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/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index f90a3e4..ddf18df 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -47,7 +47,8 @@
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.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
/**
* A view used to draw both layers of an {@link AdaptiveIconDrawable}.
@@ -174,7 +175,7 @@
if (mIsAdaptiveIcon) {
if (!isOpening && progress >= shapeProgressStart) {
if (mRevealAnimator == null) {
- mRevealAnimator = IconShape.INSTANCE.get(getContext()).getShape()
+ mRevealAnimator = ThemeManager.INSTANCE.get(getContext()).getIconShape()
.createRevealAnimator(this, mStartRevealRect,
mOutline, mTaskCornerRadius, !isOpening);
mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
@@ -259,7 +260,7 @@
if (!isFolderIcon) {
Utilities.scaleRectAboutCenter(mStartRevealRect,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ ShapeDelegate.getNormalizationScale());
}
if (dp.isLandscape) {
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..8cdf380 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)
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..508c9a4 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
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/nonquickstep/DeviceProfileDumpTest.kt b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
index f6e45a3..05cf926 100644
--- a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
@@ -40,7 +40,6 @@
Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION,
)
setFlagsRule.setFlags(false, Flags.FLAG_ONE_GRID_SPECS)
- setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES)
}
@Test
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;
}