Merge "Fix crash when pausing work profile." into main
diff --git a/Android.bp b/Android.bp
index 9d7aa73..223e2c2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -66,6 +66,8 @@
srcs: [
"quickstep/src/**/*.kt",
"quickstep/src/**/*.java",
+ ],
+ device_common_srcs: [
":launcher-quickstep-processed-protolog-src",
],
}
@@ -90,7 +92,7 @@
],
}
-genrule {
+java_genrule {
name: "launcher-quickstep-processed-protolog-src",
srcs: [
":protolog-impl",
@@ -108,7 +110,7 @@
out: ["launcher.quickstep.protolog.srcjar"],
}
-genrule {
+java_genrule {
name: "gen-launcher.quickstep.protolog.pb",
srcs: [
":launcher-quickstep-unprocessed-protolog-src",
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index c71b833..d334cf4 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -369,6 +369,13 @@
}
flag {
+ name: "work_scheduler_in_work_profile"
+ namespace: "launcher"
+ description: "Enables work scheduler view above the work pause button in work profile."
+ bug: "361589193"
+}
+
+flag {
name: "one_grid_specs"
namespace: "launcher"
description: "Defines the new specs for grids based on OneGrid"
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index c59978f..4335f76 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -47,4 +47,11 @@
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ name: "enable_desktop_windowing_carousel_detach"
+ namespace: "launcher_overview"
+ description: "Makes the desktop windowing task carousel detaches from fullscreen task carousel during quickswitch."
+ bug: "353947917"
}
\ No newline at end of file
diff --git a/quickstep/res/color/taskbar_minimized_app_indicator_color.xml b/quickstep/res/color/taskbar_minimized_app_indicator_color.xml
new file mode 100644
index 0000000..1596fe1
--- /dev/null
+++ b/quickstep/res/color/taskbar_minimized_app_indicator_color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?attr/materialColorOutline"/>
+</selector>
diff --git a/quickstep/res/color/taskbar_running_app_indicator_color.xml b/quickstep/res/color/taskbar_running_app_indicator_color.xml
new file mode 100644
index 0000000..5dc9781
--- /dev/null
+++ b/quickstep/res/color/taskbar_running_app_indicator_color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?attr/materialColorTertiary"/>
+</selector>
diff --git a/quickstep/res/layout/taskbar_overflow_button.xml b/quickstep/res/layout/taskbar_overflow_view.xml
similarity index 86%
rename from quickstep/res/layout/taskbar_overflow_button.xml
rename to quickstep/res/layout/taskbar_overflow_view.xml
index 20104f2..7444e59 100644
--- a/quickstep/res/layout/taskbar_overflow_button.xml
+++ b/quickstep/res/layout/taskbar_overflow_view.xml
@@ -15,8 +15,7 @@
-->
<!-- Note: The actual size will match the taskbar icon sizes in TaskbarView#onLayout(). -->
-<com.android.launcher3.views.IconButtonView xmlns:android="http://schemas.android.com/apk/res/android"
- style="@style/BaseIcon.Workspace.Taskbar"
+<com.android.launcher3.taskbar.TaskbarOverflowView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/taskbar_icon_min_touch_size"
android:layout_height="@dimen/taskbar_icon_min_touch_size"
android:backgroundTint="@android:color/transparent"
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index 0d3825f..b699d93 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -22,8 +22,7 @@
<string name="recent_task_option_pin" msgid="7929860679018978258">"تثبيت"</string>
<string name="recent_task_option_freeform" msgid="48863056265284071">"شكل مجاني"</string>
<string name="recent_task_option_desktop" msgid="8280879717125435668">"الكمبيوتر المكتبي"</string>
- <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
- <skip />
+ <string name="recent_task_option_external_display" msgid="4533840664313389484">"نقل التطبيق إلى شاشة خارجية"</string>
<string name="recent_task_desktop" msgid="8081113562549637334">"كمبيوتر مكتبي"</string>
<string name="recents_empty_message" msgid="7040467240571714191">"ما مِن عناصر تم استخدامها مؤخرًا"</string>
<string name="accessibility_app_usage_settings" msgid="6312864233673544149">"إعدادات استخدام التطبيق"</string>
diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml
index 890959a..b30b000 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -22,8 +22,7 @@
<string name="recent_task_option_pin" msgid="7929860679018978258">"固定"</string>
<string name="recent_task_option_freeform" msgid="48863056265284071">"フリーフォーム"</string>
<string name="recent_task_option_desktop" msgid="8280879717125435668">"デスクトップ"</string>
- <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
- <skip />
+ <string name="recent_task_option_external_display" msgid="4533840664313389484">"外部ディスプレイに移動する"</string>
<string name="recent_task_desktop" msgid="8081113562549637334">"パソコン"</string>
<string name="recents_empty_message" msgid="7040467240571714191">"最近のアイテムはありません"</string>
<string name="accessibility_app_usage_settings" msgid="6312864233673544149">"アプリの使用状況の設定"</string>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index d5216a5..2634b94 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -22,8 +22,7 @@
<string name="recent_task_option_pin" msgid="7929860679018978258">"Закачи"</string>
<string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
<string name="recent_task_option_desktop" msgid="8280879717125435668">"Работна површина"</string>
- <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
- <skip />
+ <string name="recent_task_option_external_display" msgid="4533840664313389484">"Префрлете се на надворешниот екран"</string>
<string name="recent_task_desktop" msgid="8081113562549637334">"За компјутер"</string>
<string name="recents_empty_message" msgid="7040467240571714191">"Нема неодамнешни ставки"</string>
<string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Поставки за користење на апликациите"</string>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index 1f93cb6..99b53d9 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -22,8 +22,7 @@
<string name="recent_task_option_pin" msgid="7929860679018978258">"ปักหมุด"</string>
<string name="recent_task_option_freeform" msgid="48863056265284071">"รูปแบบอิสระ"</string>
<string name="recent_task_option_desktop" msgid="8280879717125435668">"เดสก์ท็อป"</string>
- <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
- <skip />
+ <string name="recent_task_option_external_display" msgid="4533840664313389484">"ย้ายไปยังจอแสดงผลภายนอก"</string>
<string name="recent_task_desktop" msgid="8081113562549637334">"เดสก์ท็อป"</string>
<string name="recents_empty_message" msgid="7040467240571714191">"ไม่มีรายการล่าสุด"</string>
<string name="accessibility_app_usage_settings" msgid="6312864233673544149">"การตั้งค่าการใช้แอป"</string>
diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml
index 2bf4a13..3a32551 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -22,8 +22,7 @@
<string name="recent_task_option_pin" msgid="7929860679018978258">"Ghim"</string>
<string name="recent_task_option_freeform" msgid="48863056265284071">"Dạng tự do"</string>
<string name="recent_task_option_desktop" msgid="8280879717125435668">"Máy tính"</string>
- <!-- no translation found for recent_task_option_external_display (4533840664313389484) -->
- <skip />
+ <string name="recent_task_option_external_display" msgid="4533840664313389484">"Chuyển sang màn hình ngoài"</string>
<string name="recent_task_desktop" msgid="8081113562549637334">"Máy tính"</string>
<string name="recents_empty_message" msgid="7040467240571714191">"Không có mục gần đây nào"</string>
<string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Cài đặt mức sử dụng ứng dụng"</string>
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index db5ff19..3ae2b89 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -38,7 +38,6 @@
<string name="nav_handle_long_press_handler_class" translatable="false"></string>
<string name="contextual_search_invoker_class" translatable="false"></string>
<string name="contextual_search_state_manager_class" translatable="false"></string>
- <string name="api_wrapper_class" translatable="false">com.android.launcher3.uioverrides.SystemApiWrapper</string>
<!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
determines how many thumbnails will be fetched in the background. -->
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 5f35007..782a705 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -111,6 +111,7 @@
<dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
<dimen name="motion_pause_detector_speed_slow">0.15dp</dimen>
<dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
+ <dimen name="motion_pause_detector_speed_trackpad_somewhat_fast">0.7dp</dimen>
<dimen name="motion_pause_detector_speed_fast">1.4dp</dimen>
<dimen name="motion_pause_detector_min_displacement_from_app">36dp</dimen>
<dimen name="quickstep_fling_threshold_speed">0.5dp</dimen>
@@ -360,6 +361,7 @@
<dimen name="taskbar_running_app_indicator_width">12dp</dimen>
<dimen name="taskbar_running_app_indicator_top_margin">4dp</dimen>
<dimen name="taskbar_minimized_app_indicator_width">6dp</dimen>
+ <dimen name="taskbar_overflow_button_preview_stroke">2dp</dimen>
<!-- Transient taskbar -->
<dimen name="transient_taskbar_padding">12dp</dimen>
@@ -502,6 +504,7 @@
<dimen name="keyboard_quick_switch_recents_icon_size">20dp</dimen>
<dimen name="keyboard_quick_switch_desktop_icon_size">32dp</dimen>
<dimen name="keyboard_quick_switch_margin_top">56dp</dimen>
+ <dimen name="keyboard_quick_switch_margin_bottom">24dp</dimen>
<dimen name="keyboard_quick_switch_margin_ends">16dp</dimen>
<dimen name="keyboard_quick_switch_view_spacing">16dp</dimen>
<dimen name="keyboard_quick_switch_view_small_spacing">4dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index dd2ff2d..6916a1d 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -32,6 +32,8 @@
import androidx.core.animation.addListener
import com.android.app.animation.Interpolators
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
/**
@@ -77,7 +79,13 @@
val launchAnimator =
createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
- val minimizeAnimator = createMinimizeAnimator(minimizeChange, transaction, finishCallback)
+ val minimizeAnimator =
+ MinimizeAnimator.create(
+ context.resources.displayMetrics,
+ minimizeChange,
+ transaction,
+ finishCallback,
+ )
return listOf(launchAnimator, minimizeAnimator)
}
@@ -96,7 +104,7 @@
): Animator {
val boundsAnimator =
WindowAnimator.createBoundsAnimator(
- context,
+ context.resources.displayMetrics,
launchBoundsAnimationDef,
change,
transaction,
@@ -115,32 +123,6 @@
}
}
- private fun createMinimizeAnimator(
- change: Change,
- transaction: Transaction,
- onAnimFinish: (Animator) -> Unit,
- ): Animator {
- val boundsAnimator =
- WindowAnimator.createBoundsAnimator(
- context,
- minimizeBoundsAnimationDef,
- change,
- transaction,
- )
- val alphaAnimator =
- ValueAnimator.ofFloat(1f, 0f).apply {
- duration = MINIMIZE_ANIM_ALPHA_DURATION_MS
- interpolator = Interpolators.LINEAR
- addUpdateListener { animation ->
- transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
- }
- }
- return AnimatorSet().apply {
- playTogether(boundsAnimator, alphaAnimator)
- addListener(onEnd = { animation -> onAnimFinish(animation) })
- }
- }
-
companion object {
private val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
@@ -154,13 +136,5 @@
startScale = 0.97f,
interpolator = Interpolators.STANDARD_DECELERATE,
)
-
- private val minimizeBoundsAnimationDef =
- WindowAnimator.BoundsAnimationParams(
- durationMs = 200,
- endOffsetYDp = 12f,
- endScale = 0.97f,
- interpolator = Interpolators.STANDARD_ACCELERATE,
- )
}
}
diff --git a/quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt b/quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt
deleted file mode 100644
index 1a99a36..0000000
--- a/quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt
+++ /dev/null
@@ -1,100 +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.desktop
-
-import android.animation.RectEvaluator
-import android.animation.ValueAnimator
-import android.content.Context
-import android.graphics.Rect
-import android.util.TypedValue
-import android.view.SurfaceControl
-import android.view.animation.Interpolator
-import android.window.TransitionInfo
-
-/** Creates animations that can be applied to windows/surfaces. */
-object WindowAnimator {
-
- /** Parameters defining a window bounds animation. */
- data class BoundsAnimationParams(
- val durationMs: Long,
- val startOffsetYDp: Float = 0f,
- val endOffsetYDp: Float = 0f,
- val startScale: Float = 1f,
- val endScale: Float = 1f,
- val interpolator: Interpolator,
- )
-
- /**
- * Creates an animator to reposition and scale the bounds of the leash of the given change.
- *
- * @param boundsAnimDef the parameters for the animation itself (duration, scale, position)
- * @param change the change to which the animation should be applied
- * @param transaction the transaction to apply the animation to
- */
- fun createBoundsAnimator(
- context: Context,
- boundsAnimDef: BoundsAnimationParams,
- change: TransitionInfo.Change,
- transaction: SurfaceControl.Transaction,
- ): ValueAnimator {
- val startBounds =
- createBounds(
- context,
- change.startAbsBounds,
- boundsAnimDef.startScale,
- boundsAnimDef.startOffsetYDp,
- )
- val leash = change.leash
- val endBounds =
- createBounds(
- context,
- change.startAbsBounds,
- boundsAnimDef.endScale,
- boundsAnimDef.endOffsetYDp,
- )
- return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply {
- duration = boundsAnimDef.durationMs
- interpolator = boundsAnimDef.interpolator
- addUpdateListener { animation ->
- val animBounds = animation.animatedValue as Rect
- val animScale = 1 - (1 - boundsAnimDef.endScale) * animation.animatedFraction
- transaction
- .setPosition(leash, animBounds.left.toFloat(), animBounds.top.toFloat())
- .setScale(leash, animScale, animScale)
- .apply()
- }
- }
- }
-
- private fun createBounds(context: Context, origBounds: Rect, scale: Float, offsetYDp: Float) =
- Rect(origBounds).apply {
- check(scale in 0.0..1.0)
- // Scale the bounds down with an anchor in the center
- inset(
- (origBounds.width().toFloat() * (1 - scale) / 2).toInt(),
- (origBounds.height().toFloat() * (1 - scale) / 2).toInt(),
- )
- val offsetYPx =
- TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- offsetYDp,
- context.resources.displayMetrics,
- )
- .toInt()
- offset(/* dx= */ 0, offsetYPx)
- }
-}
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 29e1f4e..2f4c6f6 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -59,7 +59,6 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.InstanceIdSequence;
@@ -156,9 +155,6 @@
state.containerId);
FixedContainerItems fci = new FixedContainerItems(state.containerId,
state.storage.read(mApp.getContext(), factory, ums.allUsers::get));
- if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- bindPredictionItems(callbacks, fci);
- }
mDataModel.extraItems.put(state.containerId, fci);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 9912c6c..711a49a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -63,6 +63,11 @@
private int mTaskListChangeId = -1;
// Only empty before the recent tasks list has been loaded the first time
@NonNull private List<GroupTask> mTasks = new ArrayList<>();
+ // Set of task IDs filtered out of tasks in recents model to generate list of tasks to show in
+ // the Keyboard Quick Switch view. Non empty only if the view has been shown in response to
+ // toggling taskbar overflow button.
+ @NonNull private Set<Integer> mExcludedTaskIds = Collections.emptySet();
+
private int mNumHiddenTasks = 0;
// Initialized in init
@@ -90,10 +95,12 @@
return;
}
int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex();
+ boolean wasOpenedFromTaskbar = mQuickSwitchViewController.wasOpenedFromTaskbar();
onDestroy();
if (currentFocusedIndex != -1) {
mControllers.taskbarActivityContext.getMainThreadHandler().post(
- () -> openQuickSwitchView(currentFocusedIndex));
+ () -> openQuickSwitchView(currentFocusedIndex, mExcludedTaskIds,
+ wasOpenedFromTaskbar));
}
}
@@ -102,10 +109,19 @@
}
/**
- * Opens the view with a filtered list of tasks.
+ * Opens or closes the view in response to taskbar action. The view shows a filtered list of
+ * tasks.
* @param taskIdsToExclude A list of tasks to exclude in the opened view.
*/
- void openQuickSwitchView(@NonNull Set<Integer> taskIdsToExclude) {
+ void toggleQuickSwitchViewForTaskbar(@NonNull Set<Integer> taskIdsToExclude) {
+ // Close the view if its shown, and was opened from the taskbar.
+ if (mQuickSwitchViewController != null
+ && !mQuickSwitchViewController.isCloseAnimationRunning()
+ && mQuickSwitchViewController.wasOpenedFromTaskbar()) {
+ closeQuickSwitchView(true);
+ return;
+ }
+
openQuickSwitchView(-1, taskIdsToExclude, true);
}
@@ -117,10 +133,16 @@
@NonNull Set<Integer> taskIdsToExclude,
boolean wasOpenedFromTaskbar) {
if (mQuickSwitchViewController != null) {
- if (!mQuickSwitchViewController.isCloseAnimationRunning()) {
+ if (!mQuickSwitchViewController.isCloseAnimationRunning()
+ && mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
return;
}
- // Allow the KQS to be reopened during the close animation to make it more responsive
+
+ // Allow the KQS to be reopened during the close animation to make it more responsive.
+ // Similarly, if KQS was opened in different mode (from taskbar vs. keyboard event),
+ // close it so it can be reopened in the correct mode.
+ // TODO(b/368119679) Consider updating list of shown tasks in place, or at least reopen
+ // the view in the same vertical location.
closeQuickSwitchView(false);
}
mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
@@ -139,9 +161,8 @@
final boolean onDesktop =
mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible();
- // TODO(b/368119679) For now we will re-process the task list every time, but this can be
- // optimized if we have the same set of task ids to exclude.
- if (mModel.isTaskListValid(mTaskListChangeId) && !Flags.taskbarOverflow()) {
+ if (mModel.isTaskListValid(mTaskListChangeId)
+ && taskIdsToExclude.equals(mExcludedTaskIds)) {
// When we are opening the KQS with no focus override, check if the first task is
// running. If not, focus that first task.
mQuickSwitchViewController.openQuickSwitchView(
@@ -157,6 +178,7 @@
return;
}
+ mExcludedTaskIds = taskIdsToExclude;
mTaskListChangeId = mModel.getTasks((tasks) -> {
mHasDesktopTask = false;
mWasDesktopTaskFilteredOut = false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 50a253c..05d34b5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -131,6 +131,15 @@
}
@Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mViewCallbacks != null) {
+ mViewCallbacks.onViewDetchedFromWindow();
+ }
+ }
+
+ @Override
protected void onFinishInflate() {
super.onFinishInflate();
mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane);
@@ -281,6 +290,10 @@
return mDesktopTaskIndex;
}
+ void resetViewCallbacks() {
+ mViewCallbacks = null;
+ }
+
protected Animator getCloseAnimation() {
AnimatorSet closeAnimation = new AnimatorSet();
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index a80c11c..3f71870 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -20,6 +20,7 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.content.res.Resources;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -30,6 +31,7 @@
import androidx.annotation.Nullable;
import com.android.internal.jank.Cuj;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.desktop.DesktopAppLaunchTransition;
@@ -67,6 +69,9 @@
private boolean mOnDesktop;
private boolean mWasDesktopTaskFilteredOut;
+ private boolean mWasOpenedFromTaskbar;
+
+ private boolean mDetachingFromWindow = false;
protected KeyboardQuickSwitchViewController(
@NonNull TaskbarControllers controllers,
@@ -83,6 +88,10 @@
return mCurrentFocusIndex;
}
+ protected boolean wasOpenedFromTaskbar() {
+ return mWasOpenedFromTaskbar;
+ }
+
protected void openQuickSwitchView(
@NonNull List<GroupTask> tasks,
int numHiddenTasks,
@@ -96,6 +105,7 @@
mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
mOnDesktop = onDesktop;
mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
+ mWasOpenedFromTaskbar = wasOpenedFromTaskbar;
mKeyboardQuickSwitchView.applyLoadPlan(
mOverlayContext,
@@ -115,6 +125,12 @@
BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(
mKeyboardQuickSwitchView.getLayoutParams());
+ final Resources resources = mKeyboardQuickSwitchView.getResources();
+ final int marginHorizontal = resources.getDimensionPixelSize(
+ R.dimen.keyboard_quick_switch_margin_ends);
+ final int marginBottom = resources.getDimensionPixelSize(
+ R.dimen.keyboard_quick_switch_margin_bottom);
+ lp.setMargins(marginHorizontal, 0, marginHorizontal, marginBottom);
lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
mKeyboardQuickSwitchView.setLayoutParams(lp);
@@ -229,7 +245,12 @@
private void onCloseComplete() {
mCloseAnimation = null;
- mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+ // Reset the view callbacks to prevent `onDetachedFromWindow` getting called in response to
+ // the `removeView(mKeyboardQuickSwitchView)` call.
+ mKeyboardQuickSwitchView.resetViewCallbacks();
+ if (!mDetachingFromWindow) {
+ mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+ }
mControllerCallbacks.onCloseComplete();
InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
}
@@ -246,6 +267,7 @@
pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex);
pw.println(prefix + "\tmOnDesktop=" + mOnDesktop);
pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
+ pw.println(prefix + "\tmWasOpenedFromTaskbar=" + mWasOpenedFromTaskbar);
}
/**
@@ -319,5 +341,11 @@
boolean isAspectRatioSquare() {
return mControllerCallbacks.isAspectRatioSquare();
}
+
+ void onViewDetchedFromWindow() {
+ mDetachingFromWindow = true;
+ closeQuickSwitchView(false);
+ mDetachingFromWindow = false;
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 042bc9a..09dbeb6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -36,7 +36,6 @@
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.model.data.ItemInfo;
@@ -69,17 +68,14 @@
public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1;
public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2;
public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3;
- public static final int LAUNCHER_PAUSE_PROGRESS_INDEX = 4;
- public static final int DISPLAY_PROGRESS_COUNT = 5;
+ public static final int DISPLAY_PROGRESS_COUNT = 4;
private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat(
this::onInAppDisplayProgressChanged);
private final MultiPropertyFactory<AnimatedFloat> mTaskbarInAppDisplayProgressMultiProp =
new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress,
AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max);
- private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat(
- this::launcherPauseProgressUpdate);
private final QuickstepLauncher mLauncher;
private final HomeVisibilityState mHomeState;
@@ -195,33 +191,6 @@
}
/**
- * Called when Launcher Activity is paused/resumed.
- * <p>
- * To avoid UI clash between taskbar & bottom sheet, shift nav buttons down on launcher
- * pause/resume at home.
- * @param paused if launcher is currently paused.
- */
- public void onLauncherPausedOrResumed(boolean paused) {
- if (!FeatureFlags.enableHomeTransitionListener()) {
- onLauncherVisibilityChanged(mLauncher.hasBeenResumed());
- return;
- }
-
- // Animate navbar iff pause/resume from home, NOT to/from app (avoid overriding existing
- // animations).
- boolean launcherPauseOrResumeFromHome = mHomeState.isHomeVisible() && mControllers
- .taskbarAutohideSuspendController.isSuspendedForTransientTaskbarInLauncher();
- if (launcherPauseOrResumeFromHome) {
- mLauncherPauseProgress.animateToValue(paused ? 1.0f : 0.0f).start();
- }
- }
-
- private void launcherPauseProgressUpdate() {
- onTaskbarInAppDisplayProgressUpdate(
- mLauncherPauseProgress.value, LAUNCHER_PAUSE_PROGRESS_INDEX);
- }
-
- /**
* Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
*/
@Override
@@ -395,20 +364,18 @@
}
if (mControllers.uiController.isIconAlignedWithHotseat()
&& !mTaskbarLauncherStateController.isAnimatingToLauncher()) {
- // Only animate nav button position while home and not animating home, otherwise let
+ // Only animate the nav buttons while home and not animating home, otherwise let
// the TaskbarViewController handle it.
mControllers.navbarButtonsViewController
- .getNavButtonTranslationYForInAppDisplay()
+ .getTaskbarNavButtonTranslationYForInAppDisplay()
.updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
* mTaskbarInAppDisplayProgress.value);
- if (!mLauncher.isPaused()) {
- mControllers.navbarButtonsViewController
- .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
- }
+ mControllers.navbarButtonsViewController
+ .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
}
}
- @Override
+ /** Returns true iff any in-app display progress > 0. */
public boolean shouldUseInAppLayout() {
return mTaskbarInAppDisplayProgress.value > 0;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index cfcbd2f..7d8e93c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -183,7 +183,7 @@
private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat(
this::updateNavButtonTranslationY);
- private final AnimatedFloat mNavButtonTranslationYForInAppDisplay = new AnimatedFloat(
+ private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat(
this::updateNavButtonTranslationY);
private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat(
this::updateNavButtonTranslationY);
@@ -704,8 +704,8 @@
}
/** Use to set the translationY for the all nav+contextual buttons when in Launcher */
- public AnimatedFloat getNavButtonTranslationYForInAppDisplay() {
- return mNavButtonTranslationYForInAppDisplay;
+ public AnimatedFloat getTaskbarNavButtonTranslationYForInAppDisplay() {
+ return mTaskbarNavButtonTranslationYForInAppDisplay;
}
/** Use to set the dark intensity for the all nav+contextual buttons */
@@ -751,20 +751,18 @@
if (mContext.isPhoneButtonNavMode()) {
return;
}
- mLastSetNavButtonTranslationY = calculateNavButtonTranslationY();
- mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY);
- }
+ final float normalTranslationY = mTaskbarNavButtonTranslationY.value;
+ final float imeAdjustmentTranslationY = mTaskbarNavButtonTranslationYForIme.value;
+ TaskbarUIController uiController = mControllers.uiController;
+ final float inAppDisplayAdjustmentTranslationY =
+ (uiController instanceof LauncherTaskbarUIController
+ && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout())
+ ? mTaskbarNavButtonTranslationYForInAppDisplay.value : 0;
- /**
- * Calculates the translationY of the nav buttons based on the current device state.
- */
- private float calculateNavButtonTranslationY() {
- float translationY =
- mTaskbarNavButtonTranslationY.value + mTaskbarNavButtonTranslationYForIme.value;
- if (mControllers.uiController.shouldUseInAppLayout()) {
- translationY += mNavButtonTranslationYForInAppDisplay.value;
- }
- return translationY;
+ mLastSetNavButtonTranslationY = normalTranslationY
+ + imeAdjustmentTranslationY
+ + inAppDisplayAdjustmentTranslationY;
+ mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY);
}
/**
@@ -1164,7 +1162,7 @@
pw.println(prefix + "\t\tmTaskbarNavButtonTranslationY="
+ mTaskbarNavButtonTranslationY.value);
pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForInAppDisplay="
- + mNavButtonTranslationYForInAppDisplay.value);
+ + mTaskbarNavButtonTranslationYForInAppDisplay.value);
pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForIme="
+ mTaskbarNavButtonTranslationYForIme.value);
pw.println(prefix + "\t\tmTaskbarNavButtonDarkIntensity="
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index f3741b2..e22de06 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -29,7 +29,6 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY;
import static com.android.launcher3.Flags.enableCursorHoverStates;
-import static com.android.launcher3.Flags.taskbarOverflow;
import static com.android.launcher3.Utilities.calculateTextHeight;
import static com.android.launcher3.Utilities.isRunningInTestHarness;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
@@ -297,6 +296,7 @@
BubbleStashController bubbleStashController = isTransientTaskbar
? new TransientBubbleStashController(dimensionsProvider, this)
: new PersistentBubbleStashController(dimensionsProvider);
+ bubbleStashController.setHotseatVerticalCenter(launcherDp.getHotseatVerticalCenter());
bubbleControllersOptional = Optional.of(new BubbleControllers(
new BubbleBarController(this, bubbleBarView),
new BubbleBarViewController(this, bubbleBarView, bubbleBarContainer),
@@ -362,8 +362,11 @@
/** Updates {@link DeviceProfile} instances for any Taskbar windows. */
public void updateDeviceProfile(DeviceProfile launcherDp) {
applyDeviceProfile(launcherDp);
-
mControllers.taskbarOverlayController.updateLauncherDeviceProfile(launcherDp);
+ mControllers.bubbleControllers.ifPresent(bubbleControllers -> {
+ int hotseatVertCenter = launcherDp.getHotseatVerticalCenter();
+ bubbleControllers.bubbleStashController.setHotseatVerticalCenter(hotseatVertCenter);
+ });
AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE);
// Reapply fullscreen to take potential new screen size into account.
setTaskbarWindowFullscreen(mIsFullscreen);
@@ -1213,9 +1216,7 @@
boolean shouldCloseAllOpenViews = true;
Object tag = view.getTag();
- if (taskbarOverflow()) {
- mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
- }
+ mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
if (tag instanceof GroupTask groupTask) {
handleGroupTaskLaunch(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index ade8f8c..a89bc3a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -265,7 +265,8 @@
!DisplayController.isPinnedTaskbar(activityContext) ||
!isTooltipEnabled ||
!shouldShowSearchEdu ||
- userHasSeenSearchEdu
+ userHasSeenSearchEdu ||
+ !controllers.taskbarStashController.isTaskbarVisibleAndNotStashing
) {
return
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 058dd07..a8ce10f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -21,7 +21,6 @@
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Region
-import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
import android.os.Binder
import android.os.IBinder
import android.view.DisplayInfo
@@ -145,11 +144,12 @@
// if bubble bar is visible or animating new bubble, add bar bounds to the touch region
if (isBubbleBarVisible || isAnimatingNewBubble) {
defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
+ defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.flyoutBounds)
}
}
if (
taskbarStashController.isInApp ||
- taskbarStashController.isInOverview ||
+ controllers.uiController.isInOverviewUi ||
DisplayController.showLockedTaskbarOnHome(context)
) {
// only add the taskbar touch region if not on home
@@ -258,7 +258,7 @@
// When in gesture nav, report the stashed height to the IME, to allow hiding the
// IME navigation bar.
val imeInsetsSize =
- if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
+ if (context.isGestureNav) {
getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity)
} else {
getInsetsForGravity(taskbarHeightForIme, gravity)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
new file mode 100644
index 0000000..fad5ca3
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Reorderable;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiTranslateDelegate;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * View used as overflow icon within task bar, when the list of recent/running apps overflows the
+ * available display bounds - if display is not wide enough to show all running apps in the taskbar,
+ * this icon is added to the taskbar as an entry point to open UI that surfaces all running apps.
+ * The icon contains icon representations of up to 4 more recent tasks in overflow, stacked on top
+ * each other in counter clockwise manner (icons of tasks partially overlapping with each other).
+ */
+public class TaskbarOverflowView extends FrameLayout implements Reorderable {
+ private final List<Task> mItems = new ArrayList<Task>();
+ private int mIconSize;
+ private int mPadding;
+ private Paint mItemBackgroundPaint;
+ private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
+ private float mScaleForReorderBounce = 1f;
+
+ public TaskbarOverflowView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public TaskbarOverflowView(Context context) {
+ super(context);
+ init();
+ }
+
+ /**
+ * Inflates the taskbar overflow button view.
+ * @param resId The resource to inflate the view from.
+ * @param group The parent view.
+ * @param iconSize The size of the overflow button icon.
+ * @param padding The internal padding of the overflow view.
+ * @return A taskbar overflow button.
+ */
+ public static TaskbarOverflowView inflateIcon(int resId, ViewGroup group, int iconSize,
+ int padding) {
+ LayoutInflater inflater = LayoutInflater.from(group.getContext());
+ TaskbarOverflowView icon = (TaskbarOverflowView) inflater.inflate(resId, group, false);
+
+ icon.mIconSize = iconSize;
+ icon.mPadding = padding;
+ return icon;
+ }
+
+ private void init() {
+ mItemBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mItemBackgroundPaint.setColor(getContext().getColor(R.color.taskbar_background));
+
+ setWillNotDraw(false);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ boolean isRtlLayout = Utilities.isRtl(getResources());
+ float radius = mIconSize / 2.0f - mPadding;
+ float itemPreviewStrokeWidth =
+ getResources().getDimension(R.dimen.taskbar_overflow_button_preview_stroke);
+
+ int itemsToShow = Math.min(mItems.size(), 4);
+ for (int i = itemsToShow - 1; i >= 0; --i) {
+ Drawable icon = mItems.get(mItems.size() - i - 1).icon;
+ if (icon == null) {
+ continue;
+ }
+
+ // Set the item icon size so two items fit within the overflow icon with stroke width
+ // included, and overlap of 4 stroke width sizes between base item preview items.
+ // 2 * strokeWidth + 2 * itemIconSize - 4 * strokeWidth = iconSize = 2 * radius.
+ float itemIconSize = radius + itemPreviewStrokeWidth;
+ // Offset item icon from center so item icon stroke edge mateches the parent icon edge.
+ float itemCenterOffset = radius - itemIconSize / 2 - itemPreviewStrokeWidth;
+
+ float itemCenterX = getItemXOffset(itemCenterOffset, isRtlLayout, i, itemsToShow);
+ float itemCenterY = getItemYOffset(itemCenterOffset, i, itemsToShow);
+
+ Drawable iconCopy = icon.getConstantState().newDrawable().mutate();
+ iconCopy.setBounds(0, 0, (int) itemIconSize, (int) itemIconSize);
+
+ canvas.save();
+ float itemIconRadius = itemIconSize / 2;
+ canvas.translate(
+ mPadding + itemCenterX + radius - itemIconRadius,
+ mPadding + itemCenterY + radius - itemIconRadius);
+ canvas.drawCircle(itemIconRadius, itemIconRadius,
+ itemIconRadius + itemPreviewStrokeWidth, mItemBackgroundPaint);
+ iconCopy.draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ /**
+ * Clears the list of tasks tracked by the view.
+ */
+ public void clearItems() {
+ mItems.clear();
+ invalidate();
+ }
+
+ /**
+ * Update the view to represent a new list of recent tasks.
+ * @param items Items to be shown in the view.
+ */
+ public void setItems(List<Task> items) {
+ mItems.clear();
+ mItems.addAll(items);
+ invalidate();
+ }
+
+ /**
+ * Called when a task is updated. If the task is contained within the view, it's cached value
+ * gets updated. If the task is shown within the icon, invalidates the view, so the task icon
+ * gets updated.
+ * @param task The updated task.
+ */
+ public void updateTaskIsShown(Task task) {
+ for (int i = 0; i < mItems.size(); ++i) {
+ if (mItems.get(i).key.id == task.key.id) {
+ mItems.set(i, task);
+ if (i >= mItems.size() - 4) {
+ invalidate();
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public MultiTranslateDelegate getTranslateDelegate() {
+ return mTranslateDelegate;
+ }
+
+ @Override
+ public float getReorderBounceScale() {
+ return mScaleForReorderBounce;
+ }
+
+ @Override
+ public void setReorderBounceScale(float scale) {
+ mScaleForReorderBounce = scale;
+ super.setScaleX(scale);
+ super.setScaleY(scale);
+ }
+
+ private float getItemXOffset(float baseOffset, boolean isRtl, int itemIndex, int itemCount) {
+ // Item with index 1 is on the left in all cases.
+ if (itemIndex == 1) {
+ return (isRtl ? 1 : -1) * baseOffset;
+ }
+
+ // First item is centered if total number of items shown is 3, on the right otherwise.
+ if (itemIndex == 0) {
+ if (itemCount == 3) {
+ return 0;
+ }
+ return (isRtl ? -1 : 1) * baseOffset;
+ }
+
+ // Last item is on the right when there are more than 2 items (case which is already handled
+ // as `itemIndex == 1`).
+ if (itemIndex == itemCount - 1) {
+ return (isRtl ? -1 : 1) * baseOffset;
+ }
+
+ return (isRtl ? 1 : -1) * baseOffset;
+ }
+
+ private float getItemYOffset(float baseOffset, int itemIndex, int itemCount) {
+ // If icon contains two items, they are both centered vertically.
+ if (itemCount == 2) {
+ return 0;
+ }
+ // First half of items is on top, later half is on bottom.
+ return (itemIndex + 1 <= itemCount / 2 ? -1 : 1) * baseOffset;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index 7848b7e..bcfc718 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -30,6 +30,7 @@
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_UNPINNED
import com.android.launcher3.taskbar.TaskbarDividerPopupView.Companion.createAndPopulate
import java.io.PrintWriter
+import kotlin.jvm.optionals.getOrNull
/** Controls taskbar pinning through a popup view. */
class TaskbarPinningController(private val context: TaskbarActivityContext) :
@@ -119,7 +120,11 @@
taskbarViewController.taskbarIconScaleForPinning.animateToValue(animateToValue),
taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue),
)
-
+ controllers.bubbleControllers.getOrNull()?.bubbleBarViewController?.let {
+ // if bubble bar is not visible no need to add it`s animations
+ if (!it.isBubbleBarVisible) return@let
+ animatorSet.playTogether(it.bubbleBarPinning.animateToValue(animateToValue))
+ }
animatorSet.interpolator = Interpolators.EMPHASIZED
return animatorSet
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index b19da6b..c1dd216 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -410,12 +410,20 @@
return mIsStashed;
}
- /** Sets the hotseat stashed. */
+ /**
+ * Sets the hotseat stashed.
+ * b/373429249 - we might change this behavior if we remove the scrim, that's why we're keeping
+ * this method
+ */
public void stashHotseat(boolean stash) {
mControllers.uiController.stashHotseat(stash);
}
- /** Instantly un-stashes the hotseat. */
+ /**
+ * Instantly un-stashes the hotseat.
+ * * b/373429249 - we might change this behavior if we remove the scrim, that's why we're
+ * keeping this method
+ */
public void unStashHotseatInstantly() {
mControllers.uiController.unStashHotseatInstantly();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 7030088..f7f5cf6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -91,14 +91,6 @@
protected void onStashedInAppChanged() { }
/**
- * Whether the Taskbar should use in-app layout.
- * @return {@code true} iff in-app display progress > 0 or Launcher Activity paused.
- */
- public boolean shouldUseInAppLayout() {
- return false;
- }
-
- /**
* Called when taskbar icon layout bounds change.
*/
protected void onIconLayoutBoundsChanged() { }
@@ -126,6 +118,8 @@
* Manually closes the overlay window.
*/
public void hideOverlayWindow() {
+ mControllers.keyboardQuickSwitchController.closeQuickSwitchView();
+
if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext)
|| mControllers.taskbarAllAppsController.isOpen()) {
mControllers.taskbarOverlayController.hideWindow();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index fcb583a..55bcb23 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -64,12 +64,12 @@
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.views.IconButtonView;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.TaskViewType;
import com.android.systemui.shared.recents.model.Task;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@@ -106,7 +106,7 @@
@Nullable private TaskbarDividerContainer mTaskbarDividerContainer;
// Only non-null when device supports having a Taskbar Overflow button.
- @Nullable private IconButtonView mTaskbarOverflowView;
+ @Nullable private TaskbarOverflowView mTaskbarOverflowView;
/**
* Whether the divider is between Hotseat icons and Recents,
@@ -122,6 +122,8 @@
private final int mMaxNumIcons;
+ private final int mAllAppsButtonTranslationOffset;
+
public TaskbarView(@NonNull Context context) {
this(context, null);
}
@@ -141,8 +143,6 @@
mActivityContext = ActivityContext.lookupContext(context);
mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds();
Resources resources = getResources();
- boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivityContext)
- && !mActivityContext.isPhoneMode();
mIsRtl = Utilities.isRtl(resources);
mTransientTaskbarMinWidth = resources.getDimension(R.dimen.transient_taskbar_min_width);
@@ -173,18 +173,19 @@
setWillNotDraw(false);
mAllAppsButtonContainer = new TaskbarAllAppsButtonContainer(context);
+ mAllAppsButtonTranslationOffset = (int) getResources().getDimension(
+ mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(isTransientTaskbar()));
if (enableTaskbarPinning() || enableRecentsInTaskbar()) {
mTaskbarDividerContainer = new TaskbarDividerContainer(context);
}
if (Flags.taskbarOverflow()) {
- mTaskbarOverflowView = (IconButtonView) LayoutInflater.from(context)
- .inflate(R.layout.taskbar_overflow_button, this, false);
- mTaskbarOverflowView.setIconDrawable(
- resources.getDrawable(R.drawable.taskbar_overflow_icon));
- mTaskbarOverflowView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+ mTaskbarOverflowView = TaskbarOverflowView.inflateIcon(
+ R.layout.taskbar_overflow_view, this,
+ mIconTouchSize, mItemPadding);
}
+
// TODO: Disable touch events on QSB otherwise it can crash.
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
@@ -197,11 +198,13 @@
private int calculateMaxNumIcons() {
DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
int availableWidth = deviceProfile.widthPx;
+ int defaultEdgeMargin =
+ (int) getResources().getDimension(deviceProfile.inv.inlineNavButtonsEndSpacing);
// Reserve space required for edge margins, or for navbar if shown. If task bar needs to be
// center aligned with nav bar shown, reserve space on both sides.
- availableWidth -= Math.max(deviceProfile.edgeMarginPx, deviceProfile.hotseatBarEndOffset);
- availableWidth -= Math.max(deviceProfile.edgeMarginPx,
+ availableWidth -= Math.max(defaultEdgeMargin, deviceProfile.hotseatBarEndOffset);
+ availableWidth -= Math.max(defaultEdgeMargin,
mShouldTryStartAlign ? 0 : deviceProfile.hotseatBarEndOffset);
// The space taken by an item icon used during layout.
@@ -222,9 +225,7 @@
enableTaskbarPinning() && !mActivityContext.isThreeButtonNav();
availableWidth -= iconSize - (int) getResources().getDimension(
mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(
- forceTransientTaskbarSize || (
- DisplayController.isTransientTaskbar(mActivityContext)
- && !mActivityContext.isPhoneMode())));
+ forceTransientTaskbarSize || isTransientTaskbar()));
++additionalIcons;
return Math.floorDiv(availableWidth, iconSize) + additionalIcons;
@@ -449,24 +450,47 @@
int nonTaskIconsToBeAdded = 1;
boolean supportsOverflow = Flags.taskbarOverflow();
+ int overflowSize = 0;
if (supportsOverflow) {
int numberOfSupportedRecents = 0;
for (GroupTask task : recentTasks) {
// TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
- if (!task.hasMultipleTasks()) {
+ if (!task.supportsMultipleTasks()) {
++numberOfSupportedRecents;
}
}
- if (nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded > mMaxNumIcons
- && mTaskbarOverflowView != null) {
+
+ overflowSize =
+ nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded - mMaxNumIcons;
+ if (overflowSize > 0 && mTaskbarOverflowView != null) {
addView(mTaskbarOverflowView, nextViewIndex++);
+ } else if (mTaskbarOverflowView != null) {
+ mTaskbarOverflowView.clearItems();
}
}
+ List<Task> overflownTasks = null;
+ // An extra item needs to be added to overflow button to account for the space taken up by
+ // the overflow button.
+ final int itemsToAddToOverflow = overflowSize > 0 ? overflowSize + 1 : 0;
+ if (overflowSize > 0) {
+ overflownTasks = new ArrayList<Task>(itemsToAddToOverflow);
+ }
+
// Add Recent/Running icons.
for (GroupTask task : recentTasks) {
- if (supportsOverflow && nextViewIndex + nonTaskIconsToBeAdded >= mMaxNumIcons) {
- break;
+ if (mTaskbarOverflowView != null && overflownTasks != null
+ && overflownTasks.size() < itemsToAddToOverflow) {
+ // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+ if (task.supportsMultipleTasks()) {
+ continue;
+ }
+
+ overflownTasks.add(task.task1);
+ if (overflownTasks.size() == itemsToAddToOverflow) {
+ mTaskbarOverflowView.setItems(overflownTasks);
+ }
+ continue;
}
// Replace any Recent views with the appropriate type if it's not already that type.
@@ -668,6 +692,15 @@
mIconLayoutBounds.right = iconEnd;
mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
+
+ // With rtl layout, the all apps button will be translated by `allAppsButtonOffset` after
+ // layout completion (by `TaskbarViewController`). Offset the icon end by the same amount
+ // when laying out icons, so the taskbar content remains centered after all apps button
+ // translation.
+ if (layoutRtl) {
+ iconEnd += mAllAppsButtonTranslationOffset;
+ }
+
int count = getChildCount();
for (int i = count; i > 0; i--) {
View child = getChildAt(i - 1);
@@ -699,6 +732,15 @@
mIconLayoutBounds.left = iconEnd;
+ // Adjust the icon layout bounds by the amount by which all apps button will be translated
+ // post layout to maintain margin between all apps button and the edge of the transient
+ // taskbar background. Done for ltr layout only - for rtl layout, the offset needs to be
+ // adjusted on the right, which is done by offsetting `iconEnd` after setting
+ // `mIconLayoutBounds.right`.
+ if (!layoutRtl) {
+ mIconLayoutBounds.left += mAllAppsButtonTranslationOffset;
+ }
+
if (mIconLayoutBounds.right - mIconLayoutBounds.left < mTransientTaskbarMinWidth) {
int center = mIconLayoutBounds.centerX();
int distanceFromCenter = (int) mTransientTaskbarMinWidth / 2;
@@ -741,12 +783,13 @@
/**
* Returns the space used by the icons
*/
- public int getIconLayoutWidth() {
+ private int getIconLayoutWidth() {
int countExcludingQsb = getChildCount();
DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
if (deviceProfile.isQsbInline) {
countExcludingQsb--;
}
+
int iconLayoutBoundsWidth =
countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
@@ -755,17 +798,28 @@
// All Apps icon, divider icon, and first app icon in taskbar
iconLayoutBoundsWidth -= mItemMarginLeftRight * 4;
}
+
+ // The all apps button container gets offset horizontally, reducing the overall taskbar
+ // view size.
+ iconLayoutBoundsWidth -= mAllAppsButtonTranslationOffset;
+
return iconLayoutBoundsWidth;
}
/**
- * Returns the app icons currently shown in the taskbar.
+ * Returns the app icons currently shown in the taskbar. The returned list does not include qsb,
+ * but it includes all apps button and icon divider views.
*/
public View[] getIconViews() {
final int count = getChildCount();
- View[] icons = new View[count];
+ if (count == 0) {
+ return new View[0];
+ }
+ View[] icons = new View[count - (mActivityContext.getDeviceProfile().isQsbInline ? 1 : 0)];
+ int insertionPoint = 0;
for (int i = 0; i < count; i++) {
- icons[i] = getChildAt(i);
+ if (getChildAt(i) == mQsb) continue;
+ icons[insertionPoint++] = getChildAt(i);
}
return icons;
}
@@ -789,7 +843,7 @@
* Returns the taskbar overflow view in the taskbar.
*/
@Nullable
- public IconButtonView getTaskbarOverflowView() {
+ public TaskbarOverflowView getTaskbarOverflowView() {
return mTaskbarOverflowView;
}
@@ -847,12 +901,25 @@
// Ignore, we just implement Insettable to draw behind system insets.
}
+ private boolean isTransientTaskbar() {
+ return DisplayController.isTransientTaskbar(mActivityContext)
+ && !mActivityContext.isPhoneMode();
+ }
+
public boolean areIconsVisible() {
// Consider the overall visibility
return getVisibility() == VISIBLE;
}
/**
+ * @return The all apps button horizontal offset used to calculate the taskbar contents width
+ * during layout.
+ */
+ public int getAllAppsButtonTranslationXOffsetUsedForLayout() {
+ return mAllAppsButtonTranslationOffset;
+ }
+
+ /**
* Maps {@code op} over all the child views.
*/
public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 4591f9b..5d769d2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -148,7 +148,7 @@
return new View.OnClickListener() {
@Override
public void onClick(View v) {
- mControllers.keyboardQuickSwitchController.openQuickSwitchView(
+ mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar(
mControllers.taskbarViewController.getTaskIdsForPinnedApps());
}
};
@@ -159,7 +159,7 @@
return new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
- mControllers.keyboardQuickSwitchController.openQuickSwitchView(
+ mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar(
mControllers.taskbarViewController.getTaskIdsForPinnedApps());
return true;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 253d025..87e19be 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -233,7 +233,7 @@
mTaskbarNavButtonTranslationY =
controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY();
mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController
- .getNavButtonTranslationYForInAppDisplay();
+ .getTaskbarNavButtonTranslationYForInAppDisplay();
mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
@@ -359,10 +359,6 @@
return mTaskbarView.getIconLayoutBounds();
}
- public int getIconLayoutWidth() {
- return mTaskbarView.getIconLayoutWidth();
- }
-
public View[] getIconViews() {
return mTaskbarView.getIconViews();
}
@@ -442,6 +438,19 @@
float allAppIconTranslateRange = mapRange(scale, transientTaskbarAllAppsOffset,
persistentTaskbarAllAppsOffset);
+ // Task icons are laid out so the taskbar content is centered. The taskbar width (used for
+ // centering taskbar icons) depends on the all apps button X translation, and is different
+ // for persistent and transient taskbar. If the offset used for current taskbar layout is
+ // different than the offset used in final taskbar state, the icons may jump when the
+ // animation completes, and the taskbar is replaced. Adjust item transform to account for
+ // this mismatch.
+ float sizeDiffTranslationRange =
+ mapRange(scale,
+ (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout()
+ - transientTaskbarAllAppsOffset) / 2,
+ (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout()
+ - persistentTaskbarAllAppsOffset) / 2);
+
// no x translation required when all apps button is the only icon in taskbar.
if (iconViews.length <= 1) {
allAppIconTranslateRange = 0f;
@@ -449,6 +458,7 @@
if (mIsRtl) {
allAppIconTranslateRange *= -1;
+ sizeDiffTranslationRange *= -1;
}
if (mActivity.isThreeButtonNav()) {
@@ -457,25 +467,18 @@
return;
}
- float taskbarCenterX =
- mTaskbarView.getLeft() + (mTaskbarView.getRight() - mTaskbarView.getLeft()) / 2.0f;
-
float finalMarginScale = mapRange(scale, 0f, mTransientIconSize - mPersistentIconSize);
- float halfIconCount = iconViews.length / 2.0f;
+ // The index of the "middle" icon which will be used as a index from which the icon margins
+ // will be scaled. If number of icons is even, using the middle point between indices of two
+ // central icons.
+ float middleIndex = (iconViews.length - 1) / 2.0f;
for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) {
View iconView = iconViews[iconIndex];
MultiTranslateDelegate translateDelegate =
((Reorderable) iconView).getTranslateDelegate();
- float iconCenterX =
- iconView.getLeft() + (iconView.getRight() - iconView.getLeft()) / 2.0f;
- if (iconCenterX <= taskbarCenterX) {
- translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue(
- finalMarginScale * (halfIconCount - iconIndex));
- } else {
- translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue(
- -finalMarginScale * (iconIndex - halfIconCount));
- }
+ translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue(
+ finalMarginScale * (middleIndex - iconIndex) + sizeDiffTranslationRange);
if (iconView.equals(mTaskbarView.getAllAppsButtonContainer())) {
mTaskbarView.getAllAppsButtonContainer().setTranslationXForTaskbarAllAppsIcon(
@@ -488,18 +491,14 @@
* Calculates visual taskbar view width.
*/
public float getCurrentVisualTaskbarWidth() {
- if (mTaskbarView.getIconViews().length == 0) {
+ View[] iconViews = mTaskbarView.getIconViews();
+ if (iconViews.length == 0) {
return 0;
}
- View[] iconViews = mTaskbarView.getIconViews();
+ float left = iconViews[0].getX();
- int leftIndex = mActivity.getDeviceProfile().isQsbInline && !mIsRtl ? 1 : 0;
- int rightIndex = mActivity.getDeviceProfile().isQsbInline && mIsRtl
- ? iconViews.length - 2
- : iconViews.length - 1;
-
- float left = iconViews[leftIndex].getX();
+ int rightIndex = iconViews.length - 1;
float right = iconViews[rightIndex].getRight() + iconViews[rightIndex].getTranslationX();
return right - left + (2 * mTaskbarLeftRightMargin);
@@ -1090,6 +1089,8 @@
if (groupTask.containsTask(task.key.id)) {
mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask);
}
+ } else if (view instanceof TaskbarOverflowView overflowButton) {
+ overflowButton.updateTaskIsShown(task);
}
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c5d649e..d91d10a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -38,6 +38,7 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
+import com.android.app.animation.Interpolators;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.taskbar.BarsLocationAnimatorHelper;
@@ -86,7 +87,7 @@
private static final int MAX_BUBBLES = 5;
private static final int MAX_VISIBLE_BUBBLES_COLLAPSED = 2;
private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200;
- private static final int WIDTH_ANIMATION_DURATION_MS = 200;
+ private static final int WIDTH_ANIMATION_DURATION_MS = 400;
private static final int SCALE_ANIMATION_DURATION_MS = 200;
/**
@@ -143,7 +144,7 @@
// An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
// collapsed state and 1 to the fully expanded state.
- private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
+ private ValueAnimator mWidthAnimator = createExpansionAnimator(/* expanding = */ false);
/** An animator used for animating individual bubbles in the bubble bar while expanded. */
@Nullable
@@ -207,35 +208,6 @@
mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarExpandedHeight());
setBackgroundDrawable(mBubbleBarBackground);
-
- mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS);
-
- addAnimationCallBacks(mWidthAnimator,
- /* onStart= */ () -> mBubbleBarBackground.showArrow(true),
- /* onEnd= */ () -> {
- mBubbleBarBackground.showArrow(mIsBarExpanded);
- if (!mIsBarExpanded && mReorderRunnable != null) {
- mReorderRunnable.run();
- mReorderRunnable = null;
- }
- // If the bar was just collapsed and the overflow was the last bubble that was
- // selected, set the first bubble as selected.
- if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null
- && mSelectedBubbleView != null
- && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) {
- BubbleView firstBubble = (BubbleView) getChildAt(0);
- mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
- }
- // If the bar was just expanded, remove the dot from the selected bubble.
- if (mIsBarExpanded && mSelectedBubbleView != null) {
- mSelectedBubbleView.markSeen();
- }
- updateLayoutParams();
- },
- /* onUpdate= */ animator -> {
- updateBubblesLayoutProperties(mBubbleBarLocation);
- invalidate();
- });
}
@@ -332,6 +304,17 @@
}
/**
+ * Set the bubble icons size and spacing between the bubbles and the borders of the bubble
+ * bar.
+ */
+ public void setIconSizeAndPaddingForPinning(float newIconSize, float newBubbleBarPadding) {
+ mBubbleBarPadding = newBubbleBarPadding;
+ mIconScale = newIconSize / mIconSize;
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+
+ /**
* Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders.
*
* @param newIconSize new icon size
@@ -1251,11 +1234,8 @@
mIsBarExpanded = isBarExpanded;
updateArrowForSelected(/* shouldAnimate= */ false);
setOrUnsetClickListener();
- if (isBarExpanded) {
- mWidthAnimator.start();
- } else {
- mWidthAnimator.reverse();
- }
+ mWidthAnimator = createExpansionAnimator(isBarExpanded);
+ mWidthAnimator.start();
updateBubbleAccessibilityStates();
announceExpandedStateChange();
}
@@ -1492,6 +1472,46 @@
return bubbles;
}
+ /** Creates an animator based on the expanding or collapsing action. */
+ private ValueAnimator createExpansionAnimator(boolean expanding) {
+ float startValue = expanding ? 0 : 1;
+ if ((mWidthAnimator != null && mWidthAnimator.isRunning())) {
+ startValue = (float) mWidthAnimator.getAnimatedValue();
+ mWidthAnimator.cancel();
+ }
+ float endValue = expanding ? 1 : 0;
+ ValueAnimator animator = ValueAnimator.ofFloat(startValue, endValue);
+ animator.setDuration(WIDTH_ANIMATION_DURATION_MS);
+ animator.setInterpolator(Interpolators.EMPHASIZED);
+ addAnimationCallBacks(animator,
+ /* onStart= */ () -> mBubbleBarBackground.showArrow(true),
+ /* onEnd= */ () -> {
+ mBubbleBarBackground.showArrow(mIsBarExpanded);
+ if (!mIsBarExpanded && mReorderRunnable != null) {
+ mReorderRunnable.run();
+ mReorderRunnable = null;
+ }
+ // If the bar was just collapsed and the overflow was the last bubble that was
+ // selected, set the first bubble as selected.
+ if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null
+ && mSelectedBubbleView != null
+ && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) {
+ BubbleView firstBubble = (BubbleView) getChildAt(0);
+ mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
+ }
+ // If the bar was just expanded, remove the dot from the selected bubble.
+ if (mIsBarExpanded && mSelectedBubbleView != null) {
+ mSelectedBubbleView.markSeen();
+ }
+ updateLayoutParams();
+ },
+ /* onUpdate= */ anim -> {
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ });
+ return animator;
+ }
+
/**
* Returns the distance between the top left corner of the bubble bar to the center of the dot
* of the selected bubble.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 76d3606..31b1ea0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -18,6 +18,10 @@
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
+import static com.android.launcher3.Utilities.mapRange;
+import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT;
+import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
+
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.content.res.Resources;
@@ -34,6 +38,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.app.animation.Interpolators;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
@@ -41,11 +46,14 @@
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.taskbar.TaskbarInsetsController;
+import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.TaskbarStashController;
import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController;
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner;
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.SystemUiProxy;
@@ -68,6 +76,9 @@
private static final float APP_ICON_LARGE_DP = 52f;
/** The dot size is defined as a percentage of the icon size. */
private static final float DOT_TO_BUBBLE_SIZE_RATIO = 0.228f;
+ public static final int TASKBAR_FADE_IN_DURATION_MS = 150;
+ public static final int TASKBAR_FADE_IN_DELAY_MS = 50;
+ public static final int TASKBAR_FADE_OUT_DURATION_MS = 100;
private final SystemUiProxy mSystemUiProxy;
private final TaskbarActivityContext mActivity;
private final BubbleBarView mBarView;
@@ -101,6 +112,10 @@
this::updateTranslationY);
private final AnimatedFloat mBubbleOffsetY = new AnimatedFloat(
this::updateBubbleOffsetY);
+ private final AnimatedFloat mBubbleBarPinning = new AnimatedFloat(pinningProgress -> {
+ updateTranslationY();
+ setBubbleBarScaleAndPadding(pinningProgress);
+ });
// Modified when swipe up is happening on the bubble bar or task bar.
private float mBubbleBarSwipeUpTranslationY;
@@ -120,8 +135,9 @@
private BubbleBarViewAnimator mBubbleBarViewAnimator;
private final FrameLayout mBubbleBarContainer;
private BubbleBarFlyoutController mBubbleBarFlyoutController;
-
+ private TaskbarSharedState mTaskbarSharedState;
private final TimeSource mTimeSource = System::currentTimeMillis;
+ private final int mTaskbarTranslationDelta;
@Nullable
private BubbleBarBoundsChangeListener mBoundsChangeListener;
@@ -135,18 +151,20 @@
mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
mIconSize = activity.getResources().getDimensionPixelSize(
R.dimen.bubblebar_icon_size);
+ mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity);
}
/** Initializes controller. */
public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers,
TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) {
+ mTaskbarSharedState = controllers.getSharedState();
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleBarController = bubbleControllers.bubbleBarController;
mBubbleDragController = bubbleControllers.bubbleDragController;
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
mBubbleBarFlyoutController = new BubbleBarFlyoutController(
- mBubbleBarContainer, createFlyoutPositioner(), createFlyoutTopBoundaryListener());
+ mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks());
mBubbleBarViewAnimator = new BubbleBarViewAnimator(
mBarView, mBubbleStashController, mBubbleBarFlyoutController,
mBubbleBarController::showExpandedView);
@@ -167,6 +185,10 @@
mBoundsChangeListener.onBoundsChanged();
}
});
+ float pinningValue = DisplayController.isTransientTaskbar(mActivity)
+ ? PINNING_TRANSIENT
+ : PINNING_PERSISTENT;
+ mBubbleBarPinning.updateValue(pinningValue);
mBarView.setController(new BubbleBarView.Controller() {
@Override
public float getBubbleBarTranslationY() {
@@ -175,7 +197,9 @@
@Override
public void onBubbleBarTouched() {
- BubbleBarViewController.this.onBubbleBarTouched();
+ if (isAnimatingNewBubble()) {
+ interruptAnimationForTouch();
+ }
}
@Override
@@ -220,6 +244,11 @@
};
}
+ /** Returns animated float property responsible for pinning transition animation. */
+ public AnimatedFloat getBubbleBarPinning() {
+ return mBubbleBarPinning;
+ }
+
private BubbleBarFlyoutPositioner createFlyoutPositioner() {
return new BubbleBarFlyoutPositioner() {
@@ -273,8 +302,8 @@
};
}
- private BubbleBarFlyoutController.TopBoundaryListener createFlyoutTopBoundaryListener() {
- return new BubbleBarFlyoutController.TopBoundaryListener() {
+ private FlyoutCallbacks createFlyoutCallbacks() {
+ return new FlyoutCallbacks() {
@Override
public void extendTopBoundary(int space) {
int defaultSize = mActivity.getDefaultTaskbarWindowSize();
@@ -285,6 +314,12 @@
public void resetTopBoundary() {
mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize());
}
+
+ @Override
+ public void flyoutClicked() {
+ interruptAnimationForTouch();
+ expandBubbleBar();
+ }
};
}
@@ -304,12 +339,10 @@
}
}
- private void onBubbleBarTouched() {
- if (isAnimatingNewBubble()) {
- mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating();
- mBubbleStashController.onNewBubbleAnimationInterrupted(false,
- mBarView.getTranslationY());
- }
+ /** Interrupts the running animation for a touch event on the bubble bar or flyout. */
+ private void interruptAnimationForTouch() {
+ mBubbleBarViewAnimator.interruptForTouch();
+ mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
}
private void expandBubbleBar() {
@@ -463,6 +496,12 @@
return mBarView.getBubbleBarBounds();
}
+ /** Returns the bounds of the flyout view if it exists, or {@code null} otherwise. */
+ @Nullable
+ public Rect getFlyoutBounds() {
+ return mBubbleBarFlyoutController.getFlyoutBounds();
+ }
+
/** Checks that bubble bar is visible and that the motion event is within bounds. */
public boolean isEventOverBubbleBar(MotionEvent event) {
if (!isBubbleBarVisible()) return false;
@@ -588,9 +627,11 @@
updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate);
}
-
private int getBubbleBarIconSizeFromDeviceProfile(Resources res) {
- DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+ return getBubbleBarIconSizeFromDeviceProfile(res, mActivity.getDeviceProfile());
+ }
+
+ private int getBubbleBarIconSizeFromDeviceProfile(Resources res, DeviceProfile deviceProfile) {
DisplayMetrics dm = res.getDisplayMetrics();
float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
APP_ICON_SMALL_DP, dm);
@@ -605,7 +646,10 @@
}
private int getBubbleBarPaddingFromDeviceProfile(Resources res) {
- DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+ return getBubbleBarPaddingFromDeviceProfile(res, mActivity.getDeviceProfile());
+ }
+
+ private int getBubbleBarPaddingFromDeviceProfile(Resources res, DeviceProfile deviceProfile) {
DisplayMetrics dm = res.getDisplayMetrics();
float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
APP_ICON_MEDIUM_DP, dm);
@@ -646,7 +690,53 @@
private void updateTranslationY() {
mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY
- + mBubbleBarStashTranslationY);
+ + mBubbleBarStashTranslationY + getBubbleBarTranslationYForTaskbarPinning());
+ }
+
+ /** Computes translation y for taskbar pinning. */
+ private float getBubbleBarTranslationYForTaskbarPinning() {
+ if (mTaskbarSharedState == null) return 0f;
+ float pinningProgress = mBubbleBarPinning.value;
+ if (mTaskbarSharedState.startTaskbarVariantIsTransient) {
+ return mapRange(pinningProgress, /* min = */ 0f, mTaskbarTranslationDelta);
+ } else {
+ return mapRange(pinningProgress, -mTaskbarTranslationDelta, /* max = */ 0f);
+ }
+ }
+
+ private void setBubbleBarScaleAndPadding(float pinningProgress) {
+ Resources res = mActivity.getResources();
+ // determine icon scale for pinning
+ int persistentIconSize = res.getDimensionPixelSize(
+ R.dimen.bubblebar_icon_size_persistent_taskbar);
+ int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res,
+ mActivity.getTransientTaskbarDeviceProfile());
+ float pinningIconSize = mapRange(pinningProgress, transientIconSize, persistentIconSize);
+
+ // determine bubble bar padding for pinning
+ int persistentPadding = res.getDimensionPixelSize(
+ R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+ int transientPadding = getBubbleBarPaddingFromDeviceProfile(res,
+ mActivity.getTransientTaskbarDeviceProfile());
+ float pinningPadding = mapRange(pinningProgress, transientPadding, persistentPadding);
+ mBarView.setIconSizeAndPaddingForPinning(pinningIconSize, pinningPadding);
+ }
+
+ /**
+ * Calculates the vertical difference in the bubble bar positions for pinned and transient
+ * taskbar modes.
+ */
+ private int getBubbleBarTranslationDeltaForTaskbar(TaskbarActivityContext activity) {
+ Resources res = activity.getResources();
+ int persistentBubbleSize = res
+ .getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
+ int persistentSpacingSize = res
+ .getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+ int persistentBubbleBarSize = persistentBubbleSize + persistentSpacingSize * 2;
+ int persistentTaskbarHeight = activity.getPersistentTaskbarDeviceProfile().taskbarHeight;
+ int persistentBubbleBarY = (persistentTaskbarHeight - persistentBubbleBarSize) / 2;
+ int transientBubbleBarY = activity.getTransientTaskbarDeviceProfile().taskbarBottomMargin;
+ return transientBubbleBarY - persistentBubbleBarY;
}
private void updateScaleX(float scale) {
@@ -843,22 +933,21 @@
/**
* Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in
- * app or overview. Set the hotseat stashed state if on launcher home screen. If not on launcher
- * home screen and hotseat is stashed immediately un-stashes the hotseat.
+ * app or overview.
*/
private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) {
- if (mBubbleStashController.isBubblesShowingOnHome()) {
- mTaskbarStashController.stashHotseat(isBubbleBarExpanded);
- } else if (!mBubbleStashController.isTransientTaskBar()) {
- boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
- mTaskbarViewPropertiesProvider
- .getIconsAlpha()
- .animateToValue(hideTaskbar ? 0 : 1)
- .start();
- }
if (!mBubbleStashController.isBubblesShowingOnHome()
- && mTaskbarStashController.isHiddenForBubbles()) {
- mTaskbarStashController.unStashHotseatInstantly();
+ && !mBubbleStashController.isTransientTaskBar()) {
+ boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
+ Animator taskbarAlphaAnimator = mTaskbarViewPropertiesProvider.getIconsAlpha()
+ .animateToValue(hideTaskbar ? 0 : 1);
+ taskbarAlphaAnimator.setDuration(hideTaskbar
+ ? TASKBAR_FADE_OUT_DURATION_MS : TASKBAR_FADE_IN_DURATION_MS);
+ if (!hideTaskbar) {
+ taskbarAlphaAnimator.setStartDelay(TASKBAR_FADE_IN_DELAY_MS);
+ }
+ taskbarAlphaAnimator.setInterpolator(Interpolators.LINEAR);
+ taskbarAlphaAnimator.start();
}
}
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 8a52ca9..78e5dbd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -471,8 +471,8 @@
bubbleStashController.updateTaskbarTouchRegion()
}
- /** Handles touching the animating bubble bar. */
- fun onBubbleBarTouchedWhileAnimating() {
+ /** Interrupts the animation due to touching the bubble bar or flyout. */
+ fun interruptForTouch() {
PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
cancelFlyout()
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
index d6400bb..f389d7e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -16,13 +16,13 @@
package com.android.launcher3.taskbar.bubbles.flyout
+import android.graphics.Rect
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.animation.ValueAnimator
import com.android.launcher3.R
import com.android.systemui.util.addListener
-import com.android.systemui.util.doOnEnd
/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
class BubbleBarFlyoutController
@@ -30,7 +30,7 @@
constructor(
private val container: FrameLayout,
private val positioner: BubbleBarFlyoutPositioner,
- private val topBoundaryListener: TopBoundaryListener,
+ private val callbacks: FlyoutCallbacks,
private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container),
) {
@@ -47,6 +47,15 @@
FADE,
}
+ /** The bounds of the flyout. */
+ val flyoutBounds: Rect?
+ get() {
+ val flyout = this.flyout ?: return null
+ val rect = Rect(flyout.bounds)
+ rect.offset(0, flyout.translationY.toInt())
+ return rect
+ }
+
fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
flyout?.let(container::removeView)
val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)
@@ -63,29 +72,63 @@
lp.marginEnd = horizontalMargin
container.addView(flyout, lp)
+ this.flyout = flyout
+ flyout.showFromCollapsed(message) { showFlyout(AnimationType.COLLAPSE, onEnd) }
+ }
+
+ private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
+ val flyout = this.flyout ?: return
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
- animator.addUpdateListener { _ ->
- flyout.updateExpansionProgress(animator.animatedValue as Float)
+ when (animationType) {
+ AnimationType.FADE ->
+ animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
+ AnimationType.COLLAPSE ->
+ animator.addUpdateListener { _ ->
+ flyout.updateExpansionProgress(animator.animatedValue as Float)
+ }
}
animator.addListener(
- onStart = {
- val flyoutTop = flyout.top + flyout.translationY
- // If the top position of the flyout is negative, then it's bleeding over the
- // top boundary of its parent view
- if (flyoutTop < 0) topBoundaryListener.extendTopBoundary(space = -flyoutTop.toInt())
+ onStart = { extendTopBoundary() },
+ onEnd = {
+ endAction()
+ flyout.setOnClickListener { callbacks.flyoutClicked() }
},
- onEnd = { onEnd() },
)
- flyout.showFromCollapsed(message) { animator.start() }
- this.flyout = flyout
+ animator.start()
+ }
+
+ fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+ val flyout = flyout ?: return
+ hideFlyout(AnimationType.FADE) {
+ flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
+ }
+ }
+
+ fun updateFlyoutWhileExpanding(message: BubbleBarFlyoutMessage) {
+ val flyout = flyout ?: return
+ flyout.updateData(message) { extendTopBoundary() }
+ }
+
+ private fun extendTopBoundary() {
+ val flyout = flyout ?: return
+ val flyoutTop = flyout.top + flyout.translationY
+ // If the top position of the flyout is negative, then it's bleeding over the
+ // top boundary of its parent view
+ if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
}
fun cancelFlyout(endAction: () -> Unit) {
- hideFlyout(AnimationType.FADE, endAction)
+ hideFlyout(AnimationType.FADE) {
+ cleanupFlyoutView()
+ endAction()
+ }
}
fun collapseFlyout(endAction: () -> Unit) {
- hideFlyout(AnimationType.COLLAPSE, endAction)
+ hideFlyout(AnimationType.COLLAPSE) {
+ cleanupFlyoutView()
+ endAction()
+ }
}
private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
@@ -100,21 +143,15 @@
flyout.updateExpansionProgress(animator.animatedValue as Float)
}
}
- animator.doOnEnd {
- container.removeView(flyout)
- this@BubbleBarFlyoutController.flyout = null
- topBoundaryListener.resetTopBoundary()
- endAction()
- }
+ animator.addListener(onStart = { flyout.setOnClickListener(null) }, onEnd = { endAction() })
animator.start()
}
- /** Notifies when the top boundary of the flyout view changes. */
- interface TopBoundaryListener {
- /** Requests to extend the top boundary of the parent to fully include the flyout. */
- fun extendTopBoundary(space: Int)
-
- /** Resets the top boundary of the parent. */
- fun resetTopBoundary()
+ private fun cleanupFlyoutView() {
+ container.removeView(flyout)
+ this@BubbleBarFlyoutController.flyout = null
+ callbacks.resetTopBoundary()
}
+
+ fun hasFlyout() = flyout != null
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
index 6903c87..af8aaf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -24,6 +24,7 @@
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF
+import android.graphics.Rect
import android.graphics.RectF
import android.view.LayoutInflater
import android.view.View
@@ -138,6 +139,9 @@
*/
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
+ /** The bounds of the flyout relative to the parent view. */
+ val bounds = Rect()
+
init {
LayoutInflater.from(context).inflate(R.layout.bubblebar_flyout, this, true)
id = R.id.bubble_bar_flyout_view
@@ -174,6 +178,14 @@
applyConfigurationColors(resources.configuration)
}
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ bounds.left = left
+ bounds.top = top
+ bounds.right = right
+ bounds.bottom = bottom
+ }
+
/** Sets the data for the flyout and starts playing the expand animation. */
fun showFromCollapsed(flyoutMessage: BubbleBarFlyoutMessage, expandAnimation: () -> Unit) {
icon.alpha = 0f
@@ -186,6 +198,8 @@
} else {
-positioner.distanceToCollapsedPosition.x
}
+ // TODO: b/277815200 - before collapsing, recalculate translationToCollapsedPosition because
+ // the collapsed position may have changed
val tyToCollapsedPosition =
positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
@@ -205,6 +219,12 @@
scheduler.runAfterLayout(expandAnimation)
}
+ /** Updates the content of the flyout and schedules [afterLayout] to run after a layout pass. */
+ fun updateData(flyoutMessage: BubbleBarFlyoutMessage, afterLayout: () -> Unit) {
+ setData(flyoutMessage)
+ scheduler.runAfterLayout(afterLayout)
+ }
+
private fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
if (flyoutMessage.icon != null) {
icon.visibility = VISIBLE
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
new file mode 100644
index 0000000..e2f010a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.flyout
+
+/** Callbacks that the flyout uses to notify of events. */
+interface FlyoutCallbacks {
+ /** Requests to extend the top boundary of the parent to fully include the flyout. */
+ fun extendTopBoundary(space: Int)
+
+ /** Resets the top boundary of the parent. */
+ fun resetTopBoundary()
+
+ /** The flyout was clicked. */
+ fun flyoutClicked()
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
index a78890b..831faa1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -42,12 +42,6 @@
/** Provides taskbar height in pixels. */
fun getTaskbarHeight(): Int
-
- /** Provides hotseat bottom space in pixels. */
- fun getHotseatBottomSpace(): Int
-
- /** Provides hotseat height in pixels. */
- fun getHotseatHeight(): Int
}
/** Execute passed action only after controllers are initiated. */
@@ -94,7 +88,7 @@
taskbarInsetsController: TaskbarInsetsController,
bubbleBarViewController: BubbleBarViewController,
bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
- controllersAfterInitAction: ControllersAfterInitAction
+ controllersAfterInitAction: ControllersAfterInitAction,
)
/** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */
@@ -127,6 +121,9 @@
/** Set a bubble bar location */
fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation)
+ /** Set the hotseat vertical center that bubble bar will align with. */
+ fun setHotseatVerticalCenter(hotseatVerticalCenter: Int)
+
/**
* Stashes the bubble bar (transform to the handle view), or just shrink width of the expanded
* bubble bar based on the controller implementation.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
index a55763b..886b9f0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
@@ -27,13 +27,9 @@
class DeviceProfileDimensionsProviderAdapter(
private val taskbarActivityContext: TaskbarActivityContext
) : TaskbarHotseatDimensionsProvider {
- override fun getTaskbarBottomSpace(): Int = deviceProfile().taskbarBottomMargin
+ override fun getTaskbarBottomSpace(): Int = taskbarDp().taskbarBottomMargin
- override fun getTaskbarHeight(): Int = deviceProfile().taskbarHeight
+ override fun getTaskbarHeight(): Int = taskbarDp().taskbarHeight
- override fun getHotseatBottomSpace(): Int = deviceProfile().hotseatBarBottomSpacePx
-
- override fun getHotseatHeight(): Int = deviceProfile().hotseatCellHeightPx
-
- private fun deviceProfile(): DeviceProfile = taskbarActivityContext.deviceProfile
+ private fun taskbarDp(): DeviceProfile = taskbarActivityContext.deviceProfile
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 722dfe7..c117ad4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -36,7 +36,7 @@
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
class PersistentBubbleStashController(
- private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
+ private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider
) : BubbleStashController {
private lateinit var taskbarInsetsController: TaskbarInsetsController
@@ -45,6 +45,7 @@
private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory<View>.MultiProperty
private lateinit var bubbleBarScaleAnimator: AnimatedFloat
private lateinit var controllersAfterInitAction: ControllersAfterInitAction
+ private var hotseatVerticalCenter: Int = 0
override var launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP
set(state) {
@@ -92,17 +93,15 @@
override val bubbleBarTranslationYForHotseat: Float
get() {
- val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace()
- val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight()
- val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
- return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2
+ val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight
+ return -hotseatVerticalCenter + bubbleBarHeight / 2
}
override fun init(
taskbarInsetsController: TaskbarInsetsController,
bubbleBarViewController: BubbleBarViewController,
bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
- controllersAfterInitAction: ControllersAfterInitAction
+ controllersAfterInitAction: ControllersAfterInitAction,
) {
this.taskbarInsetsController = taskbarInsetsController
this.bubbleBarViewController = bubbleBarViewController
@@ -119,13 +118,17 @@
animatorSet.playTogether(
bubbleBarScaleAnimator.animateToValue(1f),
bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
- bubbleBarAlphaAnimator.animateToValue(1f)
+ bubbleBarAlphaAnimator.animateToValue(1f),
)
}
updateTouchRegionOnAnimationEnd(animatorSet)
animatorSet.setDuration(BAR_STASH_DURATION).start()
}
+ override fun setHotseatVerticalCenter(hotseatVerticalCenter: Int) {
+ this.hotseatVerticalCenter = hotseatVerticalCenter
+ }
+
override fun showBubbleBarImmediate() = showBubbleBarImmediate(bubbleBarTranslationY)
override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 9e7d1c4..fbeecaa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -78,6 +78,7 @@
context.resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f
private var animator: AnimatorSet? = null
+ private var hotseatVerticalCenter: Int = 0
override var isStashed: Boolean = false
@VisibleForTesting set
@@ -118,10 +119,8 @@
override val bubbleBarTranslationYForHotseat: Float
get() {
- val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace()
- val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight()
- val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
- return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2
+ val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight
+ return -hotseatVerticalCenter + bubbleBarHeight / 2
}
override val bubbleBarTranslationYForTaskbar: Float =
@@ -176,6 +175,10 @@
.start()
}
+ override fun setHotseatVerticalCenter(hotseatVerticalCenter: Int) {
+ this.hotseatVerticalCenter = hotseatVerticalCenter
+ }
+
override fun showBubbleBarImmediate() {
showBubbleBarImmediate(bubbleBarTranslationY)
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4ad65e1..228dc91 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -419,8 +419,10 @@
mDepthController.setActivityStarted(isStarted());
}
- if ((changeBits & ACTIVITY_STATE_RESUMED) != 0 && mTaskbarUIController != null) {
- mTaskbarUIController.onLauncherPausedOrResumed(isPaused());
+ if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) {
+ if (!FeatureFlags.enableHomeTransitionListener() && mTaskbarUIController != null) {
+ mTaskbarUIController.onLauncherVisibilityChanged(hasBeenResumed());
+ }
}
super.onActivityFlagsChanged(changeBits);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index f542b8c..374db6a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -41,15 +41,20 @@
import com.android.launcher3.Flags.privateSpaceSysAppsSeparation
import com.android.launcher3.R
import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.proxy.ProxyActivityStarter
import com.android.launcher3.util.ApiWrapper
import com.android.launcher3.util.Executors
import com.android.launcher3.util.StartActivityParams
import com.android.launcher3.util.UserIconInfo
import com.android.quickstep.util.FadeOutRemoteTransition
+import javax.inject.Inject
/** A wrapper for the hidden API calls */
-open class SystemApiWrapper(context: Context?) : ApiWrapper(context) {
+@LauncherAppSingleton
+open class SystemApiWrapper @Inject constructor(@ApplicationContext context: Context?) :
+ ApiWrapper(context) {
override fun getPersons(si: ShortcutInfo) = si.persons ?: Utilities.EMPTY_PERSON_ARRAY
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index e87ac2f..ca388c6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.uioverrides.states;
+import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
@@ -91,7 +92,7 @@
@Override
public boolean detachDesktopCarousel() {
- return true;
+ return enableDesktopWindowingCarouselDetach();
}
@Override
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 015a449..cff352c 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -309,7 +309,9 @@
* changes in the WM hierarchy (ie. starting recents transition when you are already over home).
*/
public boolean useSyntheticRecentsTransition() {
- return mRunningTask.isHomeTask() && Flags.enableFallbackOverviewInWindow();
+ return mRunningTask.isHomeTask()
+ && (Flags.enableFallbackOverviewInWindow()
+ || Flags.enableLauncherOverviewInWindow());
}
/**
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 66112c1..1f6c671 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -155,7 +155,7 @@
mContainerInterface.onAssistantVisibilityChanged(0.f);
}
- if (SEPARATE_RECENTS_ACTIVITY.get()) {
+ if (SEPARATE_RECENTS_ACTIVITY.get() || Flags.enableLauncherOverviewInWindow()) {
mIsDefaultHome = false;
if (defaultHome == null) {
defaultHome = mMyHomeIntent.getComponent();
@@ -179,7 +179,7 @@
} else {
// The default home app is a different launcher. Use the fallback Overview instead.
- if (Flags.enableFallbackOverviewInWindow()) {
+ if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
mContainerInterface = FallbackWindowInterface.getInstance();
} else {
mContainerInterface = FallbackActivityInterface.INSTANCE;
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index c3b9736..2828a84 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -3,7 +3,6 @@
import static com.android.launcher3.taskbar.TaskbarThresholdUtils.getFromNavThreshold;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
@@ -208,8 +207,9 @@
RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
try {
- return observer.getContainerInterface()
- .getCreatedContainer().getRootView().getRootWindowInsets();
+ RecentsViewContainer container = observer.getContainerInterface().getCreatedContainer();
+
+ return container == null ? null : container.getRootView().getRootWindowInsets();
} finally {
observer.onDestroy();
rads.destroy();
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 324ca1b..b19f651 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -196,6 +196,7 @@
@Override
public void onHandleConfigurationChanged() {
+ Trace.instant(Trace.TRACE_TAG_APP, "recentsActivity_onHandleConfigurationChanged");
initDeviceProfile();
AbstractFloatingView.closeOpenViews(this, true,
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 7d5bd37..8fc1a78 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -109,7 +109,8 @@
boolean isOpeningHome = Arrays.stream(appTargets).filter(app -> app.mode == MODE_OPENING
&& app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME)
.count() > 0;
- if (appCount == 0 && (!Flags.enableFallbackOverviewInWindow() || isOpeningHome)) {
+ if (appCount == 0 && (!(Flags.enableFallbackOverviewInWindow()
+ || Flags.enableLauncherOverviewInWindow()) || isOpeningHome)) {
ActiveGestureProtoLogProxy.logOnRecentsAnimationStartCancelled();
// Edge case, if there are no closing app targets, then Launcher has nothing to handle
notifyAnimationCanceled();
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 8adc11a..06b2972 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -94,7 +94,7 @@
RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
for (int i = 0; i < numHandles; i++) {
TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy);
- tvs.setIsDesktopTask(forDesktop);
+ tvs.setIsDesktopTask(forDesktop , i);
TransformParams transformParams = new TransformParams();
handles[i] = new RemoteTargetHandle(tvs, transformParams);
}
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 80c07196..79abc0f 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -180,6 +180,7 @@
}
}
};
+ runOnDestroy(() -> mOrientationListener.disable());
mNeedsInit = false;
}
@@ -212,6 +213,7 @@
r.run();
}
mNeedsInit = true;
+ mOnDestroyActions.clear();
}
public boolean isTaskListFrozen() {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index c1d7ffa..cd39c09 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -54,15 +54,16 @@
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.android.internal.logging.InstanceId;
import com.android.internal.util.ScreenshotRequest;
import com.android.internal.view.AppearanceRegion;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.ContextualSearchInvoker;
import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
@@ -109,14 +110,17 @@
import java.util.LinkedHashMap;
import java.util.List;
+import javax.inject.Inject;
+
/**
* Holds the reference to SystemUI.
*/
-public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable {
+@LauncherAppSingleton
+public class SystemUiProxy implements ISystemUiProxy, NavHandle {
private static final String TAG = "SystemUiProxy";
- public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
- new MainThreadInitializedObject<>(SystemUiProxy::new);
+ public static final DaggerSingletonObject<SystemUiProxy> INSTANCE =
+ new DaggerSingletonObject<>(QuickstepBaseAppComponent::getSystemUiProxy);
private static final int MSG_SET_SHELF_HEIGHT = 1;
private static final int MSG_SET_LAUNCHER_KEEP_CLEAR_AREA_HEIGHT = 2;
@@ -188,8 +192,8 @@
@Nullable
private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider;
- @VisibleForTesting
- protected SystemUiProxy(Context context) {
+ @Inject
+ public SystemUiProxy(@ApplicationContext Context context) {
mContext = context;
mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync);
final Intent baseIntent = new Intent().setPackage(mContext.getPackageName());
@@ -206,9 +210,6 @@
}
@Override
- public void close() { }
-
- @Override
public void onBackPressed() {
if (mSystemUiProxy != null) {
try {
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 56c978a..0b6794c 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -295,7 +295,8 @@
// TODO:(b/365777482) if flag is enabled, but on launcher it will crash.
if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
- && Flags.enableFallbackOverviewInWindow()){
+ && (Flags.enableFallbackOverviewInWindow()
+ || Flags.enableLauncherOverviewInWindow())) {
mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent, options,
mCallbacks, gestureState.useSyntheticRecentsTransition());
mRecentsWindowsManager.startRecentsWindow(mCallbacks);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 1481ef2..032e755 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -675,7 +675,7 @@
mDesktopVisibilityController = new DesktopVisibilityController(this);
mTaskbarManager = new TaskbarManager(
this, mAllAppsActionManager, mNavCallbacks, mDesktopVisibilityController);
- if(Flags.enableFallbackOverviewInWindow()) {
+ if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
mRecentsWindowManager = new RecentsWindowManager(this);
}
mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
@@ -927,9 +927,6 @@
BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null;
boolean isOnBubbles = bubbleControllers != null
&& BubbleBarInputConsumer.isEventOnBubbles(tac, event);
- if (isInSwipeUpTouchRegion && tac != null) {
- tac.closeKeyboardQuickSwitchView();
- }
if (mDeviceState.isButtonNavMode()
&& mDeviceState.supportsAssistantGestureInButtonNav()) {
reasonString.append("in three button mode which supports Assistant gesture");
@@ -1410,8 +1407,10 @@
}
public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
+ boolean recentsInWindow =
+ Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
return mOverviewComponentObserver.isHomeAndOverviewSame()
- ? mLauncherSwipeHandlerFactory : (Flags.enableFallbackOverviewInWindow()
+ ? mLauncherSwipeHandlerFactory : (recentsInWindow
? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
}
diff --git a/quickstep/src/com/android/quickstep/ViewUtils.java b/quickstep/src/com/android/quickstep/ViewUtils.java
index 3b58dfc..cf6b04e 100644
--- a/quickstep/src/com/android/quickstep/ViewUtils.java
+++ b/quickstep/src/com/android/quickstep/ViewUtils.java
@@ -23,6 +23,7 @@
import com.android.launcher3.Utilities;
+import java.util.ArrayList;
import java.util.function.BooleanSupplier;
/**
@@ -129,4 +130,18 @@
}
}
}
+
+ /**
+ * Adds the view to the list of accessible children.
+ *
+ * @param view The view to add.
+ * @param outChildren The list of accessible children.
+ */
+ public static void addAccessibleChildToList(View view, ArrayList<View> outChildren) {
+ if (view.includeForAccessibility()) {
+ outChildren.add(view);
+ } else {
+ view.addChildrenForAccessibility(outChildren);
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
index ab77a7f..3870b9b 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
@@ -15,7 +15,9 @@
*/
package com.android.quickstep.dagger;
+import com.android.launcher3.uioverrides.SystemApiWrapper;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl;
+import com.android.launcher3.util.ApiWrapper;
import com.android.launcher3.util.PluginManagerWrapper;
import dagger.Binds;
@@ -25,4 +27,5 @@
public abstract class QuickStepModule {
@Binds abstract PluginManagerWrapper bindPluginManagerWrapper(PluginManagerWrapperImpl impl);
+ @Binds abstract ApiWrapper bindApiWrapper(SystemApiWrapper systemApiWrapper);
}
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index 977c036..b2670e8 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -19,6 +19,7 @@
import com.android.launcher3.dagger.LauncherAppComponent;
import com.android.launcher3.dagger.LauncherBaseAppComponent;
import com.android.launcher3.model.WellbeingModel;
+import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.AsyncClockEventDelegate;
/**
@@ -34,4 +35,6 @@
WellbeingModel getWellbeingModel();
AsyncClockEventDelegate getAsyncClockEventDelegate();
+
+ SystemUiProxy getSystemUiProxy();
}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 5a4c769..daad6b7 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -78,7 +78,7 @@
}
private static BaseContainerInterface<RecentsState, ?> getContainerInterface() {
- return Flags.enableFallbackOverviewInWindow()
+ return (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow())
? FallbackWindowInterface.getInstance()
: FallbackActivityInterface.INSTANCE;
}
@@ -294,7 +294,8 @@
}
// disabling this so app icons aren't drawn on top of recent tasks.
- if (isOverlayEnabled && !Flags.enableFallbackOverviewInWindow()) {
+ if (isOverlayEnabled && !(Flags.enableFallbackOverviewInWindow()
+ || Flags.enableLauncherOverviewInWindow())) {
runActionOnRemoteHandles(remoteTargetHandle ->
remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
}
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index 082b96c..34783c7 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -15,6 +15,7 @@
*/
package com.android.quickstep.fallback;
+import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
@@ -152,7 +153,7 @@
@Override
public boolean detachDesktopCarousel() {
- return hasFlag(FLAG_DETACH_DESKTOP_CAROUSEL);
+ return hasFlag(FLAG_DETACH_DESKTOP_CAROUSEL) && enableDesktopWindowingCarouselDetach();
}
/**
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 3017df2..74f9901 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -18,6 +18,7 @@
import android.animation.AnimatorSet
import android.app.ActivityOptions
+import android.content.ComponentName
import android.content.Context
import android.content.LocusId
import android.os.Bundle
@@ -276,6 +277,10 @@
}
}
+ override fun getComponentName(): ComponentName {
+ return ComponentName(this, RecentsWindowManager::class.java)
+ }
+
override fun canStartHomeSafely(): Boolean {
val overviewCommandHelper = tisBindHelper.overviewCommandHelper
return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely()
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index e19b338..c4198db 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -157,6 +157,7 @@
mStartDisplacement = continuingPreviousGesture ? 0 : -mTouchSlop;
mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+
}
@Override
@@ -426,8 +427,9 @@
notifyGestureStarted(true /*isLikelyToStartNewTask*/);
} else {
// todo differentiate intent based on if we are on home or in app for overview in window
- Intent intent = new Intent(Flags.enableFallbackOverviewInWindow()
- ? mInteractionHandler.getHomeIntent()
+ boolean useHomeIntentForWindow = Flags.enableFallbackOverviewInWindow()
+ || Flags.enableLauncherOverviewInWindow();
+ Intent intent = new Intent(useHomeIntentForWindow ? mInteractionHandler.getHomeIntent()
: mInteractionHandler.getLaunchIntent());
intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 44b8b8d..f2b9976 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -113,6 +113,10 @@
instance =
factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras)
scope[modelClass.simpleName] = instance!!
+ log(
+ "instance of $modelClass" +
+ " (${instance.hashCode()}) added to scope ${scope.scopeId}"
+ )
}
}
return instance!!
@@ -148,6 +152,13 @@
fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope =
scopes[scopeId] ?: createScope(scopeId)
+ fun removeScope(scope: Any) {
+ val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+ scopes[scopeId]?.close()
+ scopes.remove(scopeId)
+ log("Scope $scopeId removed")
+ }
+
// TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely.
// Each class should be responsible for providing a factory function to create a new instance.
@Suppress("UNCHECKED_CAST")
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index e7416ec..eb9c047 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -32,6 +32,7 @@
import com.android.launcher3.Utilities
import com.android.launcher3.util.ViewPool
import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.inject
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
@@ -54,7 +55,7 @@
class TaskThumbnailView : ConstraintLayout, ViewPool.Reusable {
private val viewData: TaskThumbnailViewData by RecentsDependencies.inject(this)
- private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)
+ private lateinit var viewModel: TaskThumbnailViewModel
private lateinit var viewAttachedScope: CoroutineScope
@@ -91,6 +92,7 @@
super.onAttachedToWindow()
viewAttachedScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
+ viewModel = RecentsDependencies.get(this)
viewModel.uiState
.onEach { viewModelUiState ->
Log.d(TAG, "viewModelUiState changed from $uiState to: $viewModelUiState")
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index 9253dbf..c82ed9a 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.task.util
import android.util.Log
+import android.view.View.OnLayoutChangeListener
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
@@ -41,31 +42,35 @@
private lateinit var overlayInitializedScope: CoroutineScope
private var uiState: TaskOverlayUiState = Disabled
- private val viewModel: TaskOverlayViewModel by lazy {
- TaskOverlayViewModel(
- task = task,
- recentsViewData = RecentsDependencies.get(),
- getThumbnailPositionUseCase = RecentsDependencies.get(),
- recentTasksRepository = RecentsDependencies.get()
- )
- }
+ private lateinit var viewModel: TaskOverlayViewModel
// TODO(b/331753115): TaskOverlay should listen for state changes and react.
val enabledState: Enabled
get() = uiState as Enabled
+ private val snapshotLayoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ (uiState as? Enabled)?.let { initOverlay(it) }
+ }
+
fun getThumbnailMatrix() = getThumbnailPositionState().matrix
private fun getThumbnailPositionState() =
viewModel.getThumbnailPositionState(
overlay.snapshotView.width,
overlay.snapshotView.height,
- overlay.snapshotView.isLayoutRtl
+ overlay.snapshotView.isLayoutRtl,
)
fun init() {
overlayInitializedScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskOverlayHelper"))
+ viewModel =
+ TaskOverlayViewModel(
+ task = task,
+ recentsViewData = RecentsDependencies.get(),
+ getThumbnailPositionUseCase = RecentsDependencies.get(),
+ recentTasksRepository = RecentsDependencies.get(),
+ )
viewModel.overlayState
.onEach {
uiState = it
@@ -76,9 +81,7 @@
}
}
.launchIn(overlayInitializedScope)
- overlay.snapshotView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
- (uiState as? Enabled)?.let { initOverlay(it) }
- }
+ overlay.snapshotView.addOnLayoutChangeListener(snapshotLayoutChangeListener)
}
private fun initOverlay(enabledState: Enabled) {
@@ -96,6 +99,7 @@
fun destroy() {
overlayInitializedScope.cancel()
uiState = Disabled
+ overlay.snapshotView.removeOnLayoutChangeListener(snapshotLayoutChangeListener)
reset()
}
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index 8762e86..623bc53 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -39,6 +39,7 @@
// The percentage of the previous speed that determines whether this is a rapid deceleration.
// The bigger this number, the easier it is to trigger the first pause.
private static final float RAPID_DECELERATION_FACTOR = 0.6f;
+ private static final float RAPID_DECELERATION_FACTOR_TRACKPAD = 0.85f;
/** If no motion is added for this amount of time, assume the motion has paused. */
private static final long FORCE_PAUSE_TIMEOUT = 300;
@@ -57,6 +58,7 @@
private final float mSpeedVerySlow;
private final float mSpeedSlow;
private final float mSpeedSomewhatFast;
+ private final float mSpeedTrackpadSomewhatFast;
private final float mSpeedFast;
private final Alarm mForcePauseTimeout;
private final boolean mMakePauseHarderToTrigger;
@@ -95,6 +97,8 @@
mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
+ mSpeedTrackpadSomewhatFast = res.getDimension(
+ R.dimen.motion_pause_detector_speed_trackpad_somewhat_fast);
mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
mForcePauseTimeout = new Alarm();
mForcePauseTimeout.setOnAlarmListener(alarm -> {
@@ -183,7 +187,9 @@
// takes too long, so also check for a rapid deceleration.
boolean isRapidDeceleration =
speed < previousSpeed * getRapidDecelerationFactor();
- isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
+ boolean notSuperFast = speed < mSpeedSomewhatFast
+ || (mIsTrackpadGesture && speed < mSpeedTrackpadSomewhatFast);
+ isPaused = isRapidDeceleration && notSuperFast;
isPausedReason = new ActiveGestureLog.CompoundString(
"Didn't have back to back slow speeds, checking for rapid "
+ " deceleration on first pause only");
@@ -265,7 +271,8 @@
private float getRapidDecelerationFactor() {
return mIsTrackpadGesture ? Float.parseFloat(
Utilities.getSystemProperty("trackpad_in_app_swipe_up_deceleration_factor",
- String.valueOf(RAPID_DECELERATION_FACTOR))) : RAPID_DECELERATION_FACTOR;
+ String.valueOf(RAPID_DECELERATION_FACTOR_TRACKPAD)))
+ : RAPID_DECELERATION_FACTOR;
}
public interface OnMotionPauseListener {
diff --git a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
index 2f0a6df..db02f55 100644
--- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
+++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
@@ -16,10 +16,13 @@
package com.android.quickstep.util
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.RectF
+import android.util.Log
import android.view.View
import android.view.animation.PathInterpolator
import androidx.core.graphics.transform
@@ -41,6 +44,8 @@
import com.android.launcher3.uioverrides.QuickstepLauncher
import com.android.quickstep.views.RecentsView
+const val TAG = "ScalingWorkspaceRevealAnim"
+
/**
* Creates an animation where the workspace and hotseat fade in while revealing from the center of
* the screen outwards radially. This is used in conjunction with the swipe up to home animation.
@@ -197,6 +202,19 @@
workspace.setLayerType(View.LAYER_TYPE_HARDWARE, null)
hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null)
animation.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationCancel(animation: Animator) {
+ super.onAnimationCancel(animation)
+ Log.d(TAG, "onAnimationCancel")
+ }
+
+ override fun onAnimationPause(animation: Animator) {
+ super.onAnimationPause(animation)
+ Log.d(TAG, "onAnimationPause")
+ }
+ }
+ )
+ animation.addListener(
AnimatorListeners.forEndCallback(
Runnable {
workspace.setLayerType(View.LAYER_TYPE_NONE, null)
@@ -207,6 +225,8 @@
Animations.setOngoingAnimation(workspace, animation = null)
Animations.setOngoingAnimation(hotseat, animation = null)
}
+
+ Log.d(TAG, "alpha of workspace at the end of animation: ${workspace.alpha}")
}
)
)
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index f5be103..a4b8fec 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -120,6 +120,7 @@
private boolean mScaleToCarouselTaskSize = false;
private int mTaskRectTranslationX;
private int mTaskRectTranslationY;
+ private int mDesktopTaskIndex = 0;
public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) {
mContext = context;
@@ -290,8 +291,9 @@
/**
* Sets whether this task is part of desktop tasks in overview.
*/
- public void setIsDesktopTask(boolean desktop) {
+ public void setIsDesktopTask(boolean desktop, int index) {
mIsDesktopTask = desktop;
+ mDesktopTaskIndex = index;
}
/**
@@ -545,9 +547,9 @@
// In shell transitions, the animation leashes are reparented to an animation container
// so we can bump layers as needed.
builder.setLayer(mDrawsBelowRecents
- ? Integer.MIN_VALUE + app.prefixOrderIndex
// 1000 is an arbitrary number to give room for multiple layers.
- : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex);
+ ? Integer.MIN_VALUE + 1000 + app.prefixOrderIndex - mDesktopTaskIndex
+ : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex - mDesktopTaskIndex);
}
}
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
index 0a97793..32e0e13 100644
--- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
@@ -30,6 +30,7 @@
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.util.FloatProperty;
+import android.util.Log;
import android.view.View;
import com.android.app.animation.Interpolators;
@@ -51,6 +52,8 @@
*/
public class WorkspaceRevealAnim {
+ private static final String TAG = "WorkspaceRevealAnim";
+
// Should be used for animations running alongside this WorkspaceRevealAnim.
public static final int DURATION_MS = 350;
private static final FloatProperty<Workspace<?>> WORKSPACE_SCALE_PROPERTY =
@@ -97,6 +100,19 @@
mAnimators.setDuration(DURATION_MS);
mAnimators.setInterpolator(Interpolators.DECELERATED_EASE);
+ mAnimators.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ Log.d(TAG, "onAnimationCancel");
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ Log.d(TAG, "onAnimationEnd: workspace alpha = " + workspace.getAlpha());
+ }
+ });
}
private <T extends View> void addRevealAnimatorsForView(T v, FloatProperty<T> scaleProperty) {
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 8c854e7..15b0a6b 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -25,7 +25,6 @@
import android.util.AttributeSet
import android.util.Log
import android.view.Gravity
-import android.view.LayoutInflater
import android.view.View
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
@@ -40,6 +39,8 @@
import com.android.launcher3.util.rects.set
import com.android.quickstep.BaseContainerInterface
import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.ViewUtils
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.util.RecentsOrientedState
import com.android.systemui.shared.recents.model.Task
@@ -53,14 +54,29 @@
override fun computeTaskCornerRadius(context: Context) =
computeWindowCornerRadius(context)
}
+
private val taskThumbnailViewDeprecatedPool =
- ViewPool<TaskThumbnailViewDeprecated>(
- context,
- this,
- R.layout.task_thumbnail_deprecated,
- VIEW_POOL_MAX_SIZE,
- VIEW_POOL_INITIAL_SIZE,
- )
+ if (!enableRefactorTaskThumbnail()) {
+ ViewPool<TaskThumbnailViewDeprecated>(
+ context,
+ this,
+ R.layout.task_thumbnail_deprecated,
+ VIEW_POOL_MAX_SIZE,
+ VIEW_POOL_INITIAL_SIZE,
+ )
+ } else null
+
+ private val taskThumbnailViewPool =
+ if (enableRefactorTaskThumbnail()) {
+ ViewPool<TaskThumbnailView>(
+ context,
+ this,
+ R.layout.task_thumbnail,
+ VIEW_POOL_MAX_SIZE,
+ VIEW_POOL_INITIAL_SIZE,
+ )
+ } else null
+
private val tempPointF = PointF()
private val tempRect = Rect()
private lateinit var backgroundView: View
@@ -117,9 +133,9 @@
tasks.map { task ->
val snapshotView =
if (enableRefactorTaskThumbnail()) {
- LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false)
+ taskThumbnailViewPool!!.view
} else {
- taskThumbnailViewDeprecatedPool.view
+ taskThumbnailViewDeprecatedPool!!.view
}
addView(
@@ -148,9 +164,11 @@
super.onRecycle()
visibility = VISIBLE
taskContainers.forEach {
- if (!enableRefactorTaskThumbnail()) {
- removeView(it.thumbnailViewDeprecated)
- taskThumbnailViewDeprecatedPool.recycle(it.thumbnailViewDeprecated)
+ removeView(it.snapshotView)
+ if (enableRefactorTaskThumbnail()) {
+ taskThumbnailViewPool!!.recycle(it.thumbnailView)
+ } else {
+ taskThumbnailViewDeprecatedPool!!.recycle(it.thumbnailViewDeprecated)
}
}
}
@@ -296,10 +314,15 @@
override fun getThumbnailFullscreenParams() = snapshotDrawParams
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
+ super.addChildrenForAccessibility(outChildren)
+ ViewUtils.addAccessibleChildToList(backgroundView, outChildren)
+ }
+
companion object {
private const val TAG = "DesktopTaskView"
private const val DEBUG = false
- private const val VIEW_POOL_MAX_SIZE = 10
+ private const val VIEW_POOL_MAX_SIZE = 5
// As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool.
private const val VIEW_POOL_INITIAL_SIZE = 0
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index b38d0d7..2d0f15e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -5058,7 +5058,8 @@
mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
mSplitSelectStateController
.setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal
- && mSplitHiddenTaskView != null);
+ && mSplitHiddenTaskView != null
+ && !(mSplitHiddenTaskView instanceof DesktopTaskView));
// Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 6cb7741..959516f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -29,6 +29,7 @@
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.TaskUtils
+import com.android.quickstep.ViewUtils.addAccessibleChildToList
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.getScope
@@ -157,12 +158,13 @@
fun destroy() {
digitalWellBeingToast?.destroy()
- if (enableRefactorTaskThumbnail()) {
- taskView.removeView(thumbnailView)
- }
snapshotView.scaleX = 1f
snapshotView.scaleY = 1f
overlay.destroy()
+ if (enableRefactorTaskThumbnail()) {
+ RecentsDependencies.getInstance().removeScope(snapshotView)
+ RecentsDependencies.getInstance().removeScope(this)
+ }
}
fun bindThumbnailView() {
@@ -181,12 +183,4 @@
showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
}
-
- private fun addAccessibleChildToList(view: View, outChildren: ArrayList<View>) {
- if (view.includeForAccessibility()) {
- outChildren.add(view)
- } else {
- view.addChildrenForAccessibility(outChildren)
- }
- }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index cc64dba..28ecf96 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -82,6 +82,7 @@
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.task.viewmodel.TaskViewModel
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
@@ -723,20 +724,23 @@
@StagePosition stagePosition: Int,
taskOverlayFactory: TaskOverlayFactory,
): TaskContainer {
- val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!!
+ val existingThumbnailView: View = findViewById(thumbnailViewId)!!
val snapshotView =
- if (enableRefactorTaskThumbnail()) {
- thumbnailViewDeprecated.visibility = GONE
- val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated)
- LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false).also {
- it.id = thumbnailViewId
- addView(it, indexOfSnapshotView, thumbnailViewDeprecated.layoutParams)
+ when {
+ !enableRefactorTaskThumbnail() -> existingThumbnailView
+ existingThumbnailView is TaskThumbnailView -> existingThumbnailView
+ else -> {
+ val indexOfSnapshotView = indexOfChild(existingThumbnailView)
+ LayoutInflater.from(context)
+ .inflate(R.layout.task_thumbnail, this, false)
+ .also {
+ it.id = thumbnailViewId
+ addView(it, indexOfSnapshotView, existingThumbnailView.layoutParams)
+ removeView(existingThumbnailView)
+ }
}
- } else {
- thumbnailViewDeprecated
}
val iconView = getOrInflateIconView(iconViewId)
- val digitalWellBeingToast = findViewById<DigitalWellBeingToast>(digitalWellbeingBannerId)!!
return TaskContainer(
this,
task,
@@ -744,7 +748,7 @@
iconView,
TransformingTouchDelegate(iconView.asView()),
stagePosition,
- digitalWellBeingToast,
+ findViewById(digitalWellbeingBannerId)!!,
findViewById(showWindowViewId)!!,
taskOverlayFactory,
)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
index 59900b1..46a7733 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
@@ -24,6 +24,7 @@
import com.android.launcher3.taskbar.rules.TaskbarModeRule
import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarSandboxComponent
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
@@ -33,36 +34,28 @@
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
-import org.junit.rules.TestRule
import org.junit.runner.RunWith
-import org.junit.runners.model.Statement
@RunWith(LauncherMultivalentJUnit::class)
@EmulatedDevices(["pixelTablet2023"])
class TaskbarAutohideSuspendControllerTest {
- @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
- @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this)
- @get:Rule(order = 2)
- val systemUiProxyRule = TestRule { base, _ ->
- object : Statement() {
- override fun evaluate() {
- getInstrumentation().runOnMainSync {
- context.putObject(
- SystemUiProxy.INSTANCE,
- object : SystemUiProxy(context) {
- override fun notifyTaskbarAutohideSuspend(suspend: Boolean) {
- latestSuspendNotification = suspend
- }
- },
- )
+ @get:Rule(order = 0)
+ val context =
+ TaskbarWindowSandboxContext.create {
+ builder: TaskbarSandboxComponent.Builder,
+ sandboxContext: TaskbarWindowSandboxContext ->
+ builder.bindSystemUiProxy(
+ object : SystemUiProxy(sandboxContext) {
+ override fun notifyTaskbarAutohideSuspend(suspend: Boolean) {
+ latestSuspendNotification = suspend
+ }
}
- base.evaluate()
- }
+ )
}
- }
- @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
- @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+ @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this)
+ @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+ @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
@InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController
@InjectController lateinit var stashController: TaskbarStashController
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
index 12e84b8..b1bb472 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
@@ -24,6 +24,7 @@
import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarSandboxComponent
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
@@ -42,7 +43,20 @@
@RunWith(LauncherMultivalentJUnit::class)
@EmulatedDevices(["pixelTablet2023"])
class TaskbarScrimViewControllerTest {
- @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+ @get:Rule(order = 0)
+ val context =
+ TaskbarWindowSandboxContext.create {
+ builder: TaskbarSandboxComponent.Builder,
+ sandboxContext: TaskbarWindowSandboxContext ->
+ builder.bindSystemUiProxy(
+ object : SystemUiProxy(sandboxContext) {
+ override fun onBackPressed() {
+ super.onBackPressed()
+ backPressed = true
+ }
+ }
+ )
+ }
@get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context)
@get:Rule(order = 2) val animatorTestRule = AnimatorTestRule(this)
@get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
@@ -53,6 +67,8 @@
private val animationDuration =
context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
+ private var backPressed = false
+
@Test
@TaskbarMode(PINNED)
fun testOnTaskbarVisibleChanged_onlyTaskbarVisible_noScrim() {
@@ -130,16 +146,6 @@
@Test
@TaskbarMode(PINNED)
fun testOnClick_scrimShown_performsSystemBack() {
- var backPressed = false
- context.putObject(
- SystemUiProxy.INSTANCE,
- object : SystemUiProxy(context) {
- override fun onBackPressed() {
- backPressed = true
- }
- },
- )
-
getInstrumentation().runOnMainSync {
scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
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 b37048a..b0d01d3 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
@@ -40,6 +40,7 @@
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks
import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.wm.shell.shared.animation.PhysicsAnimator
@@ -179,9 +180,7 @@
// verify the hide bubble animation is pending
assertThat(animatorScheduler.delayedBlock).isNotNull()
- InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.onBubbleBarTouchedWhileAnimating()
- }
+ InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForTouch() }
waitForFlyoutToHide()
@@ -992,18 +991,20 @@
override val collapsedElevation = 1f
override val distanceToRevealTriangle = 10f
}
- val topBoundaryListener =
- object : BubbleBarFlyoutController.TopBoundaryListener {
+ val flyoutCallbacks =
+ object : FlyoutCallbacks {
override fun extendTopBoundary(space: Int) {}
override fun resetTopBoundary() {}
+
+ override fun flyoutClicked() {}
}
val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
flyoutController =
BubbleBarFlyoutController(
flyoutContainer,
flyoutPositioner,
- topBoundaryListener,
+ flyoutCallbacks,
flyoutScheduler,
)
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index 527bdaa..50bb9bc 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -44,7 +44,7 @@
private lateinit var flyoutController: BubbleBarFlyoutController
private lateinit var flyoutContainer: FrameLayout
- private lateinit var topBoundaryListener: FakeTopBoundaryListener
+ private lateinit var flyoutCallbacks: FakeFlyoutCallbacks
private val context = ApplicationProvider.getApplicationContext<Context>()
private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message")
private var onLeft = true
@@ -67,15 +67,10 @@
override val collapsedElevation = 1f
override val distanceToRevealTriangle = 50f
}
- topBoundaryListener = FakeTopBoundaryListener()
+ flyoutCallbacks = FakeFlyoutCallbacks()
val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
flyoutController =
- BubbleBarFlyoutController(
- flyoutContainer,
- positioner,
- topBoundaryListener,
- flyoutScheduler,
- )
+ BubbleBarFlyoutController(flyoutContainer, positioner, flyoutCallbacks, flyoutScheduler)
}
@Test
@@ -120,11 +115,13 @@
fun hideFlyout_removedFromContainer() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ assertThat(flyoutController.hasFlyout()).isTrue()
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.collapseFlyout {}
animatorTestRule.advanceTimeBy(300)
}
assertThat(flyoutContainer.childCount).isEqualTo(0)
+ assertThat(flyoutController.hasFlyout()).isFalse()
}
@Test
@@ -140,7 +137,7 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animatorTestRule.advanceTimeBy(300)
}
- assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(50)
+ assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
}
@Test
@@ -153,7 +150,7 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync {
animatorTestRule.advanceTimeBy(300)
}
- assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(0)
+ assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(0)
}
@Test
@@ -164,7 +161,7 @@
flyoutController.collapseFlyout {}
animatorTestRule.advanceTimeBy(300)
}
- assertThat(topBoundaryListener.topBoundaryReset).isTrue()
+ assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
}
@Test
@@ -178,13 +175,82 @@
animatorTestRule.advanceTimeBy(300)
assertThat(flyoutView.alpha).isEqualTo(0f)
}
- assertThat(topBoundaryListener.topBoundaryReset).isTrue()
+ assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
}
- class FakeTopBoundaryListener : BubbleBarFlyoutController.TopBoundaryListener {
+ @Test
+ fun clickFlyout_notifiesCallback() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ assertThat(flyoutContainer.childCount).isEqualTo(1)
+ val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+ assertThat(flyoutView.alpha).isEqualTo(1f)
+ animatorTestRule.advanceTimeBy(300)
+ flyoutView.performClick()
+ }
+ assertThat(flyoutCallbacks.flyoutClicked).isTrue()
+ }
+
+ @Test
+ fun updateFlyoutWhileExpanding() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ assertThat(flyoutController.hasFlyout()).isTrue()
+ val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+ assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("message")
+ // advance the animation about halfway
+ animatorTestRule.advanceTimeBy(100)
+ }
+ assertThat(flyoutController.hasFlyout()).isTrue()
+
+ val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+ // set negative translation to verify that the top boundary extends as a result of
+ // updating while expanding
+ flyout.translationY = -50f
+ flyoutController.updateFlyoutWhileExpanding(newFlyoutMessage)
+ assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("new message")
+ }
+ assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+ }
+
+ @Test
+ fun updateFlyoutFullyExpanded() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ animatorTestRule.advanceTimeBy(300)
+ }
+ assertThat(flyoutController.hasFlyout()).isTrue()
+
+ val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+ // set negative translation to verify that the top boundary extends as a result of
+ // updating while fully expanded
+ flyout.translationY = -50f
+ flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {}
+
+ // advance the timer so that the fade out animation plays
+ animatorTestRule.advanceTimeBy(250)
+ assertThat(flyout.alpha).isEqualTo(0)
+ assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("new message")
+
+ // advance the timer so that the fade in animation plays
+ animatorTestRule.advanceTimeBy(250)
+ assertThat(flyout.alpha).isEqualTo(1)
+ }
+ assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+ }
+
+ class FakeFlyoutCallbacks : FlyoutCallbacks {
var topBoundaryExtendedSpace = 0
var topBoundaryReset = false
+ var flyoutClicked = false
override fun extendTopBoundary(space: Int) {
topBoundaryExtendedSpace = space
@@ -193,5 +259,9 @@
override fun resetTopBoundary() {
topBoundaryReset = true
}
+
+ override fun flyoutClicked() {
+ flyoutClicked = true
+ }
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
index 5dc78a9..00b42bc 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -48,6 +48,7 @@
companion object {
const val BUBBLE_BAR_HEIGHT = 100f
+ const val HOTSEAT_VERTICAL_CENTER = 95
const val HOTSEAT_TRANSLATION_Y = -45f
const val TASK_BAR_TRANSLATION_Y = -5f
}
@@ -74,11 +75,12 @@
PersistentBubbleStashController(DefaultDimensionsProvider())
setUpBubbleBarView()
setUpBubbleBarController()
+ persistentTaskBarStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
persistentTaskBarStashController.init(
taskbarInsetsController,
bubbleBarViewController,
null,
- ImmediateAction()
+ ImmediateAction(),
)
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
index 0f8a2c3..96c2f45 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
@@ -23,21 +23,13 @@
class DefaultDimensionsProvider(
private val taskBarBottomSpace: Int = TASKBAR_BOTTOM_SPACE,
private val taskBarHeight: Int = TASKBAR_HEIGHT,
- private val hotseatBottomSpace: Int = HOTSEAT_BOTTOM_SPACE,
- private val hotseatHeight: Int = HOTSEAT_HEIGHT
) : BubbleStashController.TaskbarHotseatDimensionsProvider {
override fun getTaskbarBottomSpace(): Int = taskBarBottomSpace
override fun getTaskbarHeight(): Int = taskBarHeight
- override fun getHotseatBottomSpace(): Int = hotseatBottomSpace
-
- override fun getHotseatHeight(): Int = hotseatHeight
-
companion object {
const val TASKBAR_BOTTOM_SPACE = 0
const val TASKBAR_HEIGHT = 110
- const val HOTSEAT_BOTTOM_SPACE = 20
- const val HOTSEAT_HEIGHT = 150
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index 8b277e7..64416dd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -60,6 +60,7 @@
companion object {
const val TASKBAR_BOTTOM_SPACE = 5
+ const val HOTSEAT_VERTICAL_CENTER = 95
const val BUBBLE_BAR_WIDTH = 200
const val BUBBLE_BAR_HEIGHT = 100
const val HOTSEAT_TRANSLATION_Y = -45f
@@ -108,6 +109,7 @@
setUpStashedHandleView()
setUpBubbleStashedHandleViewController()
PhysicsAnimatorTestUtils.prepareForTest()
+ mTransientBubbleStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
mTransientBubbleStashController.init(
taskbarInsetsController,
bubbleBarViewController,
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index 2d3bfd6..a331a25 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -24,8 +24,13 @@
import androidx.test.core.app.ApplicationProvider
import com.android.launcher3.FakeLauncherPrefs
import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
import com.android.launcher3.util.SandboxApplication
+import com.android.quickstep.SystemUiProxy
+import dagger.BindsInstance
+import dagger.Component
import org.junit.rules.ExternalResource
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
@@ -50,7 +55,14 @@
private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay"
/** Creates a [SandboxApplication] for Taskbar tests. */
- fun create(): TaskbarWindowSandboxContext {
+ fun create(
+ daggerComponentBinder:
+ ((
+ builder: TaskbarSandboxComponent.Builder,
+ sandboxContext: TaskbarWindowSandboxContext,
+ ) -> TaskbarSandboxComponent.Builder)? =
+ null
+ ): TaskbarWindowSandboxContext {
val base = ApplicationProvider.getApplicationContext<Context>()
val displayManager = checkNotNull(base.getSystemService(DisplayManager::class.java))
@@ -67,10 +79,21 @@
)
}
- return TaskbarWindowSandboxContext(
- SandboxApplication(base.createDisplayContext(virtualDisplay.display)),
- virtualDisplay,
- )
+ val sandboxApplication =
+ SandboxApplication(base.createDisplayContext(virtualDisplay.display))
+ val taskbarWindowSandboxContext =
+ TaskbarWindowSandboxContext(sandboxApplication, virtualDisplay)
+
+ if (daggerComponentBinder != null) {
+ sandboxApplication.initDaggerComponent(
+ daggerComponentBinder(
+ DaggerTaskbarSandboxComponent.builder(),
+ taskbarWindowSandboxContext,
+ )
+ )
+ }
+
+ return taskbarWindowSandboxContext
}
}
}
@@ -80,3 +103,14 @@
override fun after() = virtualDisplay.release()
}
}
+
+@LauncherAppSingleton
+@Component
+interface TaskbarSandboxComponent : LauncherAppComponent {
+ @Component.Builder
+ interface Builder : LauncherAppComponent.Builder {
+ @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
+
+ override fun build(): TaskbarSandboxComponent
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index 1f88743..6212b59 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -20,6 +20,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.R
+import com.android.launcher3.taskbar.rules.DaggerTaskbarSandboxComponent
import com.android.launcher3.util.LauncherModelHelper
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.shared.system.InputConsumerController
@@ -58,7 +59,9 @@
@Before
fun setup() {
- sandboxContext.putObject(SystemUiProxy.INSTANCE, systemUiProxy)
+ sandboxContext.initDaggerComponent(
+ DaggerTaskbarSandboxComponent.builder().bindSystemUiProxy(systemUiProxy)
+ )
val deviceState = mock(RecentsAnimationDeviceState::class.java)
whenever(deviceState.rotationTouchHelper).thenReturn(mock(RotationTouchHelper::class.java))
gestureState = spy(GestureState(OverviewComponentObserver(sandboxContext, deviceState), 0))
@@ -71,7 +74,7 @@
gestureState,
0,
false,
- inputConsumerController
+ inputConsumerController,
)
underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false)
}
@@ -83,7 +86,7 @@
verify(systemUiProxy)
.updateContextualEduStats(
/* isTrackpadGesture= */ eq(true),
- eq(GestureType.HOME.toString())
+ eq(GestureType.HOME.toString()),
)
}
@@ -93,7 +96,7 @@
verify(systemUiProxy)
.updateContextualEduStats(
/* isTrackpadGesture= */ eq(false),
- eq(GestureType.HOME.toString())
+ eq(GestureType.HOME.toString()),
)
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
index 9bc1c59..2c275f4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -22,7 +22,7 @@
import android.platform.test.annotations.PlatinumTest;
import com.android.launcher3.tapl.Overview;
-import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask;
+import com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer;
import com.android.launcher3.tapl.OverviewTaskMenu;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -76,7 +76,7 @@
taskMenu.touchOutsideTaskMenuToDismiss();
OverviewTaskMenu splitMenu = overview.getCurrentTask().tapMenu(
- OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT);
+ OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT);
assertTrue("App info item not appearing in expanded split task's menu.",
splitMenu.hasMenuItem("App info"));
splitMenu.touchOutsideTaskMenuToDismiss();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
index 694a382..2a8afbf 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -21,6 +21,8 @@
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import com.android.launcher3.BuildConfig
+import com.android.launcher3.tapl.LaunchedAppState
+import com.android.launcher3.tapl.OverviewTask
import com.android.launcher3.ui.AbstractLauncherUiTest
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape
import com.android.launcher3.uioverrides.QuickstepLauncher
@@ -45,22 +47,16 @@
@Test
@PortraitLandscape
fun enterDesktopViaOverviewMenu() {
- // Move last launched TEST_ACTIVITY_2 into Desktop
- mLauncher.workspace
- .switchToOverview()
- .getTestActivityTask(TEST_ACTIVITY_2)
- .tapMenu()
- .tapDesktopMenuItem()
- assertTestAppLaunched(TEST_ACTIVITY_2)
+ mLauncher.workspace.switchToOverview()
+ moveTaskToDesktop(TEST_ACTIVITY_2) // Move last launched TEST_ACTIVITY_2 into Desktop
// Scroll back to TEST_ACTIVITY_1, then move it into Desktop
mLauncher
.goHome()
.switchToOverview()
.apply { flingForward() }
- .getTestActivityTask(TEST_ACTIVITY_1)
- .tapMenu()
- .tapDesktopMenuItem()
+ .also { moveTaskToDesktop(TEST_ACTIVITY_1) }
+
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
// Launch static DesktopTaskView
@@ -73,6 +69,83 @@
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
}
+ @Test
+ @PortraitLandscape
+ fun dismissFocusedTasks_thenDesktopIsCentered() {
+ // Create DesktopTaskView
+ mLauncher.goHome().switchToOverview()
+ moveTaskToDesktop(TEST_ACTIVITY_2)
+
+ // Create a new task activity to be the focused task
+ mLauncher.goHome()
+ startTestActivity(TEST_ACTIVITY_EXTRA)
+
+ val overview = mLauncher.goHome().switchToOverview()
+
+ // Dismiss focused task
+ val focusedTask1 = overview.currentTask
+ assertTaskContentDescription(focusedTask1, TEST_ACTIVITY_EXTRA)
+ focusedTask1.dismiss()
+
+ // Dismiss new focused task
+ val focusedTask2 = overview.currentTask
+ assertTaskContentDescription(focusedTask2, TEST_ACTIVITY_1)
+ focusedTask2.dismiss()
+
+ // Dismiss DesktopTaskView
+ val desktopTask = overview.currentTask
+ assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue()
+ desktopTask.dismiss()
+
+ assertWithMessage("Still have tasks after dismissing all the tasks")
+ .that(mLauncher.workspace.switchToOverview().hasTasks())
+ .isFalse()
+ }
+
+ @Test
+ @PortraitLandscape
+ fun dismissFocusedTask_thenDesktopTask_thenFocusedTaskIsCentered() {
+ // Create extra activity to be DesktopTaskView
+ startTestActivity(TEST_ACTIVITY_EXTRA)
+ mLauncher.goHome().switchToOverview()
+ val desktop = moveTaskToDesktop(TEST_ACTIVITY_EXTRA)
+
+ val overview = desktop.switchToOverview()
+
+ // Dismiss focused task
+ val focusedTask1 = overview.getTestActivityTask(TEST_ACTIVITY_2)
+ assertTaskContentDescription(focusedTask1, TEST_ACTIVITY_2)
+ focusedTask1.dismiss()
+
+ // Dismiss DesktopTaskView
+ val desktopTask = overview.currentTask
+ assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue()
+ desktopTask.dismiss()
+
+ // Dismiss focused task
+ val focusedTask2 = overview.currentTask
+ assertTaskContentDescription(focusedTask2, TEST_ACTIVITY_1)
+ focusedTask2.dismiss()
+
+ assertWithMessage("Still have tasks after dismissing all the tasks")
+ .that(mLauncher.workspace.switchToOverview().hasTasks())
+ .isFalse()
+ }
+
+ private fun assertTaskContentDescription(task: OverviewTask, activityIndex: Int) {
+ assertWithMessage("The current task content description is not TestActivity$activityIndex.")
+ .that(task.containsContentDescription("TestActivity$activityIndex"))
+ .isTrue()
+ }
+
+ private fun moveTaskToDesktop(activityIndex: Int): LaunchedAppState {
+ return mLauncher.overview
+ .getTestActivityTask(activityIndex)
+ .tapMenu()
+ .tapDesktopMenuItem()
+ .also { assertTestAppLaunched(activityIndex) }
+ }
+
private fun startTestAppsWithCheck() {
TEST_ACTIVITIES.forEach {
startTestActivity(it)
@@ -91,7 +164,7 @@
.that(
mDevice.wait(
Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")),
- DEFAULT_UI_TIMEOUT
+ DEFAULT_UI_TIMEOUT,
)
)
.isTrue()
@@ -100,6 +173,7 @@
companion object {
const val TEST_ACTIVITY_1 = 2
const val TEST_ACTIVITY_2 = 3
+ const val TEST_ACTIVITY_EXTRA = 4
val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2)
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/WindowAnimatorTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/WindowAnimatorTest.kt
deleted file mode 100644
index e5e6df3..0000000
--- a/quickstep/tests/src/com/android/quickstep/desktop/WindowAnimatorTest.kt
+++ /dev/null
@@ -1,139 +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.desktop
-
-import android.animation.ValueAnimator
-import android.content.Context
-import android.content.res.Resources
-import android.graphics.Rect
-import android.util.DisplayMetrics
-import android.view.SurfaceControl
-import android.window.TransitionInfo
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
-import com.android.app.animation.Interpolators
-import com.android.launcher3.desktop.WindowAnimator
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyFloat
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class WindowAnimatorTest {
-
- private val context = mock<Context>()
- private val resources = mock<Resources>()
- private val transaction = mock<SurfaceControl.Transaction>()
- private val change = mock<TransitionInfo.Change>()
- private val leash = mock<SurfaceControl>()
-
- private val displayMetrics = DisplayMetrics().apply { density = 1f }
-
- @Before
- fun setup() {
- whenever(context.resources).thenReturn(resources)
- whenever(resources.displayMetrics).thenReturn(displayMetrics)
- whenever(change.leash).thenReturn(leash)
- whenever(change.startAbsBounds).thenReturn(START_BOUNDS)
- whenever(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction)
- whenever(transaction.setScale(any(), anyFloat(), anyFloat())).thenReturn(transaction)
- }
-
- @Test
- fun createBoundsAnimator_returnsCorrectDefaultAnimatorParams() = runOnUiThread {
- val boundsAnimParams =
- WindowAnimator.BoundsAnimationParams(
- durationMs = 100L,
- interpolator = Interpolators.STANDARD_ACCELERATE,
- )
-
- val valueAnimator =
- WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
-
- assertThat(valueAnimator.duration).isEqualTo(100L)
- assertThat(valueAnimator.interpolator).isEqualTo(Interpolators.STANDARD_ACCELERATE)
- assertStartAndEndBounds(valueAnimator, startBounds = START_BOUNDS, endBounds = START_BOUNDS)
- }
-
- @Test
- fun createBoundsAnimator_startScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
- val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
- whenever(change.startAbsBounds).thenReturn(bounds)
- val boundsAnimParams =
- WindowAnimator.BoundsAnimationParams(
- durationMs = 100L,
- startOffsetYDp = 10f,
- startScale = 0.5f,
- interpolator = Interpolators.STANDARD_ACCELERATE,
- )
-
- val valueAnimator =
- WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
-
- assertStartAndEndBounds(
- valueAnimator,
- startBounds =
- Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
- endBounds = bounds,
- )
- }
-
- @Test
- fun createBoundsAnimator_endScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
- val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
- whenever(change.startAbsBounds).thenReturn(bounds)
- val boundsAnimParams =
- WindowAnimator.BoundsAnimationParams(
- durationMs = 100L,
- endOffsetYDp = 10f,
- endScale = 0.5f,
- interpolator = Interpolators.STANDARD_ACCELERATE,
- )
-
- val valueAnimator =
- WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
-
- assertStartAndEndBounds(
- valueAnimator,
- startBounds = bounds,
- endBounds = Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
- )
- }
-
- private fun assertStartAndEndBounds(
- valueAnimator: ValueAnimator,
- startBounds: Rect,
- endBounds: Rect,
- ) {
- valueAnimator.start()
- valueAnimator.animatedValue
- assertThat(valueAnimator.animatedValue).isEqualTo(startBounds)
- valueAnimator.end()
- assertThat(valueAnimator.animatedValue).isEqualTo(endBounds)
- }
-
- companion object {
- private val START_BOUNDS =
- Rect(/* left= */ 10, /* top= */ 20, /* right= */ 30, /* bottom= */ 40)
- }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
index 82361aa..99c74be 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
@@ -43,11 +43,11 @@
val currentTask = overviewWithSplitPair.currentTask
currentTask.containsContentDescription(
By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(),
- OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT
+ OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT,
)
currentTask.containsContentDescription(
By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(),
- OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT
+ OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT,
)
return overviewWithSplitPair
}
diff --git a/res/drawable/work_mode_fab_background.xml b/res/drawable/work_mode_fab_background.xml
index 6be33e8..fd948d1 100644
--- a/res/drawable/work_mode_fab_background.xml
+++ b/res/drawable/work_mode_fab_background.xml
@@ -19,6 +19,9 @@
<shape android:shape="rectangle">
<corners android:radius="@dimen/work_fab_radius" />
<solid android:color="@color/work_fab_bg_color" />
+ <padding
+ android:left="@dimen/work_mode_fab_background_horizontal_padding"
+ android:right="@dimen/work_mode_fab_background_horizontal_padding"/>
</shape>
</item>
</ripple>
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 71c77b5..33a50b0 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -15,7 +15,7 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto">
- <FrameLayout
+ <LinearLayout
android:id="@+id/widgets_two_pane_sheet_paged_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -23,40 +23,17 @@
android:layout_gravity="start"
android:clipChildren="false"
android:clipToPadding="false"
- android:layout_alignParentStart="true">
- <!-- Note: the paddingHorizontal has to be on WidgetPagedView level so that talkback
- correctly orders the lists to be after the search and suggestions header. See b/209579563.
- -->
- <com.android.launcher3.widget.picker.WidgetPagedView
- android:id="@+id/widgets_view_pager"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipToPadding="false"
- android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
- android:descendantFocusability="afterDescendants"
- launcher:pageIndicator="@+id/tabs" >
-
- <com.android.launcher3.widget.picker.WidgetsRecyclerView
- android:id="@+id/primary_widgets_list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipToPadding="false" />
-
- <com.android.launcher3.widget.picker.WidgetsRecyclerView
- android:id="@+id/work_widgets_list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipToPadding="false" />
-
- </com.android.launcher3.widget.picker.WidgetPagedView>
-
+ android:layout_alignParentStart="true"
+ android:orientation="vertical">
<!-- SearchAndRecommendationsView without the tab layout as well -->
<!-- Note: the horizontal padding matches with the WidgetPagedView -->
- <com.android.launcher3.views.StickyHeaderLayout
+ <LinearLayout
android:id="@+id/search_and_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToOutline="true"
+ android:elevation="1dp"
+ android:background="?attr/widgetPickerPrimarySurfaceColor"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
android:orientation="vertical">
@@ -67,6 +44,7 @@
android:orientation="horizontal"
android:background="?attr/widgetPickerPrimarySurfaceColor"
android:gravity="center_vertical"
+ android:layout_marginBottom="8dp"
launcher:layout_sticky="true">
<FrameLayout
android:layout_width="0dp"
@@ -98,7 +76,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/suggestions_header"
- android:layout_marginTop="8dp"
android:orientation="horizontal"
android:background="?attr/widgetPickerPrimarySurfaceColor"
launcher:layout_sticky="true">
@@ -140,6 +117,31 @@
style="?android:attr/borderlessButtonStyle" />
</com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
- </com.android.launcher3.views.StickyHeaderLayout>
- </FrameLayout>
+ </LinearLayout>
+ <!-- Note: the paddingHorizontal has to be on WidgetPagedView level so that talkback
+ correctly orders the lists to be after the search and suggestions header. See b/209579563.
+ -->
+ <com.android.launcher3.widget.picker.WidgetPagedView
+ android:id="@+id/widgets_view_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+ android:descendantFocusability="afterDescendants"
+ launcher:pageIndicator="@+id/tabs" >
+
+ <com.android.launcher3.widget.picker.WidgetsRecyclerView
+ android:id="@+id/primary_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false" />
+
+ <com.android.launcher3.widget.picker.WidgetsRecyclerView
+ android:id="@+id/work_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false" />
+
+ </com.android.launcher3.widget.picker.WidgetPagedView>
+ </LinearLayout>
</merge>
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index c6b3b74..94f141b 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -15,28 +15,22 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto">
- <FrameLayout
+ <LinearLayout
android:id="@+id/widgets_two_pane_sheet_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="start"
android:layout_gravity="start"
android:clipChildren="false"
- android:layout_alignParentStart="true">
-
- <com.android.launcher3.widget.picker.WidgetsRecyclerView
- android:id="@+id/primary_widgets_list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
- android:clipToPadding="false" />
-
+ android:layout_alignParentStart="true"
+ android:orientation="vertical">
<!-- SearchAndRecommendationsView without the tab layout as well -->
- <com.android.launcher3.views.StickyHeaderLayout
+ <LinearLayout
android:id="@+id/search_and_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToOutline="true"
+ android:background="?attr/widgetPickerPrimarySurfaceColor"
android:orientation="vertical">
<LinearLayout
@@ -83,6 +77,13 @@
android:background="?attr/widgetPickerPrimarySurfaceColor"
launcher:layout_sticky="true">
</FrameLayout>
- </com.android.launcher3.views.StickyHeaderLayout>
- </FrameLayout>
+ </LinearLayout>
+
+ <com.android.launcher3.widget.picker.WidgetsRecyclerView
+ android:id="@+id/primary_widgets_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+ android:clipToPadding="false" />
+ </LinearLayout>
</merge>
\ No newline at end of file
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index b3484c9..fc59e56 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -23,18 +23,16 @@
android:gravity="center_vertical"
android:background="@drawable/work_mode_fab_background"
android:forceHasOverlappingRendering="false"
- android:contentDescription="@string/work_apps_pause_btn_text"
- android:paddingStart="@dimen/work_mode_fab_background_start_padding"
- android:paddingEnd="@dimen/work_mode_fab_background_end_padding"
- android:animateLayoutChanges="true">
+ android:contentDescription="@string/work_apps_pause_btn_text">
<ImageView
android:id="@+id/work_icon"
android:layout_width="@dimen/work_fab_icon_size"
android:layout_height="@dimen/work_fab_icon_size"
+ android:layout_marginVertical="@dimen/work_fab_icon_vertical_margin"
android:importantForAccessibility="no"
- android:layout_marginEnd="@dimen/work_fab_icon_end_margin"
android:src="@drawable/ic_corp_off"
android:tint="@color/work_fab_icon_color"
+ android:layout_marginStart="@dimen/work_fab_icon_start_margin_expanded"
android:scaleType="center"/>
<TextView
android:id="@+id/pause_text"
@@ -46,8 +44,8 @@
android:includeFontPadding="false"
android:textDirection="locale"
android:text="@string/work_apps_pause_btn_text"
+ android:layout_marginStart="@dimen/work_fab_text_start_margin"
android:layout_marginEnd="@dimen/work_fab_text_end_margin"
- android:ellipsize="end"
android:maxLines="1"
style="@style/TextHeadline"/>
</com.android.launcher3.allapps.WorkModeSwitch>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 5ddf8a2..6941139 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -189,7 +189,7 @@
<string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Nyahjeda"</string>
<string name="developer_options_filter_hint" msgid="5896817443635989056">"Tapis"</string>
<string name="remote_action_failed" msgid="1383965239183576790">"Gagal: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
- <string name="private_space_label" msgid="2359721649407947001">"Ruang privasi"</string>
+ <string name="private_space_label" msgid="2359721649407947001">"Ruang persendirian"</string>
<string name="private_space_secondary_label" msgid="9203933341714508907">"Ketik untuk menyediakan atau membuka"</string>
<string name="ps_container_title" msgid="4391796149519594205">"Persendirian"</string>
<string name="ps_container_settings" msgid="6059734123353320479">"Tetapan Ruang Peribadi"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 504218b..b0b7aa2 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -79,7 +79,6 @@
<string name="contextual_edu_manager_class" translatable="false"></string>
<!-- Used for determining category of a widget presented in widget recommendations. -->
<string name="widget_recommendation_category_provider_class" translatable="false"></string>
- <string name="api_wrapper_class" translatable="false"></string>
<!-- Default packages -->
<string name="wallpaper_picker_package" translatable="false"></string>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 731e24e..037687d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -156,15 +156,16 @@
<dimen name="work_fab_height">56dp</dimen>
<dimen name="work_fab_radius">16dp</dimen>
<dimen name="work_fab_icon_size">24dp</dimen>
- <dimen name="work_fab_icon_end_margin">12dp</dimen>
- <dimen name="work_fab_text_end_margin">16dp</dimen>
+ <dimen name="work_fab_icon_vertical_margin">16dp</dimen>
+ <dimen name="work_fab_icon_start_margin_expanded">4dp</dimen>
+ <dimen name="work_fab_text_start_margin">8dp</dimen>
+ <dimen name="work_fab_text_end_margin">10dp</dimen>
<dimen name="work_card_padding_horizontal">10dp</dimen>
<dimen name="work_fab_width">214dp</dimen>
<dimen name="work_card_button_height">52dp</dimen>
<dimen name="work_fab_margin">16dp</dimen>
<dimen name="work_fab_margin_bottom">20dp</dimen>
- <dimen name="work_mode_fab_background_start_padding">16dp</dimen>
- <dimen name="work_mode_fab_background_end_padding">4dp</dimen>
+ <dimen name="work_mode_fab_background_horizontal_padding">16dp</dimen>
<dimen name="work_profile_footer_padding">20dp</dimen>
<dimen name="work_edu_card_margin">16dp</dimen>
<dimen name="work_edu_card_radius">16dp</dimen>
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 3774ae3..2e75261 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -306,10 +306,6 @@
removeActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_DEFERRED_RESUMED);
}
- public boolean isPaused() {
- return !hasBeenResumed() && (mActivityFlags & ACTIVITY_STATE_DEFERRED_RESUMED) == 0;
- }
-
/**
* Sets the activity to appear as resumed.
*/
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 177b28c..50e78ac 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -80,7 +80,7 @@
updateTheme();
}
- private void updateTheme() {
+ protected void updateTheme() {
if (mThemeRes != Themes.getActivityThemeRes(this)) {
recreate();
}
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 8862550..4305703 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -24,6 +24,7 @@
import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_LANDSCAPE;
import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT;
import static com.android.launcher3.Utilities.dpiFromPx;
+import static com.android.launcher3.Utilities.isEnglishLanguage;
import static com.android.launcher3.Utilities.pxFromSp;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.icons.GraphicsUtils.getShapePath;
@@ -1344,8 +1345,14 @@
}
if ((Flags.enableTwolineToggle()
&& LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context))) {
- // Add extra textHeight to the existing allAppsCellHeight.
- allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx);
+ if (!isEnglishLanguage(context)) {
+ // Set toggle preference value to false if not english here as it's possible the
+ // preference is stale after language change.
+ LauncherPrefs.get(context).put(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE, false);
+ } else {
+ // Add extra textHeight to the existing allAppsCellHeight.
+ allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx);
+ }
}
updateHotseatSizes(iconSizePx);
@@ -2019,6 +2026,18 @@
}
/**
+ * Returns the number of pixels the hotseat icons vertical center is translated from the bottom
+ * of the screen.
+ */
+ public int getHotseatVerticalCenter() {
+ return hotseatBarSizePx
+ - (isQsbInline ? 0 : hotseatQsbVisualHeight)
+ - hotseatQsbSpace
+ - (hotseatCellHeightPx / 2)
+ + ((hotseatCellHeightPx - iconSizePx) / 2);
+ }
+
+ /**
* Returns the number of pixels the taskbar is translated from the bottom of the screen.
*/
public int getTaskbarOffsetY() {
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 6b478be..27602af 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -186,9 +186,6 @@
*/
public void adjustForBubbleBar(boolean isBubbleBarVisible) {
DeviceProfile dp = mActivity.getDeviceProfile();
- if (!dp.shouldAdjustHotseatForBubbleBar(getContext(), isBubbleBarVisible)) {
- return;
- }
ShortcutAndWidgetContainer icons = getShortcutsAndWidgets();
AnimatorSet animatorSet = new AnimatorSet();
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 5ea7bd9..ece6540 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -34,6 +34,7 @@
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.Trace;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
@@ -497,7 +498,11 @@
public void setCurrentGrid(Context context, String gridName) {
LauncherPrefs.get(context).put(GRID_NAME, gridName);
- MAIN_EXECUTOR.execute(() -> onConfigChanged(context.getApplicationContext()));
+ MAIN_EXECUTOR.execute(() -> {
+ Trace.beginSection("InvariantDeviceProfile#setCurrentGrid");
+ onConfigChanged(context.getApplicationContext());
+ Trace.endSection();
+ });
}
private Object[] toModelState() {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8547eb4..983cf8d 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1345,7 +1345,8 @@
if (requestArgs != null) {
setWaitingForResult(requestArgs);
}
- mPendingActivityRequestCode = savedState.getInt(RUNTIME_STATE_PENDING_REQUEST_CODE);
+ mPendingActivityRequestCode = savedState.getInt(
+ RUNTIME_STATE_PENDING_REQUEST_CODE, mPendingActivityRequestCode);
mPendingActivityResult = savedState.getParcelable(RUNTIME_STATE_PENDING_ACTIVITY_RESULT);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 42a28d6..b6da164 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -163,8 +163,7 @@
LockedUserState.get(context).runOnUserUnlocked(() -> {
CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
- cwm.setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
- mOnTerminateCallback.add(() -> cwm.setWidgetRefreshCallback(null));
+ mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
IconObserver observer = new IconObserver();
SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java
index 4c82e56..678901b 100644
--- a/src/com/android/launcher3/LauncherApplication.java
+++ b/src/com/android/launcher3/LauncherApplication.java
@@ -26,15 +26,25 @@
*/
public class LauncherApplication extends Application {
- private LauncherBaseAppComponent mAppComponent;
+ private volatile LauncherBaseAppComponent mAppComponent;
@Override
public void onCreate() {
super.onCreate();
MainProcessInitializer.initialize(this);
- initDagger();
}
public LauncherAppComponent getAppComponent() {
+ if (mAppComponent == null) {
+ synchronized (this) {
+ // Check for null again, as it may have been assigned on a different thread. This
+ // avoids holding synchronization locks everytime.
+ if (mAppComponent == null) {
+ // Initialize the dagger component on demand as content providers can get
+ // accessed before the Launcher application (b/36917845#comment4)
+ initDaggerComponent(DaggerLauncherAppComponent.builder());
+ }
+ }
+ }
// Since supertype setters will return a supertype.builder and @Component.Builder types
// must not have any generic types.
// We need to cast mAppComponent to {@link LauncherAppComponent} since appContext()
@@ -42,7 +52,10 @@
return (LauncherAppComponent) mAppComponent;
}
- protected void initDagger() {
- mAppComponent = DaggerLauncherAppComponent.builder().appContext(this).build();
+ /**
+ * Init with the desired dagger component.
+ */
+ public void initDaggerComponent(LauncherAppComponent.Builder componentBuilder) {
+ mAppComponent = componentBuilder.appContext(this).build();
}
}
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
index 2617b93..a96495d 100644
--- a/src/com/android/launcher3/LauncherBackupAgent.java
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -1,5 +1,7 @@
package com.android.launcher3;
+import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED;
+
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
@@ -10,10 +12,13 @@
import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.stream.Collectors;
public class LauncherBackupAgent extends BackupAgent {
-
private static final String TAG = "LauncherBackupAgent";
+ private static final String DB_FILE_PREFIX = "launcher";
+ private static final String DB_FILE_SUFFIX = ".db";
@Override
public void onCreate() {
@@ -47,7 +52,34 @@
@Override
public void onRestoreFinished() {
- FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
RestoreDbTask.setPending(this);
+ FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
+ markIfFilesWereNotActuallyRestored();
+ }
+
+ /**
+ * When restore is finished, we check to see if any db files were successfully restored. If not,
+ * our restore will fail later, but will report a different cause. This is important to split
+ * out the metric failures that are launcher's fault, and those that are due to bugs in the
+ * backup/restore code itself.
+ */
+ private void markIfFilesWereNotActuallyRestored() {
+ File directory = new File(getDatabasePath(InvariantDeviceProfile.INSTANCE.get(this).dbFile)
+ .getParent());
+ if (!directory.exists()) {
+ FileLog.e(TAG, "restore failed as target database directory doesn't exist");
+ } else {
+ // Check for any db file that was restored, and collect as list
+ String fileNames = Arrays.stream(directory.listFiles())
+ .map(File::getName)
+ .filter(n -> n.startsWith(DB_FILE_PREFIX) && n.endsWith(DB_FILE_SUFFIX))
+ .collect(Collectors.joining(", "));
+ if (fileNames.isBlank()) {
+ FileLog.e(TAG, "no database files were successfully restored");
+ LauncherPrefs.get(this).putSync(NO_DB_FILES_RESTORED.to(true));
+ } else {
+ FileLog.d(TAG, "database files successfully restored: " + fileNames);
+ }
+ }
}
}
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index a013eaa..85ecd58 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -25,7 +25,6 @@
import android.util.Pair
import androidx.annotation.WorkerThread
import com.android.launcher3.celllayout.CellPosMapper
-import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.icons.IconCache
import com.android.launcher3.model.AddWorkspaceItemsTask
import com.android.launcher3.model.AllAppsList
@@ -67,8 +66,8 @@
private val context: Context,
private val mApp: LauncherAppState,
private val iconCache: IconCache,
- private val appFilter: AppFilter,
- private val mPmHelper: PackageManagerHelper,
+ appFilter: AppFilter,
+ mPmHelper: PackageManagerHelper,
isPrimaryInstance: Boolean,
) {
@@ -304,9 +303,6 @@
launcherBinder.bindAllApps()
launcherBinder.bindDeepShortcuts()
launcherBinder.bindWidgets()
- if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- this.modelDelegate.bindAllModelExtras(callbacksList)
- }
return true
} else {
mLoaderTask =
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 7ebfc18..5c03644 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -126,6 +126,9 @@
EncryptionType.ENCRYPTED,
)
@JvmField
+ val NO_DB_FILES_RESTORED =
+ nonRestorableItem("no_db_files_restored", false, EncryptionType.DEVICE_PROTECTED)
+ @JvmField
val IS_FIRST_LOAD_AFTER_RESTORE =
nonRestorableItem(FIRST_LOAD_AFTER_RESTORE_KEY, false, EncryptionType.ENCRYPTED)
@JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 9192e13..71a2589 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -616,6 +616,14 @@
}
/**
+ * Utility method to know if a device's primary language is English.
+ */
+ public static boolean isEnglishLanguage(Context context) {
+ return context.getResources().getConfiguration().locale.getLanguage()
+ .equals(Locale.ENGLISH.getLanguage());
+ }
+
+ /**
* Returns the full drawable for info as multiple layers of AdaptiveIconDrawable. The second
* drawable in the Pair is the badge used with the icon.
*
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 1094768..0dd2791 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -296,6 +296,10 @@
// Add the search box above everything else in this container (if the flag is enabled,
// it's added to drag layer in onAttach instead).
addView(mSearchContainer);
+ // The search container is visually at the top of the all apps UI, and should thus be
+ // focused by default. It's added to end of the children list, so it needs to be
+ // explicitly marked as focused by default.
+ mSearchContainer.setFocusedByDefault(true);
}
mSearchUiManager = (SearchUiManager) mSearchContainer;
}
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 6049574..f1f72b2 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -15,10 +15,18 @@
*/
package com.android.launcher3.allapps;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Rect;
+import android.text.TextUtils;
import android.util.AttributeSet;
+import android.view.ViewGroup;
import android.view.WindowInsets;
+import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -26,10 +34,12 @@
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
+import com.android.app.animation.Interpolators;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatedPropertySetter;
import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
import com.android.launcher3.model.StringCache;
import com.android.launcher3.views.ActivityContext;
@@ -39,9 +49,14 @@
public class WorkModeSwitch extends LinearLayout implements Insettable,
KeyboardInsetAnimationCallback.KeyboardInsetListener {
+ 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;
+ private static final int TEXT_ALPHA_EXPAND_DELAY = 80;
+ private static final int TEXT_ALPHA_COLLAPSE_DELAY = 0;
private static final int FLAG_FADE_ONGOING = 1 << 1;
private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
- private static final int FLAG_PROFILE_TOGGLE_ONGOING = 1 << 3;
+ private static final int FLAG_IS_EXPAND = 1 << 3;
private static final int SCROLL_THRESHOLD_DP = 10;
private final Rect mInsets = new Rect();
@@ -49,11 +64,15 @@
private int mFlags;
private final ActivityContext mActivityContext;
private final Context mContext;
+ private final int mTextMarginStart;
+ private final int mTextMarginEnd;
+ private final int mIconMarginStart;
// Threshold when user scrolls up/down to determine when should button extend/collapse
private final int mScrollThreshold;
private TextView mTextView;
-
+ private ImageView mIcon;
+ private ValueAnimator mPauseFABAnim;
public WorkModeSwitch(@NonNull Context context) {
this(context, null, 0);
@@ -68,6 +87,12 @@
mContext = context;
mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
mActivityContext = ActivityContext.lookupContext(getContext());
+ mTextMarginStart = mContext.getResources().getDimensionPixelSize(
+ R.dimen.work_fab_text_start_margin);
+ mTextMarginEnd = mContext.getResources().getDimensionPixelSize(
+ R.dimen.work_fab_text_end_margin);
+ mIconMarginStart = mContext.getResources().getDimensionPixelSize(
+ R.dimen.work_fab_icon_start_margin_expanded);
}
@Override
@@ -75,11 +100,13 @@
super.onFinishInflate();
mTextView = findViewById(R.id.pause_text);
+ mIcon = findViewById(R.id.work_icon);
setSelected(true);
KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
new KeyboardInsetAnimationCallback(this);
setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
-
+ // Expand is the default state upon initialization.
+ addFlag(FLAG_IS_EXPAND);
setInsets(mActivityContext.getDeviceProfile().getInsets());
updateStringFromCache();
}
@@ -114,18 +141,18 @@
@Override
public boolean isEnabled() {
- return super.isEnabled() && getVisibility() == VISIBLE && mFlags == 0;
+ return super.isEnabled() && getVisibility() == VISIBLE;
}
public void animateVisibility(boolean visible) {
clearAnimation();
if (visible) {
- setFlag(FLAG_FADE_ONGOING);
+ addFlag(FLAG_FADE_ONGOING);
setVisibility(VISIBLE);
extend();
animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
} else if (getVisibility() != GONE) {
- setFlag(FLAG_FADE_ONGOING);
+ addFlag(FLAG_FADE_ONGOING);
animate().alpha(0).withEndAction(() -> {
removeFlag(FLAG_FADE_ONGOING);
setVisibility(GONE);
@@ -156,6 +183,79 @@
super.setTranslationY(Math.min(translationY, -mInsets.bottom));
}
+
+ private void animatePillTransition(boolean isExpanding) {
+ if (!shouldAnimate(isExpanding)) {
+ return;
+ }
+ AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
+ mTextView.measure(0,0);
+ int currentWidth = mTextView.getWidth();
+ int fullWidth = mTextView.getMeasuredWidth();
+ float from = isExpanding ? 0 : currentWidth;
+ float to = isExpanding ? fullWidth : 0;
+ mPauseFABAnim = ObjectAnimator.ofFloat(from, to);
+ mPauseFABAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+ mPauseFABAnim.setInterpolator(Interpolators.STANDARD);
+ mPauseFABAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float translation = (float) valueAnimator.getAnimatedValue();
+ float translationFraction = translation / fullWidth;
+ ViewGroup.MarginLayoutParams textViewLayoutParams =
+ (ViewGroup.MarginLayoutParams) mTextView.getLayoutParams();
+ textViewLayoutParams.width = (int) translation;
+ textViewLayoutParams.setMarginStart((int) (mTextMarginStart * translationFraction));
+ textViewLayoutParams.setMarginEnd((int) (mTextMarginEnd * translationFraction));
+ mTextView.setLayoutParams(textViewLayoutParams);
+ ViewGroup.MarginLayoutParams iconLayoutParams =
+ (ViewGroup.MarginLayoutParams) mIcon.getLayoutParams();
+ iconLayoutParams.setMarginStart((int) (mIconMarginStart * translationFraction));
+ mIcon.setLayoutParams(iconLayoutParams);
+ }
+ });
+ mPauseFABAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ if (isExpanding) {
+ addFlag(FLAG_IS_EXPAND);
+ } else {
+ mTextView.setVisibility(GONE);
+ removeFlag(FLAG_IS_EXPAND);
+ }
+ mTextView.setHorizontallyScrolling(false);
+ mTextView.setEllipsize(TextUtils.TruncateAt.END);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animator) {
+ mTextView.setHorizontallyScrolling(true);
+ mTextView.setVisibility(VISIBLE);
+ mTextView.setEllipsize(null);
+ }
+ });
+ animatorSet.playTogether(mPauseFABAnim, updatePauseTextAlpha(isExpanding));
+ animatorSet.start();
+ }
+
+
+ private ValueAnimator updatePauseTextAlpha(boolean expand) {
+ float from = expand ? 0 : 1;
+ float to = expand ? 1 : 0;
+ ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
+ alphaAnim.setDuration(expand ? TEXT_EXPAND_OPACITY_DURATION
+ : TEXT_COLLAPSE_OPACITY_DURATION);
+ alphaAnim.setStartDelay(expand ? TEXT_ALPHA_EXPAND_DELAY : TEXT_ALPHA_COLLAPSE_DELAY);
+ alphaAnim.setInterpolator(Interpolators.LINEAR);
+ alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ mTextView.setAlpha((float) valueAnimator.getAnimatedValue());
+ }
+ });
+ return alphaAnim;
+ }
+
private void setInsets(Rect rect, Insets insets) {
rect.set(insets.left, insets.top, insets.right, insets.bottom);
}
@@ -166,7 +266,7 @@
@Override
public void onTranslationStart() {
- setFlag(FLAG_TRANSLATION_ONGOING);
+ addFlag(FLAG_TRANSLATION_ONGOING);
}
@Override
@@ -174,7 +274,7 @@
removeFlag(FLAG_TRANSLATION_ONGOING);
}
- private void setFlag(int flag) {
+ private void addFlag(int flag) {
mFlags |= flag;
}
@@ -182,12 +282,25 @@
mFlags &= ~flag;
}
+ private boolean containsFlag(int flag) {
+ return (mFlags & flag) == flag;
+ }
+
public void extend() {
- mTextView.setVisibility(VISIBLE);
+ animatePillTransition(true);
}
public void shrink(){
- mTextView.setVisibility(GONE);
+ animatePillTransition(false);
+ }
+
+ /**
+ * Determines if the button should animate based on current state. It should animate the button
+ * only if it is not in the same state it is animating to.
+ */
+ private boolean shouldAnimate(boolean expanding) {
+ return expanding != containsFlag(FLAG_IS_EXPAND)
+ && (mPauseFABAnim == null || !mPauseFABAnim.isRunning());
}
public int getScrollThreshold() {
diff --git a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
index e6654b1..b05539a 100644
--- a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
+++ b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
@@ -24,9 +24,10 @@
RestoreError.WIDGETS_DISABLED,
RestoreError.PROFILE_NOT_RESTORED,
RestoreError.WIDGET_REMOVED,
+ RestoreError.DATABASE_FILE_NOT_RESTORED,
RestoreError.GRID_MIGRATION_FAILURE,
RestoreError.NO_SEARCH_WIDGET,
- RestoreError.INVALID_WIDGET_ID
+ RestoreError.INVALID_WIDGET_ID,
)
annotation class RestoreError {
companion object {
@@ -38,6 +39,7 @@
const val APP_NOT_INSTALLED = "app_not_installed"
const val WIDGETS_DISABLED = "widgets_disabled"
const val PROFILE_NOT_RESTORED = "profile_not_restored"
+ const val DATABASE_FILE_NOT_RESTORED = "db_file_not_restored"
const val WIDGET_REMOVED = "widget_not_found"
const val GRID_MIGRATION_FAILURE = "grid_migration_failed"
const val NO_SEARCH_WIDGET = "no_search_widget"
@@ -52,7 +54,7 @@
return ResourceBasedOverride.Overrides.getObject(
LauncherRestoreEventLogger::class.java,
context,
- R.string.launcher_restore_event_logger_class
+ R.string.launcher_restore_event_logger_class,
)
}
}
@@ -117,7 +119,7 @@
open fun logFavoritesItemsRestoreFailed(
favoritesId: Int,
count: Int,
- @RestoreError error: String?
+ @RestoreError error: String?,
) {
// no-op
}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 8fe1b34..9e38824 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -62,17 +62,6 @@
* and set a default value for the flag. This will be the default value on Debug builds.
* <p>
*/
- // TODO(Block 3): Clean up flags
- public static final BooleanFlag ENABLE_WORKSPACE_LOADING_OPTIMIZATION = getDebugFlag(251502424,
- "ENABLE_WORKSPACE_LOADING_OPTIMIZATION", DISABLED,
- "load the current workspace screen visible to the user before the rest rather than "
- + "loading all of them at once.");
-
- public static final BooleanFlag CHANGE_MODEL_DELEGATE_LOADING_ORDER = getDebugFlag(251502424,
- "CHANGE_MODEL_DELEGATE_LOADING_ORDER", DISABLED,
- "changes the timing of the loading and binding of delegate items during "
- + "data preparation for loading the home screen");
-
// TODO(Block 6): Clean up flags
public static final BooleanFlag SECONDARY_DRAG_N_DROP_TO_PIN = getDebugFlag(270395140,
"SECONDARY_DRAG_N_DROP_TO_PIN", DISABLED,
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 4537785..5d0c44e 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -19,7 +19,9 @@
import android.content.Context;
import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.util.ApiWrapper;
import com.android.launcher3.util.DaggerSingletonTracker;
+import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PluginManagerWrapper;
import com.android.launcher3.util.ScreenOnTracker;
import com.android.launcher3.util.SettingsCache;
@@ -40,10 +42,12 @@
DaggerSingletonTracker getDaggerSingletonTracker();
RefreshRateTracker getRefreshRateTracker();
InstallSessionHelper getInstallSessionHelper();
+ ApiWrapper getApiWrapper();
ScreenOnTracker getScreenOnTracker();
SettingsCache getSettingsCache();
CustomWidgetManager getCustomWidgetManager();
PluginManagerWrapper getPluginManagerWrapper();
+ PackageManagerHelper getPackageManagerHelper();
/** Builder for LauncherBaseAppComponent. */
interface Builder {
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index e3c2d36..1dd7d45 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -55,7 +55,7 @@
import com.android.launcher3.model.BaseLauncherBinder;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.model.GridSizeMigrationUtil;
+import com.android.launcher3.model.GridSizeMigrationDBController;
import com.android.launcher3.model.LoaderTask;
import com.android.launcher3.model.ModelDbController;
import com.android.launcher3.provider.LauncherDbUtils;
@@ -284,7 +284,7 @@
private void loadModelData() {
final Context inflationContext = getPreviewContext();
final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName);
- if (GridSizeMigrationUtil.needsToMigrate(inflationContext, idp)) {
+ if (GridSizeMigrationDBController.needsToMigrate(inflationContext, idp)) {
// Start the migration
PreviewContext previewContext = new PreviewContext(inflationContext, idp);
// Copy existing data to preview DB
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 5faa2b8..b51f855 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -36,7 +36,6 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.Workspace;
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -59,11 +58,9 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Set;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@@ -100,13 +97,29 @@
public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
Trace.beginSection("BaseLauncherBinder#bindWorkspace");
try {
- if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
- DisjointWorkspaceBinder workspaceBinder =
- initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
- workspaceBinder.bindCurrentWorkspacePages(isBindSync);
- workspaceBinder.bindOtherWorkspacePages();
- } else {
- bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+ // Save a copy of all the bg-thread collections
+ ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
+ ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
+ final IntArray orderedScreenIds = new IntArray();
+ ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
+ final int workspaceItemCount;
+ synchronized (mBgDataModel) {
+ workspaceItems.addAll(mBgDataModel.workspaceItems);
+ appWidgets.addAll(mBgDataModel.appWidgets);
+ orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
+ mBgDataModel.extraItems.forEach(extraItems::add);
+ if (incrementBindId) {
+ mBgDataModel.lastBindId++;
+ mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
+ }
+ mMyBindingId = mBgDataModel.lastBindId;
+ workspaceItemCount = mBgDataModel.itemsIdMap.size();
+ }
+
+ for (Callbacks cb : mCallbacksList) {
+ new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+ workspaceItems, appWidgets, extraItems, orderedScreenIds)
+ .bind(isBindSync, workspaceItemCount);
}
} finally {
Trace.endSection();
@@ -114,53 +127,6 @@
}
/**
- * Initializes the WorkspaceBinder for binding.
- *
- * @param incrementBindId this is used to stop previously started binding tasks that are
- * obsolete but still queued.
- * @param workspacePages this allows the Launcher to add the correct workspace screens.
- */
- public DisjointWorkspaceBinder initWorkspaceBinder(boolean incrementBindId,
- IntArray workspacePages) {
-
- synchronized (mBgDataModel) {
- if (incrementBindId) {
- mBgDataModel.lastBindId++;
- mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
- }
- mMyBindingId = mBgDataModel.lastBindId;
- return new DisjointWorkspaceBinder(workspacePages);
- }
- }
-
- private void bindWorkspaceAllAtOnce(boolean incrementBindId, boolean isBindSync) {
- // Save a copy of all the bg-thread collections
- ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
- ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
- final IntArray orderedScreenIds = new IntArray();
- ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
- final int workspaceItemCount;
- synchronized (mBgDataModel) {
- workspaceItems.addAll(mBgDataModel.workspaceItems);
- appWidgets.addAll(mBgDataModel.appWidgets);
- orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
- mBgDataModel.extraItems.forEach(extraItems::add);
- if (incrementBindId) {
- mBgDataModel.lastBindId++;
- mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
- }
- mMyBindingId = mBgDataModel.lastBindId;
- workspaceItemCount = mBgDataModel.itemsIdMap.size();
- }
-
- for (Callbacks cb : mCallbacksList) {
- new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
- workspaceItems, appWidgets, extraItems, orderedScreenIds)
- .bind(isBindSync, workspaceItemCount);
- }
- }
-
- /**
* BindDeepShortcuts is abstract because it is a no-op for the go launcher.
*/
public void bindDeepShortcuts() {
@@ -347,10 +313,8 @@
bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
}
- if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- mExtraItems.forEach(item ->
- executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
- }
+ mExtraItems.forEach(item ->
+ executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
RunnableList pendingTasks = new RunnableList();
Executor pendingExecutor = pendingTasks::add;
@@ -440,126 +404,4 @@
});
}
}
-
- private class DisjointWorkspaceBinder {
- private final IntArray mOrderedScreenIds;
- private final IntSet mCurrentScreenIds = new IntSet();
- private final Set<Integer> mBoundItemIds = new HashSet<>();
-
- protected DisjointWorkspaceBinder(IntArray orderedScreenIds) {
- mOrderedScreenIds = orderedScreenIds;
-
- for (Callbacks cb : mCallbacksList) {
- mCurrentScreenIds.addAll(cb.getPagesToBindSynchronously(orderedScreenIds));
- }
- if (mCurrentScreenIds.size() == 0) {
- mCurrentScreenIds.add(Workspace.FIRST_SCREEN_ID);
- }
- }
-
- /**
- * Binds the currently loaded items in the Data Model. Also signals to the Callbacks[]
- * that these items have been bound and their respective screens are ready to be shown.
- *
- * If this method is called after all the items on the workspace screen have already been
- * loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will
- * not bind any items.
- */
- protected void bindCurrentWorkspacePages(boolean isBindSync) {
- // Save a copy of all the bg-thread collections
- ArrayList<ItemInfo> workspaceItems;
- ArrayList<LauncherAppWidgetInfo> appWidgets;
- ArrayList<FixedContainerItems> fciList = new ArrayList<>();
- final int workspaceItemCount;
- synchronized (mBgDataModel) {
- workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
- appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
- if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- mBgDataModel.extraItems.forEach(fciList::add);
- }
- workspaceItemCount = mBgDataModel.itemsIdMap.size();
- }
-
- workspaceItems.forEach(it -> mBoundItemIds.add(it.id));
- appWidgets.forEach(it -> mBoundItemIds.add(it.id));
- if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- fciList.forEach(item ->
- executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
- }
-
- sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
- // Tell the workspace that we're about to start binding items
- executeCallbacksTask(c -> {
- c.clearPendingBinds();
- c.startBinding();
- }, mUiExecutor);
-
- // Bind workspace screens
- executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
-
- bindWorkspaceItems(workspaceItems);
- bindAppWidgets(appWidgets);
- executeCallbacksTask(c -> {
- MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
- RunnableList onCompleteSignal = new RunnableList();
- onCompleteSignal.executeAllAndDestroy();
- c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal,
- workspaceItemCount, isBindSync);
- }, mUiExecutor);
- }
-
- protected void bindOtherWorkspacePages() {
- // Save a copy of all the bg-thread collections
- ArrayList<ItemInfo> workspaceItems;
- ArrayList<LauncherAppWidgetInfo> appWidgets;
-
- synchronized (mBgDataModel) {
- workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
- appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
- }
-
- workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id));
- appWidgets.removeIf(it -> mBoundItemIds.contains(it.id));
-
- sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
- bindWorkspaceItems(workspaceItems);
- bindAppWidgets(appWidgets);
-
- executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor);
- mUiExecutor.execute(() -> {
- MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
- ItemInstallQueue.INSTANCE.get(mApp.getContext())
- .resumeModelPush(FLAG_LOADER_RUNNING);
- });
-
- StringCache cacheClone = mBgDataModel.stringCache.clone();
- executeCallbacksTask(c -> c.bindStringCache(cacheClone), mUiExecutor);
- }
-
- private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems) {
- // Bind the workspace items
- int count = workspaceItems.size();
- for (int i = 0; i < count; i += ITEMS_CHUNK) {
- final int start = i;
- final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
- executeCallbacksTask(
- c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
- mUiExecutor);
- }
- }
-
- private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets) {
- // Bind the widgets, one at a time
- int count = appWidgets.size();
- for (int i = 0; i < count; i++) {
- final ItemInfo widget = appWidgets.get(i);
- executeCallbacksTask(
- c -> c.bindItems(Collections.singletonList(widget), false),
- mUiExecutor);
- }
- }
- }
}
diff --git a/src/com/android/launcher3/model/DbEntry.java b/src/com/android/launcher3/model/DbEntry.java
new file mode 100644
index 0000000..c0c51da
--- /dev/null
+++ b/src/com/android/launcher3/model/DbEntry.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.ContentWriter;
+
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DbEntry extends ItemInfo implements Comparable<DbEntry> {
+
+ private static final String TAG = "DbEntry";
+
+ String mIntent;
+ String mProvider;
+ Map<String, Set<Integer>> mFolderItems = new HashMap<>();
+
+ /**
+ * Id of the specific widget.
+ */
+ public int appWidgetId = NO_ID;
+
+ /** Comparator according to the reading order */
+ @Override
+ public int compareTo(DbEntry another) {
+ if (screenId != another.screenId) {
+ return Integer.compare(screenId, another.screenId);
+ }
+ if (cellY != another.cellY) {
+ return Integer.compare(cellY, another.cellY);
+ }
+ return Integer.compare(cellX, another.cellX);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DbEntry)) return false;
+ DbEntry entry = (DbEntry) o;
+ return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getEntryMigrationId());
+ }
+
+ /**
+ * Puts the updated DbEntry values into ContentValues which we then use to insert the
+ * entry to the DB.
+ */
+ public void updateContentValues(ContentValues values) {
+ values.put(LauncherSettings.Favorites.SCREEN, screenId);
+ values.put(LauncherSettings.Favorites.CELLX, cellX);
+ values.put(LauncherSettings.Favorites.CELLY, cellY);
+ values.put(LauncherSettings.Favorites.SPANX, spanX);
+ values.put(LauncherSettings.Favorites.SPANY, spanY);
+ }
+
+ @Override
+ public void writeToValues(@NonNull ContentWriter writer) {
+ super.writeToValues(writer);
+ writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
+ }
+
+ @Override
+ public void readFromValues(@NonNull ContentValues values) {
+ super.readFromValues(values);
+ appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID);
+ }
+
+ /**
+ * This id is not used in the DB is only used while doing the migration and it identifies
+ * an entry on each workspace. For example two calculator icons would have the same
+ * migration id even thought they have different database ids.
+ */
+ public String getEntryMigrationId() {
+ switch (itemType) {
+ case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+ case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
+ return getFolderMigrationId();
+ case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+ // mProvider is the app the widget belongs to and appWidgetId it's the unique
+ // is of the widget, we need both because if you remove a widget and then add it
+ // again, then it can change and the WidgetProvider would not know the widget.
+ return mProvider + appWidgetId;
+ case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+ final String intentStr = cleanIntentString(mIntent);
+ try {
+ Intent i = Intent.parseUri(intentStr, 0);
+ return Objects.requireNonNull(i.getComponent()).toString();
+ } catch (Exception e) {
+ return intentStr;
+ }
+ default:
+ return cleanIntentString(mIntent);
+ }
+ }
+
+ /**
+ * This method should return an id that should be the same for two folders containing the
+ * same elements.
+ */
+ @NonNull
+ private String getFolderMigrationId() {
+ return mFolderItems.keySet().stream()
+ .map(intentString -> mFolderItems.get(intentString).size()
+ + cleanIntentString(intentString))
+ .sorted()
+ .collect(Collectors.joining(","));
+ }
+
+ /**
+ * This is needed because sourceBounds can change and make the id of two equal items
+ * different.
+ */
+ @NonNull
+ private String cleanIntentString(@NonNull String intentStr) {
+ try {
+ Intent i = Intent.parseUri(intentStr, 0);
+ i.setSourceBounds(null);
+ return i.toURI();
+ } catch (URISyntaxException e) {
+ Log.e(TAG, "Unable to parse Intent string", e);
+ return intentStr;
+ }
+
+ }
+}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
similarity index 83%
rename from src/com/android/launcher3/model/GridSizeMigrationUtil.java
rename to src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 4c017e9..d8ca095 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -28,7 +28,6 @@
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
-import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
@@ -45,15 +44,12 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
-import com.android.launcher3.util.ContentWriter;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
-import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -61,7 +57,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -69,12 +64,12 @@
* This class takes care of shrinking the workspace (by maximum of one row and one column), as a
* result of restoring from a larger device or device density change.
*/
-public class GridSizeMigrationUtil {
+public class GridSizeMigrationDBController {
- private static final String TAG = "GridSizeMigrationUtil";
+ private static final String TAG = "GridSizeMigrationDBController";
private static final boolean DEBUG = true;
- private GridSizeMigrationUtil() {
+ private GridSizeMigrationDBController() {
// Util class should not be instantiated
}
@@ -85,7 +80,7 @@
return needsToMigrate(new DeviceGridState(context), new DeviceGridState(idp));
}
- private static boolean needsToMigrate(
+ static boolean needsToMigrate(
DeviceGridState srcDeviceState, DeviceGridState destDeviceState) {
boolean needsToMigrate = !destDeviceState.isCompatible(srcDeviceState);
if (needsToMigrate) {
@@ -95,6 +90,9 @@
return needsToMigrate;
}
+ /**
+ * @return all the workspace and hotseat entries in the db.
+ */
@VisibleForTesting
public static List<DbEntry> readAllEntries(SQLiteDatabase db, String tableName,
Context context) {
@@ -198,7 +196,7 @@
Collectors.joining(",\n", "[", "]"))
+ "\n Removing Items:"
+ dstWorkspaceItems.stream().filter(entry ->
- toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
+ toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
Collectors.joining(",\n", "[", "]"))
+ "\n Adding Workspace Items:"
+ workspaceToBeAdded.stream().map(DbEntry::toString).collect(
@@ -291,7 +289,7 @@
});
}
- private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
+ static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
String srcTableName, String destTableName, List<Integer> idsInUse) {
int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName, idsInUse);
if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER
@@ -330,10 +328,9 @@
} else {
values.put(LauncherSettings.Favorites.CONTAINER, folderId);
}
- newId = helper.generateNewItemId();
- while (idsInUse.contains(newId)) {
+ do {
newId = helper.generateNewItemId();
- }
+ } while (idsInUse.contains(newId));
values.put(LauncherSettings.Favorites._ID, newId);
helper.getWritableDatabase().insert(destTableName, null, values);
}
@@ -341,7 +338,7 @@
return newId;
}
- private static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) {
+ static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) {
db.delete(tableName,
Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null);
}
@@ -387,7 +384,7 @@
private static boolean findPlacementForEntry(@NonNull final DbEntry entry,
@NonNull final Point next, @NonNull final Point trg,
@NonNull final GridOccupancy occupied, final int screenId) {
- for (int y = next.y; y < trg.y; y++) {
+ for (int y = next.y; y < trg.y; y++) {
for (int x = next.x; x < trg.x; x++) {
boolean fits = occupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX,
@@ -413,7 +410,7 @@
private static void solveHotseatPlacement(
@NonNull final DatabaseHelper helper, final int hotseatSize,
@NonNull final DbReader srcReader, @NonNull final DbReader destReader,
- @NonNull final List<DbEntry> placedHotseatItems,
+ @NonNull final List<DbEntry> placedHotseatItems,
@NonNull final List<DbEntry> itemsToPlace, List<Integer> idsInUse) {
final boolean[] occupied = new boolean[hotseatSize];
@@ -436,15 +433,26 @@
}
}
- @VisibleForTesting
+ static void copyCurrentGridToNewGrid(
+ @NonNull Context context,
+ @NonNull DeviceGridState destDeviceState,
+ @NonNull DatabaseHelper target,
+ @NonNull SQLiteDatabase source) {
+ // Only use this strategy when comparing the previous grid to the new grid and the
+ // columns are the same and the destination has more rows
+ copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
+ destDeviceState.writeToPrefs(context);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static class DbReader {
- private final SQLiteDatabase mDb;
- private final String mTableName;
- private final Context mContext;
- private int mLastScreenId = -1;
+ final SQLiteDatabase mDb;
+ final String mTableName;
+ final Context mContext;
+ int mLastScreenId = -1;
- private final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
+ final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
new ArrayMap<>();
public DbReader(SQLiteDatabase db, String tableName, Context context) {
@@ -529,7 +537,7 @@
LauncherSettings.Favorites.INTENT, // 7
LauncherSettings.Favorites.APPWIDGET_PROVIDER, // 8
LauncherSettings.Favorites.APPWIDGET_ID}, // 9
- LauncherSettings.Favorites.CONTAINER + " = "
+ LauncherSettings.Favorites.CONTAINER + " = "
+ LauncherSettings.Favorites.CONTAINER_DESKTOP);
final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
@@ -648,118 +656,4 @@
return mDb.query(mTableName, columns, where, null, null, null, null);
}
}
-
- public static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
-
- private String mIntent;
- private String mProvider;
- private Map<String, Set<Integer>> mFolderItems = new HashMap<>();
-
- /**
- * Id of the specific widget.
- */
- public int appWidgetId = NO_ID;
-
- /** Comparator according to the reading order */
- @Override
- public int compareTo(DbEntry another) {
- if (screenId != another.screenId) {
- return Integer.compare(screenId, another.screenId);
- }
- if (cellY != another.cellY) {
- return Integer.compare(cellY, another.cellY);
- }
- return Integer.compare(cellX, another.cellX);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- DbEntry entry = (DbEntry) o;
- return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(getEntryMigrationId());
- }
-
- public void updateContentValues(ContentValues values) {
- values.put(LauncherSettings.Favorites.SCREEN, screenId);
- values.put(LauncherSettings.Favorites.CELLX, cellX);
- values.put(LauncherSettings.Favorites.CELLY, cellY);
- values.put(LauncherSettings.Favorites.SPANX, spanX);
- values.put(LauncherSettings.Favorites.SPANY, spanY);
- }
-
- @Override
- public void writeToValues(@NonNull ContentWriter writer) {
- super.writeToValues(writer);
- writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
- }
-
- @Override
- public void readFromValues(@NonNull ContentValues values) {
- super.readFromValues(values);
- appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID);
- }
-
- /** This id is not used in the DB is only used while doing the migration and it identifies
- * an entry on each workspace. For example two calculator icons would have the same
- * migration id even thought they have different database ids.
- */
- public String getEntryMigrationId() {
- switch (itemType) {
- case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
- case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
- return getFolderMigrationId();
- case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
- // mProvider is the app the widget belongs to and appWidgetId it's the unique
- // is of the widget, we need both because if you remove a widget and then add it
- // again, then it can change and the WidgetProvider would not know the widget.
- return mProvider + appWidgetId;
- case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
- final String intentStr = cleanIntentString(mIntent);
- try {
- Intent i = Intent.parseUri(intentStr, 0);
- return Objects.requireNonNull(i.getComponent()).toString();
- } catch (Exception e) {
- return intentStr;
- }
- default:
- return cleanIntentString(mIntent);
- }
- }
-
- /**
- * This method should return an id that should be the same for two folders containing the
- * same elements.
- */
- @NonNull
- private String getFolderMigrationId() {
- return mFolderItems.keySet().stream()
- .map(intentString -> mFolderItems.get(intentString).size()
- + cleanIntentString(intentString))
- .sorted()
- .collect(Collectors.joining(","));
- }
-
- /**
- * This is needed because sourceBounds can change and make the id of two equal items
- * different.
- */
- @NonNull
- private String cleanIntentString(@NonNull String intentStr) {
- try {
- Intent i = Intent.parseUri(intentStr, 0);
- i.setSourceBounds(null);
- return i.toURI();
- } catch (URISyntaxException e) {
- Log.e(TAG, "Unable to parse Intent string", e);
- return intentStr;
- }
-
- }
- }
}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.java b/src/com/android/launcher3/model/GridSizeMigrationLogic.java
new file mode 100644
index 0000000..f8c8f77
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
+import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
+import static com.android.launcher3.model.GridSizeMigrationDBController.copyCurrentGridToNewGrid;
+import static com.android.launcher3.model.GridSizeMigrationDBController.insertEntryInDb;
+import static com.android.launcher3.model.GridSizeMigrationDBController.needsToMigrate;
+import static com.android.launcher3.model.GridSizeMigrationDBController.removeEntryFromDb;
+import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
+import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.provider.LauncherDbUtils;
+import com.android.launcher3.util.CellAndSpan;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class GridSizeMigrationLogic {
+
+ private static final String TAG = "GridSizeMigrationLogic";
+ private static final boolean DEBUG = true;
+
+ /**
+ * Migrates the grid size from srcDeviceState to destDeviceState and make those changes
+ * in the target DB, using the source DB to determine what to add/remove/move/resize
+ * in the destination DB.
+ */
+ public void migrateGrid(
+ @NonNull Context context,
+ @NonNull DeviceGridState srcDeviceState,
+ @NonNull DeviceGridState destDeviceState,
+ @NonNull DatabaseHelper target,
+ @NonNull SQLiteDatabase source) {
+ if (!needsToMigrate(srcDeviceState, destDeviceState)) {
+ return;
+ }
+
+ boolean isFirstLoad = LauncherPrefs.get(context).get(IS_FIRST_LOAD_AFTER_RESTORE);
+ Log.d(TAG, "Begin grid migration. First load: " + isFirstLoad);
+
+ // This is a special case where if the grid is the same amount of columns but a larger
+ // amount of rows we simply copy over the source grid to the destination grid, rather
+ // than undergoing the general grid migration.
+ if (shouldMigrateToStrictlyTallerGrid(isFirstLoad, srcDeviceState, destDeviceState)) {
+ copyCurrentGridToNewGrid(context, destDeviceState, target, source);
+ return;
+ }
+ copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
+
+ long migrationStartTime = System.currentTimeMillis();
+ try (LauncherDbUtils.SQLiteTransaction t =
+ new LauncherDbUtils.SQLiteTransaction(target.getWritableDatabase())) {
+ GridSizeMigrationDBController.DbReader srcReader = new GridSizeMigrationDBController
+ .DbReader(t.getDb(), TMP_TABLE, context);
+ GridSizeMigrationDBController.DbReader destReader =
+ new GridSizeMigrationDBController.DbReader(
+ t.getDb(), TABLE_NAME, context);
+
+ Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows());
+
+ // Here we keep all the DB ids we have in the destination DB such that we don't assign
+ // an item that we want to add to the destination DB the same id as an already existing
+ // item.
+ List<Integer> idsInUse = new ArrayList<>();
+
+ // Migrate hotseat.
+ migrateHotseat(destDeviceState.getNumHotseat(), srcReader, destReader, target,
+ idsInUse);
+ // Migrate workspace.
+ migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse);
+
+ dropTable(t.getDb(), TMP_TABLE);
+ t.commit();
+ } catch (Exception e) {
+ Log.e(TAG, "Error during grid migration", e);
+ } finally {
+ Log.v(TAG, "Workspace migration completed in "
+ + (System.currentTimeMillis() - migrationStartTime));
+
+ // Save current configuration, so that the migration does not run again.
+ destDeviceState.writeToPrefs(context);
+ }
+ }
+
+ /**
+ * Handles hotseat migration.
+ */
+ @VisibleForTesting
+ public void migrateHotseat(int destHotseatSize,
+ GridSizeMigrationDBController.DbReader srcReader,
+ GridSizeMigrationDBController.DbReader destReader,
+ DatabaseHelper helper, List<Integer> idsInUse) {
+ final List<DbEntry> srcHotseatItems =
+ srcReader.loadHotseatEntries();
+ final List<DbEntry> dstHotseatItems =
+ destReader.loadHotseatEntries();
+
+ final List<DbEntry> hotseatToBeAdded =
+ getItemsToBeAdded(srcHotseatItems, dstHotseatItems);
+
+ final IntArray toBeRemoved = new IntArray();
+ toBeRemoved.addAll(getItemsToBeRemoved(srcHotseatItems, dstHotseatItems));
+
+ if (DEBUG) {
+ Log.d(TAG, "Start hotseat migration:"
+ + "\n Removing Hotseat Items:"
+ + dstHotseatItems.stream().filter(entry -> toBeRemoved
+ .contains(entry.id)).map(DbEntry::toString)
+ .collect(Collectors.joining(",\n", "[", "]"))
+ + "\n Adding Hotseat Items:"
+ + hotseatToBeAdded.stream().map(DbEntry::toString)
+ .collect(Collectors.joining(",\n", "[", "]"))
+ );
+ }
+
+ // Removes the items that we need to remove from the destination DB.
+ if (!toBeRemoved.isEmpty()) {
+ removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved);
+ }
+
+ placeHotseatItems(hotseatToBeAdded, dstHotseatItems, destHotseatSize, helper, srcReader,
+ destReader, idsInUse);
+ }
+
+ private void placeHotseatItems(List<DbEntry> hotseatToBeAdded,
+ List<DbEntry> dstHotseatItems, int destHotseatSize,
+ DatabaseHelper helper, GridSizeMigrationDBController.DbReader srcReader,
+ GridSizeMigrationDBController.DbReader destReader, List<Integer> idsInUse) {
+ if (hotseatToBeAdded.isEmpty()) {
+ return;
+ }
+
+ idsInUse.addAll(dstHotseatItems.stream().map(entry -> entry.id).toList());
+
+ Collections.sort(hotseatToBeAdded);
+
+ List<DbEntry> placementSolutionHotseat =
+ solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded);
+ for (DbEntry entryToPlace: placementSolutionHotseat) {
+ insertEntryInDb(helper, entryToPlace, srcReader.mTableName, destReader.mTableName,
+ idsInUse);
+ }
+ }
+
+
+ /**
+ * Handles workspace migration.
+ */
+ @VisibleForTesting
+ public void migrateWorkspace(GridSizeMigrationDBController.DbReader srcReader,
+ GridSizeMigrationDBController.DbReader destReader, DatabaseHelper helper,
+ Point targetSize, List<Integer> idsInUse) {
+ final List<DbEntry> srcWorkspaceItems =
+ srcReader.loadAllWorkspaceEntries();
+
+ final List<DbEntry> dstWorkspaceItems =
+ destReader.loadAllWorkspaceEntries();
+
+ final IntArray toBeRemoved = new IntArray();
+
+ List<DbEntry> workspaceToBeAdded =
+ getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems);
+ toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems));
+
+ if (DEBUG) {
+ Log.d(TAG, "Start workspace migration:"
+ + "\n Source Device:"
+ + srcWorkspaceItems.stream().map(
+ DbEntry::toString)
+ .collect(Collectors.joining(",\n", "[", "]"))
+ + "\n Target Device:"
+ + dstWorkspaceItems.stream().map(
+ DbEntry::toString)
+ .collect(Collectors.joining(",\n", "[", "]"))
+ + "\n Removing Workspace Items:"
+ + dstWorkspaceItems.stream().filter(entry -> toBeRemoved
+ .contains(entry.id)).map(
+ DbEntry::toString)
+ .collect(Collectors.joining(",\n", "[", "]"))
+ + "\n Adding Workspace Items:"
+ + workspaceToBeAdded.stream().map(
+ DbEntry::toString)
+ .collect(Collectors.joining(",\n", "[", "]"))
+ );
+ }
+
+ // Removes the items that we need to remove from the destination DB.
+ if (!toBeRemoved.isEmpty()) {
+ removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved);
+ }
+
+ placeWorkspaceItems(workspaceToBeAdded, dstWorkspaceItems, targetSize.x, targetSize.y,
+ helper, srcReader, destReader, idsInUse);
+ }
+
+ private void placeWorkspaceItems(
+ List<DbEntry> workspaceToBeAdded,
+ List<DbEntry> dstWorkspaceItems,
+ int trgX, int trgY, DatabaseHelper helper,
+ GridSizeMigrationDBController.DbReader srcReader,
+ GridSizeMigrationDBController.DbReader destReader, List<Integer> idsInUse) {
+ if (workspaceToBeAdded.isEmpty()) {
+ return;
+ }
+
+ idsInUse.addAll(dstWorkspaceItems.stream().map(entry -> entry.id).toList());
+
+ Collections.sort(workspaceToBeAdded);
+
+
+ // First we create a collection of the screens
+ List<Integer> screens = new ArrayList<>();
+ for (int screenId = 0; screenId <= destReader.mLastScreenId; screenId++) {
+ screens.add(screenId);
+ }
+
+ // Then we place the items on the screens
+ WorkspaceItemsToPlace itemsToPlace =
+ new WorkspaceItemsToPlace(workspaceToBeAdded);
+ for (int screenId : screens) {
+ if (DEBUG) {
+ Log.d(TAG, "Migrating " + screenId);
+ }
+ itemsToPlace = solveGridPlacement(
+ destReader.mContext, screenId, trgX, trgY, itemsToPlace.mRemainingItemsToPlace,
+ destReader.mWorkspaceEntriesByScreenId.get(screenId));
+ placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse);
+ while (!itemsToPlace.mPlacementSolution.isEmpty()) {
+ insertEntryInDb(helper, itemsToPlace.mPlacementSolution.remove(0),
+ srcReader.mTableName, destReader.mTableName, idsInUse);
+ }
+ if (itemsToPlace.mRemainingItemsToPlace.isEmpty()) {
+ break;
+ }
+ }
+
+ // In case the new grid is smaller, there might be some leftover items that don't fit on
+ // any of the screens, in this case we add them to new screens until all of them are placed.
+ int screenId = destReader.mLastScreenId + 1;
+ while (!itemsToPlace.mRemainingItemsToPlace.isEmpty()) {
+ itemsToPlace = solveGridPlacement(destReader.mContext, screenId,
+ trgX, trgY, itemsToPlace.mRemainingItemsToPlace,
+ destReader.mWorkspaceEntriesByScreenId.get(screenId));
+ placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse);
+ screenId++;
+ }
+ }
+
+ private void placeItems(WorkspaceItemsToPlace itemsToPlace, DatabaseHelper helper,
+ GridSizeMigrationDBController.DbReader srcReader,
+ GridSizeMigrationDBController.DbReader destReader, List<Integer> idsInUse) {
+ while (!itemsToPlace.mPlacementSolution.isEmpty()) {
+ insertEntryInDb(helper, itemsToPlace.mPlacementSolution.remove(0),
+ srcReader.mTableName, destReader.mTableName, idsInUse);
+ }
+ }
+
+
+ /**
+ * Only migrate the grid in this manner if the target grid is taller and not wider.
+ */
+ private boolean shouldMigrateToStrictlyTallerGrid(boolean isFirstLoad,
+ @NonNull DeviceGridState srcDeviceState,
+ @NonNull DeviceGridState destDeviceState) {
+ if (isFirstLoad
+ && Flags.enableGridMigrationFix()
+ && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
+ && srcDeviceState.getRows() < destDeviceState.getRows()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Finds all the items that are in the old grid which aren't in the new grid, meaning they
+ * need to be added to the new grid.
+ *
+ * @return a list of DbEntry's which we need to add.
+ */
+ private List<DbEntry> getItemsToBeAdded(
+ @NonNull final List<DbEntry> src,
+ @NonNull final List<DbEntry> dest) {
+ Map<DbEntry, Integer> entryCountDiff =
+ calcDiff(src, dest);
+ List<DbEntry> toBeAdded = new ArrayList<>();
+ src.forEach(entry -> {
+ if (entryCountDiff.get(entry) > 0) {
+ toBeAdded.add(entry);
+ entryCountDiff.put(entry, entryCountDiff.get(entry) - 1);
+ }
+ });
+ return toBeAdded;
+ }
+
+ /**
+ * Finds all the items that are in the new grid which aren't in the old grid, meaning they
+ * need to be removed from the new grid.
+ *
+ * @return an IntArray of item id's which we need to remove.
+ */
+ private IntArray getItemsToBeRemoved(
+ @NonNull final List<DbEntry> src,
+ @NonNull final List<DbEntry> dest) {
+ Map<DbEntry, Integer> entryCountDiff =
+ calcDiff(src, dest);
+ IntArray toBeRemoved = new IntArray();
+ dest.forEach(entry -> {
+ if (entryCountDiff.get(entry) < 0) {
+ toBeRemoved.add(entry.id);
+ if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+ entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add));
+ }
+ entryCountDiff.put(entry, entryCountDiff.get(entry) + 1);
+ }
+ });
+ return toBeRemoved;
+ }
+
+ /**
+ * Calculates the difference between the old and new grid items in terms of how many of each
+ * item there are. E.g. if the old grid had 2 Calculator icons but the new grid has 0, then the
+ * difference there would be 2. While if the old grid has 0 Calculator icons and the
+ * new grid has 1, then the difference would be -1.
+ *
+ * @return a Map with each DbEntry as a key and the count of said entry as the value.
+ */
+ private Map<DbEntry, Integer> calcDiff(
+ @NonNull final List<DbEntry> src,
+ @NonNull final List<DbEntry> dest) {
+ Map<DbEntry, Integer> entryCountDiff = new HashMap<>();
+ src.forEach(entry ->
+ entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) + 1));
+ dest.forEach(entry ->
+ entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) - 1));
+ return entryCountDiff;
+ }
+
+ private List<DbEntry> solveHotseatPlacement(final int hotseatSize,
+ @NonNull final List<DbEntry> placedHotseatItems,
+ @NonNull final List<DbEntry> itemsToPlace) {
+ List<DbEntry> placementSolution = new ArrayList<>();
+ List<DbEntry> remainingItemsToPlace =
+ new ArrayList<>(itemsToPlace);
+ final boolean[] occupied = new boolean[hotseatSize];
+ for (DbEntry entry : placedHotseatItems) {
+ occupied[entry.screenId] = true;
+ }
+
+ for (int i = 0; i < occupied.length; i++) {
+ if (!occupied[i] && !remainingItemsToPlace.isEmpty()) {
+ DbEntry entry = remainingItemsToPlace.remove(0);
+ entry.screenId = i;
+ // These values does not affect the item position, but we should set them
+ // to something other than -1.
+ entry.cellX = i;
+ entry.cellY = 0;
+
+ placementSolution.add(entry);
+ occupied[entry.screenId] = true;
+ }
+ }
+ return placementSolution;
+ }
+
+ private WorkspaceItemsToPlace solveGridPlacement(
+ Context context,
+ final int screenId, final int trgX, final int trgY,
+ @NonNull final List<DbEntry> sortedItemsToPlace,
+ List<DbEntry> existedEntries) {
+ WorkspaceItemsToPlace itemsToPlace = new WorkspaceItemsToPlace(sortedItemsToPlace);
+ final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
+ final Point trg = new Point(trgX, trgY);
+ final Point next = new Point(0, screenId == 0
+ && (FeatureFlags.QSB_ON_FIRST_SCREEN
+ && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(context)
+ .getBoolean(SMARTSPACE_ON_HOME_SCREEN, true))
+ && !SHOULD_SHOW_FIRST_PAGE_WIDGET)
+ ? 1 /* smartspace */ : 0);
+ if (existedEntries != null) {
+ for (DbEntry entry : existedEntries) {
+ occupied.markCells(entry, true);
+ }
+ }
+ Iterator<DbEntry> iterator =
+ itemsToPlace.mRemainingItemsToPlace.iterator();
+ while (iterator.hasNext()) {
+ final DbEntry entry = iterator.next();
+ if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
+ iterator.remove();
+ continue;
+ }
+ CellAndSpan placement = findPlacementForEntry(
+ entry, next.x, next.y, trg, occupied);
+ if (placement != null) {
+ entry.screenId = screenId;
+ entry.cellX = placement.cellX;
+ entry.cellY = placement.cellY;
+ entry.spanX = placement.spanX;
+ entry.spanY = placement.spanY;
+ occupied.markCells(entry, true);
+ next.set(entry.cellX + entry.spanX, entry.cellY);
+ itemsToPlace.mPlacementSolution.add(entry);
+ iterator.remove();
+ }
+ }
+ return itemsToPlace;
+ }
+
+ /**
+ * Search for the next possible placement of an item. (mNextStartX, mNextStartY) serves as
+ * a memoization of last placement, we can start our search for next placement from there
+ * to speed up the search.
+ *
+ * @return NewEntryPlacement object if we found a valid placement, null if we didn't.
+ */
+ private CellAndSpan findPlacementForEntry(
+ @NonNull final DbEntry entry,
+ int startPosX, int startPosY, @NonNull final Point trg,
+ @NonNull final GridOccupancy occupied) {
+ for (int y = startPosY; y < trg.y; y++) {
+ for (int x = startPosX; x < trg.x; x++) {
+ boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY);
+ if (minFits) {
+ return (new CellAndSpan(x, y, entry.minSpanX, entry.minSpanY));
+ }
+ }
+ startPosX = 0;
+ }
+ return null;
+ }
+
+ private static class WorkspaceItemsToPlace {
+ List<DbEntry> mRemainingItemsToPlace;
+ List<DbEntry> mPlacementSolution;
+
+ WorkspaceItemsToPlace(List<DbEntry> sortedItemsToPlace) {
+ mRemainingItemsToPlace = new ArrayList<>(sortedItemsToPlace);
+ mPlacementSolution = new ArrayList<>();
+ }
+
+ }
+}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 09d1146..06d8b59 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -287,11 +287,6 @@
}
logASplit("loadAllApps");
- if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
- mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
- logASplit("allAppsDelegateItems");
- }
verifyNotStopped();
mLauncherBinder.bindAllApps();
logASplit("bindAllApps");
@@ -356,12 +351,6 @@
prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(true));
}
- if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
- logASplit("otherDelegateItems");
- verifyNotStopped();
- }
-
updateHandler.updateIcons(allWidgetsList,
CachedObjectCachingLogic.INSTANCE,
mApp.getModel()::onWidgetLabelsUpdated);
@@ -413,13 +402,6 @@
}
logASplit("loadWorkspace");
- if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- verifyNotStopped();
- mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
- mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
- mModelDelegate.markActive();
- logASplit("workspaceDelegateItems");
- }
mBgDataModel.isFirstPagePinnedItemEnabled = FeatureFlags.QSB_ON_FIRST_SCREEN
&& (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(
mApp.getContext()).getBoolean(SMARTSPACE_ON_HOME_SCREEN, true));
@@ -435,7 +417,15 @@
final WidgetInflater widgetInflater = new WidgetInflater(context);
ModelDbController dbController = mApp.getModel().getModelDbController();
- dbController.tryMigrateDB(restoreEventLogger);
+ if (Flags.gridMigrationRefactor()) {
+ try {
+ dbController.attemptMigrateDb(restoreEventLogger);
+ } catch (Exception e) {
+ FileLog.e(TAG, "Failed to migrate grid", e);
+ }
+ } else {
+ dbController.tryMigrateDB(restoreEventLogger);
+ }
Log.d(TAG, "loadWorkspace: loading default favorites");
dbController.loadDefaultFavoritesIfNecessary();
@@ -482,14 +472,12 @@
IOUtils.closeSilently(c);
}
- if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
- mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
- mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
- mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
- mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
- mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
- mModelDelegate.markActive();
- }
+ mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
+ mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+ mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
+ mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+ mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
+ mModelDelegate.markActive();
// Break early if we've stopped loading
if (mStopped) {
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 5d66d16..8c3e860 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -20,15 +20,16 @@
import static android.util.Base64.NO_WRAP;
import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
+import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
import static com.android.launcher3.LauncherSettings.Settings.BLOB_KEY_PREFIX;
-import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY;
import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
+import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY;
import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
import android.app.blob.BlobHandle;
@@ -97,6 +98,7 @@
private static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
public static final String EXTRA_DB_NAME = "db_name";
+ public static final String DATA_TYPE_DB_FILE = "database_file";
protected DatabaseHelper mOpenHelper;
@@ -289,13 +291,115 @@
/**
+ * Resets the launcher DB if we should reset it.
+ */
+ public void resetLauncherDb(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
+ if (restoreEventLogger != null) {
+ sendMetricsForFailedMigration(restoreEventLogger, getDb());
+ }
+ FileLog.d(TAG, "Migration failed: resetting launcher database");
+ createEmptyDB();
+ LauncherPrefs.get(mContext).putSync(
+ getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
+
+ // Write the grid state to avoid another migration
+ new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext);
+ }
+
+ /**
+ * Determines if we should reset the DB.
+ */
+ private boolean shouldResetDb() {
+ if (isThereExistingDb()) {
+ return true;
+ }
+ if (!isGridMigrationNecessary()) {
+ return false;
+ }
+ if (isCurrentDbSameAsTarget()) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isThereExistingDb() {
+ if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
+ // If we already have a new DB, ignore migration
+ FileLog.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isGridMigrationNecessary() {
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
+ if (GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
+ return true;
+ }
+ FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+ return false;
+ }
+
+ private boolean isCurrentDbSameAsTarget() {
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
+ String targetDbName = new DeviceGridState(idp).getDbFile();
+ if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
+ FileLog.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Migrates the DB. If the migration failed, it clears the DB.
+ */
+ public void attemptMigrateDb(LauncherRestoreEventLogger restoreEventLogger) throws Exception {
+ createDbIfNotExists();
+
+ if (shouldResetDb()) {
+ resetLauncherDb(restoreEventLogger);
+ return;
+ }
+
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
+ DatabaseHelper oldHelper = mOpenHelper;
+ mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
+ : createDatabaseHelper(true /* forMigration */);
+ try {
+ // This is the current grid we have, given by the mContext
+ DeviceGridState srcDeviceState = new DeviceGridState(mContext);
+ // This is the state we want to migrate to that is given by the idp
+ DeviceGridState destDeviceState = new DeviceGridState(idp);
+
+ GridSizeMigrationLogic gridSizeMigrationLogic = new GridSizeMigrationLogic();
+ gridSizeMigrationLogic.migrateGrid(mContext, srcDeviceState, destDeviceState,
+ mOpenHelper, oldHelper.getWritableDatabase());
+ } catch (Exception e) {
+ resetLauncherDb(restoreEventLogger);
+ throw new Exception("Failed to migrate grid", e);
+ } finally {
+ if (mOpenHelper != oldHelper) {
+ oldHelper.close();
+ }
+ }
+ }
+
+ /**
* Migrates the DB if needed. If the migration failed, it clears the DB.
*/
public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
if (!migrateGridIfNeeded()) {
if (restoreEventLogger != null) {
- sendMetricsForFailedMigration(restoreEventLogger, getDb());
+ if (LauncherPrefs.get(mContext).get(NO_DB_FILES_RESTORED)) {
+ restoreEventLogger.logLauncherItemsRestoreFailed(DATA_TYPE_DB_FILE, 1,
+ RestoreError.DATABASE_FILE_NOT_RESTORED);
+ LauncherPrefs.get(mContext).put(NO_DB_FILES_RESTORED, false);
+ FileLog.d(TAG, "There is no data to migrate: resetting launcher database");
+ } else {
+ restoreEventLogger.logLauncherItemsRestored(DATA_TYPE_DB_FILE, 1);
+ sendMetricsForFailedMigration(restoreEventLogger, getDb());
+ }
}
FileLog.d(TAG, "Migration failed: resetting launcher database");
createEmptyDB();
@@ -304,6 +408,8 @@
// Write the grid state to avoid another migration
new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext);
+ } else if (restoreEventLogger != null) {
+ restoreEventLogger.logLauncherItemsRestored(DATA_TYPE_DB_FILE, 1);
}
}
@@ -317,17 +423,17 @@
createDbIfNotExists();
if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
// If we have already create a new DB, ignore migration
- Log.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
+ FileLog.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
return false;
}
InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
- if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
- Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+ if (!GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
+ FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
return true;
}
String targetDbName = new DeviceGridState(idp).getDbFile();
if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
- Log.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
+ FileLog.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
return false;
}
DatabaseHelper oldHelper = mOpenHelper;
@@ -338,7 +444,7 @@
DeviceGridState srcDeviceState = new DeviceGridState(mContext);
// This is the state we want to migrate to that is given by the idp
DeviceGridState destDeviceState = new DeviceGridState(idp);
- return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, srcDeviceState,
+ return GridSizeMigrationDBController.migrateGridIfNeeded(mContext, srcDeviceState,
destDeviceState, mOpenHelper, oldHelper.getWritableDatabase());
} catch (Exception e) {
FileLog.e(TAG, "Failed to migrate grid", e);
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
deleted file mode 100644
index 3ae643e..0000000
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.provider;
-
-import static com.android.launcher3.LauncherSettings.Favorites.getColumns;
-import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ShortcutInfo;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Icon;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserManager;
-import android.text.TextUtils;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.model.LoaderCursor;
-import com.android.launcher3.model.UserManagerState;
-import com.android.launcher3.pm.PinRequestHelper;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.PackageManagerHelper;
-
-/**
- * A set of utility methods for Launcher DB used for DB updates and migration.
- */
-public class LauncherDbUtils {
- /**
- * Returns a string which can be used as a where clause for DB query to match the given itemId
- */
- public static String itemIdMatch(int itemId) {
- return "_id=" + itemId;
- }
-
- public static IntArray queryIntArray(boolean distinct, SQLiteDatabase db, String tableName,
- String columnName, String selection, String groupBy, String orderBy) {
- IntArray out = new IntArray();
- try (Cursor c = db.query(distinct, tableName, new String[] { columnName }, selection, null,
- groupBy, null, orderBy, null)) {
- while (c.moveToNext()) {
- out.add(c.getInt(0));
- }
- }
- return out;
- }
-
- public static boolean tableExists(SQLiteDatabase db, String tableName) {
- try (Cursor c = db.query(true, "sqlite_master", new String[] {"tbl_name"},
- "tbl_name = ?", new String[] {tableName},
- null, null, null, null, null)) {
- return c.getCount() > 0;
- }
- }
-
- public static void dropTable(SQLiteDatabase db, String tableName) {
- db.execSQL("DROP TABLE IF EXISTS " + tableName);
- }
-
- /** Copy fromTable in fromDb to toTable in toDb. */
- public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
- String toTable, Context context) {
- long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
- Process.myUserHandle());
- dropTable(toDb, toTable);
- Favorites.addTableToDb(toDb, userSerial, false, toTable);
- if (fromDb != toDb) {
- toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
- toDb.execSQL(
- "INSERT INTO " + toTable + " SELECT " + getColumns(userSerial)
- + " FROM from_db." + fromTable);
- toDb.execSQL("DETACH DATABASE 'from_db'");
- } else {
- toDb.execSQL("INSERT INTO " + toTable + " SELECT " + getColumns(userSerial) + " FROM "
- + fromTable);
- }
- }
-
- /**
- * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher.
- * Removes any invalid shortcut or any shortcut which requires some permission to launch
- */
- public static void migrateLegacyShortcuts(Context context, SQLiteDatabase db) {
- Cursor c = db.query(
- Favorites.TABLE_NAME, null, "itemType = 1", null, null, null, null);
- UserManagerState ums = new UserManagerState();
- PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
- ums.init(UserCache.INSTANCE.get(context),
- context.getSystemService(UserManager.class));
- LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper,
- null);
- IntSet deletedShortcuts = new IntSet();
-
- while (lc.moveToNext()) {
- if (lc.user != Process.myUserHandle()) {
- deletedShortcuts.add(lc.id);
- continue;
- }
- Intent intent = lc.parseIntent();
- if (intent == null) {
- deletedShortcuts.add(lc.id);
- continue;
- }
- if (TextUtils.isEmpty(lc.getTitle())) {
- deletedShortcuts.add(lc.id);
- continue;
- }
-
- // Make sure the target intent can be launched without any permissions. Otherwise remove
- // the shortcut
- ResolveInfo ri = context.getPackageManager().resolveActivity(intent, 0);
- if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
- deletedShortcuts.add(lc.id);
- continue;
- }
- PersistableBundle extras = new PersistableBundle();
- extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, ri.activityInfo.packageName);
- ShortcutInfo.Builder infoBuilder = new ShortcutInfo.Builder(
- context, "migrated_shortcut-" + lc.id)
- .setIntent(intent)
- .setExtras(extras)
- .setShortLabel(lc.getTitle());
-
- Bitmap bitmap = null;
- byte[] iconData = lc.getIconBlob();
- if (iconData != null) {
- bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
- }
- if (bitmap != null) {
- infoBuilder.setIcon(Icon.createWithBitmap(bitmap));
- }
-
- ShortcutInfo info = infoBuilder.build();
- try {
- if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
- deletedShortcuts.add(lc.id);
- continue;
- }
- } catch (Exception e) {
- deletedShortcuts.add(lc.id);
- continue;
- }
- ContentValues update = new ContentValues();
- update.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_DEEP_SHORTCUT);
- update.put(Favorites.INTENT,
- ShortcutKey.makeIntent(info.getId(), context.getPackageName()).toUri(0));
- db.update(Favorites.TABLE_NAME, update, "_id = ?",
- new String[] {Integer.toString(lc.id)});
- }
- lc.close();
- if (!deletedShortcuts.isEmpty()) {
- db.delete(Favorites.TABLE_NAME,
- Utilities.createDbSelectionQuery(Favorites._ID, deletedShortcuts.getArray()),
- null);
- }
-
- // Drop the unused columns
- db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconPackage;");
- db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconResource;");
- }
-
- /**
- * Utility class to simplify managing sqlite transactions
- */
- public static class SQLiteTransaction implements AutoCloseable {
- private final SQLiteDatabase mDb;
-
- public SQLiteTransaction(SQLiteDatabase db) {
- mDb = db;
- db.beginTransaction();
- }
-
- public void commit() {
- mDb.setTransactionSuccessful();
- }
-
- @Override
- public void close() {
- mDb.endTransaction();
- }
-
- public SQLiteDatabase getDb() {
- return mDb;
- }
- }
-}
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt
new file mode 100644
index 0000000..3c68e46
--- /dev/null
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.provider
+
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Icon
+import android.os.PersistableBundle
+import android.os.Process
+import android.os.UserManager
+import android.text.TextUtils
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.LoaderCursor
+import com.android.launcher3.model.UserManagerState
+import com.android.launcher3.pm.PinRequestHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.PackageManagerHelper
+
+/** A set of utility methods for Launcher DB used for DB updates and migration. */
+object LauncherDbUtils {
+ /**
+ * Returns a string which can be used as a where clause for DB query to match the given itemId
+ */
+ @JvmStatic fun itemIdMatch(itemId: Int): String = "_id=$itemId"
+
+ @JvmStatic
+ fun queryIntArray(
+ distinct: Boolean,
+ db: SQLiteDatabase,
+ tableName: String,
+ columnName: String,
+ selection: String?,
+ groupBy: String?,
+ orderBy: String?,
+ ): IntArray {
+ val out = IntArray()
+ db.query(
+ distinct,
+ tableName,
+ arrayOf(columnName),
+ selection,
+ null,
+ groupBy,
+ null,
+ orderBy,
+ null,
+ )
+ .use { c ->
+ while (c.moveToNext()) {
+ out.add(c.getInt(0))
+ }
+ }
+ return out
+ }
+
+ @JvmStatic
+ fun tableExists(db: SQLiteDatabase, tableName: String): Boolean =
+ db.query(
+ /* distinct = */ true,
+ /* table = */ "sqlite_master",
+ /* columns = */ arrayOf("tbl_name"),
+ /* selection = */ "tbl_name = ?",
+ /* selectionArgs = */ arrayOf(tableName),
+ /* groupBy = */ null,
+ /* having = */ null,
+ /* orderBy = */ null,
+ /* limit = */ null,
+ /* cancellationSignal = */ null,
+ )
+ .use { c ->
+ return c.count > 0
+ }
+
+ @JvmStatic
+ fun dropTable(db: SQLiteDatabase, tableName: String) =
+ db.execSQL("DROP TABLE IF EXISTS $tableName")
+
+ /** Copy fromTable in fromDb to toTable in toDb. */
+ @JvmStatic
+ fun copyTable(
+ fromDb: SQLiteDatabase,
+ fromTable: String,
+ toDb: SQLiteDatabase,
+ toTable: String,
+ context: Context,
+ ) {
+ val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
+ dropTable(toDb, toTable)
+ LauncherSettings.Favorites.addTableToDb(toDb, userSerial, false, toTable)
+ if (fromDb != toDb) {
+ toDb.run {
+ execSQL("ATTACH DATABASE '${fromDb.path}' AS from_db")
+ execSQL(
+ "INSERT INTO $toTable SELECT ${LauncherSettings.Favorites.getColumns(userSerial)} FROM from_db.$fromTable"
+ )
+ execSQL("DETACH DATABASE 'from_db'")
+ }
+ } else {
+ toDb.run {
+ execSQL(
+ "INSERT INTO $toTable SELECT ${
+ LauncherSettings.Favorites.getColumns(
+ userSerial
+ )
+ } FROM $fromTable"
+ )
+ }
+ }
+ }
+
+ /**
+ * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
+ * shortcut or any shortcut which requires some permission to launch
+ */
+ @JvmStatic
+ fun migrateLegacyShortcuts(context: Context, db: SQLiteDatabase) {
+ val c =
+ db.query(
+ LauncherSettings.Favorites.TABLE_NAME,
+ null,
+ "itemType = 1",
+ null,
+ null,
+ null,
+ null,
+ )
+ val pmHelper = PackageManagerHelper.INSTANCE[context]
+ val ums = UserManagerState()
+ ums.run {
+ init(UserCache.INSTANCE[context], context.getSystemService(UserManager::class.java))
+ }
+ val lc = LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper, null)
+ val deletedShortcuts = IntSet()
+
+ while (lc.moveToNext()) {
+ if (lc.user !== Process.myUserHandle()) {
+ deletedShortcuts.add(lc.id)
+ continue
+ }
+ val intent = lc.parseIntent()
+ if (intent == null) {
+ deletedShortcuts.add(lc.id)
+ continue
+ }
+ if (TextUtils.isEmpty(lc.title)) {
+ deletedShortcuts.add(lc.id)
+ continue
+ }
+
+ // Make sure the target intent can be launched without any permissions. Otherwise remove
+ // the shortcut
+ val ri = context.packageManager.resolveActivity(intent, 0)
+ if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
+ deletedShortcuts.add(lc.id)
+ continue
+ }
+ val extras =
+ PersistableBundle().apply {
+ putString(
+ IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE,
+ ri.activityInfo.packageName,
+ )
+ }
+ val infoBuilder =
+ ShortcutInfo.Builder(context, "migrated_shortcut-${lc.id}")
+ .setIntent(intent)
+ .setExtras(extras)
+ .setShortLabel(lc.title)
+
+ var bitmap: Bitmap? = null
+ val iconData = lc.iconBlob
+ if (iconData != null) {
+ bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.size)
+ }
+ if (bitmap != null) {
+ infoBuilder.setIcon(Icon.createWithBitmap(bitmap))
+ }
+
+ val info = infoBuilder.build()
+ try {
+ if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
+ deletedShortcuts.add(lc.id)
+ continue
+ }
+ } catch (e: Exception) {
+ deletedShortcuts.add(lc.id)
+ continue
+ }
+ val update =
+ ContentValues().apply {
+ put(
+ LauncherSettings.Favorites.ITEM_TYPE,
+ LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+ )
+ put(
+ LauncherSettings.Favorites.INTENT,
+ ShortcutKey.makeIntent(info.id, context.packageName).toUri(0),
+ )
+ }
+ db.update(
+ LauncherSettings.Favorites.TABLE_NAME,
+ update,
+ "_id = ?",
+ arrayOf(lc.id.toString()),
+ )
+ }
+ lc.close()
+ if (deletedShortcuts.isEmpty.not()) {
+ db.delete(
+ /* table = */ LauncherSettings.Favorites.TABLE_NAME,
+ /* whereClause = */ Utilities.createDbSelectionQuery(
+ LauncherSettings.Favorites._ID,
+ deletedShortcuts.array,
+ ),
+ /* whereArgs = */ null,
+ )
+ }
+
+ // Drop the unused columns
+ db.run {
+ execSQL("ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconPackage;")
+ execSQL(
+ "ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconResource;"
+ )
+ }
+ }
+
+ /** Utility class to simplify managing sqlite transactions */
+ class SQLiteTransaction(val db: SQLiteDatabase) : AutoCloseable {
+ init {
+ db.beginTransaction()
+ }
+
+ fun commit() = db.setTransactionSuccessful()
+
+ override fun close() = db.endTransaction()
+ }
+}
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 54b2eae..079191f 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -24,6 +24,7 @@
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Trace;
import android.view.LayoutInflater;
import android.view.View;
@@ -176,8 +177,10 @@
@Override
public void onConfigurationChanged(Configuration newConfig) {
+ Trace.beginSection("statefulActivity#onConfigurationChanged");
handleConfigurationChanged(newConfig);
super.onConfigurationChanged(newConfig);
+ Trace.endSection();
}
/**
diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java
index 21f91acd..467a7ec 100644
--- a/src/com/android/launcher3/util/ApiWrapper.java
+++ b/src/com/android/launcher3/util/ApiWrapper.java
@@ -17,7 +17,6 @@
package com.android.launcher3.util;
import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_HOME_ROLE;
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
import android.app.ActivityOptions;
import android.app.Person;
@@ -38,24 +37,30 @@
import com.android.launcher3.BuildConfig;
import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import javax.inject.Inject;
+
/**
* A wrapper for the hidden API calls
*/
-public class ApiWrapper implements ResourceBasedOverride, SafeCloseable {
+@LauncherAppSingleton
+public class ApiWrapper {
- public static final MainThreadInitializedObject<ApiWrapper> INSTANCE =
- forOverride(ApiWrapper.class, R.string.api_wrapper_class);
+ public static final DaggerSingletonObject<ApiWrapper> INSTANCE = new DaggerSingletonObject<>(
+ LauncherAppComponent::getApiWrapper);
protected final Context mContext;
- public ApiWrapper(Context context) {
+ @Inject
+ public ApiWrapper(@ApplicationContext Context context) {
mContext = context;
}
@@ -166,9 +171,6 @@
return appInfo.sourceDir;
}
- @Override
- public void close() { }
-
private static class NoopDrawable extends ColorDrawable {
@Override
public int getIntrinsicHeight() {
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 0b45118..26912eb 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -255,7 +255,9 @@
|| config.fontScale != mInfo.fontScale
|| !mInfo.mScreenSizeDp.equals(
new PortraitSize(config.screenHeightDp, config.screenWidthDp))
- || mWindowContext.getDisplay().getRotation() != mInfo.rotation) {
+ || mWindowContext.getDisplay().getRotation() != mInfo.rotation
+ || WindowManagerProxy.INSTANCE.get(mContext).showLockedTaskbarOnHome(mWindowContext)
+ != mInfo.showLockedTaskbarOnHome()) {
notifyConfigChange();
}
}
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index 9a70298..356a551 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -150,7 +150,6 @@
public SandboxContext(Context base) {
attachBaseContext(base);
- initDagger();
}
@Override
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index e51609a..4b60d98 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -42,6 +42,9 @@
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -51,16 +54,19 @@
import java.util.List;
import java.util.Objects;
+import javax.inject.Inject;
+
/**
* Utility methods using package manager
*/
-public class PackageManagerHelper implements SafeCloseable{
+@LauncherAppSingleton
+public class PackageManagerHelper {
private static final String TAG = "PackageManagerHelper";
@NonNull
- public static final MainThreadInitializedObject<PackageManagerHelper> INSTANCE =
- new MainThreadInitializedObject<>(PackageManagerHelper::new);
+ public static DaggerSingletonObject<PackageManagerHelper> INSTANCE =
+ new DaggerSingletonObject<>(LauncherBaseAppComponent::getPackageManagerHelper);
@NonNull
private final Context mContext;
@@ -73,17 +79,15 @@
private final String[] mLegacyMultiInstanceSupportedApps;
- public PackageManagerHelper(@NonNull final Context context) {
+ @Inject
+ public PackageManagerHelper(@ApplicationContext final Context context) {
mContext = context;
mPm = context.getPackageManager();
mLauncherApps = Objects.requireNonNull(context.getSystemService(LauncherApps.class));
mLegacyMultiInstanceSupportedApps = mContext.getResources().getStringArray(
- R.array.config_appsSupportMultiInstancesSplit);
+ R.array.config_appsSupportMultiInstancesSplit);
}
- @Override
- public void close() { }
-
/**
* Returns the installing app package for the given package
*/
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 9dddc18..20cce8f 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -39,8 +39,8 @@
import com.android.launcher3.dagger.LauncherBaseAppComponent;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
-import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.PluginManagerWrapper;
+import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.systemui.plugins.CustomWidgetPlugin;
@@ -51,7 +51,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.function.Consumer;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Stream;
import javax.inject.Inject;
@@ -70,7 +70,7 @@
private final Context mContext;
private final HashMap<ComponentName, CustomWidgetPlugin> mPlugins;
private final List<CustomAppWidgetProviderInfo> mCustomWidgets;
- private Consumer<PackageUserKey> mWidgetRefreshCallback;
+ private final List<Runnable> mWidgetRefreshCallbacks = new CopyOnWriteArrayList<>();
private final @NonNull AppWidgetManager mAppWidgetManager;
@Inject
@@ -111,23 +111,20 @@
@Override
public void onPluginConnected(CustomWidgetPlugin plugin, Context context) {
- List<AppWidgetProviderInfo> providers = mAppWidgetManager
- .getInstalledProvidersForProfile(Process.myUserHandle());
- if (providers.isEmpty()) return;
- Parcel parcel = Parcel.obtain();
- providers.get(0).writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
- CustomAppWidgetProviderInfo info = newInfo(plugin, parcel);
- parcel.recycle();
- mPlugins.put(info.provider, plugin);
- mCustomWidgets.add(info);
+ CustomAppWidgetProviderInfo info = getAndAddInfo(new ComponentName(
+ PLUGIN_PKG, CLS_CUSTOM_WIDGET_PREFIX + plugin.getClass().getName()));
+ if (info != null) {
+ plugin.updateWidgetInfo(info, mContext);
+ mPlugins.put(info.provider, plugin);
+ mWidgetRefreshCallbacks.forEach(MAIN_EXECUTOR::execute);
+ }
}
@Override
public void onPluginDisconnected(CustomWidgetPlugin plugin) {
- ComponentName cn = getWidgetProviderComponent(plugin);
- mPlugins.remove(cn);
- mCustomWidgets.removeIf(w -> w.getComponent().equals(cn));
+ // Leave the providerInfo as plugins can get disconnected/reconnected multiple times
+ mPlugins.values().remove(plugin);
+ mWidgetRefreshCallbacks.forEach(MAIN_EXECUTOR::execute);
}
@VisibleForTesting
@@ -138,9 +135,11 @@
/**
* Inject a callback function to refresh the widgets.
+ * @return a closeable to remove this callback
*/
- public void setWidgetRefreshCallback(Consumer<PackageUserKey> cb) {
- mWidgetRefreshCallback = cb;
+ public SafeCloseable addWidgetRefreshCallback(Runnable callback) {
+ mWidgetRefreshCallbacks.add(callback);
+ return () -> mWidgetRefreshCallbacks.remove(callback);
}
/**
@@ -149,8 +148,9 @@
public void onViewCreated(LauncherAppWidgetHostView view) {
CustomAppWidgetProviderInfo info = (CustomAppWidgetProviderInfo) view.getAppWidgetInfo();
CustomWidgetPlugin plugin = mPlugins.get(info.provider);
- if (plugin == null) return;
- plugin.onViewCreated(view);
+ if (plugin != null) {
+ plugin.onViewCreated(view);
+ }
}
/**
@@ -166,14 +166,13 @@
*/
@Nullable
public LauncherAppWidgetProviderInfo getWidgetProvider(ComponentName cn) {
- return mCustomWidgets.stream()
+ LauncherAppWidgetProviderInfo info = mCustomWidgets.stream()
.filter(w -> w.getComponent().equals(cn)).findAny().orElse(null);
- }
-
- private CustomAppWidgetProviderInfo newInfo(CustomWidgetPlugin plugin, Parcel parcel) {
- CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false);
- info.provider = getWidgetProviderComponent(plugin);
- plugin.updateWidgetInfo(info, mContext);
+ if (info == null) {
+ // If the info is not present, add a placeholder info since the
+ // plugin might get loaded later
+ info = getAndAddInfo(cn);
+ }
return info;
}
@@ -184,8 +183,24 @@
return CUSTOM_WIDGET_ID - mCustomWidgets.indexOf(getWidgetProvider(componentName));
}
- private ComponentName getWidgetProviderComponent(CustomWidgetPlugin plugin) {
- return new ComponentName(
- PLUGIN_PKG, CLS_CUSTOM_WIDGET_PREFIX + plugin.getClass().getName());
+ @Nullable
+ private CustomAppWidgetProviderInfo getAndAddInfo(ComponentName cn) {
+ for (CustomAppWidgetProviderInfo info : mCustomWidgets) {
+ if (info.provider.equals(cn)) return info;
+ }
+
+ List<AppWidgetProviderInfo> providers = mAppWidgetManager
+ .getInstalledProvidersForProfile(Process.myUserHandle());
+ if (providers.isEmpty()) return null;
+ Parcel parcel = Parcel.obtain();
+ providers.get(0).writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false);
+ parcel.recycle();
+
+ info.provider = cn;
+ info.initialLayout = 0;
+ mCustomWidgets.add(info);
+ return info;
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index c8ad564..1860977 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -145,7 +145,9 @@
protected DeviceProfile mDeviceProfile;
protected TextView mNoWidgetsView;
- protected StickyHeaderLayout mSearchScrollView;
+ protected LinearLayout mSearchScrollView;
+ // Reference to the mSearchScrollView when it is is a sticky header.
+ private @Nullable StickyHeaderLayout mStickyHeaderLayout;
protected WidgetRecommendationsView mWidgetRecommendationsView;
protected LinearLayout mWidgetRecommendationsContainer;
protected View mTabBar;
@@ -220,7 +222,11 @@
protected void setupViews() {
mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
- mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
+ if (mSearchScrollView instanceof StickyHeaderLayout) {
+ mStickyHeaderLayout = (StickyHeaderLayout) mSearchScrollView;
+ mStickyHeaderLayout.setCurrentRecyclerView(
+ findViewById(R.id.primary_widgets_list_view));
+ }
mNoWidgetsView = findViewById(R.id.no_widgets_text);
mFastScroller = findViewById(R.id.fast_scroller);
mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
@@ -284,7 +290,9 @@
reset();
resetExpandedHeaders();
mCurrentWidgetsRecyclerView = recyclerView;
- mSearchScrollView.setCurrentRecyclerView(recyclerView);
+ if (mStickyHeaderLayout != null) {
+ mStickyHeaderLayout.setCurrentRecyclerView(recyclerView);
+ }
}
}
@@ -313,7 +321,9 @@
mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
}
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
- mSearchScrollView.reset(/* animate= */ true);
+ if (mStickyHeaderLayout != null) {
+ mStickyHeaderLayout.reset(/* animate= */ true);
+ }
}
@VisibleForTesting
@@ -1051,7 +1061,7 @@
}
private int getEmptySpaceHeight() {
- return mSearchScrollView.getHeaderHeight();
+ return mStickyHeaderLayout != null ? mStickyHeaderLayout.getHeaderHeight() : 0;
}
void setup(WidgetsRecyclerView recyclerView) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 8dd1de4..3d3a669 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -396,15 +396,6 @@
LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (layoutManager == null) return;
- if (position == mVisibleEntries.size() - 2
- && mVisibleEntries.get(mVisibleEntries.size() - 1)
- instanceof WidgetsListContentEntry) {
- // If the selected header is in the last position and its content is showing, then
- // scroll to the final position so the last list of widgets will show.
- layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
- return;
- }
-
// Scroll to the header view's current offset, accounting for the recycler view's padding.
// If the header view couldn't be found, then it will appear at the top of the list.
layoutManager.scrollToPositionWithOffset(
diff --git a/tests/Android.bp b/tests/Android.bp
index 9667277..35a2275 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -148,6 +148,7 @@
platform_apis: true,
test_config: "Launcher3Tests.xml",
data: [":Launcher3"],
+ plugins: ["dagger2-compiler"],
test_suites: ["general-tests"],
}
@@ -237,6 +238,7 @@
"truth",
],
instrumentation_for: "Launcher3",
+ plugins: ["dagger2-compiler"],
upstream: true,
strict_mode: false,
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
index b04bcca..f73a9d3 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
@@ -41,6 +41,8 @@
import com.android.launcher3.LauncherSettings.Favorites.SPANX
import com.android.launcher3.LauncherSettings.Favorites.SPANY
import com.android.launcher3.LauncherSettings.Favorites._ID
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.pm.UserCache
import com.android.launcher3.util.ApiWrapper
@@ -54,6 +56,8 @@
import com.android.launcher3.util.UserIconInfo.TYPE_WORK
import com.android.launcher3.widget.LauncherWidgetHolder
import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
import java.io.StringReader
import org.junit.After
import org.junit.Before
@@ -162,7 +166,9 @@
@Test
fun work_item_added_to_home() {
val apiWrapperMock = spy(ApiWrapper.INSTANCE[targetContext])
- targetContext.putObject(ApiWrapper.INSTANCE, apiWrapperMock)
+ targetContext.initDaggerComponent(
+ DaggerAutoInstallsLayoutTestComponent.builder().bindApiWrapper(apiWrapperMock)
+ )
doReturn(
mapOf(
myUserHandle() to UserIconInfo(myUserHandle(), TYPE_MAIN, 0),
@@ -198,7 +204,7 @@
callback,
SourceResources.wrap(targetContext.resources),
{ Xml.newPullParser().also { it.setInput(StringReader(build())) } },
- TAG_WORKSPACE
+ TAG_WORKSPACE,
)
class MyCallback : LayoutParserCallback {
@@ -214,3 +220,14 @@
}
}
}
+
+@LauncherAppSingleton
+@Component
+interface AutoInstallsLayoutTestComponent : LauncherAppComponent {
+ @Component.Builder
+ interface Builder : LauncherAppComponent.Builder {
+ @BindsInstance fun bindApiWrapper(wrapper: ApiWrapper): Builder
+
+ override fun build(): AutoInstallsLayoutTestComponent
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
index 0c3081f..a9082e2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
@@ -22,6 +22,7 @@
import android.content.Context;
+import com.android.launcher3.Flags;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
@@ -59,7 +60,11 @@
runOnExecutorSync(MODEL_EXECUTOR, () -> {
ModelDbController controller = model.getModelDbController();
// Migrate any previous data so that the DB state is correct
- controller.tryMigrateDB(null /* restoreEventLogger */);
+ if (Flags.gridMigrationRefactor()) {
+ controller.attemptMigrateDb(null /* restoreEventLogger */);
+ } else {
+ controller.tryMigrateDB(null /* restoreEventLogger */);
+ }
// Create DB again to load fresh data
controller.createEmptyDB();
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
similarity index 80%
rename from tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index f57e8a1..7933331 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -22,13 +22,16 @@
import android.database.sqlite.SQLiteDatabase
import android.graphics.Point
import android.os.Process
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
import com.android.launcher3.LauncherSettings.Favorites.*
-import com.android.launcher3.model.GridSizeMigrationUtil.DbReader
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
import com.android.launcher3.pm.UserCache
import com.android.launcher3.provider.LauncherDbUtils
import com.android.launcher3.util.LauncherModelHelper
@@ -38,10 +41,10 @@
import org.junit.Test
import org.junit.runner.RunWith
-/** Unit tests for [GridSizeMigrationUtil] */
+/** Unit tests for [GridSizeMigrationDBController, GridSizeMigrationLogic] */
@SmallTest
@RunWith(AndroidJUnit4::class)
-class GridSizeMigrationUtilTest {
+class GridSizeMigrationTest {
private lateinit var modelHelper: LauncherModelHelper
private lateinit var context: Context
@@ -82,9 +85,22 @@
modelHelper.destroy()
}
- /** Old migration logic, should be modified once is not needed anymore */
@Test
@Throws(Exception::class)
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun testMigrationRefactorFlagOn() {
+ testMigration()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun testMigrationRefactorFlagOff() {
+ testMigration()
+ }
+
+ /** Old migration logic, should be modified once is not needed anymore */
+ @Throws(Exception::class)
fun testMigration() {
// Src Hotseat icons
addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
@@ -113,15 +129,34 @@
idp.numRows = 4
val srcReader = DbReader(db, TMP_TABLE, context)
val destReader = DbReader(db, TABLE_NAME, context)
- GridSizeMigrationUtil.migrate(
- dbHelper,
- srcReader,
- destReader,
- idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
- )
+ if (Flags.gridMigrationRefactor()) {
+ var gridSizeMigrationLogic = GridSizeMigrationLogic()
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ idp.numDatabaseHotseatIcons,
+ srcReader,
+ destReader,
+ dbHelper,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ srcReader,
+ destReader,
+ dbHelper,
+ Point(idp.numColumns, idp.numRows),
+ idsInUse,
+ )
+ } else {
+ GridSizeMigrationDBController.migrate(
+ dbHelper,
+ srcReader,
+ destReader,
+ idp.numDatabaseHotseatIcons,
+ Point(idp.numColumns, idp.numRows),
+ DeviceGridState(context),
+ DeviceGridState(idp),
+ )
+ }
// Check hotseat items
var c =
@@ -187,9 +222,22 @@
assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2))
}
- /** Old migration logic, should be modified once is not needed anymore */
@Test
@Throws(Exception::class)
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun testMigrationBackAndForthRefactorFlagOn() {
+ testMigrationBackAndForth()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun testMigrationBackAndForthRefactorFlagOff() {
+ testMigrationBackAndForth()
+ }
+
+ /** Old migration logic, should be modified once is not needed anymore */
+ @Throws(Exception::class)
fun testMigrationBackAndForth() {
// Hotseat items in grid A
// 1 2 _ 3 4
@@ -224,15 +272,34 @@
val readerGridA = DbReader(db, TMP_TABLE, context)
val readerGridB = DbReader(db, TABLE_NAME, context)
// migrate from A -> B
- GridSizeMigrationUtil.migrate(
- dbHelper,
- readerGridA,
- readerGridB,
- idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
- )
+ if (Flags.gridMigrationRefactor()) {
+ var gridSizeMigrationLogic = GridSizeMigrationLogic()
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ idp.numDatabaseHotseatIcons,
+ readerGridA,
+ readerGridB,
+ dbHelper,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ readerGridA,
+ readerGridB,
+ dbHelper,
+ Point(idp.numColumns, idp.numRows),
+ idsInUse,
+ )
+ } else {
+ GridSizeMigrationDBController.migrate(
+ dbHelper,
+ readerGridA,
+ readerGridB,
+ idp.numDatabaseHotseatIcons,
+ Point(idp.numColumns, idp.numRows),
+ DeviceGridState(context),
+ DeviceGridState(idp),
+ )
+ }
// Check hotseat items in grid B
var c =
@@ -280,15 +347,8 @@
addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 2, testPackage9)
// migrate from B -> A
- GridSizeMigrationUtil.migrate(
- dbHelper,
- readerGridB,
- readerGridA,
- 5,
- Point(5, 5),
- DeviceGridState(idp),
- DeviceGridState(context),
- )
+ migrateGrid(dbHelper, readerGridB, readerGridA, 5, 5, 5)
+
// Check hotseat items in grid A
c =
db.query(
@@ -339,14 +399,13 @@
db.delete(TMP_TABLE, "$_ID=7", null)
// migrate from A -> B
- GridSizeMigrationUtil.migrate(
+ migrateGrid(
dbHelper,
readerGridA,
readerGridB,
idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
+ idp.numColumns,
+ idp.numRows,
)
// Check hotseat items in grid B
@@ -392,6 +451,44 @@
assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
}
+ private fun migrateGrid(
+ dbHelper: DatabaseHelper,
+ srcReader: DbReader,
+ destReader: DbReader,
+ destHotseatSize: Int,
+ pointX: Int,
+ pointY: Int,
+ ) {
+ if (Flags.gridMigrationRefactor()) {
+ var gridSizeMigrationLogic = GridSizeMigrationLogic()
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ idp.numDatabaseHotseatIcons,
+ srcReader,
+ destReader,
+ dbHelper,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ srcReader,
+ destReader,
+ dbHelper,
+ Point(idp.numColumns, idp.numRows),
+ idsInUse,
+ )
+ } else {
+ GridSizeMigrationDBController.migrate(
+ dbHelper,
+ srcReader,
+ destReader,
+ destHotseatSize,
+ Point(pointX, pointY),
+ DeviceGridState(idp),
+ DeviceGridState(context),
+ )
+ }
+ }
+
private fun verifyHotseat(c: Cursor, idp: InvariantDeviceProfile, expected: List<String?>) {
assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons)
val screenIndex = c.getColumnIndex(SCREEN)
@@ -421,6 +518,17 @@
}
@Test
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateToLargerHotseatRefactorFlagOn() {
+ migrateToLargerHotseat()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateToLargerHotseatRefactorFlagOff() {
+ migrateToLargerHotseat()
+ }
+
fun migrateToLargerHotseat() {
val srcHotseatItems =
intArrayOf(
@@ -471,14 +579,13 @@
idp.numRows = 4
val srcReader = DbReader(db, TMP_TABLE, context)
val destReader = DbReader(db, TABLE_NAME, context)
- GridSizeMigrationUtil.migrate(
+ migrateGrid(
dbHelper,
srcReader,
destReader,
idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
+ idp.numColumns,
+ idp.numRows,
)
// Check hotseat items
@@ -516,6 +623,17 @@
}
@Test
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateFromLargerHotseatRefactorFlagOn() {
+ migrateFromLargerHotseat()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateFromLargerHotseatRefactorFlagOff() {
+ migrateFromLargerHotseat()
+ }
+
fun migrateFromLargerHotseat() {
addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
addItem(ITEM_TYPE_DEEP_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
@@ -528,14 +646,13 @@
idp.numRows = 4
val srcReader = DbReader(db, TMP_TABLE, context)
val destReader = DbReader(db, TABLE_NAME, context)
- GridSizeMigrationUtil.migrate(
+ migrateGrid(
dbHelper,
srcReader,
destReader,
idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
+ idp.numColumns,
+ idp.numRows,
)
// Check hotseat items
@@ -573,11 +690,24 @@
c.close()
}
+ @Test
+ @Throws(Exception::class)
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateFromSmallerGridBigDifferenceRefactorFlagOn() {
+ migrateFromSmallerGridBigDifference()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateFromSmallerGridBigDifferenceRefactorFlagOff() {
+ migrateFromSmallerGridBigDifference()
+ }
+
/**
* Migrating from a smaller grid to a large one should reflow the pages if the column difference
* is more than 2
*/
- @Test
@Throws(Exception::class)
fun migrateFromSmallerGridBigDifference() {
enableNewMigrationLogic("2,2")
@@ -594,14 +724,13 @@
idp.numRows = 5
val srcReader = DbReader(db, TMP_TABLE, context)
val destReader = DbReader(db, TABLE_NAME, context)
- GridSizeMigrationUtil.migrate(
+ migrateGrid(
dbHelper,
srcReader,
destReader,
idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
+ idp.numColumns,
+ idp.numRows,
)
// Get workspace items
@@ -636,9 +765,22 @@
assertThat(locMap[testPackage5]).isEqualTo(0)
}
- /** Migrating from a larger grid to a smaller, we reflow from page 0 */
@Test
@Throws(Exception::class)
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateFromLargerGridRefactorFlagOn() {
+ migrateFromLargerGrid()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun migrateFromLargerGridRefactorFlagOff() {
+ migrateFromLargerGrid()
+ }
+
+ /** Migrating from a larger grid to a smaller, we reflow from page 0 */
+ @Throws(Exception::class)
fun migrateFromLargerGrid() {
enableNewMigrationLogic("5,5")
@@ -654,14 +796,13 @@
idp.numRows = 4
val srcReader = DbReader(db, TMP_TABLE, context)
val destReader = DbReader(db, TABLE_NAME, context)
- GridSizeMigrationUtil.migrate(
+ migrateGrid(
dbHelper,
srcReader,
destReader,
idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp),
+ idp.numColumns,
+ idp.numRows,
)
// Get workspace items
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 308f200..a3a680e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -207,4 +207,21 @@
.onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
assertFalse(displayController.getInfo().isTransientTaskbar())
}
+
+ @Test
+ @UiThreadTest
+ fun testLockedTaskbarChangeOnConfigurationChanged() {
+ whenever(windowManagerProxy.showLockedTaskbarOnHome(any())).thenReturn(true)
+ whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(true)
+ whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+ whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+ DisplayController.enableTaskbarModePreferenceForTests(true)
+ assertTrue(displayController.getInfo().isTransientTaskbar())
+
+ displayController.onConfigurationChanged(configuration)
+
+ verify(displayInfoChangeListener)
+ .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+ assertFalse(displayController.getInfo().isTransientTaskbar())
+ }
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
index 6bd182b..8d072d8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
@@ -1,6 +1,7 @@
package com.android.launcher3.util
import android.content.ContentValues
+import com.android.launcher3.Flags
import com.android.launcher3.LauncherModel
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID
@@ -30,7 +31,8 @@
loadModelSync()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
modelDbController.run {
- tryMigrateDB(null /* restoreEventLogger */)
+ if (Flags.gridMigrationRefactor()) attemptMigrateDb(null /* restoreEventLogger */)
+ else tryMigrateDB(null /* restoreEventLogger */)
createEmptyDB()
clearEmptyDbFlag()
}
@@ -67,12 +69,12 @@
tableName: String = Favorites.TABLE_NAME,
appWidgetId: Int = -1,
appWidgetSource: Int = -1,
- appWidgetProvider: String? = null
+ appWidgetProvider: String? = null,
) {
loadModelSync()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
val controller: ModelDbController = modelDbController
- controller.tryMigrateDB(null /* restoreEventLogger */)
+ controller.attemptMigrateDb(null /* restoreEventLogger */)
modelDbController.newTransaction().use { transaction ->
val values =
ContentValues().apply {
diff --git a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
index 35ac0a1..b4ee090 100644
--- a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
+++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
@@ -16,6 +16,8 @@
package com.android.launcher3.backuprestore
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -52,10 +54,24 @@
setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_NARROW_GRID_RESTORE)
}
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun oldDatabasesNotPresentAfterRestoreRefactorFlagEnabled() {
+ oldDatabasesNotPresentAfterRestore()
+ }
+
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun oldDatabasesNotPresentAfterRestoreRefactorFlagDisabled() {
+ oldDatabasesNotPresentAfterRestore()
+ }
+
@Test
fun oldDatabasesNotPresentAfterRestore() {
val dbController = ModelDbController(getInstrumentation().targetContext)
- dbController.tryMigrateDB(null)
+ if (Flags.gridMigrationRefactor()) {
+ dbController.attemptMigrateDb(null)
+ } else {
+ dbController.tryMigrateDB(null)
+ }
TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
assert(backAndRestoreRule.getDatabaseFiles().size == 1) {
"There should only be one database after restoring, the last one used. Actual databases ${backAndRestoreRule.getDatabaseFiles()}"
diff --git a/tests/src/com/android/launcher3/model/GridMigrationTest.kt b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
index 15222a4..666ec16 100644
--- a/tests/src/com/android/launcher3/model/GridMigrationTest.kt
+++ b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
@@ -52,11 +52,15 @@
phoneContext,
dbFileName,
{ UserCache.INSTANCE.get(phoneContext).getSerialNumberForUser(it) },
- {}
+ {},
)
- fun readEntries(): List<GridSizeMigrationUtil.DbEntry> =
- GridSizeMigrationUtil.readAllEntries(dbHelper.readableDatabase, TABLE_NAME, phoneContext)
+ fun readEntries(): List<DbEntry> =
+ GridSizeMigrationDBController.readAllEntries(
+ dbHelper.readableDatabase,
+ TABLE_NAME,
+ phoneContext,
+ )
}
/**
@@ -80,7 +84,7 @@
TestToPhoneFileCopier(
src = "databases/GridMigrationTest/$DB_FILE",
dest = "databases/$DB_FILE",
- removeOnFinish = true
+ removeOnFinish = true,
)
@Before
@@ -89,13 +93,24 @@
}
private fun migrate(src: GridMigrationData, dst: GridMigrationData) {
- GridSizeMigrationUtil.migrateGridIfNeeded(
- phoneContext,
- src.gridState,
- dst.gridState,
- dst.dbHelper,
- src.dbHelper.readableDatabase
- )
+ if (Flags.gridMigrationRefactor()) {
+ val gridSizeMigrationLogic = GridSizeMigrationLogic()
+ gridSizeMigrationLogic.migrateGrid(
+ phoneContext,
+ src.gridState,
+ dst.gridState,
+ dst.dbHelper,
+ src.dbHelper.readableDatabase,
+ )
+ } else {
+ GridSizeMigrationDBController.migrateGridIfNeeded(
+ phoneContext,
+ src.gridState,
+ dst.gridState,
+ dst.dbHelper,
+ src.dbHelper.readableDatabase,
+ )
+ }
}
/**
@@ -115,10 +130,8 @@
}
private fun compare(dst: GridMigrationData, target: GridMigrationData) {
- val sort = compareBy<GridSizeMigrationUtil.DbEntry>({ it.cellX }, { it.cellY })
- val mapF = { it: GridSizeMigrationUtil.DbEntry ->
- EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank)
- }
+ val sort = compareBy<DbEntry>({ it.cellX }, { it.cellY })
+ val mapF = { it: DbEntry -> EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank) }
val entriesDst = dst.readEntries().sortedWith(sort).map(mapF)
val entriesTarget = target.readEntries().sortedWith(sort).map(mapF)
@@ -149,7 +162,7 @@
TestToPhoneFileCopier(
src = "databases/GridMigrationTest/result5x5to3x3.db",
dest = "databases/result5x5to3x3.db",
- removeOnFinish = true
+ removeOnFinish = true,
)
@Test
@@ -160,10 +173,10 @@
GridMigrationData(
null, // in memory db, to download a new db change null for the filename of the
// db name to store it. Do not use existing names.
- DeviceGridState(3, 3, 3, TYPE_PHONE, "")
+ DeviceGridState(3, 3, 3, TYPE_PHONE, ""),
),
target =
- GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, ""))
+ GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, "")),
)
@JvmField
@@ -172,7 +185,7 @@
TestToPhoneFileCopier(
src = "databases/GridMigrationTest/result5x5to4x7.db",
dest = "databases/result5x5to4x7.db",
- removeOnFinish = true
+ removeOnFinish = true,
)
@Test
@@ -183,10 +196,10 @@
GridMigrationData(
null, // in memory db, to download a new db change null for the filename of the
// db name to store it. Do not use existing names.
- DeviceGridState(4, 7, 4, TYPE_PHONE, "")
+ DeviceGridState(4, 7, 4, TYPE_PHONE, ""),
),
target =
- GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, ""))
+ GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, "")),
)
@JvmField
@@ -195,7 +208,7 @@
TestToPhoneFileCopier(
src = "databases/GridMigrationTest/result5x5to5x8.db",
dest = "databases/result5x5to5x8.db",
- removeOnFinish = true
+ removeOnFinish = true,
)
@Test
@@ -206,10 +219,10 @@
GridMigrationData(
null, // in memory db, to download a new db change null for the filename of the
// db name to store it. Do not use existing names.
- DeviceGridState(5, 8, 5, TYPE_PHONE, "")
+ DeviceGridState(5, 8, 5, TYPE_PHONE, ""),
),
target =
- GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, ""))
+ GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, "")),
)
@JvmField
@@ -218,7 +231,7 @@
TestToPhoneFileCopier(
src = "databases/GridMigrationTest/flagged_result5x5to5x8.db",
dest = "databases/flagged_result5x5to5x8.db",
- removeOnFinish = true
+ removeOnFinish = true,
)
@Test
@@ -230,13 +243,13 @@
GridMigrationData(
null, // in memory db, to download a new db change null for the filename of the
// db name to store it. Do not use existing names.
- DeviceGridState(5, 8, 5, TYPE_PHONE, "")
+ DeviceGridState(5, 8, 5, TYPE_PHONE, ""),
),
target =
GridMigrationData(
"flagged_result5x5to5x8.db",
- DeviceGridState(5, 8, 5, TYPE_PHONE, "")
- )
+ DeviceGridState(5, 8, 5, TYPE_PHONE, ""),
+ ),
)
}
}
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index b17cd4d..ef7242f 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -3,6 +3,8 @@
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -203,7 +205,8 @@
}
@Test
- fun `When launcher_broadcast_installed_apps and is restore then send installed item broadcast`() {
+ @DisableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+ fun `When secure setting true and is restore then send installed item broadcast`() {
// Given
val spyContext = spy(context)
`when`(app.context).thenReturn(spyContext)
@@ -271,6 +274,76 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+ fun `When broadcast flag true and is restore then send installed item broadcast`() {
+ // Given
+ val spyContext = spy(context)
+ `when`(app.context).thenReturn(spyContext)
+ whenever(
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ any(),
+ any(),
+ any(),
+ any(),
+ )
+ )
+ .thenReturn(listOf(expectedBroadcastModel))
+
+ whenever(
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(
+ spyContext,
+ listOf(expectedBroadcastModel),
+ )
+ )
+ .thenCallRealMethod()
+
+ Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 0)
+ RestoreDbTask.setPending(spyContext)
+
+ // When
+ LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+ .runSyncOnBackgroundThread()
+
+ // Then
+ val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(spyContext).sendBroadcast(argumentCaptor.capture())
+ val actualBroadcastIntent = argumentCaptor.value
+ assertEquals(expectedBroadcastModel.installerPackage, actualBroadcastIntent.`package`)
+ assertEquals(
+ ArrayList(expectedBroadcastModel.installedWorkspaceItems),
+ actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems"),
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.installedHotseatItems),
+ actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems"),
+ )
+ assertEquals(
+ ArrayList(
+ expectedBroadcastModel.firstScreenInstalledWidgets +
+ expectedBroadcastModel.secondaryScreenInstalledWidgets
+ ),
+ actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems"),
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingCollectionItems),
+ actualBroadcastIntent.getStringArrayListExtra("folderItem"),
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingWorkspaceItems),
+ actualBroadcastIntent.getStringArrayListExtra("workspaceItem"),
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingHotseatItems),
+ actualBroadcastIntent.getStringArrayListExtra("hotseatItem"),
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingWidgetItems),
+ actualBroadcastIntent.getStringArrayListExtra("widgetItem"),
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
fun `When not a restore then installed item broadcast not sent`() {
// Given
val spyContext = spy(context)
@@ -304,7 +377,8 @@
}
@Test
- fun `When launcher_broadcast_installed_apps false then installed item broadcast not sent`() {
+ @DisableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+ fun `When broadcast flag and secure setting false then installed item broadcast not sent`() {
// Given
val spyContext = spy(context)
`when`(app.context).thenReturn(spyContext)
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
index 03d0195..b96dbcd 100644
--- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -20,17 +20,21 @@
import android.database.sqlite.SQLiteDatabase
import android.graphics.Point
import android.os.Process
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.Flags
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.celllayout.testgenerator.ValidGridMigrationTestCaseGenerator
import com.android.launcher3.celllayout.testgenerator.generateItemsForTest
import com.android.launcher3.model.DatabaseHelper
import com.android.launcher3.model.DeviceGridState
-import com.android.launcher3.model.GridSizeMigrationUtil
+import com.android.launcher3.model.GridSizeMigrationDBController
+import com.android.launcher3.model.GridSizeMigrationLogic
import com.android.launcher3.pm.UserCache
import com.android.launcher3.provider.LauncherDbUtils
import com.android.launcher3.util.rule.TestStabilityRule
@@ -130,22 +134,52 @@
addItemsToDb(dbHelper.writableDatabase, dstGrid)
LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use {
- GridSizeMigrationUtil.migrate(
- dbHelper,
- GridSizeMigrationUtil.DbReader(it.db, srcGrid.tableName, context),
- GridSizeMigrationUtil.DbReader(it.db, dstGrid.tableName, context),
- dstGrid.size.x,
- dstGrid.size,
- srcGrid.toGridState(),
- dstGrid.toGridState(),
- )
+ if (Flags.gridMigrationRefactor()) {
+ val gridSizeMigrationLogic = GridSizeMigrationLogic()
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ dstGrid.size.x,
+ GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+ GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+ dbHelper,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+ GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+ dbHelper,
+ dstGrid.size,
+ idsInUse,
+ )
+ } else {
+ GridSizeMigrationDBController.migrate(
+ dbHelper,
+ GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+ GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+ dstGrid.size.x,
+ dstGrid.size,
+ srcGrid.toGridState(),
+ dstGrid.toGridState(),
+ )
+ }
it.commit()
}
return readDb(dstGrid.tableName, dbHelper.readableDatabase)
}
@Test
- fun runTestCase() {
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun runTestCaseRefactorFlagEnabled() {
+ runTestCase()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun runTestCaseRefactorFlagDisabled() {
+ runTestCase()
+ }
+
+ private fun runTestCase() {
val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
for (i in 0..SMALL_TEST_SIZE) {
val testCase = caseGenerator.generateTestCase(isDestEmpty = true)
@@ -163,7 +197,18 @@
}
@Test
- fun mergeBoards() {
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun mergeBoardsRefactorFlagEnabled() {
+ mergeBoards()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun mergeBoardsRefactorFlagDisabled() {
+ mergeBoards()
+ }
+
+ private fun mergeBoards() {
val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
for (i in 0..SMALL_TEST_SIZE) {
val testCase = caseGenerator.generateTestCase(isDestEmpty = false)
@@ -187,7 +232,20 @@
// This test takes about 4 minutes, there is no need to run it in presubmit.
@Stability(flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT)
@Test
- fun runExtensiveTestCases() {
+ @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun runExtensiveTestCasesRefactorFlagEnabled() {
+ runExtensiveTestCases()
+ }
+
+ // This test takes about 4 minutes, there is no need to run it in presubmit.
+ @Stability(flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT)
+ @Test
+ @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+ fun runExtensiveTestCasesRefactorFlagDisabled() {
+ runExtensiveTestCases()
+ }
+
+ private fun runExtensiveTestCases() {
val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
for (i in 0..LARGE_TEST_SIZE) {
val testCase = caseGenerator.generateTestCase(isDestEmpty = true)
diff --git a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
index f54668c..ae54e95 100644
--- a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
+++ b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
@@ -62,6 +62,8 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.R;
import com.android.launcher3.allapps.PrivateProfileManager;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.logging.StatsLogManager.StatsLogger;
import com.android.launcher3.model.data.AppInfo;
@@ -79,6 +81,9 @@
import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
+import dagger.BindsInstance;
+import dagger.Component;
+
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -115,8 +120,10 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ mSandboxContext.initDaggerComponent(
+ DaggerSystemShortcutTest_TestComponent.builder().bindApiWrapper(
+ ApiWrapper.INSTANCE.get(mSandboxContext)));
mSandboxContext.putObject(UserCache.INSTANCE, mUserCache);
- mSandboxContext.putObject(ApiWrapper.INSTANCE, mApiWrapper);
mTestContext = new TestSandboxModelContextWrapper(mSandboxContext) {
@Override
public StatsLogManager getStatsLogManager() {
@@ -405,4 +412,16 @@
systemShortcut.onClick(mView);
verify(mSandboxContext).startActivity(any());
}
+
+ @LauncherAppSingleton
+ @Component
+ interface TestComponent extends LauncherAppComponent {
+ @Component.Builder
+ interface Builder extends LauncherAppComponent.Builder {
+ @BindsInstance Builder bindApiWrapper(ApiWrapper wrapper);
+
+ @Override
+ TestComponent build();
+ }
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 0edcfea..1002ca4 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -369,7 +369,6 @@
}
}
-
int getTaskCount() {
return getTasks().size();
}
@@ -441,7 +440,7 @@
"Not expecting an actions bar: device is tablet and task is not centered");
return false;
}
- if (task.isTaskSplit() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
+ if (task.isGrouped() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
testLogD(TAG, "Not expecting an actions bar: device is phone and task is split");
// Overview actions aren't visible for split screen tasks, except for save app pair
// button on tablets.
@@ -504,11 +503,11 @@
"want to assert overview actions view visibility="
+ isActionsViewVisible()
+ ", focused task is "
- + (task == null ? "null" : (task.isTaskSplit() ? "split" : "not split"))
+ + (task == null ? "null" : (task.isGrouped() ? "split" : "not split"))
)) {
if (isActionsViewVisible()) {
- if (task.isTaskSplit()) {
+ if (task.isGrouped()) {
mLauncher.waitForOverviewObject("action_save_app_pair");
} else {
mLauncher.waitForOverviewObject("action_buttons");
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 9a8d952..5fd4dac 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -16,9 +16,10 @@
package com.android.launcher3.tapl;
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.DEFAULT;
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT;
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.DEFAULT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.DESKTOP;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT;
import android.graphics.Rect;
@@ -69,11 +70,11 @@
* divider between.
*/
int getVisibleHeight() {
- if (isTaskSplit()) {
+ if (isGrouped()) {
return getCombinedSplitTaskHeight();
}
- UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
+ UiObject2 taskSnapshot1 = findObjectInTask((isDesktop() ? DESKTOP : DEFAULT).snapshotRes);
return taskSnapshot1.getVisibleBounds().height();
}
@@ -102,7 +103,7 @@
* divider between.
*/
int getVisibleWidth() {
- if (isTaskSplit()) {
+ if (isGrouped()) {
return getCombinedSplitTaskWidth();
}
@@ -164,8 +165,11 @@
dismissBySwipingUp();
+ long numNonDesktopTasks = mOverview.getCurrentTasksForTablet()
+ .stream().filter(t -> !t.isDesktop()).count();
+
try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("dismissed")) {
- if (taskWasFocused) {
+ if (taskWasFocused && numNonDesktopTasks > 0) {
mLauncher.assertNotNull("No task became focused",
mOverview.getFocusedTaskForTablet());
}
@@ -256,7 +260,7 @@
/** Taps the task menu of the split task. Returns the split task's menu object. */
@NonNull
- public OverviewTaskMenu tapMenu(OverviewSplitTask task) {
+ public OverviewTaskMenu tapMenu(OverviewTaskContainer task) {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to tap the task menu")) {
@@ -270,10 +274,6 @@
}
}
- boolean isTaskSplit() {
- return findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes) != null;
- }
-
private UiObject2 findObjectInTask(String resName) {
return mTask.findObject(mLauncher.getOverviewObjectSelector(resName));
}
@@ -285,8 +285,8 @@
* TODO(b/342627272): remove Nullable support once the bug causing it to be null is fixed.
*/
public boolean containsContentDescription(@Nullable String expected,
- OverviewSplitTask overviewSplitTask) {
- String actual = findObjectInTask(overviewSplitTask.snapshotRes).getContentDescription();
+ OverviewTaskContainer overviewTaskContainer) {
+ String actual = findObjectInTask(overviewTaskContainer.snapshotRes).getContentDescription();
if (actual == null && expected == null) {
return true;
}
@@ -315,21 +315,31 @@
}
}
+ boolean isGrouped() {
+ return mType == TaskViewType.GROUPED;
+ }
+
+ public boolean isDesktop() {
+ return mType == TaskViewType.DESKTOP;
+ }
+
/**
- * Enum used to specify which task is retrieved when it is a split task.
+ * Enum used to specify which resource name should be used depending on the type of the task.
*/
- public enum OverviewSplitTask {
+ public enum OverviewTaskContainer {
// The main task when the task is not split.
DEFAULT("snapshot", "icon"),
// The first task in split task.
SPLIT_TOP_OR_LEFT("snapshot", "icon"),
// The second task in split task.
- SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon");
+ SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"),
+ // The desktop task.
+ DESKTOP("background", "icon");
public final String snapshotRes;
public final String iconAppRes;
- OverviewSplitTask(String snapshotRes, String iconAppRes) {
+ OverviewTaskContainer(String snapshotRes, String iconAppRes) {
this.snapshotRes = snapshotRes;
this.iconAppRes = iconAppRes;
}