Merge "Don't allow Desktop tasks to go outside Overview task bounds" 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 6ff3bb2..949acc1 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"
@@ -494,8 +501,8 @@
}
flag {
- name: "enable_launcher_pill"
+ name: "enable_contrast_tiles"
namespace: "launcher"
- description: "Enable Workspace Launcher Pill in Simple Mode View."
+ description: "Enable launcher app contrast tiles."
bug: "341217082"
-}
\ No newline at end of file
+}
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/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/dimens.xml b/quickstep/res/values/dimens.xml
index 451ba55..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>
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/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index 929e793..e3bcb0d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -25,20 +25,24 @@
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.statemanager.StateManager;
-import com.android.quickstep.RecentsActivity;
+import com.android.launcher3.statemanager.StatefulContainer;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.fallback.RecentsState;
import com.android.quickstep.util.TISBindHelper;
import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
import java.util.stream.Stream;
/**
* A data source which integrates with the fallback RecentsActivity instance (for 3P launchers).
+ * @param <T> The type of the RecentsViewContainer that will handle Recents state changes.
*/
-public class FallbackTaskbarUIController extends TaskbarUIController {
+public class FallbackTaskbarUIController
+ <T extends RecentsViewContainer & StatefulContainer<RecentsState>>
+ extends TaskbarUIController {
- private final RecentsActivity mRecentsActivity;
+ private final T mRecentsContainer;
private final StateManager.StateListener<RecentsState> mStateListener =
new StateManager.StateListener<RecentsState>() {
@@ -63,23 +67,23 @@
}
};
- public FallbackTaskbarUIController(RecentsActivity recentsActivity) {
- mRecentsActivity = recentsActivity;
+ public FallbackTaskbarUIController(T recentsContainer) {
+ mRecentsContainer = recentsContainer;
}
@Override
protected void init(TaskbarControllers taskbarControllers) {
super.init(taskbarControllers);
- mRecentsActivity.setTaskbarUIController(this);
- mRecentsActivity.getStateManager().addStateListener(mStateListener);
+ mRecentsContainer.setTaskbarUIController(this);
+ mRecentsContainer.getStateManager().addStateListener(mStateListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
getRecentsView().setTaskLaunchListener(null);
- mRecentsActivity.setTaskbarUIController(null);
- mRecentsActivity.getStateManager().removeStateListener(mStateListener);
+ mRecentsContainer.setTaskbarUIController(null);
+ mRecentsContainer.getStateManager().removeStateListener(mStateListener);
}
/**
@@ -109,7 +113,7 @@
@Override
public RecentsView getRecentsView() {
- return mRecentsActivity.getOverviewPanel();
+ return mRecentsContainer.getOverviewPanel();
}
@Override
@@ -131,11 +135,11 @@
@Nullable
@Override
protected TISBindHelper getTISBindHelper() {
- return mRecentsActivity.getTISBindHelper();
+ return mRecentsContainer.getTISBindHelper();
}
@Override
protected String getTaskbarUIControllerName() {
- return "FallbackTaskbarUIController";
+ return "FallbackTaskbarUIController<" + mRecentsContainer.getClass().getSimpleName() + ">";
}
}
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 2902d55..7a63f74 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -31,12 +31,14 @@
import androidx.annotation.Nullable;
import com.android.internal.jank.Cuj;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.desktop.DesktopAppLaunchTransition;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.GroupTask;
@@ -69,6 +71,9 @@
private boolean mOnDesktop;
private boolean mWasDesktopTaskFilteredOut;
+ private boolean mWasOpenedFromTaskbar;
+
+ private boolean mDetachingFromWindow = false;
protected KeyboardQuickSwitchViewController(
@NonNull TaskbarControllers controllers,
@@ -85,6 +90,10 @@
return mCurrentFocusIndex;
}
+ protected boolean wasOpenedFromTaskbar() {
+ return mWasOpenedFromTaskbar;
+ }
+
protected void openQuickSwitchView(
@NonNull List<GroupTask> tasks,
int numHiddenTasks,
@@ -94,10 +103,20 @@
boolean hasDesktopTask,
boolean wasDesktopTaskFilteredOut,
boolean wasOpenedFromTaskbar) {
- positionView(wasOpenedFromTaskbar);
+ final boolean isTransientTaskBar = DisplayController.isTransientTaskbar(
+ mControllers.taskbarActivityContext);
+ positionView(wasOpenedFromTaskbar, isTransientTaskBar);
+
+ // Keep the taskbar unstashed if the KQS is opened.
+ if (wasOpenedFromTaskbar && isTransientTaskBar) {
+ mControllers.taskbarStashController.updateTaskbarTimeout(/* isAutohideSuspended= */
+ true);
+ }
+
mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
mOnDesktop = onDesktop;
mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
+ mWasOpenedFromTaskbar = wasOpenedFromTaskbar;
mKeyboardQuickSwitchView.applyLoadPlan(
mOverlayContext,
@@ -109,7 +128,7 @@
/* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
}
- protected void positionView(boolean wasOpenedFromTaskbar) {
+ protected void positionView(boolean wasOpenedFromTaskbar, boolean isTransientTaskbar) {
if (!wasOpenedFromTaskbar) {
// Keep the default positioning.
return;
@@ -120,8 +139,16 @@
final Resources resources = mKeyboardQuickSwitchView.getResources();
final int marginHorizontal = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_margin_ends);
- final int marginBottom = resources.getDimensionPixelSize(
+
+ final DeviceProfile dp = mControllers.taskbarActivityContext.getDeviceProfile();
+ // Calculate the additional margin space that the KQS should move up for the transient
+ // taskbar. The value of spaceForTaskbar is the distance between the bottom of the KQS
+ // view with 0 bottom margin to the top of the transient taskbar view.
+ final int spaceForTaskbar = isTransientTaskbar ? dp.taskbarHeight + dp.taskbarBottomMargin
+ - dp.stashedTaskbarHeight : 0;
+ final int marginBottom = spaceForTaskbar + 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;
@@ -237,7 +264,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);
}
@@ -254,6 +286,7 @@
pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex);
pw.println(prefix + "\tmOnDesktop=" + mOnDesktop);
pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
+ pw.println(prefix + "\tmWasOpenedFromTaskbar=" + mWasOpenedFromTaskbar);
}
/**
@@ -327,5 +360,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 6f1e96f..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;
@@ -1217,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/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 4a85acc..5a63ca6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -169,7 +169,7 @@
taskbarOverlayController.init(this);
taskbarAllAppsController.init(this, sharedState.allAppsVisible);
navButtonController.init(this);
- bubbleControllers.ifPresent(controllers -> controllers.init(this));
+ bubbleControllers.ifPresent(controllers -> controllers.init(sharedState, this));
taskbarInsetsController.init(this);
voiceInteractionWindowController.init(this);
taskbarRecentAppsController.init(this);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 4a6b6d4..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
@@ -150,7 +149,7 @@
}
if (
taskbarStashController.isInApp ||
- taskbarStashController.isInOverview ||
+ controllers.uiController.isInOverviewUi ||
DisplayController.showLockedTaskbarOnHome(context)
) {
// only add the taskbar touch region if not on home
@@ -259,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/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index c18cf28..ab4b1b6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -71,7 +71,9 @@
import com.android.quickstep.AllAppsActionManager;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.views.RecentsViewContainer;
import com.android.systemui.shared.statusbar.phone.BarTransitions;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -115,7 +117,6 @@
private WindowManager mWindowManager;
private FrameLayout mTaskbarRootLayout;
private boolean mAddedWindow;
- private boolean mIsSuspended;
private final TaskbarNavButtonController mNavButtonController;
private final ComponentCallbacks mComponentCallbacks;
@@ -131,6 +132,8 @@
private TaskbarActivityContext mTaskbarActivityContext;
private StatefulActivity mActivity;
+ private RecentsViewContainer mRecentsViewContainer;
+
/**
* Cache a copy here so we can initialize state whenever taskbar is recreated, since
* this class does not get re-initialized w/ new taskbars.
@@ -404,9 +407,28 @@
}
mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
+ if (activity instanceof RecentsViewContainer recentsViewContainer) {
+ setRecentsViewContainer(recentsViewContainer);
+ }
+ }
+
+ /**
+ * Sets the current RecentsViewContainer, from which we create a TaskbarUIController.
+ */
+ public void setRecentsViewContainer(@NonNull RecentsViewContainer recentsViewContainer) {
+ if (mRecentsViewContainer == recentsViewContainer) {
+ return;
+ }
+ if (mRecentsViewContainer == mActivity) {
+ // When switching to RecentsWindowManager (not an Activity), the old mActivity is not
+ // destroyed, nor is there a new Activity to replace it. Thus if we don't clear it here,
+ // it will not get re-set properly if we return to the Activity (e.g. NexusLauncher).
+ mActivityOnDestroyCallback.run();
+ }
+ mRecentsViewContainer = recentsViewContainer;
if (mTaskbarActivityContext != null) {
mTaskbarActivityContext.setUIController(
- createTaskbarUIControllerForActivity(mActivity));
+ createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
}
}
@@ -429,12 +451,18 @@
/**
* Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
*/
- private TaskbarUIController createTaskbarUIControllerForActivity(StatefulActivity activity) {
- if (activity instanceof QuickstepLauncher) {
- return new LauncherTaskbarUIController((QuickstepLauncher) activity);
+ private TaskbarUIController createTaskbarUIControllerForRecentsViewContainer(
+ RecentsViewContainer container) {
+ if (container instanceof QuickstepLauncher quickstepLauncher) {
+ return new LauncherTaskbarUIController(quickstepLauncher);
}
- if (activity instanceof RecentsActivity) {
- return new FallbackTaskbarUIController((RecentsActivity) activity);
+ // If a 3P Launcher is default, always use FallbackTaskbarUIController regardless of
+ // whether the recents container is RecentsActivity or RecentsWindowManager.
+ if (container instanceof RecentsActivity recentsActivity) {
+ return new FallbackTaskbarUIController<>(recentsActivity);
+ }
+ if (container instanceof RecentsWindowManager recentsWindowManager) {
+ return new FallbackTaskbarUIController<>(recentsWindowManager);
}
return TaskbarUIController.DEFAULT;
}
@@ -446,8 +474,6 @@
*/
@VisibleForTesting
public synchronized void recreateTaskbar() {
- if (mIsSuspended) return;
-
Trace.beginSection("recreateTaskbar");
try {
DeviceProfile dp = mUserUnlocked ?
@@ -484,9 +510,9 @@
mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
mTaskbarActivityContext.init(mSharedState);
- if (mActivity != null) {
+ if (mRecentsViewContainer != null) {
mTaskbarActivityContext.setUIController(
- createTaskbarUIControllerForActivity(mActivity));
+ createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
}
if (enableTaskbarNoRecreate()) {
@@ -663,16 +689,6 @@
}
}
- /**
- * Removes Taskbar from the window manager and prevents recreation if {@code true}.
- * <p>
- * Suspending is for testing purposes only; avoid calling this method in production.
- */
- @VisibleForTesting
- public void setSuspended(boolean isSuspended) {
- mIsSuspended = isSuspended;
- }
-
private void addTaskbarRootViewToWindow() {
if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
mWindowManager.addView(mTaskbarRootLayout,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
index 729cbe9..a64dab1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
@@ -30,6 +30,10 @@
import android.view.InsetsFrameProvider;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleInfo;
+
+import java.util.List;
/**
* State shared across different taskbar instance
@@ -69,6 +73,15 @@
public boolean allAppsVisible = false;
+ public BubbleBarLocation bubbleBarLocation;
+
+ public List<BubbleInfo> bubbleInfoItems;
+
+ /** Returns whether there are a saved bubbles. */
+ public boolean hasSavedBubbles() {
+ return bubbleInfoItems != null && !bubbleInfoItems.isEmpty();
+ }
+
// LauncherTaskbarUIController#mTaskbarInAppDisplayProgressMultiProp
public float[] inAppDisplayProgressMultiPropValues = new float[DISPLAY_PROGRESS_COUNT];
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/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 6f2d459..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);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index b22fd6f..30e4e47 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -35,6 +35,7 @@
import android.util.ArrayMap;
import android.util.Log;
+import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
import com.android.launcher3.util.Executors.SimpleThreadFactory;
import com.android.quickstep.SystemUiProxy;
@@ -47,6 +48,7 @@
import com.android.wm.shell.shared.bubbles.RemovedBubble;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -112,6 +114,7 @@
private BubbleBarItem mSelectedBubble;
+ private TaskbarSharedState mSharedState;
private ImeVisibilityChecker mImeVisibilityChecker;
private BubbleBarViewController mBubbleBarViewController;
private BubbleStashController mBubbleStashController;
@@ -173,12 +176,25 @@
public void onDestroy() {
mSystemUiProxy.setBubblesListener(null);
+ // Saves bubble bar state
+ BubbleInfo[] bubbleInfoItems = new BubbleInfo[mBubbles.size()];
+ mBubbles.values().forEach(bubbleBarBubble -> {
+ int index = mBubbleBarViewController.bubbleViewIndex(bubbleBarBubble.getView());
+ if (index < 0 || index >= bubbleInfoItems.length) {
+ Log.e(TAG, "Found improper index: " + index + " for " + bubbleBarBubble);
+ } else {
+ bubbleInfoItems[index] = bubbleBarBubble.getInfo();
+ }
+ });
+ mSharedState.bubbleInfoItems = Arrays.asList(bubbleInfoItems);
}
/** Initializes controllers. */
public void init(BubbleControllers bubbleControllers,
BubbleBarLocationListener bubbleBarLocationListener,
- ImeVisibilityChecker imeVisibilityChecker) {
+ ImeVisibilityChecker imeVisibilityChecker,
+ TaskbarSharedState sharedState) {
+ mSharedState = sharedState;
mImeVisibilityChecker = imeVisibilityChecker;
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
mBubbleStashController = bubbleControllers.bubbleStashController;
@@ -188,6 +204,7 @@
mBubbleBarLocationListener = bubbleBarLocationListener;
bubbleControllers.runAfterInit(() -> {
+ restoreSavedState(sharedState);
mBubbleBarViewController.setHiddenForBubbles(
!sBubbleBarEnabled || mBubbles.isEmpty());
mBubbleStashedHandleViewController.ifPresent(
@@ -266,6 +283,26 @@
}
}
+ private void restoreSavedState(TaskbarSharedState sharedState) {
+ if (sharedState.bubbleBarLocation != null) {
+ updateBubbleBarLocationInternal(sharedState.bubbleBarLocation);
+ }
+ List<BubbleInfo> bubbleInfos = sharedState.bubbleInfoItems;
+ if (bubbleInfos == null || bubbleInfos.isEmpty()) return;
+ // Iterate in reverse because new bubbles are added in front and the list is in order.
+ for (int i = bubbleInfos.size() - 1; i >= 0; i--) {
+ BubbleBarBubble bubble = mBubbleCreator.populateBubble(mContext,
+ bubbleInfos.get(i), mBarView, /* existingBubble = */ null);
+ if (bubble == null) {
+ Log.e(TAG, "Could not instantiate BubbleBarBubble for " + bubbleInfos.get(i));
+ continue;
+ }
+ addBubbleInternally(bubble, /* showAppBadge = */
+ mBubbleBarViewController.isExpanded() || i == 0,
+ /* isExpanding = */ false, /* suppressAnimation = */ true);
+ }
+ }
+
private void applyViewChanges(BubbleBarViewUpdate update) {
final boolean isCollapsed = (update.expandedChanged && !update.expanded)
|| (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
@@ -277,6 +314,12 @@
update.initialState || mBubbleBarViewController.isHiddenForSysui()
|| mImeVisibilityChecker.isImeVisible();
+ if (update.initialState && mSharedState.hasSavedBubbles()) {
+ // clear restored state
+ mBubbleBarViewController.removeAllBubbles();
+ mBubbles.clear();
+ }
+
BubbleBarBubble bubbleToSelect = null;
if (Flags.enableOptionalBubbleOverflow()
@@ -347,8 +390,8 @@
for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
BubbleBarBubble bubble = update.currentBubbles.get(i);
if (bubble != null) {
- mBubbles.put(bubble.getKey(), bubble);
- mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
+ addBubbleInternally(bubble, /* showAppBadge = */ !isCollapsed || i == 0,
+ isExpanding, suppressAnimation);
if (isCollapsed) {
// If we're collapsed, the most recently added bubble will be selected.
bubbleToSelect = bubble;
@@ -420,6 +463,7 @@
}
}
if (update.bubbleBarLocation != null) {
+ mSharedState.bubbleBarLocation = update.bubbleBarLocation;
if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) {
updateBubbleBarLocationInternal(update.bubbleBarLocation);
}
@@ -519,6 +563,14 @@
}
}
+ private void addBubbleInternally(BubbleBarBubble bubble, boolean showAppBadge,
+ boolean isExpanding, boolean suppressAnimation) {
+ //TODO(b/360652359): remove setting scale to the app badge once issue is fixed
+ bubble.getView().setBadgeScale(showAppBadge ? 1 : 0);
+ mBubbles.put(bubble.getKey(), bubble);
+ mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
+ }
+
/** Interface for checking whether the IME is visible. */
public interface ImeVisibilityChecker {
/** Whether the IME is visible. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 31b1ea0..96fadf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -57,6 +57,7 @@
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.SystemUiProxy;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import java.io.PrintWriter;
@@ -177,6 +178,9 @@
mBubbleBarClickListener = v -> expandBubbleBar();
mBubbleDragController.setupBubbleBarView(mBarView);
mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
+ if (!Flags.enableOptionalBubbleOverflow()) {
+ showOverflow(true);
+ }
mBarView.setOnClickListener(mBubbleBarClickListener);
mBarView.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -324,6 +328,7 @@
}
private void onBubbleClicked(BubbleView bubbleView) {
+ if (mBubbleBarPinning.isAnimating()) return;
bubbleView.markSeen();
BubbleBarItem bubble = bubbleView.getBubble();
if (bubble == null) {
@@ -872,9 +877,14 @@
/** Animates the bubble bar to notify the user about a bubble change. */
public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding,
boolean isUpdate) {
- // if we're expanded, don't animate the bubble bar. just show the notification dot.
+ // if we're not already animating another bubble, update the dot visibility. otherwise the
+ // the dot will be handled as part of the animation.
+ if (!mBubbleBarViewAnimator.isAnimating()) {
+ bubble.getView().updateDotVisibility(
+ /* animate= */ !mBubbleStashController.isStashed());
+ }
+ // if we're expanded, don't animate the bubble bar.
if (isExpanded()) {
- bubble.getView().updateDotVisibility(/* animate= */ true);
return;
}
boolean isInApp = mTaskbarStashController.isInApp();
@@ -918,7 +928,7 @@
* from Launcher.
*/
public void setExpanded(boolean isExpanded) {
- if (isExpanded != mBarView.isExpanded()) {
+ if (!mBubbleBarPinning.isAnimating() && isExpanded != mBarView.isExpanded()) {
mBarView.setExpanded(isExpanded);
adjustTaskbarAndHotseatToBubbleBarState(isExpanded);
if (!isExpanded) {
@@ -1060,6 +1070,16 @@
mSystemUiProxy.removeAllBubbles();
}
+ /** Removes all existing bubble views */
+ public void removeAllBubbles() {
+ mBarView.removeAllViews();
+ }
+
+ /** Returns the view index of the existing bubble */
+ public int bubbleViewIndex(View bubbleView) {
+ return mBarView.indexOfChild(bubbleView);
+ }
+
/**
* Set listener to be notified when bubble bar bounds have changed
*/
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index b5d94bd..d993685 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -21,6 +21,7 @@
import android.view.View;
import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleBarLocationOnDemandListener;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
@@ -79,7 +80,7 @@
* BubbleControllers instance, but should be careful to only access things that were created
* in constructors for now, as some controllers may still be waiting for init().
*/
- public void init(TaskbarControllers taskbarControllers) {
+ public void init(TaskbarSharedState taskbarSharedState, TaskbarControllers taskbarControllers) {
BubbleBarLocationCompositeListener bubbleBarLocationListeners =
new BubbleBarLocationCompositeListener(
taskbarControllers.navbarButtonsViewController,
@@ -88,7 +89,8 @@
);
bubbleBarController.init(this,
bubbleBarLocationListeners,
- taskbarControllers.navbarButtonsViewController::isImeVisible);
+ taskbarControllers.navbarButtonsViewController::isImeVisible,
+ taskbarSharedState);
bubbleStashedHandleViewController.ifPresent(
controller -> controller.init(/* bubbleControllers = */ this));
bubbleStashController.init(
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 4f3e1ae..114edf4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -299,7 +299,8 @@
return mBubble;
}
- void updateDotVisibility(boolean animate) {
+ /** Updates the dot visibility if it's not suppressed based on whether it has unseen content. */
+ public void updateDotVisibility(boolean animate) {
if (mDotSuppressedForBubbleUpdate) {
// if the dot is suppressed for an update, there's nothing to do
return;
@@ -321,16 +322,12 @@
}
}
- /**
- * Suppresses or un-suppresses drawing the dot due to an update for this bubble.
- *
- * <p>If the dot is being suppressed and is already visible, it remains visible because it is
- * used as a starting point for the animation. If the dot is being unsuppressed, it is
- * redrawn if needed.
- */
+ /** Suppresses or un-suppresses drawing the dot due to an update for this bubble. */
public void suppressDotForBubbleUpdate(boolean suppress) {
mDotSuppressedForBubbleUpdate = suppress;
- if (!suppress) {
+ if (suppress) {
+ setDotScale(0);
+ } else {
showDotIfNeeded(/* animate= */ false);
}
}
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 78e5dbd..6c354f3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -55,6 +55,8 @@
return animatingBubble.state != AnimatingBubble.State.CREATED
}
+ private var interceptedHandleAnimator = false
+
private companion object {
/** The time to show the flyout. */
const val FLYOUT_DELAY_MS: Long = 3000
@@ -133,10 +135,21 @@
dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
)
+ private fun cancelAnimationIfPending() {
+ val animatingBubble = animatingBubble ?: return
+ if (animatingBubble.state != AnimatingBubble.State.CREATED) return
+ scheduler.cancel(animatingBubble.showAnimation)
+ scheduler.cancel(animatingBubble.hideAnimation)
+ }
+
/** Animates a bubble for the state where the bubble bar is stashed. */
fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
- // TODO b/346400677: handle animations for the same bubble interrupting each other
- if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
+ if (isAnimating) {
+ interruptAndUpdateAnimatingBubble(b.view, isExpanding)
+ return
+ }
+ cancelAnimationIfPending()
+
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
@@ -165,17 +178,19 @@
* 3. The third part is the overshoot of the spring animation, where we make the bubble fully
* visible which helps avoiding further updates when we re-enter the second part.
*/
- private fun buildHandleToBubbleBarAnimation() = Runnable {
+ private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable {
moveToState(AnimatingBubble.State.ANIMATING_IN)
- // prepare the bubble bar for the animation
- bubbleBarView.visibility = VISIBLE
- bubbleBarView.alpha = 0f
- bubbleBarView.translationY = 0f
- bubbleBarView.scaleX = 1f
- bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
- bubbleBarView.setBackgroundScaleX(1f)
- bubbleBarView.setBackgroundScaleY(1f)
- bubbleBarView.relativePivotY = 0.5f
+ // prepare the bubble bar for the animation if we're starting fresh
+ if (initialVelocity == null) {
+ bubbleBarView.visibility = VISIBLE
+ bubbleBarView.alpha = 0f
+ bubbleBarView.translationY = 0f
+ bubbleBarView.scaleX = 1f
+ bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
+ bubbleBarView.setBackgroundScaleX(1f)
+ bubbleBarView.setBackgroundScaleY(1f)
+ bubbleBarView.relativePivotY = 0.5f
+ }
// this is the offset between the center of the bubble bar and the center of the stash
// handle. when the handle becomes invisible and we start animating in the bubble bar,
@@ -194,7 +209,7 @@
val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
animator.setDefaultSpringConfig(springConfig)
- animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
+ animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f)
animator.addUpdateListener { handle, values ->
val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
when {
@@ -314,7 +329,19 @@
}
}
}
- animator.addEndListener { _, _, _, canceled, _, _, _ ->
+ animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ ->
+ // PhysicsAnimator calls the end listeners when the animation is replaced with a new one
+ // if we're not in ANIMATING_OUT state, then this animation never started and we should
+ // return
+ if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener
+ if (interceptedHandleAnimator) {
+ interceptedHandleAnimator = false
+ // post this to give a PhysicsAnimator a chance to clean up its internal listeners.
+ // otherwise this end listener will be called as soon as we create a new spring
+ // animation
+ scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity))
+ return@addEndListener
+ }
animatingBubble = null
if (!canceled) bubbleStashController.stashBubbleBarImmediate()
bubbleBarView.relativePivotY = 1f
@@ -326,7 +353,7 @@
val flyout = bubble?.flyoutMessage
if (flyout != null) {
bubbleBarFlyoutController.collapseFlyout {
- onFlyoutRemoved(bubble.view)
+ onFlyoutRemoved()
animator.start()
}
} else {
@@ -336,8 +363,6 @@
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
- // TODO b/346400677: handle animations for the same bubble interrupting each other
- if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
@@ -350,8 +375,11 @@
buildBubbleBarToHandleAnimation()
} else {
Runnable {
- bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) }
- animatingBubble = null
+ moveToState(AnimatingBubble.State.ANIMATING_OUT)
+ bubbleBarFlyoutController.collapseFlyout {
+ onFlyoutRemoved()
+ animatingBubble = null
+ }
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -394,16 +422,23 @@
}
fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
- // TODO b/346400677: handle animations for the same bubble interrupting each other
- if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
+ if (isAnimating) {
+ interruptAndUpdateAnimatingBubble(b.view, isExpanding)
+ return
+ }
+ cancelAnimationIfPending()
+
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
// first bounce the bubble bar and show the flyout. Then hide the flyout.
val showAnimation = buildBubbleBarBounceAnimation()
val hideAnimation = Runnable {
- bubbleBarFlyoutController.collapseFlyout { onFlyoutRemoved(bubbleView) }
- animatingBubble = null
+ moveToState(AnimatingBubble.State.ANIMATING_OUT)
+ bubbleBarFlyoutController.collapseFlyout {
+ onFlyoutRemoved()
+ animatingBubble = null
+ }
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -449,25 +484,25 @@
val bubble = bubbleView?.bubble as? BubbleBarBubble
val flyout = bubble?.flyoutMessage
if (flyout != null) {
- bubbleView.suppressDotForBubbleUpdate(true)
bubbleBarFlyoutController.setUpAndShowFlyout(
- BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message)
- ) {
- moveToState(AnimatingBubble.State.IN)
- bubbleStashController.updateTaskbarTouchRegion()
- }
+ BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message),
+ onInit = { bubbleView.suppressDotForBubbleUpdate(true) },
+ onEnd = {
+ moveToState(AnimatingBubble.State.IN)
+ bubbleStashController.updateTaskbarTouchRegion()
+ },
+ )
} else {
moveToState(AnimatingBubble.State.IN)
}
}
private fun cancelFlyout() {
- val bubbleView = animatingBubble?.bubbleView
- bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved(bubbleView) }
+ bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved() }
}
- private fun onFlyoutRemoved(bubbleView: BubbleView?) {
- bubbleView?.suppressDotForBubbleUpdate(false)
+ private fun onFlyoutRemoved() {
+ animatingBubble?.bubbleView?.suppressDotForBubbleUpdate(false)
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -507,6 +542,117 @@
}
}
+ private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) {
+ val animatingBubble = animatingBubble ?: return
+ when (animatingBubble.state) {
+ AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started
+ AnimatingBubble.State.ANIMATING_IN ->
+ updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding)
+ AnimatingBubble.State.IN ->
+ updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding)
+ AnimatingBubble.State.ANIMATING_OUT ->
+ updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding)
+ }
+ }
+
+ private fun updateAnimationWhileAnimatingIn(
+ animatingBubble: AnimatingBubble,
+ bubbleView: BubbleView,
+ isExpanding: Boolean,
+ ) {
+ this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+ if (!bubbleBarFlyoutController.hasFlyout()) {
+ // if the flyout does not yet exist, then we're only animating the bubble bar.
+ // the animating bubble has been updated, so the when the flyout expands it will
+ // show the right message. we only need to update the dot visibility.
+ bubbleView.updateDotVisibility(/* animate= */ !bubbleStashController.isStashed)
+ return
+ }
+
+ val bubble = bubbleView.bubble as? BubbleBarBubble
+ val flyout = bubble?.flyoutMessage
+ if (flyout != null) {
+ // the flyout is currently expanding and we need to update it with new data
+ bubbleView.suppressDotForBubbleUpdate(true)
+ bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout)
+ } else {
+ // the flyout is expanding but we don't have new flyout data to update it with,
+ // so cancel the expanding flyout.
+ cancelFlyout()
+ }
+ }
+
+ private fun updateAnimationWhileIn(
+ animatingBubble: AnimatingBubble,
+ bubbleView: BubbleView,
+ isExpanding: Boolean,
+ ) {
+ // unsuppress the current bubble because we are about to hide its flyout
+ animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+ this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+ // we're currently idle, waiting for the hide animation to start. update the flyout
+ // data and reschedule the hide animation to run later to give the user a chance to
+ // see the new flyout.
+ val hideAnimation = animatingBubble.hideAnimation
+ scheduler.cancel(hideAnimation)
+ scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+ val bubble = bubbleView.bubble as? BubbleBarBubble
+ val flyout = bubble?.flyoutMessage
+ if (flyout != null) {
+ bubbleView.suppressDotForBubbleUpdate(true)
+ bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) {
+ bubbleStashController.updateTaskbarTouchRegion()
+ }
+ } else {
+ cancelFlyout()
+ }
+ }
+
+ private fun updateAnimationWhileAnimatingOut(
+ animatingBubble: AnimatingBubble,
+ bubbleView: BubbleView,
+ isExpanding: Boolean,
+ ) {
+ // unsuppress the current bubble because we are about to hide its flyout
+ animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+ this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+ // the hide animation already started so it can't be canceled, just post it again
+ val hideAnimation = animatingBubble.hideAnimation
+ scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+ val bubble = bubbleView.bubble as? BubbleBarBubble
+ val flyout = bubble?.flyoutMessage
+ if (bubbleBarFlyoutController.hasFlyout()) {
+ // the flyout is collapsing. update it with the new flyout
+ if (flyout != null) {
+ moveToState(AnimatingBubble.State.ANIMATING_IN)
+ bubbleView.suppressDotForBubbleUpdate(true)
+ bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) {
+ moveToState(AnimatingBubble.State.IN)
+ bubbleStashController.updateTaskbarTouchRegion()
+ }
+ } else {
+ cancelFlyout()
+ moveToState(AnimatingBubble.State.IN)
+ }
+ } else {
+ // the flyout is already gone. if we're animating the handle cancel it. the
+ // animation itself can handle morphing back into the bubble bar and restarting
+ // and show the flyout.
+ val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator()
+ if (handleAnimator != null && handleAnimator.isRunning()) {
+ interceptedHandleAnimator = true
+ handleAnimator.cancel()
+ }
+
+ // if we're not animating the handle, then the hide animation simply hides the
+ // flyout, but if the flyout is gone then the animation has ended.
+ }
+ }
+
private fun cancelHideAnimation() {
val hideAnimation = animatingBubble?.hideAnimation ?: return
scheduler.cancel(hideAnimation)
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 fdbbbb0..39e9fac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -39,11 +39,14 @@
}
private var flyout: BubbleBarFlyoutView? = null
+ private var animator: ValueAnimator? = null
private val horizontalMargin =
container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
private enum class AnimationType {
- COLLAPSE,
+ /** Morphs the flyout between a dot and a rounded rectangle. */
+ MORPH,
+ /** Fades the flyout in or out. */
FADE,
}
@@ -56,7 +59,7 @@
return rect
}
- fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+ fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onInit: () -> Unit, onEnd: () -> Unit) {
flyout?.let(container::removeView)
val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)
@@ -72,55 +75,109 @@
lp.marginEnd = horizontalMargin
container.addView(flyout, lp)
- val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
- 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) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
- },
- onEnd = {
- onEnd()
- flyout.setOnClickListener { callbacks.flyoutClicked() }
- },
- )
- flyout.showFromCollapsed(message) { animator.start() }
this.flyout = flyout
+ flyout.showFromCollapsed(message) {
+ flyout.updateExpansionProgress(0f)
+ onInit()
+ showFlyout(AnimationType.MORPH, onEnd)
+ }
}
- fun cancelFlyout(endAction: () -> Unit) {
- hideFlyout(AnimationType.FADE, endAction)
- }
-
- fun collapseFlyout(endAction: () -> Unit) {
- hideFlyout(AnimationType.COLLAPSE, endAction)
- }
-
- private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
- // TODO: b/277815200 - stop the current animation if it's running
+ private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
val flyout = this.flyout ?: return
- val animator = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATION_DURATION_MS)
+ val startValue = getCurrentAnimatedValueIfRunning() ?: 0f
+ val duration = (ANIMATION_DURATION_MS * (1f - startValue)).toLong()
+ animator?.cancel()
+ val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration)
+ this.animator = animator
when (animationType) {
AnimationType.FADE ->
animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
- AnimationType.COLLAPSE ->
+ AnimationType.MORPH ->
animator.addUpdateListener { _ ->
flyout.updateExpansionProgress(animator.animatedValue as Float)
}
}
animator.addListener(
- onStart = { flyout.setOnClickListener(null) },
+ onStart = { extendTopBoundary() },
onEnd = {
- container.removeView(flyout)
- this@BubbleBarFlyoutController.flyout = null
- callbacks.resetTopBoundary()
endAction()
+ flyout.setOnClickListener { callbacks.flyoutClicked() }
},
)
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() }
+ }
+
+ fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+ val flyout = flyout ?: return
+ animator?.pause()
+ animator?.removeAllListeners()
+ flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) }
+ }
+
+ 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) {
+ cleanupFlyoutView()
+ endAction()
+ }
+ }
+
+ fun collapseFlyout(endAction: () -> Unit) {
+ hideFlyout(AnimationType.MORPH) {
+ cleanupFlyoutView()
+ endAction()
+ }
+ }
+
+ private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
+ val flyout = this.flyout ?: return
+ val startValue = getCurrentAnimatedValueIfRunning() ?: 1f
+ val duration = (ANIMATION_DURATION_MS * startValue).toLong()
+ animator?.cancel()
+ val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration)
+ this.animator = animator
+ when (animationType) {
+ AnimationType.FADE ->
+ animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
+ AnimationType.MORPH ->
+ animator.addUpdateListener { _ ->
+ flyout.updateExpansionProgress(animator.animatedValue as Float)
+ }
+ }
+ animator.addListener(onStart = { flyout.setOnClickListener(null) }, onEnd = { endAction() })
+ animator.start()
+ }
+
+ private fun cleanupFlyoutView() {
+ container.removeView(flyout)
+ this@BubbleBarFlyoutController.flyout = null
+ callbacks.resetTopBoundary()
+ }
+
+ fun hasFlyout() = flyout != null
+
+ private fun getCurrentAnimatedValueIfRunning(): Float? {
+ val animator = animator ?: return null
+ return if (animator.isRunning) animator.animatedValue as Float else 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 bb8a392..af8aaf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -198,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)
@@ -217,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/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4ad65e1..fe68ebc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -139,6 +139,7 @@
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.taskbar.LauncherTaskbarUIController;
import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepHolderFactory;
@@ -419,8 +420,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);
@@ -1087,10 +1090,12 @@
);
}
- public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
- mTaskbarUIController = taskbarUIController;
+ @Override
+ public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
+ mTaskbarUIController = (LauncherTaskbarUIController) taskbarUIController;
}
+ @Override
public @Nullable LauncherTaskbarUIController getTaskbarUIController() {
return mTaskbarUIController;
}
@@ -1397,6 +1402,7 @@
}
@NonNull
+ @Override
public TISBindHelper getTISBindHelper() {
return mTISBindHelper;
}
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/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 05bef35..a5cc32a 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -119,6 +119,11 @@
topTaskTracker.onTaskMovedToFront(taskInfo);
});
}
+
+ @Override
+ public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ mMainThreadExecutor.execute(() -> topTaskTracker.onTaskChanged(taskInfo));
+ }
});
// We may receive onRunningTaskAppeared events later for tasks which have already been
// included in the list returned by mSysUiProxy.getRunningTasks(), or may receive
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index b19f651..6075294 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -70,6 +70,7 @@
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.taskbar.FallbackTaskbarUIController;
import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.util.ActivityOptionsWrapper;
import com.android.launcher3.util.ContextTracker;
import com.android.launcher3.util.RunnableList;
@@ -115,7 +116,7 @@
private FallbackRecentsView mFallbackRecentsView;
private OverviewActionsView<?> mActionsView;
private TISBindHelper mTISBindHelper;
- private @Nullable FallbackTaskbarUIController mTaskbarUIController;
+ private @Nullable FallbackTaskbarUIController<RecentsActivity> mTaskbarUIController;
private StateManager<RecentsState, RecentsActivity> mStateManager;
@@ -174,11 +175,14 @@
mTISBindHelper.runOnBindToTouchInteractionService(r);
}
- public void setTaskbarUIController(FallbackTaskbarUIController taskbarUIController) {
- mTaskbarUIController = taskbarUIController;
+ @Override
+ public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
+ mTaskbarUIController = (FallbackTaskbarUIController<RecentsActivity>) taskbarUIController;
}
- public FallbackTaskbarUIController getTaskbarUIController() {
+ @Nullable
+ @Override
+ public FallbackTaskbarUIController<RecentsActivity> getTaskbarUIController() {
return mTaskbarUIController;
}
@@ -515,6 +519,7 @@
}
@NonNull
+ @Override
public TISBindHelper getTISBindHelper() {
return mTISBindHelper;
}
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/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/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 23a1ec7..71b6573 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -135,6 +135,15 @@
}
}
+ public void onTaskChanged(RunningTaskInfo taskInfo) {
+ for (int i = 0; i < mOrderedTaskList.size(); i++) {
+ if (mOrderedTaskList.get(i).taskId == taskInfo.taskId) {
+ mOrderedTaskList.set(i, taskInfo);
+ break;
+ }
+ }
+ }
+
@Override
public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
// If a task is not visible anymore or has been moved to undefined, stop tracking it.
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 1481ef2..e8f38be 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();
@@ -776,10 +776,13 @@
mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame);
RecentsViewContainer newOverviewContainer =
mOverviewComponentObserver.getContainerInterface().getCreatedContainer();
- if (newOverviewContainer != null
- && newOverviewContainer instanceof StatefulActivity activity) {
- //TODO(b/368030750) refactor taskbarManager to accept RecentsViewContainer
- mTaskbarManager.setActivity(activity);
+ if (newOverviewContainer != null) {
+ if (newOverviewContainer instanceof StatefulActivity activity) {
+ // This will also call setRecentsViewContainer() internally.
+ mTaskbarManager.setActivity(activity);
+ } else {
+ mTaskbarManager.setRecentsViewContainer(newOverviewContainer);
+ }
}
mTISBinder.onOverviewTargetChange();
}
@@ -927,9 +930,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 +1410,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/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..78224ae 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
@@ -39,6 +40,7 @@
import com.android.launcher3.statemanager.StateManager
import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory
import com.android.launcher3.statemanager.StatefulContainer
+import com.android.launcher3.taskbar.TaskbarUIController
import com.android.launcher3.util.ContextTracker
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.RunnableList
@@ -116,6 +118,7 @@
private var callbacks: RecentsAnimationCallbacks? = null
+ private var taskbarUIController: TaskbarUIController? = null
private var tisBindHelper: TISBindHelper = TISBindHelper(this) {}
// Callback array that corresponds to events defined in @ActivityEvent
@@ -276,6 +279,10 @@
}
}
+ override fun getComponentName(): ComponentName {
+ return ComponentName(this, RecentsWindowManager::class.java)
+ }
+
override fun canStartHomeSafely(): Boolean {
val overviewCommandHelper = tisBindHelper.overviewCommandHelper
return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely()
@@ -285,6 +292,18 @@
return tisBindHelper.desktopVisibilityController
}
+ override fun setTaskbarUIController(taskbarUIController: TaskbarUIController?) {
+ this.taskbarUIController = taskbarUIController
+ }
+
+ override fun getTaskbarUIController(): TaskbarUIController? {
+ return taskbarUIController
+ }
+
+ override fun getTISBindHelper(): TISBindHelper {
+ return tisBindHelper
+ }
+
fun registerInitListener(onInitListener: Predicate<Boolean>) {
this.onInitListener = onInitListener
}
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/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/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/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index d8036aa..b04753b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -16,8 +16,6 @@
package com.android.quickstep.views;
-import android.app.Activity;
-import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.LocusId;
@@ -27,13 +25,16 @@
import android.view.View;
import android.view.Window;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.ScrimView;
+import com.android.quickstep.util.TISBindHelper;
/**
* Interface to be implemented by the parent view of RecentsView
@@ -212,4 +213,10 @@
@Nullable
DesktopVisibilityController getDesktopVisibilityController();
+
+ void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController);
+
+ @Nullable TaskbarUIController getTaskbarUIController();
+
+ @NonNull TISBindHelper getTISBindHelper();
}
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..cfa12e2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
@@ -33,36 +33,27 @@
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 ->
+ builder.bindSystemUiProxy(
+ object : SystemUiProxy(this) {
+ override fun notifyTaskbarAutohideSuspend(suspend: Boolean) {
+ super.notifyTaskbarAutohideSuspend(suspend)
+ 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..3912051 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
@@ -42,7 +42,18 @@
@RunWith(LauncherMultivalentJUnit::class)
@EmulatedDevices(["pixelTablet2023"])
class TaskbarScrimViewControllerTest {
- @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+ @get:Rule(order = 0)
+ val context =
+ TaskbarWindowSandboxContext.create { builder ->
+ builder.bindSystemUiProxy(
+ object : SystemUiProxy(this) {
+ 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 +64,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 +143,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 b0d01d3..48f3fc2 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
@@ -25,6 +25,7 @@
import android.view.View
import android.view.View.VISIBLE
import android.widget.FrameLayout
+import android.widget.TextView
import androidx.core.animation.AnimatorTestRule
import androidx.core.graphics.drawable.toBitmap
import androidx.dynamicanimation.animation.DynamicAnimation
@@ -834,23 +835,27 @@
// advance the animation handler by the duration of the initial lift
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(100)
}
- // the lift animation is complete; the spring back animation should start now
- InstrumentationRegistry.getInstrumentation().runOnMainSync {}
- barAnimator.assertIsRunning()
- PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true }
+ // send the expand signal in the middle of the lift animation
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.expandedWhileAnimating()
+ }
+
+ // let the lift animation complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(150)
+ }
// verify there is a pending hide animation
assertThat(animatorScheduler.delayedBlock).isNotNull()
assertThat(animator.isAnimating).isTrue()
- InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.expandedWhileAnimating()
- }
-
- // let the animation finish
+ // the lift animation is complete; the spring back animation should start now. wait for it
+ // to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ barAnimator.assertIsRunning()
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
// verify that the hide animation was canceled
@@ -923,6 +928,344 @@
assertThat(notifiedExpanded).isTrue()
}
+ @Test
+ fun interruptAnimation_whileAnimatingIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(
+ bubbleBarView,
+ bubbleStashController,
+ flyoutController,
+ onExpandedNoOp,
+ animatorScheduler,
+ )
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
+ }
+
+ // let the animation start and wait until the first frame
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+ handleAnimator.assertIsRunning()
+ assertThat(animator.isAnimating).isTrue()
+
+ val updatedBubble =
+ bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleView.setBubble(updatedBubble)
+ animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+ }
+
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(animator.isAnimating).isTrue()
+
+ waitForFlyoutToShow()
+ assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("updated message")
+
+ // run the hide animation
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(1)
+ assertThat(handle.translationY).isEqualTo(0)
+ assertThat(bubbleBarView.alpha).isEqualTo(0)
+ assertThat(animator.isAnimating).isFalse()
+ verify(bubbleStashController).stashBubbleBarImmediate()
+ }
+
+ @Test
+ fun interruptAnimation_whileIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(
+ bubbleBarView,
+ bubbleStashController,
+ flyoutController,
+ onExpandedNoOp,
+ animatorScheduler,
+ )
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
+ }
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(animator.isAnimating).isTrue()
+
+ waitForFlyoutToShow()
+
+ assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("message")
+
+ // verify the hide animation is pending
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ val updatedBubble =
+ bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleView.setBubble(updatedBubble)
+ animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+ }
+
+ // verify the hide animation was rescheduled
+ assertThat(animatorScheduler.canceledBlock).isNotNull()
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ waitForFlyoutToFadeOutAndBackIn()
+
+ assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("updated message")
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(1)
+ assertThat(handle.translationY).isEqualTo(0)
+ assertThat(bubbleBarView.alpha).isEqualTo(0)
+ assertThat(animator.isAnimating).isFalse()
+ verify(bubbleStashController).stashBubbleBarImmediate()
+ }
+
+ @Test
+ fun interruptAnimation_whileAnimatingOut_whileCollapsingFlyout() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(
+ bubbleBarView,
+ bubbleStashController,
+ flyoutController,
+ onExpandedNoOp,
+ animatorScheduler,
+ )
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
+ }
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(animator.isAnimating).isTrue()
+
+ waitForFlyoutToShow()
+
+ assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("message")
+
+ // run the hide animation
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ // interrupt the animation while the flyout is collapsing
+ val updatedBubble =
+ bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(100)
+ bubbleView.setBubble(updatedBubble)
+ animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+
+ // the flyout should now reverse and expand
+ animatorTestRule.advanceTimeBy(100)
+ }
+
+ assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("updated message")
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+ // verify the hide animation was rescheduled and run it
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(1)
+ assertThat(handle.translationY).isEqualTo(0)
+ assertThat(bubbleBarView.alpha).isEqualTo(0)
+ assertThat(animator.isAnimating).isFalse()
+ verify(bubbleStashController).stashBubbleBarImmediate()
+ }
+
+ @Test
+ fun interruptAnimation_whileAnimatingOut_barToHandle() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(
+ bubbleBarView,
+ bubbleStashController,
+ flyoutController,
+ onExpandedNoOp,
+ animatorScheduler,
+ )
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
+ }
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(animator.isAnimating).isTrue()
+
+ waitForFlyoutToShow()
+
+ assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("message")
+
+ // run the hide animation
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ // interrupt the animation while the bar is animating to the handle
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) {
+ bubbleBarView.alpha < 0.5
+ }
+
+ // we're about to interrupt the animation which will cancel the current animation and start
+ // a new one. pause the scheduler to delay starting the new animation. this allows us to run
+ // the test deterministically
+ animatorScheduler.pauseScheduler = true
+
+ val updatedBubble =
+ bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleView.setBubble(updatedBubble)
+ animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+ }
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // verify there's a new job scheduled and start it. this is starting the animation from the
+ // handle back to the bar
+ assertThat(animatorScheduler.pausedBlock).isNotNull()
+ animatorScheduler.pauseScheduler = false
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.pausedBlock!!)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ waitForFlyoutToShow()
+
+ assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("updated message")
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+ // run the hide animation
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // verify the hide animation was rescheduled and run it
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ waitForFlyoutToHide()
+
+ // let the animation start and wait for it to complete
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(1)
+ assertThat(handle.translationY).isEqualTo(0)
+ assertThat(bubbleBarView.alpha).isEqualTo(0)
+ assertThat(animator.isAnimating).isFalse()
+ verify(bubbleStashController).stashBubbleBarImmediate()
+ }
+
private fun setUpBubbleBar() {
bubbleBarView = BubbleBarView(context)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -1019,18 +1362,25 @@
private fun waitForFlyoutToShow() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(250)
}
assertThat(flyoutView).isNotNull()
}
private fun waitForFlyoutToHide() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(250)
}
assertThat(flyoutView).isNull()
}
+ private fun waitForFlyoutToFadeOutAndBackIn() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(500)
+ }
+ assertThat(flyoutView).isNotNull()
+ }
+
private fun <T> PhysicsAnimator<T>.assertIsRunning() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
assertThat(isRunning()).isTrue()
@@ -1045,20 +1395,30 @@
private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+ var pauseScheduler = false
+ var pausedBlock: Runnable? = null
+ private set
+
var delayedBlock: Runnable? = null
private set
+ var canceledBlock: Runnable? = null
+ private set
+
override fun post(block: Runnable) {
+ if (pauseScheduler) {
+ pausedBlock = block
+ return
+ }
block.run()
}
override fun postDelayed(delayMillis: Long, block: Runnable) {
- check(delayedBlock == null) { "there is already a pending block waiting to run" }
delayedBlock = block
}
override fun cancel(block: Runnable) {
- check(delayedBlock == block) { "the pending block does not match the canceled block" }
+ canceledBlock = delayedBlock
delayedBlock = null
}
}
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 0eea741..2997ac9 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
@@ -76,7 +76,7 @@
@Test
fun flyoutPosition_left() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyout = flyoutContainer.getChildAt(0)
val lp = flyout.layoutParams as FrameLayout.LayoutParams
@@ -89,7 +89,7 @@
fun flyoutPosition_right() {
onLeft = false
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyout = flyoutContainer.getChildAt(0)
val lp = flyout.layoutParams as FrameLayout.LayoutParams
@@ -101,7 +101,7 @@
@Test
fun flyoutMessage() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyout = flyoutContainer.getChildAt(0)
val sender = flyout.findViewById<TextView>(R.id.bubble_flyout_title)
@@ -114,12 +114,14 @@
@Test
fun hideFlyout_removedFromContainer() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
+ assertThat(flyoutController.hasFlyout()).isTrue()
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.collapseFlyout {}
animatorTestRule.advanceTimeBy(300)
}
assertThat(flyoutContainer.childCount).isEqualTo(0)
+ assertThat(flyoutController.hasFlyout()).isFalse()
}
@Test
@@ -128,7 +130,7 @@
// boundary
flyoutTy = -50f
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
@@ -141,7 +143,7 @@
@Test
fun showFlyout_withinBoundary() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
@@ -154,7 +156,7 @@
@Test
fun collapseFlyout_resetsTopBoundary() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.collapseFlyout {}
animatorTestRule.advanceTimeBy(300)
@@ -165,7 +167,7 @@
@Test
fun cancelFlyout_fadesOutFlyout() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
assertThat(flyoutView.alpha).isEqualTo(1f)
@@ -179,7 +181,7 @@
@Test
fun clickFlyout_notifiesCallback() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- flyoutController.setUpAndShowFlyout(flyoutMessage) {}
+ setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
assertThat(flyoutView.alpha).isEqualTo(1f)
@@ -189,6 +191,99 @@
assertThat(flyoutCallbacks.flyoutClicked).isTrue()
}
+ @Test
+ fun updateFlyoutWhileExpanding() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ setupAndShowFlyout()
+ 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 {
+ setupAndShowFlyout()
+ 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)
+ }
+
+ @Test
+ fun updateFlyoutWhileCollapsing() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ setupAndShowFlyout()
+ animatorTestRule.advanceTimeBy(300)
+ }
+ assertThat(flyoutController.hasFlyout()).isTrue()
+
+ val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ var flyoutCollapsed = false
+ flyoutController.collapseFlyout { flyoutCollapsed = true }
+ // advance the fake timer so that the collapse animation runs for 125ms
+ animatorTestRule.advanceTimeBy(125)
+
+ // update the flyout in the middle of collapsing, which should start expanding it.
+ var flyoutReversed = false
+ flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true }
+
+ // the collapse animation ran for 125ms when it was updated, so reversing it should only
+ // run for the same amount of time
+ animatorTestRule.advanceTimeBy(125)
+ val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+ assertThat(flyout.alpha).isEqualTo(1)
+ assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+ .isEqualTo("new message")
+ // verify that we never called the end action on the collapse animation
+ assertThat(flyoutCollapsed).isFalse()
+ // verify that we called the end action on the reverse animation
+ assertThat(flyoutReversed).isTrue()
+ }
+ assertThat(flyoutController.hasFlyout()).isTrue()
+ }
+
+ private fun setupAndShowFlyout() {
+ flyoutController.setUpAndShowFlyout(flyoutMessage, {}, {})
+ }
+
class FakeFlyoutCallbacks : FlyoutCallbacks {
var topBoundaryExtendedSpace = 0
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index b0d706f..096f879 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -19,12 +19,10 @@
import android.app.Instrumentation
import android.app.PendingIntent
import android.content.IIntentSender
-import android.content.Intent
-import android.provider.Settings
import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE
import android.provider.Settings.Secure.USER_SETUP_COMPLETE
+import android.provider.Settings.Secure.getUriFor
import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.ServiceTestRule
import com.android.launcher3.LauncherAppState
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarActivityContext
@@ -35,16 +33,12 @@
import com.android.launcher3.taskbar.bubbles.BubbleControllers
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
-import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
import com.android.launcher3.util.TestUtil
import com.android.quickstep.AllAppsActionManager
-import com.android.quickstep.TouchInteractionService
-import com.android.quickstep.TouchInteractionService.TISBinder
import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType
import java.util.Optional
import org.junit.Assume.assumeTrue
-import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@@ -81,11 +75,6 @@
) : TestRule {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
- private val serviceTestRule = ServiceTestRule()
-
- private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE)
- private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE)
- private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule)
private lateinit var taskbarManager: TaskbarManager
@@ -96,10 +85,6 @@
}
override fun apply(base: Statement, description: Description): Statement {
- return settingRules.apply(createStatement(base, description), description)
- }
-
- private fun createStatement(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
@@ -111,34 +96,10 @@
}
// Process secure setting annotations.
- instrumentation.runOnMainSync {
- userSetupCompleteRule.putInt(
- if (description.getAnnotation(UserSetupMode::class.java) != null) {
- 0
- } else {
- 1
- }
- )
- kidsModeRule.putInt(
- if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
- )
- }
-
- // Check for existing Taskbar instance from Launcher process.
- val launcherTaskbarManager: TaskbarManager? =
- if (!isRunningInRobolectric) {
- try {
- val tisBinder =
- serviceTestRule.bindService(
- Intent(context, TouchInteractionService::class.java)
- ) as? TISBinder
- tisBinder?.taskbarManager
- } catch (_: Exception) {
- null
- }
- } else {
- null
- }
+ context.settingsCacheSandbox[getUriFor(USER_SETUP_COMPLETE)] =
+ if (description.getAnnotation(UserSetupMode::class.java) != null) 0 else 1
+ context.settingsCacheSandbox[getUriFor(NAV_BAR_KIDS_MODE)] =
+ if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
taskbarManager =
TestUtil.getOnUiThread {
@@ -161,20 +122,12 @@
try {
TaskbarViewController.enableModelLoadingForTests(false)
- // Replace Launcher Taskbar window with test instance.
- instrumentation.runOnMainSync {
- launcherTaskbarManager?.setSuspended(true)
- taskbarManager.onUserUnlocked() // Required to complete initialization.
- }
+ // Required to complete initialization.
+ instrumentation.runOnMainSync { taskbarManager.onUserUnlocked() }
base.evaluate()
} finally {
- // Revert Taskbar window.
- instrumentation.runOnMainSync {
- taskbarManager.destroy()
- launcherTaskbarManager?.setSuspended(false)
- }
-
+ instrumentation.runOnMainSync { taskbarManager.destroy() }
TaskbarViewController.enableModelLoadingForTests(true)
}
}
@@ -238,25 +191,4 @@
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class NavBarKidsMode
-
- /** Rule for Taskbar integer-based secure settings. */
- private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule {
-
- override fun apply(base: Statement, description: Description): Statement {
- return object : Statement() {
- override fun evaluate() {
- val originalValue =
- Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0)
- try {
- base.evaluate()
- } finally {
- instrumentation.runOnMainSync { putInt(originalValue) }
- }
- }
- }
- }
-
- /** Puts [value] into secure settings under [settingName]. */
- fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value)
- }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index 2d3bfd6..8c51216 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,11 +24,24 @@
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.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCacheSandbox
+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
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/** Include additional bindings when building a [TaskbarSandboxComponent]. */
+typealias TaskbarComponentBinder =
+ TaskbarWindowSandboxContext.(TaskbarSandboxComponent.Builder) -> Unit
/**
* [SandboxApplication] for running Taskbar tests.
@@ -37,20 +50,46 @@
* [DEFAULT_DISPLAY] (i.e. test is executing on a device).
*/
class TaskbarWindowSandboxContext
-private constructor(base: SandboxApplication, val virtualDisplay: VirtualDisplay) :
- ContextWrapper(base),
- ObjectSandbox by base,
- TestRule by RuleChain.outerRule(virtualDisplayRule(virtualDisplay)).around(base) {
+private constructor(
+ private val base: SandboxApplication,
+ val virtualDisplay: VirtualDisplay,
+ private val componentBinder: TaskbarComponentBinder?,
+) : ContextWrapper(base), ObjectSandbox by base, TestRule {
- init {
- putObject(LauncherPrefs.INSTANCE, FakeLauncherPrefs(this))
+ val settingsCacheSandbox = SettingsCacheSandbox()
+
+ private val virtualDisplayRule =
+ object : ExternalResource() {
+ override fun after() = virtualDisplay.release()
+ }
+
+ private val singletonSetupRule =
+ object : ExternalResource() {
+ override fun before() {
+ val context = this@TaskbarWindowSandboxContext
+ val builder =
+ DaggerTaskbarSandboxComponent.builder()
+ .bindSystemUiProxy(SystemUiProxy(context))
+ .bindSettingsCache(settingsCacheSandbox.cache)
+ componentBinder?.invoke(context, builder)
+ base.initDaggerComponent(builder)
+
+ putObject(LauncherPrefs.INSTANCE, FakeLauncherPrefs(context))
+ }
+ }
+
+ override fun apply(statement: Statement, description: Description): Statement {
+ return RuleChain.outerRule(virtualDisplayRule)
+ .around(base)
+ .around(singletonSetupRule)
+ .apply(statement, description)
}
companion object {
private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay"
/** Creates a [SandboxApplication] for Taskbar tests. */
- fun create(): TaskbarWindowSandboxContext {
+ fun create(componentBinder: TaskbarComponentBinder? = null): TaskbarWindowSandboxContext {
val base = ApplicationProvider.getApplicationContext<Context>()
val displayManager = checkNotNull(base.getSystemService(DisplayManager::class.java))
@@ -70,13 +109,21 @@
return TaskbarWindowSandboxContext(
SandboxApplication(base.createDisplayContext(virtualDisplay.display)),
virtualDisplay,
+ componentBinder,
)
}
}
}
-private fun virtualDisplayRule(virtualDisplay: VirtualDisplay): TestRule {
- return object : ExternalResource() {
- override fun after() = virtualDisplay.release()
+@LauncherAppSingleton
+@Component
+interface TaskbarSandboxComponent : LauncherAppComponent {
+ @Component.Builder
+ interface Builder : LauncherAppComponent.Builder {
+ @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
+
+ @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder
+
+ override fun build(): TaskbarSandboxComponent
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
new file mode 100644
index 0000000..dcd5352
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.net.Uri
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+
+/**
+ * Provides a sandboxed [SettingsCache] for testing.
+ *
+ * Note that listeners registered to [cache] will never be invoked.
+ */
+class SettingsCacheSandbox {
+ private val values = mutableMapOf<Uri, Int>()
+
+ /** Fake cache that delegates [SettingsCache.getValue] to [values]. */
+ val cache =
+ mock<SettingsCache> {
+ on { getValue(any<Uri>()) } doAnswer { mock.getValue(it.getArgument(0), 1) }
+ on { getValue(any<Uri>(), any<Int>()) } doAnswer
+ {
+ values.getOrDefault(it.getArgument(0), it.getArgument(1)) == 1
+ }
+ }
+
+ operator fun get(key: Uri): Int? = values[key]
+
+ operator fun set(key: Uri, value: Int) {
+ values[key] = value
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index 1f88743..c3d865f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -20,9 +20,13 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.R
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.LauncherModelHelper
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.shared.system.InputConsumerController
+import dagger.BindsInstance
+import dagger.Component
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -58,7 +62,9 @@
@Before
fun setup() {
- sandboxContext.putObject(SystemUiProxy.INSTANCE, systemUiProxy)
+ sandboxContext.initDaggerComponent(
+ DaggerTestComponent.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 +77,7 @@
gestureState,
0,
false,
- inputConsumerController
+ inputConsumerController,
)
underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false)
}
@@ -83,7 +89,7 @@
verify(systemUiProxy)
.updateContextualEduStats(
/* isTrackpadGesture= */ eq(true),
- eq(GestureType.HOME.toString())
+ eq(GestureType.HOME.toString()),
)
}
@@ -93,7 +99,18 @@
verify(systemUiProxy)
.updateContextualEduStats(
/* isTrackpadGesture= */ eq(false),
- eq(GestureType.HOME.toString())
+ eq(GestureType.HOME.toString()),
)
}
}
+
+@LauncherAppSingleton
+@Component
+interface TestComponent : LauncherAppComponent {
+ @Component.Builder
+ interface Builder : LauncherAppComponent.Builder {
+ @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
+
+ override fun build(): TestComponent
+ }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
index 04012c0..df98606 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
@@ -33,7 +33,7 @@
@RunWith(AndroidJUnit4::class)
class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() {
- lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController
+ lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController<RecentsActivity>
lateinit var stateListener: StateManager.StateListener<RecentsState>
private val recentsActivity: RecentsActivity = mock()
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/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-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index cae77dc..a941d88 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -192,7 +192,7 @@
<string name="private_space_label" msgid="2359721649407947001">"Espace privé"</string>
<string name="private_space_secondary_label" msgid="9203933341714508907">"Touchez pour configurer ou ouvrir"</string>
<string name="ps_container_title" msgid="4391796149519594205">"Privé"</string>
- <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres de l\'Espace privé"</string>
+ <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres de l\'espace privé"</string>
<string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privé, déverrouillé."</string>
<string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privé, verrouillé."</string>
<string name="ps_container_lock_title" msgid="2640257399982364682">"Verrouiller"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 731f839..9cdb5aa 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -192,7 +192,7 @@
<string name="private_space_label" msgid="2359721649407947001">"Spazio privato"</string>
<string name="private_space_secondary_label" msgid="9203933341714508907">"Tocca per configurare o aprire"</string>
<string name="ps_container_title" msgid="4391796149519594205">"Privato"</string>
- <string name="ps_container_settings" msgid="6059734123353320479">"Impostazioni dello Spazio privato"</string>
+ <string name="ps_container_settings" msgid="6059734123353320479">"Impostazioni dello spazio privato"</string>
<string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privato, sbloccato."</string>
<string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privato, bloccato."</string>
<string name="ps_container_lock_title" msgid="2640257399982364682">"Blocca"</string>
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/attrs.xml b/res/values/attrs.xml
index 57c9bc7..16ea0cd 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -208,8 +208,16 @@
<attr name="layout_sticky" format="boolean" />
</declare-styleable>
+ <declare-styleable name="NumRows">
+ <attr name="minDeviceWidthPx" format="float"/>
+ <attr name="minDeviceHeightPx" format="float"/>
+ <attr name="numRowsNew" format="integer"/>
+ <attr name="dbFile" />
+ </declare-styleable>
+
<declare-styleable name="GridDisplayOption">
<attr name="name" format="string" />
+ <attr name="title" />
<attr name="numRows" format="integer" />
<attr name="numColumns" format="integer" />
@@ -294,6 +302,7 @@
<!-- File that contains the specs for all apps icon and text size.
Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
<attr name="allAppsCellSpecsId" format="reference" />
+ <attr name="rowCountSpecsId" format="reference" />
<!-- defaults to allAppsCellSpecsId, if not specified -->
<attr name="allAppsCellSpecsTwoPanelId" format="reference" />
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/res/values/strings.xml b/res/values/strings.xml
index 9d06021..d918698 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -355,8 +355,9 @@
<!-- Title for an app whose download has been started. -->
<string name="app_waiting_download_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> waiting to install</string>
<!-- Title for an app which is archived. -->
- <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived. Tap to download and restore.</string>
-
+ <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived.</string>
+ <!-- Accessibility Action for an app which is archived. -->
+ <string name="app_unarchiving_action">download and restore</string>
<!-- Title shown on the alert dialog prompting the user to update the application in market
in order to re-enable the disabled shortcuts -->
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
index 0f0dde2..27fddc8 100644
--- a/res/xml/backupscheme.xml
+++ b/res/xml/backupscheme.xml
@@ -3,6 +3,8 @@
<include domain="database" path="launcher.db" />
<include domain="database" path="launcher_6_by_5.db" />
+ <include domain="database" path="launcher_5_by_6.db" />
+ <include domain="database" path="launcher_4_by_6.db" />
<include domain="database" path="launcher_4_by_5.db" />
<include domain="database" path="launcher_4_by_4.db" />
<include domain="database" path="launcher_3_by_3.db" />
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/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 909272e..34cf56b 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -57,12 +57,14 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
import com.android.launcher3.dot.DotInfo;
@@ -519,6 +521,16 @@
}
}
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ if (getTag() instanceof ItemInfoWithIcon infoWithIcon && infoWithIcon.isInactiveArchive()) {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ getContext().getString(R.string.app_unarchiving_action)));
+ }
+ }
+
/** This is used for testing to forcefully set the display. */
@VisibleForTesting
public void setDisplay(int display) {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index ece6540..7acba75 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -58,9 +58,9 @@
import com.android.launcher3.testing.shared.ResourceUtils;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.LockedUserState;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.Partner;
+import com.android.launcher3.util.ResourceHelper;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.util.window.WindowManagerProxy;
@@ -186,6 +186,8 @@
@XmlRes
public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
@XmlRes
+ public int rowCountSpecsId = INVALID_RESOURCE_HANDLE;;
+ @XmlRes
public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
@XmlRes
public int allAppsSpecsId = INVALID_RESOURCE_HANDLE;
@@ -232,8 +234,6 @@
if (!newGridName.equals(gridName)) {
LauncherPrefs.get(context).put(GRID_NAME, newGridName);
}
- LockedUserState.get(context).runOnUserUnlocked(() ->
- new DeviceGridState(this).writeToPrefs(context));
DisplayController.INSTANCE.get(context).setPriorityListener(
(displayContext, info, flags) -> {
@@ -340,15 +340,29 @@
Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
@DeviceType int deviceType = displayInfo.getDeviceType();
- ArrayList<DisplayOption> allOptions =
+ List<DisplayOption> allOptions =
getPredefinedDeviceProfiles(context, gridName, deviceType,
RestoreDbTask.isPending(context));
+
+ // Filter out options that don't have the same number of columns as the grid
+ DeviceGridState deviceGridState = new DeviceGridState(context);
+ List<DisplayOption> allOptionsFilteredByColCount =
+ filterByColumnCount(allOptions, deviceGridState.getColumns());
+
DisplayOption displayOption =
- invDistWeightedInterpolate(displayInfo, allOptions, deviceType);
+ invDistWeightedInterpolate(displayInfo, allOptionsFilteredByColCount.isEmpty()
+ ? new ArrayList<>(allOptions)
+ : new ArrayList<>(allOptionsFilteredByColCount), deviceType);
initGrid(context, displayInfo, displayOption, deviceType);
return displayOption.grid.name;
}
+ private List<DisplayOption> filterByColumnCount(
+ List<DisplayOption> allOptions, int numColumns) {
+ return allOptions.stream().filter(
+ option -> option.grid.numColumns == numColumns).toList();
+ }
+
/**
* @deprecated This is a temporary solution because on the backup and restore case we modify the
* IDP, this resets it. b/332974074
@@ -383,6 +397,7 @@
isScalable = closestProfile.isScalable;
devicePaddingId = closestProfile.devicePaddingId;
workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
+ rowCountSpecsId = closestProfile.mRowCountSpecsId;
workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
allAppsSpecsId = closestProfile.mAllAppsSpecsId;
allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
@@ -495,7 +510,6 @@
mChangeListeners.remove(listener);
}
-
public void setCurrentGrid(Context context, String gridName) {
LauncherPrefs.get(context).put(GRID_NAME, gridName);
MAIN_EXECUTOR.execute(() -> {
@@ -526,7 +540,7 @@
}
}
- private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
+ private List<DisplayOption> getPredefinedDeviceProfiles(Context context,
String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
ArrayList<DisplayOption> profiles = new ArrayList<>();
@@ -539,7 +553,8 @@
&& GridOption.TAG_NAME.equals(parser.getName())) {
GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser));
- if (gridOption.isEnabled(deviceType) || allowDisabledGrid) {
+ if ((gridOption.isEnabled(deviceType) || allowDisabledGrid)
+ && (Flags.oneGridSpecs() == gridOption.isNewGridOption())) {
final int displayDepth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG
|| parser.getDepth() > displayDepth)
@@ -566,13 +581,16 @@
}
}
}
- if (filteredProfiles.isEmpty()) {
- // No grid found, use the default options
+ if (filteredProfiles.isEmpty() && TextUtils.isEmpty(gridName)) {
+ // Use the default options since gridName is empty and there's no valid grids.
for (DisplayOption option : profiles) {
if (option.canBeDefault) {
filteredProfiles.add(option);
}
}
+ } else if (filteredProfiles.isEmpty()) {
+ // In this case we had a grid selected but we couldn't find it.
+ filteredProfiles.addAll(profiles);
}
if (filteredProfiles.isEmpty()) {
throw new RuntimeException("No display option with canBeDefault=true");
@@ -581,6 +599,72 @@
}
/**
+ * Parses through the xml to find NumRows specs. Then calls findBestRowCount to get the correct
+ * row count for this GridOption.
+ *
+ * @return the result of {@link #findBestRowCount(List, Context, int)}.
+ */
+ public static NumRows getRowCount(ResourceHelper resourceHelper, Context context,
+ int deviceType) {
+ ArrayList<NumRows> rowCounts = new ArrayList<>();
+
+ try (XmlResourceParser parser = resourceHelper.getXml()) {
+ final int depth = parser.getDepth();
+ int type;
+ while (((type = parser.next()) != XmlPullParser.END_TAG
+ || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+ if ((type == XmlPullParser.START_TAG)
+ && "NumRows".equals(parser.getName())) {
+ rowCounts.add(new NumRows(context, Xml.asAttributeSet(parser)));
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ throw new RuntimeException(e);
+ }
+
+ return findBestRowCount(rowCounts, context, deviceType);
+ }
+
+ /**
+ * @return the biggest row count that fits the display dimensions spec using NumRows to
+ * determine that. If no best row count is found, return -1.
+ */
+ public static NumRows findBestRowCount(List<NumRows> list, Context context,
+ @DeviceType int deviceType) {
+ Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
+ int minWidthPx = Integer.MAX_VALUE;
+ int minHeightPx = Integer.MAX_VALUE;
+ for (WindowBounds bounds : displayInfo.supportedBounds) {
+ boolean isTablet = displayInfo.isTablet(bounds);
+ if (isTablet && deviceType == TYPE_MULTI_DISPLAY) {
+ // For split displays, take half width per page
+ minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
+ minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+ } else if (!isTablet && bounds.isLandscape()) {
+ // We will use transposed layout in this case
+ minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
+ minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
+ } else {
+ minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
+ minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+ }
+ }
+
+ NumRows selectedRow = null;
+ for (NumRows item: list) {
+ if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
+ if (selectedRow == null || selectedRow.mNumRowsNew < item.mNumRowsNew) {
+ selectedRow = item;
+ }
+ }
+ }
+ if (selectedRow != null) {
+ return selectedRow;
+ }
+ return null;
+ }
+
+ /**
* Returns the GridOption associated to the given file name or null if the fileName is not
* supported.
* Ej, launcher.db -> "normal grid", launcher_4_by_4.db -> "practical grid"
@@ -626,6 +710,7 @@
return parseAllDefinedGridOptions(context)
.stream()
.filter(go -> go.isEnabled(deviceType))
+ .filter(go -> (Flags.oneGridSpecs() == go.isNewGridOption()))
.collect(Collectors.toList());
}
@@ -709,7 +794,7 @@
}
private static DisplayOption invDistWeightedInterpolate(
- Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType) {
+ Info displayInfo, List<DisplayOption> points, @DeviceType int deviceType) {
int minWidthPx = Integer.MAX_VALUE;
int minHeightPx = Integer.MAX_VALUE;
for (WindowBounds bounds : displayInfo.supportedBounds) {
@@ -733,7 +818,7 @@
float height = dpiFromPx(minHeightPx, displayInfo.getDensityDpi());
// Sort the profiles based on the closeness to the device size
- Collections.sort(points, (a, b) ->
+ points.sort((a, b) ->
Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
dist(width, height, b.minWidthDps, b.minHeightDps)));
@@ -855,6 +940,7 @@
private static final int DONT_INLINE_QSB = 0;
public final String name;
+ public final String title;
public final int numRows;
public final int numColumns;
public final int numSearchContainerColumns;
@@ -894,17 +980,30 @@
private final int mWorkspaceCellSpecsTwoPanelId;
private final int mAllAppsCellSpecsId;
private final int mAllAppsCellSpecsTwoPanelId;
+ private final int mRowCountSpecsId;
public GridOption(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.GridDisplayOption);
name = a.getString(R.styleable.GridDisplayOption_name);
- numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+ title = a.getString(R.styleable.GridDisplayOption_title);
+ deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
+ DEVICE_CATEGORY_ALL);
+ mRowCountSpecsId = a.getResourceId(
+ R.styleable.GridDisplayOption_rowCountSpecsId, INVALID_RESOURCE_HANDLE);
+ if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
+ ResourceHelper resourceHelper = new ResourceHelper(context, mRowCountSpecsId);
+ NumRows numR = getRowCount(resourceHelper, context, deviceCategory);
+ numRows = numR.mNumRowsNew;
+ dbFile = numR.mDbFile;
+ } else {
+ numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+ dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
+ }
+
numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
numSearchContainerColumns = a.getInt(
R.styleable.GridDisplayOption_numSearchContainerColumns, numColumns);
-
- dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
defaultLayoutId = a.getResourceId(
R.styleable.GridDisplayOption_defaultLayoutId, 0);
demoModeLayoutId = a.getResourceId(
@@ -969,8 +1068,6 @@
R.styleable.GridDisplayOption_isScalable, false);
devicePaddingId = a.getResourceId(
R.styleable.GridDisplayOption_devicePaddingId, INVALID_RESOURCE_HANDLE);
- deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
- DEVICE_CATEGORY_ALL);
if (FeatureFlags.enableResponsiveWorkspace()) {
mWorkspaceSpecsId = a.getResourceId(
@@ -1053,6 +1150,28 @@
return false;
}
}
+
+ public boolean isNewGridOption() {
+ return mRowCountSpecsId != INVALID_RESOURCE_HANDLE;
+ }
+ }
+
+ public static final class NumRows {
+ final int mNumRowsNew;
+ final float mMinDeviceWidthPx;
+ final float mMinDeviceHeightPx;
+ final String mDbFile;
+
+ NumRows(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumRows);
+
+ mNumRowsNew = (int) a.getFloat(R.styleable.NumRows_numRowsNew, 0);
+ mMinDeviceWidthPx = a.getFloat(R.styleable.NumRows_minDeviceWidthPx, 0);
+ mMinDeviceHeightPx = a.getFloat(R.styleable.NumRows_minDeviceHeightPx, 0);
+ mDbFile = a.getString(R.styleable.NumRows_dbFile);
+
+ a.recycle();
+ }
}
@VisibleForTesting
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index d730cea..1148f79 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -18,6 +18,8 @@
public static final String LAUNCHER_DB = "launcher.db";
public static final String LAUNCHER_6_BY_5_DB = "launcher_6_by_5.db";
public static final String LAUNCHER_4_BY_5_DB = "launcher_4_by_5.db";
+ public static final String LAUNCHER_4_BY_6_DB = "launcher_4_by_6.db";
+ public static final String LAUNCHER_5_BY_6_DB = "launcher_5_by_6.db";
public static final String LAUNCHER_4_BY_4_DB = "launcher_4_by_4.db";
public static final String LAUNCHER_3_BY_3_DB = "launcher_3_by_3.db";
public static final String LAUNCHER_2_BY_2_DB = "launcher_2_by_2.db";
@@ -35,6 +37,8 @@
LAUNCHER_DB,
LAUNCHER_6_BY_5_DB,
LAUNCHER_4_BY_5_DB,
+ LAUNCHER_4_BY_6_DB,
+ LAUNCHER_5_BY_6_DB,
LAUNCHER_4_BY_4_DB,
LAUNCHER_3_BY_3_DB,
LAUNCHER_2_BY_2_DB));
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/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/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index e705d94..51d1c9f 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -331,6 +331,9 @@
public void setLettersToScrollLayout(
List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections) {
+ if (fastScrollSections.isEmpty()) {
+ return;
+ }
if (mLetterList != null) {
mLetterList.removeAllViews();
}
@@ -364,6 +367,8 @@
mLetterList.addView(lastLetterListTextView);
constraintTextViewsVertically(mLetterList, textViews);
mLetterList.setVisibility(VISIBLE);
+ // Set the alpha to 0 to avoid the letter list being shown when it shouldn't be.
+ mLetterList.setAlpha(0);
}
private void constraintTextViewsVertically(ConstraintLayout constraintLayout,
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/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 ecc5bb2..0fa275e 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -18,9 +18,11 @@
import android.content.Context;
+import com.android.launcher3.graphics.IconShape;
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;
@@ -39,13 +41,15 @@
*/
public interface LauncherBaseAppComponent {
DaggerSingletonTracker getDaggerSingletonTracker();
- RefreshRateTracker getRefreshRateTracker();
- InstallSessionHelper getInstallSessionHelper();
ApiWrapper getApiWrapper();
+ CustomWidgetManager getCustomWidgetManager();
+ IconShape getIconShape();
+ InstallSessionHelper getInstallSessionHelper();
+ RefreshRateTracker getRefreshRateTracker();
ScreenOnTracker getScreenOnTracker();
SettingsCache getSettingsCache();
- CustomWidgetManager getCustomWidgetManager();
PluginManagerWrapper getPluginManagerWrapper();
+ PackageManagerHelper getPackageManagerHelper();
/** Builder for LauncherBaseAppComponent. */
interface Builder {
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 259e543..a5bcd0f 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -73,6 +73,7 @@
private static final String TAG = "GridCustomizationsProvider";
private static final String KEY_NAME = "name";
+ private static final String KEY_GRID_TITLE = "grid_title";
private static final String KEY_ROWS = "rows";
private static final String KEY_COLS = "cols";
private static final String KEY_PREVIEW_COUNT = "preview_count";
@@ -117,6 +118,7 @@
for (GridOption gridOption : idp.parseAllGridOptions(getContext())) {
cursor.newRow()
.add(KEY_NAME, gridOption.name)
+ .add(KEY_GRID_TITLE, gridOption.title)
.add(KEY_ROWS, gridOption.numRows)
.add(KEY_COLS, gridOption.numColumns)
.add(KEY_PREVIEW_COUNT, 1)
diff --git a/src/com/android/launcher3/graphics/IconShape.java b/src/com/android/launcher3/graphics/IconShape.java
index 5f8f2dc..cb14587 100644
--- a/src/com/android/launcher3/graphics/IconShape.java
+++ b/src/com/android/launcher3/graphics/IconShape.java
@@ -41,10 +41,12 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.views.ClipPathView;
import org.xmlpull.v1.XmlPullParser;
@@ -54,19 +56,22 @@
import java.util.ArrayList;
import java.util.List;
+import javax.inject.Inject;
+
/**
* Abstract representation of the shape of an icon shape
*/
-public final class IconShape implements SafeCloseable {
+@LauncherAppSingleton
+public final class IconShape {
- public static final MainThreadInitializedObject<IconShape> INSTANCE =
- new MainThreadInitializedObject<>(IconShape::new);
-
+ public static DaggerSingletonObject<IconShape> INSTANCE =
+ new DaggerSingletonObject<>(LauncherBaseAppComponent::getIconShape);
private ShapeDelegate mDelegate = new Circle();
private float mNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
- private IconShape(Context context) {
+ @Inject
+ public IconShape(@ApplicationContext Context context) {
pickBestShape(context);
}
@@ -78,9 +83,6 @@
return mNormalizationScale;
}
- @Override
- public void close() { }
-
/**
* Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
*/
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
deleted file mode 100644
index c0c51da..0000000
--- a/src/com/android/launcher3/model/DbEntry.java
+++ /dev/null
@@ -1,155 +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.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/DbEntry.kt b/src/com/android/launcher3/model/DbEntry.kt
new file mode 100644
index 0000000..b79d312
--- /dev/null
+++ b/src/com/android/launcher3/model/DbEntry.kt
@@ -0,0 +1,141 @@
+/*
+ * 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 com.android.launcher3.LauncherSettings
+import com.android.launcher3.LauncherSettings.Favorites.CELLX
+import com.android.launcher3.LauncherSettings.Favorites.CELLY
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Favorites.SCREEN
+import com.android.launcher3.LauncherSettings.Favorites.SPANX
+import com.android.launcher3.LauncherSettings.Favorites.SPANY
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ContentWriter
+import java.net.URISyntaxException
+import java.util.Objects
+
+class DbEntry : ItemInfo(), Comparable<DbEntry> {
+ @JvmField var mIntent: String? = null
+ @JvmField var mProvider: String? = null
+ @JvmField var mFolderItems: MutableMap<String, Set<Int>> = HashMap()
+
+ /** Id of the specific widget. */
+ @JvmField var appWidgetId: Int = NO_ID
+
+ /** Comparator according to the reading order */
+ override fun compareTo(other: DbEntry): Int {
+ if (screenId != other.screenId) {
+ return screenId.compareTo(other.screenId)
+ }
+ if (cellY != other.cellY) {
+ return cellY.compareTo(other.cellY)
+ }
+ return cellX.compareTo(other.cellX)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is DbEntry) return false
+ return getEntryMigrationId() == other.getEntryMigrationId()
+ }
+
+ override fun hashCode(): Int = Objects.hash(getEntryMigrationId())
+
+ /**
+ * Puts the updated DbEntry values into ContentValues which we then use to insert the entry to
+ * the DB.
+ */
+ fun updateContentValues(values: ContentValues) =
+ values.apply {
+ put(SCREEN, screenId)
+ put(CELLX, cellX)
+ put(CELLY, cellY)
+ put(SPANX, spanX)
+ put(SPANY, spanY)
+ }
+
+ override fun writeToValues(writer: ContentWriter) {
+ super.writeToValues(writer)
+ writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId)
+ }
+
+ override fun readFromValues(values: ContentValues) {
+ 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.
+ */
+ private fun getEntryMigrationId(): String? {
+ when (itemType) {
+ ITEM_TYPE_FOLDER,
+ ITEM_TYPE_APP_PAIR -> return getFolderMigrationId()
+ 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
+ ITEM_TYPE_APPLICATION -> {
+ val intentStr = mIntent?.let { cleanIntentString(it) }
+ try {
+ val i = Intent.parseUri(intentStr, 0)
+ return Objects.requireNonNull(i.component).toString()
+ } catch (e: Exception) {
+ return intentStr
+ }
+ }
+
+ else -> return mIntent?.let { cleanIntentString(it) }
+ }
+ }
+
+ /**
+ * This method should return an id that should be the same for two folders containing the same
+ * elements.
+ */
+ private fun getFolderMigrationId(): String =
+ mFolderItems.keys
+ .map { intentString: String ->
+ mFolderItems[intentString]?.size.toString() + cleanIntentString(intentString)
+ }
+ .sorted()
+ .joinToString(",")
+
+ /**
+ * This is needed because sourceBounds can change and make the id of two equal items different.
+ */
+ private fun cleanIntentString(intentStr: String): String {
+ try {
+ return Intent.parseUri(intentStr, 0).apply { sourceBounds = null }.toURI()
+ } catch (e: URISyntaxException) {
+ Log.e(TAG, "Unable to parse Intent string", e)
+ return intentStr
+ }
+ }
+
+ companion object {
+ private const val TAG = "DbEntry"
+ }
+}
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 729b381..90af215 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -156,10 +156,16 @@
}
public Integer getColumns() {
+ if (TextUtils.isEmpty(mGridSizeString)) {
+ return -1;
+ }
return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[0]));
}
public Integer getRows() {
+ if (TextUtils.isEmpty(mGridSizeString)) {
+ return -1;
+ }
return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[1]));
}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 9531d5b..bad7577 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -328,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);
}
@@ -453,7 +452,7 @@
final Context mContext;
int mLastScreenId = -1;
- final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
+ final Map<Integer, List<DbEntry>> mWorkspaceEntriesByScreenId =
new ArrayMap<>();
public DbReader(SQLiteDatabase db, String tableName, Context context) {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.java b/src/com/android/launcher3/model/GridSizeMigrationLogic.java
deleted file mode 100644
index 12a14b2..0000000
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.java
+++ /dev/null
@@ -1,466 +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.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 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());
- // Migrate hotseat.
- migrateHotseat(destDeviceState.getNumHotseat(), srcReader, destReader, target);
- // Migrate workspace.
- migrateWorkspace(srcReader, destReader, target, targetSize);
-
- 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);
- }
- }
-
- private void migrateHotseat(int destHotseatSize,
- GridSizeMigrationDBController.DbReader srcReader,
- GridSizeMigrationDBController.DbReader destReader, DatabaseHelper helper) {
- 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);
- }
-
- private void placeHotseatItems(List<DbEntry> hotseatToBeAdded,
- List<DbEntry> dstHotseatItems, int destHotseatSize,
- DatabaseHelper helper, GridSizeMigrationDBController.DbReader srcReader,
- GridSizeMigrationDBController.DbReader destReader) {
- if (hotseatToBeAdded.isEmpty()) {
- return;
- }
-
- List<Integer> idsInUse = 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);
- }
- }
-
- private void migrateWorkspace(GridSizeMigrationDBController.DbReader srcReader,
- GridSizeMigrationDBController.DbReader destReader, DatabaseHelper helper,
- Point targetSize) {
-
-
- 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);
- }
-
- private void placeWorkspaceItems(
- List<DbEntry> workspaceToBeAdded,
- List<DbEntry> dstWorkspaceItems,
- int trgX, int trgY, DatabaseHelper helper,
- GridSizeMigrationDBController.DbReader srcReader,
- GridSizeMigrationDBController.DbReader destReader) {
- if (workspaceToBeAdded.isEmpty()) {
- return;
- }
-
- List<Integer> idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect(
- Collectors.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/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
new file mode 100644
index 0000000..9470abf
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -0,0 +1,524 @@
+/*
+ * 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.Context
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.get
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
+import com.android.launcher3.provider.LauncherDbUtils
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.util.GridOccupancy
+import com.android.launcher3.util.IntArray
+
+class GridSizeMigrationLogic {
+ /**
+ * 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.
+ */
+ fun migrateGrid(
+ context: Context,
+ srcDeviceState: DeviceGridState,
+ destDeviceState: DeviceGridState,
+ target: DatabaseHelper,
+ source: SQLiteDatabase,
+ ) {
+ if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) {
+ return
+ }
+
+ val isFirstLoad = get(context).get(LauncherPrefs.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)) {
+ GridSizeMigrationDBController.copyCurrentGridToNewGrid(
+ context,
+ destDeviceState,
+ target,
+ source,
+ )
+ return
+ }
+ LauncherDbUtils.copyTable(
+ source,
+ LauncherSettings.Favorites.TABLE_NAME,
+ target.writableDatabase,
+ LauncherSettings.Favorites.TMP_TABLE,
+ context,
+ )
+
+ val migrationStartTime = System.currentTimeMillis()
+ try {
+ SQLiteTransaction(target.writableDatabase).use { t ->
+ val srcReader = DbReader(t.db, LauncherSettings.Favorites.TMP_TABLE, context)
+ val destReader = DbReader(t.db, LauncherSettings.Favorites.TABLE_NAME, context)
+
+ val targetSize = Point(destDeviceState.columns, destDeviceState.rows)
+
+ // 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.
+ val idsInUse = mutableListOf<Int>()
+
+ // Migrate hotseat.
+ migrateHotseat(destDeviceState.numHotseat, srcReader, destReader, target, idsInUse)
+ // Migrate workspace.
+ migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
+
+ LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE)
+ t.commit()
+ }
+ } catch (e: Exception) {
+ 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
+ fun migrateHotseat(
+ destHotseatSize: Int,
+ srcReader: DbReader,
+ destReader: DbReader,
+ helper: DatabaseHelper,
+ idsInUse: MutableList<Int>,
+ ) {
+ val srcHotseatItems = srcReader.loadHotseatEntries()
+ val dstHotseatItems = destReader.loadHotseatEntries()
+
+ val hotseatToBeAdded = getItemsToBeAdded(srcHotseatItems, dstHotseatItems)
+ val toBeRemoved = IntArray()
+ toBeRemoved.addAll(getItemsToBeRemoved(srcHotseatItems, dstHotseatItems))
+
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ """Start hotseat migration:
+ |Removing Hotseat Items: [${dstHotseatItems.filter { toBeRemoved.contains(it.id) }
+ .joinToString(",\n") { it.toString() }}]
+ |Adding Hotseat Items: [${hotseatToBeAdded
+ .joinToString(",\n") { it.toString() }}]
+ |"""
+ .trimMargin(),
+ )
+ }
+
+ // Removes the items that we need to remove from the destination DB.
+ if (!toBeRemoved.isEmpty) {
+ GridSizeMigrationDBController.removeEntryFromDb(
+ destReader.mDb,
+ destReader.mTableName,
+ toBeRemoved,
+ )
+ }
+
+ placeHotseatItems(
+ hotseatToBeAdded,
+ dstHotseatItems,
+ destHotseatSize,
+ helper,
+ srcReader,
+ destReader,
+ idsInUse,
+ )
+ }
+
+ private fun placeHotseatItems(
+ hotseatToBeAdded: MutableList<DbEntry>,
+ dstHotseatItems: List<DbEntry>,
+ destHotseatSize: Int,
+ helper: DatabaseHelper,
+ srcReader: DbReader,
+ destReader: DbReader,
+ idsInUse: MutableList<Int>,
+ ) {
+ if (hotseatToBeAdded.isEmpty()) {
+ return
+ }
+
+ idsInUse.addAll(dstHotseatItems.map { entry: DbEntry -> entry.id })
+
+ hotseatToBeAdded.sort()
+
+ val placementSolutionHotseat =
+ solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded)
+ for (entryToPlace in placementSolutionHotseat) {
+ GridSizeMigrationDBController.insertEntryInDb(
+ helper,
+ entryToPlace,
+ srcReader.mTableName,
+ destReader.mTableName,
+ idsInUse,
+ )
+ }
+ }
+
+ @VisibleForTesting
+ fun migrateWorkspace(
+ srcReader: DbReader,
+ destReader: DbReader,
+ helper: DatabaseHelper,
+ targetSize: Point,
+ idsInUse: MutableList<Int>,
+ ) {
+ val srcWorkspaceItems = srcReader.loadAllWorkspaceEntries()
+
+ val dstWorkspaceItems = destReader.loadAllWorkspaceEntries()
+
+ val toBeRemoved = IntArray()
+
+ val workspaceToBeAdded = getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems)
+ toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems))
+
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ """Start workspace migration:
+ |Source Device: [${srcWorkspaceItems.joinToString(",\n") { it.toString() }}]
+ |Target Device: [${dstWorkspaceItems.joinToString(",\n") { it.toString() }}]
+ |Removing Workspace Items: [${dstWorkspaceItems.filter { toBeRemoved.contains(it.id) }
+ .joinToString(",\n") { it.toString() }}]
+ |Adding Workspace Items: [${workspaceToBeAdded
+ .joinToString(",\n") { it.toString() }}]
+ |"""
+ .trimMargin(),
+ )
+ }
+
+ // Removes the items that we need to remove from the destination DB.
+ if (!toBeRemoved.isEmpty) {
+ GridSizeMigrationDBController.removeEntryFromDb(
+ destReader.mDb,
+ destReader.mTableName,
+ toBeRemoved,
+ )
+ }
+
+ placeWorkspaceItems(
+ workspaceToBeAdded,
+ dstWorkspaceItems,
+ targetSize.x,
+ targetSize.y,
+ helper,
+ srcReader,
+ destReader,
+ idsInUse,
+ )
+ }
+
+ private fun placeWorkspaceItems(
+ workspaceToBeAdded: MutableList<DbEntry>,
+ dstWorkspaceItems: List<DbEntry>,
+ trgX: Int,
+ trgY: Int,
+ helper: DatabaseHelper,
+ srcReader: DbReader,
+ destReader: DbReader,
+ idsInUse: MutableList<Int>,
+ ) {
+ if (workspaceToBeAdded.isEmpty()) {
+ return
+ }
+
+ idsInUse.addAll(dstWorkspaceItems.map { entry: DbEntry -> entry.id })
+
+ workspaceToBeAdded.sort()
+
+ // First we create a collection of the screens
+ val screens: MutableList<Int> = ArrayList()
+ for (screenId in 0..destReader.mLastScreenId) {
+ screens.add(screenId)
+ }
+
+ // Then we place the items on the screens
+ var itemsToPlace = WorkspaceItemsToPlace(workspaceToBeAdded, mutableListOf())
+ for (screenId in screens) {
+ if (DEBUG) {
+ Log.d(TAG, "Migrating $screenId")
+ }
+ itemsToPlace =
+ solveGridPlacement(
+ destReader.mContext,
+ screenId,
+ trgX,
+ trgY,
+ itemsToPlace.mRemainingItemsToPlace,
+ destReader.mWorkspaceEntriesByScreenId[screenId],
+ )
+ placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+ while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+ GridSizeMigrationDBController.insertEntryInDb(
+ helper,
+ itemsToPlace.mPlacementSolution.removeAt(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.
+ var screenId = destReader.mLastScreenId + 1
+ while (itemsToPlace.mRemainingItemsToPlace.isNotEmpty()) {
+ itemsToPlace =
+ solveGridPlacement(
+ destReader.mContext,
+ screenId,
+ trgX,
+ trgY,
+ itemsToPlace.mRemainingItemsToPlace,
+ destReader.mWorkspaceEntriesByScreenId[screenId],
+ )
+ placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+ screenId++
+ }
+ }
+
+ private fun placeItems(
+ itemsToPlace: WorkspaceItemsToPlace,
+ helper: DatabaseHelper,
+ srcReader: DbReader,
+ destReader: DbReader,
+ idsInUse: List<Int>,
+ ) {
+ while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+ GridSizeMigrationDBController.insertEntryInDb(
+ helper,
+ itemsToPlace.mPlacementSolution.removeAt(0),
+ srcReader.mTableName,
+ destReader.mTableName,
+ idsInUse,
+ )
+ }
+ }
+
+ /** Only migrate the grid in this manner if the target grid is taller and not wider. */
+ private fun shouldMigrateToStrictlyTallerGrid(
+ isFirstLoad: Boolean,
+ srcDeviceState: DeviceGridState,
+ destDeviceState: DeviceGridState,
+ ): Boolean {
+ return (isFirstLoad && Flags.enableGridMigrationFix()) &&
+ srcDeviceState.columns == destDeviceState.columns &&
+ srcDeviceState.rows < destDeviceState.rows
+ }
+
+ /**
+ * 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 fun getItemsToBeAdded(src: List<DbEntry>, dest: List<DbEntry>): MutableList<DbEntry> {
+ val entryCountDiff = calcDiff(src, dest)
+ val toBeAdded: MutableList<DbEntry> = ArrayList()
+ src.forEach { entry ->
+ entryCountDiff[entry]?.let { entryDiff ->
+ if (entryDiff > 0) {
+ toBeAdded.add(entry)
+ entryCountDiff[entry] = entryDiff - 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 fun getItemsToBeRemoved(src: List<DbEntry>, dest: List<DbEntry>): IntArray {
+ val entryCountDiff = calcDiff(src, dest)
+ val toBeRemoved =
+ IntArray().apply {
+ dest.forEach { entry ->
+ entryCountDiff[entry]?.let { entryDiff ->
+ if (entryDiff < 0) {
+ add(entry.id)
+ if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+ entry.mFolderItems.values.forEach { ids ->
+ ids.forEach { value -> add(value) }
+ }
+ }
+ }
+ entryCountDiff[entry] = entryDiff.plus(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 fun calcDiff(src: List<DbEntry>, dest: List<DbEntry>): MutableMap<DbEntry, Int> {
+ val entryCountDiff: MutableMap<DbEntry, Int> = HashMap()
+ src.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) + 1 }
+ dest.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) - 1 }
+ return entryCountDiff
+ }
+
+ private fun solveHotseatPlacement(
+ hotseatSize: Int,
+ placedHotseatItems: List<DbEntry>,
+ itemsToPlace: List<DbEntry>,
+ ): List<DbEntry> {
+ val placementSolution: MutableList<DbEntry> = ArrayList()
+ val remainingItemsToPlace: MutableList<DbEntry> = ArrayList(itemsToPlace)
+ val occupied = BooleanArray(hotseatSize)
+ for (entry in placedHotseatItems) {
+ occupied[entry.screenId] = true
+ }
+
+ for (i in occupied.indices) {
+ if (!occupied[i] && remainingItemsToPlace.isNotEmpty()) {
+ val entry: DbEntry =
+ remainingItemsToPlace.removeAt(0).apply {
+ screenId = i
+ // These values does not affect the item position, but we should set them
+ // to something other than -1.
+ cellX = i
+ cellY = 0
+ }
+ placementSolution.add(entry)
+ occupied[entry.screenId] = true
+ }
+ }
+ return placementSolution
+ }
+
+ private fun solveGridPlacement(
+ context: Context,
+ screenId: Int,
+ trgX: Int,
+ trgY: Int,
+ sortedItemsToPlace: MutableList<DbEntry>,
+ existedEntries: MutableList<DbEntry>?,
+ ): WorkspaceItemsToPlace {
+ val itemsToPlace = WorkspaceItemsToPlace(sortedItemsToPlace, mutableListOf())
+ val occupied = GridOccupancy(trgX, trgY)
+ val trg = Point(trgX, trgY)
+ val next: Point =
+ if (
+ screenId == 0 &&
+ (FeatureFlags.QSB_ON_FIRST_SCREEN &&
+ (!Flags.enableSmartspaceRemovalToggle() ||
+ getPrefs(context)
+ .getBoolean(LoaderTask.SMARTSPACE_ON_HOME_SCREEN, true)) &&
+ !Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET)
+ ) {
+ Point(0, 1 /* smartspace */)
+ } else {
+ Point(0, 0)
+ }
+ if (existedEntries != null) {
+ for (entry in existedEntries) {
+ occupied.markCells(entry, true)
+ }
+ }
+ val iterator = itemsToPlace.mRemainingItemsToPlace.iterator()
+ while (iterator.hasNext()) {
+ val entry = iterator.next()
+ if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
+ iterator.remove()
+ continue
+ }
+ findPlacementForEntry(entry, next.x, next.y, trg, occupied)?.let {
+ entry.screenId = screenId
+ entry.cellX = it.cellX
+ entry.cellY = it.cellY
+ entry.spanX = it.spanX
+ entry.spanY = it.spanY
+ occupied.markCells(entry, true)
+ next[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 fun findPlacementForEntry(
+ entry: DbEntry,
+ startPosX: Int,
+ startPosY: Int,
+ trg: Point,
+ occupied: GridOccupancy,
+ ): CellAndSpan? {
+ var newStartPosX = startPosX
+ for (y in startPosY until trg.y) {
+ for (x in newStartPosX until trg.x) {
+ if (occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY)) {
+ return (CellAndSpan(x, y, entry.minSpanX, entry.minSpanY))
+ }
+ }
+ newStartPosX = 0
+ }
+ return null
+ }
+
+ private data class WorkspaceItemsToPlace(
+ val mRemainingItemsToPlace: MutableList<DbEntry>,
+ val mPlacementSolution: MutableList<DbEntry>,
+ )
+
+ companion object {
+ private const val TAG = "GridSizeMigrationLogic"
+ private const val DEBUG = true
+ }
+}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index b0108c2..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));
@@ -490,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 4f0f162..094798b 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -20,6 +20,7 @@
import static android.util.Base64.NO_WRAP;
import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
+import static com.android.launcher3.LauncherPrefs.DB_FILE;
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;
@@ -128,16 +129,20 @@
private synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
- mOpenHelper = createDatabaseHelper(false /* forMigration */);
+ String dbFile = LauncherPrefs.get(mContext).get(DB_FILE);
+ if (dbFile.isEmpty()) {
+ dbFile = InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
+ }
+ mOpenHelper = createDatabaseHelper(false /* forMigration */, dbFile);
printDBs("before: ");
RestoreDbTask.restoreIfNeeded(mContext, this);
printDBs("after: ");
}
}
- protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
+ protected DatabaseHelper createDatabaseHelper(boolean forMigration, String dbFile) {
boolean isSandbox = mContext instanceof SandboxContext;
- String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
+ String dbName = isSandbox ? null : dbFile;
// Set the flag for empty DB
Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
@@ -325,7 +330,7 @@
private boolean isThereExistingDb() {
if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
// If we already have 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 true;
}
return false;
@@ -336,7 +341,7 @@
if (GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
return true;
}
- Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+ FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
return false;
}
@@ -344,7 +349,7 @@
InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
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 true;
}
return false;
@@ -364,7 +369,7 @@
InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
DatabaseHelper oldHelper = mOpenHelper;
mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
- : createDatabaseHelper(true /* forMigration */);
+ : createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
try {
// This is the current grid we have, given by the mContext
DeviceGridState srcDeviceState = new DeviceGridState(mContext);
@@ -388,7 +393,6 @@
* Migrates the DB if needed. If the migration failed, it clears the DB.
*/
public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
-
if (!migrateGridIfNeeded()) {
if (restoreEventLogger != null) {
if (LauncherPrefs.get(mContext).get(NO_DB_FILES_RESTORED)) {
@@ -428,7 +432,7 @@
}
InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
if (!GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
- Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+ FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
return true;
}
String targetDbName = new DeviceGridState(idp).getDbFile();
@@ -438,7 +442,7 @@
}
DatabaseHelper oldHelper = mOpenHelper;
mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
- : createDatabaseHelper(true /* forMigration */);
+ : createDatabaseHelper(true /* forMigration */, targetDbName);
try {
// This is the current grid we have, given by the mContext
DeviceGridState srcDeviceState = new DeviceGridState(mContext);
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/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 775d248..59c27af 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -125,7 +125,8 @@
LauncherPrefs.get(context).removeSync(RESTORE_DEVICE);
if (Flags.enableNarrowGridRestore()) {
- String oldPhoneFileName = idp.dbFile;
+ DeviceGridState deviceGridState = new DeviceGridState(context);
+ String oldPhoneFileName = deviceGridState.getDbFile();
List<String> previousDbs = existingDbs(context);
removeOldDBs(context, oldPhoneFileName);
// The idp before this contains data about the old phone, after this it becomes the idp
@@ -148,6 +149,7 @@
context, oldPhoneDbFileName);
// The grid option could be null if current phone doesn't support the previous db.
if (oldPhoneGridOption != null) {
+
/* If the user only used the default db on the previous phone and the new default db is
* bigger than or equal to the previous one, then keep the new default db */
if (previousDbs.size() == 1 && oldPhoneGridOption.numColumns <= idp.numColumns
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/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index f183f18..72e3e79 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -52,9 +52,9 @@
public static final String WEB_APP_SEARCH_LOGGING = "WebAppSearchLogging";
/**
- * When turned on, we enable quick launch v2 related logging.
+ * When turned on, we enable quick launch related logging.
*/
- public static final String QUICK_LAUNCH_V2 = "QuickLaunchV2";
+ public static final String QUICK_LAUNCH = "QuickLaunch";
/**
* When turned on, we enable Gms Play related logging.
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/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/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 8770859..55a028b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -79,7 +79,7 @@
val statusBarNaturalPx: Int,
val statusBarRotatedPx: Int,
val gesturePx: Int,
- val cutoutPx: Int
+ val cutoutPx: Int,
)
open val deviceSpecs =
@@ -91,7 +91,7 @@
statusBarNaturalPx = 118,
statusBarRotatedPx = 74,
gesturePx = 63,
- cutoutPx = 118
+ cutoutPx = 118,
),
"tablet" to
DeviceSpec(
@@ -100,7 +100,7 @@
statusBarNaturalPx = 104,
statusBarRotatedPx = 104,
gesturePx = 0,
- cutoutPx = 0
+ cutoutPx = 0,
),
"twopanel-phone" to
DeviceSpec(
@@ -109,7 +109,7 @@
statusBarNaturalPx = 133,
statusBarRotatedPx = 110,
gesturePx = 63,
- cutoutPx = 133
+ cutoutPx = 133,
),
"twopanel-tablet" to
DeviceSpec(
@@ -118,14 +118,14 @@
statusBarNaturalPx = 110,
statusBarRotatedPx = 133,
gesturePx = 0,
- cutoutPx = 0
- )
+ cutoutPx = 0,
+ ),
)
protected fun initializeVarsForPhone(
deviceSpec: DeviceSpec,
isGestureMode: Boolean = true,
- isVerticalBar: Boolean = false
+ isVerticalBar: Boolean = false,
) {
val (naturalX, naturalY) = deviceSpec.naturalSize
val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
@@ -137,14 +137,14 @@
displayInfo,
rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0,
isGestureMode,
- densityDpi = deviceSpec.densityDpi
+ densityDpi = deviceSpec.densityDpi,
)
}
protected fun initializeVarsForTablet(
deviceSpec: DeviceSpec,
isLandscape: Boolean = false,
- isGestureMode: Boolean = true
+ isGestureMode: Boolean = true,
) {
val (naturalX, naturalY) = deviceSpec.naturalSize
val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
@@ -156,7 +156,7 @@
displayInfo,
rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
isGestureMode,
- densityDpi = deviceSpec.densityDpi
+ densityDpi = deviceSpec.densityDpi,
)
}
@@ -165,7 +165,7 @@
deviceSpecFolded: DeviceSpec,
isLandscape: Boolean = false,
isGestureMode: Boolean = true,
- isFolded: Boolean = false
+ isFolded: Boolean = false,
) {
val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
val unfoldedWindowsBounds =
@@ -182,7 +182,7 @@
val perDisplayBoundsCache =
mapOf(
unfoldedDisplayInfo to unfoldedWindowsBounds,
- foldedDisplayInfo to foldedWindowsBounds
+ foldedDisplayInfo to foldedWindowsBounds,
)
if (isFolded) {
@@ -191,7 +191,7 @@
displayInfo = foldedDisplayInfo,
rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
isGestureMode = isGestureMode,
- densityDpi = deviceSpecFolded.densityDpi
+ densityDpi = deviceSpecFolded.densityDpi,
)
} else {
initializeCommonVars(
@@ -199,7 +199,7 @@
displayInfo = unfoldedDisplayInfo,
rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
isGestureMode = isGestureMode,
- densityDpi = deviceSpecUnfolded.densityDpi
+ densityDpi = deviceSpecUnfolded.densityDpi,
)
}
}
@@ -208,7 +208,7 @@
deviceSpec: DeviceSpec,
isGestureMode: Boolean,
naturalX: Int,
- naturalY: Int
+ naturalY: Int,
): List<WindowBounds> {
val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
@@ -217,14 +217,14 @@
0,
max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx),
0,
- if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight
+ if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
)
val rotation90Insets =
Rect(
deviceSpec.cutoutPx,
deviceSpec.statusBarRotatedPx,
if (isGestureMode) 0 else buttonsNavHeight,
- if (isGestureMode) deviceSpec.gesturePx else 0
+ if (isGestureMode) deviceSpec.gesturePx else 0,
)
val rotation180Insets =
Rect(
@@ -233,29 +233,29 @@
0,
max(
if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
- deviceSpec.cutoutPx
- )
+ deviceSpec.cutoutPx,
+ ),
)
val rotation270Insets =
Rect(
if (isGestureMode) 0 else buttonsNavHeight,
deviceSpec.statusBarRotatedPx,
deviceSpec.cutoutPx,
- if (isGestureMode) deviceSpec.gesturePx else 0
+ if (isGestureMode) deviceSpec.gesturePx else 0,
)
return listOf(
WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0),
WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90),
WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180),
- WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270)
+ WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270),
)
}
private fun tabletWindowsBounds(
deviceSpec: DeviceSpec,
naturalX: Int,
- naturalY: Int
+ naturalY: Int,
): List<WindowBounds> {
val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
@@ -264,7 +264,7 @@
WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0),
WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90),
WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180),
- WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270)
+ WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270),
)
}
@@ -273,7 +273,7 @@
displayInfo: CachedDisplayInfo,
rotation: Int,
isGestureMode: Boolean = true,
- densityDpi: Int
+ densityDpi: Int,
) {
setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_TWOLINE_TOGGLE)
LauncherPrefs.get(testContext).put(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE, true)
@@ -307,6 +307,10 @@
whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING)).thenReturn(false)
whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(true)
+ whenever(launcherPrefs.get(LauncherPrefs.HOTSEAT_COUNT)).thenReturn(-1)
+ whenever(launcherPrefs.get(LauncherPrefs.DEVICE_TYPE)).thenReturn(-1)
+ whenever(launcherPrefs.get(LauncherPrefs.WORKSPACE_SIZE)).thenReturn("")
+ whenever(launcherPrefs.get(LauncherPrefs.DB_FILE)).thenReturn("")
val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
whenever(displayController.info).thenReturn(info)
whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
similarity index 94%
rename from tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index c6f291d..7933331 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -41,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
@@ -130,13 +130,21 @@
val srcReader = DbReader(db, TMP_TABLE, context)
val destReader = DbReader(db, TABLE_NAME, context)
if (Flags.gridMigrationRefactor()) {
- val gridSizeMigrationLogic = GridSizeMigrationLogic()
- gridSizeMigrationLogic.migrateGrid(
- context,
- DeviceGridState(context),
- DeviceGridState(idp),
+ var gridSizeMigrationLogic = GridSizeMigrationLogic()
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ idp.numDatabaseHotseatIcons,
+ srcReader,
+ destReader,
dbHelper,
- db,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ srcReader,
+ destReader,
+ dbHelper,
+ Point(idp.numColumns, idp.numRows),
+ idsInUse,
)
} else {
GridSizeMigrationDBController.migrate(
@@ -266,12 +274,20 @@
// migrate from A -> B
if (Flags.gridMigrationRefactor()) {
var gridSizeMigrationLogic = GridSizeMigrationLogic()
- gridSizeMigrationLogic.migrateGrid(
- context,
- DeviceGridState(context),
- DeviceGridState(idp),
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ idp.numDatabaseHotseatIcons,
+ readerGridA,
+ readerGridB,
dbHelper,
- db,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ readerGridA,
+ readerGridB,
+ dbHelper,
+ Point(idp.numColumns, idp.numRows),
+ idsInUse,
)
} else {
GridSizeMigrationDBController.migrate(
@@ -445,12 +461,20 @@
) {
if (Flags.gridMigrationRefactor()) {
var gridSizeMigrationLogic = GridSizeMigrationLogic()
- gridSizeMigrationLogic.migrateGrid(
- context,
- DeviceGridState(context),
- DeviceGridState(idp),
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ idp.numDatabaseHotseatIcons,
+ srcReader,
+ destReader,
dbHelper,
- db,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ srcReader,
+ destReader,
+ dbHelper,
+ Point(idp.numColumns, idp.numRows),
+ idsInUse,
)
} else {
GridSizeMigrationDBController.migrate(
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/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 c08237c..b96dbcd 100644
--- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -136,12 +136,20 @@
LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use {
if (Flags.gridMigrationRefactor()) {
val gridSizeMigrationLogic = GridSizeMigrationLogic()
- gridSizeMigrationLogic.migrateGrid(
- context,
- srcGrid.toGridState(),
- dstGrid.toGridState(),
+ val idsInUse = mutableListOf<Int>()
+ gridSizeMigrationLogic.migrateHotseat(
+ dstGrid.size.x,
+ GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+ GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
dbHelper,
- it.db,
+ idsInUse,
+ )
+ gridSizeMigrationLogic.migrateWorkspace(
+ GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+ GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+ dbHelper,
+ dstGrid.size,
+ idsInUse,
)
} else {
GridSizeMigrationDBController.migrate(