Merge "Revert "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..7b20eea 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,117 @@
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)
+ if (animationType == AnimationType.MORPH) {
+ flyout.updateTranslationToCollapsedPosition()
+ }
+ },
+ 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..418675c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -192,16 +192,8 @@
title.alpha = 0f
message.alpha = 0f
setData(flyoutMessage)
- val txToCollapsedPosition =
- if (positioner.isOnLeft) {
- positioner.distanceToCollapsedPosition.x
- } else {
- -positioner.distanceToCollapsedPosition.x
- }
- val tyToCollapsedPosition =
- positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
- translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
+ updateTranslationToCollapsedPosition()
collapsedSize = positioner.collapsedSize
collapsedCornerRadius = collapsedSize / 2
collapsedColor = positioner.collapsedColor
@@ -210,13 +202,19 @@
// calculate the expansion progress required before we start showing the triangle as part of
// the expansion animation
minExpansionProgressForTriangle =
- positioner.distanceToRevealTriangle / tyToCollapsedPosition
+ positioner.distanceToRevealTriangle / translationToCollapsedPosition.y
// post the request to start the expand animation to the looper so the view can measure
// itself
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
@@ -251,6 +249,22 @@
message.text = flyoutMessage.message
}
+ /**
+ * This should be called to update [translationToCollapsedPosition] before we start expanding or
+ * collapsing to make sure that we're animating the flyout to and from the correct position.
+ */
+ fun updateTranslationToCollapsedPosition() {
+ val txToCollapsedPosition =
+ if (positioner.isOnLeft) {
+ positioner.distanceToCollapsedPosition.x
+ } else {
+ -positioner.distanceToCollapsedPosition.x
+ }
+ val tyToCollapsedPosition =
+ positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
+ translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
+ }
+
/** Updates the flyout view with the progress of the animation. */
fun updateExpansionProgress(fraction: Float) {
expansionProgress = fraction
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..9a8041b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1245,21 +1245,24 @@
// - It's the focused task to be moved to the front, we immediately re-add the task
if (child instanceof TaskView && child != mSplitHiddenTaskView
&& child != mMovingTaskView) {
- TaskView taskView = (TaskView) child;
- for (int i : taskView.getTaskIds()) {
- mHasVisibleTaskData.delete(i);
- }
- if (child instanceof GroupedTaskView) {
- mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
- } else if (child instanceof DesktopTaskView) {
- mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
- } else {
- mTaskViewPool.recycle(taskView);
- }
- mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+ clearAndRecycleTaskView((TaskView) child);
}
}
+ private void clearAndRecycleTaskView(TaskView taskView) {
+ for (int taskId : taskView.getTaskIds()) {
+ mHasVisibleTaskData.delete(taskId);
+ }
+ if (taskView instanceof GroupedTaskView) {
+ mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
+ } else if (taskView instanceof DesktopTaskView) {
+ mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
+ } else {
+ mTaskViewPool.recycle(taskView);
+ }
+ mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+ }
+
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
@@ -5058,7 +5061,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
@@ -5302,6 +5306,13 @@
mSplitHiddenTaskViewIndex = -1;
if (mSplitHiddenTaskView != null) {
mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
+ // mSplitHiddenTaskView is set when split select animation starts. The TaskView is only
+ // removed when when the animation finishes. So in the case of overview being dismissed
+ // during the animation, we should not call clearAndRecycleTaskView() because it has
+ // not been removed yet.
+ if (mSplitHiddenTaskView.getParent() == null) {
+ clearAndRecycleTaskView(mSplitHiddenTaskView);
+ }
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..7367f2e 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -36,18 +36,24 @@
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.InvariantDeviceProfile.GridOption;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.shapes.AppShape;
+import com.android.launcher3.shapes.AppShapesProvider;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
import com.android.systemui.shared.Flags;
import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
@@ -55,30 +61,45 @@
/**
* Exposes various launcher grid options and allows the caller to change them.
* APIs:
- * /list_options: List the various available grip options, has following columns
- * name: name of the grid
+ * /shape_options: List of various available shape options, where each has following fields
+ * shape_key: key of the shape option
+ * title: translated title of the shape option
+ * path: path of the shape, assuming drawn on 100x100 view port
+ * is_default: true if this shape option is currently set to the system
+ *
+ * /list_options: List the various available grid options, where each has following fields
+ * name: key of the grid option
* rows: number of rows in the grid
* cols: number of columns in the grid
* preview_count: number of previews available for this grid option. The preview uri
* looks like /preview/<grid-name>/<preview index starting with 0>
- * is_default: true if this grid is currently active
+ * is_default: true if this grid option is currently set to the system
*
- * /preview: Opens a file stream for the grid preview
+ * /get_preview: Open a file stream for the grid preview
*
- * /default_grid: Call update to set the current grid, with values
- * name: name of the grid to apply
+ * /default_grid: Call update to set the current shape and grid, with values
+ * shape_key: key of the shape to apply
+ * name: key of the grid to apply
*/
public class GridCustomizationsProvider extends ContentProvider {
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";
+ // is_default means if a certain option is currently set to the system
private static final String KEY_IS_DEFAULT = "is_default";
+ private static final String KEY_SHAPE_KEY = "shape_key";
+ private static final String KEY_SHAPE_TITLE = "shape_title";
+ private static final String KEY_PATH = "path";
+ // list_options is the key for grid option list
private static final String KEY_LIST_OPTIONS = "/list_options";
+ private static final String KEY_SHAPE_OPTIONS = "/shape_options";
+ // default_grid is for setting grid and shape to system settings
private static final String KEY_DEFAULT_GRID = "/default_grid";
private static final String METHOD_GET_PREVIEW = "get_preview";
@@ -94,6 +115,7 @@
public static final String KEY_GRID_NAME = "grid_name";
private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337;
+ private static final int MESSAGE_ID_UPDATE_SHAPE = 2586;
private static final int MESSAGE_ID_UPDATE_GRID = 7414;
private static final int MESSAGE_ID_UPDATE_COLOR = 856;
@@ -109,14 +131,42 @@
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
- switch (uri.getPath()) {
+ Context context = getContext();
+ String path = uri.getPath();
+ if (context == null || path == null) {
+ return null;
+ }
+
+ switch (path) {
+ case KEY_SHAPE_OPTIONS: {
+ if (Flags.newCustomizationPickerUi()) {
+ MatrixCursor cursor = new MatrixCursor(new String[]{
+ KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
+ List<AppShape> shapes = AppShapesProvider.INSTANCE.getShapes();
+ for (int i = 0; i < shapes.size(); i++) {
+ AppShape shape = shapes.get(i);
+ cursor.newRow()
+ .add(KEY_SHAPE_KEY, shape.getKey())
+ .add(KEY_SHAPE_TITLE, shape.getTitle())
+ .add(KEY_PATH, shape.getPath())
+ // TODO (b/348664593): We should fetch the currently-set shape
+ // option from the preferences.
+ .add(KEY_IS_DEFAULT, i == 0);
+ }
+ return cursor;
+ } else {
+ return null;
+ }
+ }
case KEY_LIST_OPTIONS: {
MatrixCursor cursor = new MatrixCursor(new String[]{
- KEY_NAME, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT, KEY_IS_DEFAULT});
+ KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
+ KEY_IS_DEFAULT});
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
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)
@@ -160,6 +210,14 @@
}
switch (path) {
case KEY_DEFAULT_GRID: {
+ if (Flags.newCustomizationPickerUi()) {
+ String shapeKey = values.getAsString(KEY_SHAPE_KEY);
+ Optional<AppShape> optionalShape = AppShapesProvider.INSTANCE.getShapes()
+ .stream().filter(shape -> shape.getKey().equals(shapeKey)).findFirst();
+ String pathToSet = optionalShape.map(AppShape::getPath).orElse(null);
+ // TODO (b/348664593): Apply shapeName to the system. This needs to be a
+ // synchronous call.
+ }
String gridName = values.getAsString(KEY_NAME);
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
// Verify that this is a valid grid option
@@ -217,20 +275,30 @@
}
@Override
- public Bundle call(String method, String arg, Bundle extras) {
- if (getContext().checkPermission("android.permission.BIND_WALLPAPER",
+ public Bundle call(@NonNull String method, String arg, Bundle extras) {
+ Context context = getContext();
+ if (context == null) {
+ return null;
+ }
+
+ if (context.checkPermission("android.permission.BIND_WALLPAPER",
Binder.getCallingPid(), Binder.getCallingUid())
!= PackageManager.PERMISSION_GRANTED) {
return null;
}
- if (!METHOD_GET_PREVIEW.equals(method)) {
+ if (METHOD_GET_PREVIEW.equals(method)) {
+ return getPreview(extras);
+ } else {
return null;
}
- return getPreview(extras);
}
private synchronized Bundle getPreview(Bundle request) {
+ Context context = getContext();
+ if (context == null) {
+ return null;
+ }
RunnableList lifeCycleTracker = new RunnableList();
try {
PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
@@ -268,7 +336,9 @@
public final PreviewSurfaceRenderer renderer;
public boolean destroyed = false;
- PreviewLifecycleObserver(RunnableList lifeCycleTracker, PreviewSurfaceRenderer renderer) {
+ PreviewLifecycleObserver(
+ RunnableList lifeCycleTracker,
+ PreviewSurfaceRenderer renderer) {
this.lifeCycleTracker = lifeCycleTracker;
this.renderer = renderer;
lifeCycleTracker.add(() -> destroyed = true);
@@ -284,6 +354,17 @@
case MESSAGE_ID_UPDATE_PREVIEW:
renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
break;
+ case MESSAGE_ID_UPDATE_SHAPE:
+ if (Flags.newCustomizationPickerUi()) {
+ String shapeKey = message.getData().getString(KEY_SHAPE_KEY);
+ Optional<AppShape> optionalShape = AppShapesProvider.INSTANCE.getShapes()
+ .stream()
+ .filter(shape -> shape.getKey().equals(shapeKey))
+ .findFirst();
+ String pathToSet = optionalShape.map(AppShape::getPath).orElse(null);
+ // TODO (b/348664593): Update launcher preview with the given shape
+ }
+ break;
case MESSAGE_ID_UPDATE_GRID:
String gridName = message.getData().getString(KEY_GRID_NAME);
if (!TextUtils.isEmpty(gridName)) {
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/shapes/AppShape.kt b/src/com/android/launcher3/shapes/AppShape.kt
new file mode 100644
index 0000000..68200a0
--- /dev/null
+++ b/src/com/android/launcher3/shapes/AppShape.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.shapes
+
+class AppShape(val key: String, val title: String, val path: String)
diff --git a/src/com/android/launcher3/shapes/AppShapesProvider.kt b/src/com/android/launcher3/shapes/AppShapesProvider.kt
new file mode 100644
index 0000000..8c2f181
--- /dev/null
+++ b/src/com/android/launcher3/shapes/AppShapesProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shapes
+
+import com.android.systemui.shared.Flags
+
+object AppShapesProvider {
+
+ val shapes =
+ if (Flags.newCustomizationPickerUi())
+ listOf(
+ AppShape(
+ "arch",
+ "arch",
+ "M100 83.46C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116.884 93.916.1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0 77.614 0 100 22.386 100 50V83.46Z",
+ ),
+ AppShape(
+ "4_sided_cookie",
+ "4 sided cookie",
+ "M63.605 3C84.733-6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176-6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C-6.176 15.268 15.267-6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z",
+ ),
+ AppShape(
+ "seven_sided_cookie",
+ "7 sided cookie",
+ "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82-2.742 55.18-2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24.273 66.266-2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
+ ),
+ AppShape(
+ "sunny",
+ "sunny",
+ "M42.846 4.873C46.084-.531 53.916-.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C-.531 53.916-.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
+ ),
+ AppShape(
+ "circle",
+ "circle",
+ "M99.18 50C99.18 77.162 77.162 99.18 50 99.18 22.838 99.18.82 77.162.82 50 .82 22.839 22.838.82 50 .82 77.162.82 99.18 22.839 99.18 50Z",
+ ),
+ AppShape(
+ "square",
+ "square",
+ "M99.18 53.689C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18H46.311C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758.82 74.306.82 67.434.82 53.689L.82 46.311C.82 32.566.82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694.82 32.566.82 46.311.82L53.689.82C67.434.82 74.306.82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311V53.689Z\n",
+ ),
+ )
+ else emptyList()
+}
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(