Merge "Fix work tab accessbility issues." into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 878aa6e..4ff976d 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -501,10 +501,13 @@
}
flag {
- name: "enforce_system_radius_for_app_widgets"
+ name: "use_system_radius_for_app_widgets"
namespace: "launcher"
- description: "Enforce system radius for widget corners instead of a separate 16.dp value"
- bug: "370950552"
+ description: "Use system radius for enforced widget corners instead of a separate 16.dp value"
+ bug: "373351337"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
flag {
@@ -526,4 +529,11 @@
namespace: "launcher"
description: "Enable Taskbar LayoutTransition for Recent Apps"
bug: "343521765"
+}
+
+flag {
+ name: "enable_pinning_app_with_context_menu"
+ namespace: "launcher"
+ description: "Add options to pin/unpin to taskbar to app context menus."
+ bug: "375648361"
}
\ No newline at end of file
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 57bfb4a..8c39585 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -152,7 +152,7 @@
android:showOnLockScreen="true"
android:launchMode="singleTop"
android:exported="true"
- android:permission="android.permission.START_WIDGET_PICKER_ACTIVITY">
+ android:permission="${applicationId}.permission.START_WIDGET_PICKER_ACTIVITY">
<intent-filter>
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml b/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml
new file mode 100644
index 0000000..7d912a2
--- /dev/null
+++ b/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+ <path android:fillColor="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/>
+</vector>
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 2420a46..4118500 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -22,6 +22,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top"
android:layout_marginHorizontal="@dimen/keyboard_quick_switch_margin_ends"
+ android:layout_gravity="center_horizontal"
android:background="@drawable/keyboard_quick_switch_view_background"
android:clipToOutline="true"
android:alpha="0"
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 782a705..b221b22 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -118,6 +118,7 @@
<!-- Launcher app transition -->
<dimen name="closing_window_trans_y">115dp</dimen>
+ <dimen name="closing_freeform_window_trans_y">36dp</dimen>
<dimen name="quick_switch_scaling_scroll_threshold">100dp</dimen>
@@ -361,7 +362,6 @@
<dimen name="taskbar_running_app_indicator_width">12dp</dimen>
<dimen name="taskbar_running_app_indicator_top_margin">4dp</dimen>
<dimen name="taskbar_minimized_app_indicator_width">6dp</dimen>
- <dimen name="taskbar_overflow_button_preview_stroke">2dp</dimen>
<!-- Transient taskbar -->
<dimen name="transient_taskbar_padding">12dp</dimen>
@@ -426,6 +426,9 @@
<dimen name="taskbar_pinning_popup_menu_vertical_margin">16dp</dimen>
<dimen name="taskbar_pinning_popup_menu_min_padding_from_screen_edge">16dp</dimen>
+ <!-- Taskbar Multi Instance Menu -->
+ <dimen name="taskbar_multi_instance_menu_min_padding_from_screen_edge">8dp</dimen>
+
<!--- Floating Ime Inset height-->
<dimen name="floating_ime_inset_height">60dp</dimen>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index c423d09..6ffcb9b 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -211,6 +211,7 @@
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowShowWallpaper">false</item>
</style>
<!--
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 18337d3..e624be7 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -108,6 +108,7 @@
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
+import android.window.DesktopModeFlags;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
import android.window.WindowAnimationState;
@@ -166,11 +167,13 @@
import com.android.systemui.shared.system.BlurUtils;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.QuickStepContract;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.startingsurface.IStartingWindowListener;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@@ -214,6 +217,7 @@
public static final int CONTENT_ALPHA_DURATION = 217;
public static final int TRANSIENT_TASKBAR_TRANSITION_DURATION = 417;
+ public static final int PINNED_TASKBAR_TRANSITION_DURATION = 600;
public static final int TASKBAR_TO_APP_DURATION = 600;
// TODO(b/236145847): Tune TASKBAR_TO_HOME_DURATION to 383 after conflict with unlock animation
// is solved.
@@ -233,6 +237,7 @@
protected final Handler mHandler;
private final float mClosingWindowTransY;
+ private final float mClosingFreeformWindowTransY;
private final float mMaxShadowRadius;
private final StartingWindowListener mStartingWindowListener =
@@ -290,6 +295,8 @@
Resources res = mLauncher.getResources();
mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
+ mClosingFreeformWindowTransY =
+ res.getDimensionPixelSize(R.dimen.closing_freeform_window_trans_y);
mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius);
mLauncher.addOnDeviceProfileChangeListener(this);
@@ -1480,10 +1487,16 @@
? 0 : getWindowCornerRadius(mLauncher);
float startShadowRadius = areAllTargetsTranslucent(appTargets) ? 0 : mMaxShadowRadius;
closingAnimator.setDuration(duration);
+ boolean isFreeform = isFreeformAnimation(appTargets);
+ float translateY = isFreeform ? mClosingFreeformWindowTransY : mClosingWindowTransY;
+ float endScale = isFreeform ? 0.95f : 1f;
+ Interpolator alphaInterpolator = isFreeform
+ ? clampToDuration(LINEAR, 0, 100, duration)
+ : clampToDuration(LINEAR, 25, 125, duration);
closingAnimator.addUpdateListener(new MultiValueUpdateListener() {
- FloatProp mDy = new FloatProp(0, mClosingWindowTransY, DECELERATE_1_7);
- FloatProp mScale = new FloatProp(1f, 1f, DECELERATE_1_7);
- FloatProp mAlpha = new FloatProp(1f, 0f, clampToDuration(LINEAR, 25, 125, duration));
+ FloatProp mDy = new FloatProp(0, translateY, DECELERATE_1_7);
+ FloatProp mScale = new FloatProp(1f, endScale, DECELERATE_1_7);
+ FloatProp mAlpha = new FloatProp(1f, 0f, alphaInterpolator);
FloatProp mShadowRadius = new FloatProp(startShadowRadius, 0, DECELERATE_1_7);
@Override
@@ -1532,6 +1545,13 @@
return closingAnimator;
}
+ private boolean isFreeformAnimation(RemoteAnimationTarget[] appTargets) {
+ return DesktopModeStatus.canEnterDesktopMode(mLauncher.getApplicationContext())
+ && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()
+ && Arrays.stream(appTargets)
+ .anyMatch(app -> app.taskInfo != null && app.taskInfo.isFreeform());
+ }
+
private void addCujInstrumentation(Animator anim, int cuj) {
anim.addListener(getCujAnimationSuccessListener(cuj));
}
@@ -1726,8 +1746,21 @@
return new AnimatorBackState(rectFSpringAnim, anim);
}
- public static int getTaskbarToHomeDuration() {
- if (enableScalingRevealHomeAnimation()) {
+ /** Get animation duration for taskbar for going to home. */
+ public static int getTaskbarToHomeDuration(boolean isPinnedTaskbar) {
+ return getTaskbarToHomeDuration(false, isPinnedTaskbar);
+ }
+
+ /**
+ * Get animation duration for taskbar for going to home.
+ *
+ * @param shouldOverrideToFastAnimation should overwrite scaling reveal home animation duration
+ */
+ public static int getTaskbarToHomeDuration(boolean shouldOverrideToFastAnimation,
+ boolean isPinnedTaskbar) {
+ if (isPinnedTaskbar) {
+ return PINNED_TASKBAR_TRANSITION_DURATION;
+ } else if (enableScalingRevealHomeAnimation() && !shouldOverrideToFastAnimation) {
return TASKBAR_TO_HOME_DURATION_SLOW;
} else {
return TASKBAR_TO_HOME_DURATION_FAST;
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index 92d9516..8e80aa5 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -57,7 +57,7 @@
// Vertical padding of the icon that contributes to the expected cell height.
private final int mVerticalPadding;
// Extra padding that is used in the top app rows (prediction and search) that is not used in
- // the regular A-Z list. This only applies to single line label.
+ // the regular A-Z list.
private final int mTopRowExtraHeight;
// Helper to drawing the focus indicator.
@@ -140,7 +140,7 @@
// is not enabled. Otherwise, the extra height will increase by just the textHeight.
int extraHeight = (Flags.enableTwolineToggle() &&
LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext()))
- ? textHeight : mTopRowExtraHeight;
+ ? (textHeight + mTopRowExtraHeight) : mTopRowExtraHeight;
totalHeight += extraHeight;
return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 5744464..fd0243a 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -488,6 +488,15 @@
}
});
}
+
+ public void onEnterDesktopModeTransitionStarted(int transitionDuration) {
+
+ }
+
+ @Override
+ public void onExitDesktopModeTransitionStarted(int transitionDuration) {
+
+ }
}
/** A listener for Taskbar in Desktop Mode. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 23a5a27..3b7ad3e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -139,18 +139,42 @@
@NonNull Set<Integer> taskIdsToExclude,
boolean wasOpenedFromTaskbar) {
if (mQuickSwitchViewController != null) {
- if (!mQuickSwitchViewController.isCloseAnimationRunning()
- && mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
- return;
- }
+ if (!mQuickSwitchViewController.isCloseAnimationRunning()) {
+ if (mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
+ return;
+ }
- // 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);
+ // Relayout the KQS view instead of recreating a new one if it is the current
+ // trigger surface is different than the previous one.
+ final int currentFocusIndexOverride =
+ currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
+ ? 0 : currentFocusedIndex;
+
+ // Skip the task reload if the list is not changed.
+ if (!mModel.isTaskListValid(mTaskListChangeId) || !taskIdsToExclude.equals(
+ mExcludedTaskIds)) {
+ mExcludedTaskIds = taskIdsToExclude;
+ mTaskListChangeId = mModel.getTasks((tasks) -> {
+ processLoadedTasks(tasks, taskIdsToExclude);
+ mQuickSwitchViewController.updateQuickSwitchView(
+ mTasks,
+ mNumHiddenTasks,
+ currentFocusIndexOverride,
+ mHasDesktopTask,
+ mWasDesktopTaskFilteredOut);
+ });
+ }
+
+ mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar,
+ currentFocusIndexOverride);
+ return;
+ } else {
+ // Allow the KQS to be reopened during the close animation to make it more
+ // responsive.
+ closeQuickSwitchView(false);
+ }
}
+
mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
if (Flags.taskbarOverflow()) {
mOverlayContext.getDragLayer().addTouchController(this);
@@ -186,13 +210,7 @@
mExcludedTaskIds = taskIdsToExclude;
mTaskListChangeId = mModel.getTasks((tasks) -> {
- mHasDesktopTask = false;
- mWasDesktopTaskFilteredOut = false;
- if (onDesktop) {
- processLoadedTasksOnDesktop(tasks, taskIdsToExclude);
- } else {
- processLoadedTasks(tasks, taskIdsToExclude);
- }
+ processLoadedTasks(tasks, taskIdsToExclude);
// Check if the first task is running after the recents model has updated so that we use
// the correct index.
mQuickSwitchViewController.openQuickSwitchView(
@@ -213,6 +231,17 @@
}
private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
+ mHasDesktopTask = false;
+ mWasDesktopTaskFilteredOut = false;
+ if (mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) {
+ processLoadedTasksOnDesktop(tasks, taskIdsToExclude);
+ } else {
+ processLoadedTasksOutsideDesktop(tasks, taskIdsToExclude);
+ }
+ }
+
+ private void processLoadedTasksOutsideDesktop(List<GroupTask> tasks,
+ Set<Integer> taskIdsToExclude) {
// Only store MAX_TASK tasks, from most to least recent
Collections.reverse(tasks);
mTasks = tasks.stream()
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 05d34b5..1967dfd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -201,6 +201,8 @@
int currentFocusIndexOverride,
@NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks,
boolean useDesktopTaskView) {
+ mContent.removeAllViews();
+
mViewCallbacks = viewCallbacks;
Resources resources = context.getResources();
Resources.Theme theme = context.getTheme();
@@ -333,11 +335,17 @@
return closeAnimation;
}
- private void animateOpen(int currentFocusIndexOverride) {
+ protected void animateOpen(int currentFocusIndexOverride) {
if (mOpenAnimation != null) {
// Restart animation since currentFocusIndexOverride can change the initial scroll.
mOpenAnimation.cancel();
}
+
+ // Reset the alpha for the case where the KQS view is opened before.
+ setAlpha(0);
+ mScrollView.setAlpha(0);
+ mNoRecentItemsPane.setAlpha(0);
+
mOpenAnimation = new AnimatorSet();
Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 390112e..985cc26 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -128,6 +128,23 @@
/* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
}
+ protected void updateQuickSwitchView(
+ @NonNull List<GroupTask> tasks,
+ int numHiddenTasks,
+ int currentFocusIndexOverride,
+ boolean hasDesktopTask,
+ boolean wasDesktopTaskFilteredOut) {
+ mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
+ mKeyboardQuickSwitchView.applyLoadPlan(
+ mOverlayContext,
+ tasks,
+ numHiddenTasks,
+ /* updateTasks= */ true,
+ currentFocusIndexOverride,
+ mViewCallbacks,
+ /* useDesktopTaskView= */ !mOnDesktop && hasDesktopTask);
+ }
+
protected void positionView(boolean wasOpenedFromTaskbar, boolean isTransientTaskbar) {
if (!wasOpenedFromTaskbar) {
// Keep the default positioning.
@@ -155,6 +172,20 @@
mKeyboardQuickSwitchView.setLayoutParams(lp);
}
+ protected void updateLayoutForSurface(boolean updateLayoutFromTaskbar,
+ int currentFocusIndexOverride) {
+ BaseDragLayer.LayoutParams lp =
+ (BaseDragLayer.LayoutParams) mKeyboardQuickSwitchView.getLayoutParams();
+
+ if (updateLayoutFromTaskbar) {
+ lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
+ } else {
+ lp.width = BaseDragLayer.LayoutParams.MATCH_PARENT;
+ }
+
+ mKeyboardQuickSwitchView.animateOpen(currentFocusIndexOverride);
+ }
+
boolean isCloseAnimationRunning() {
return mCloseAnimation != null;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 4a94be7..c5be13d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -33,6 +33,7 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
+import com.android.launcher3.Hotseat;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
@@ -83,6 +84,7 @@
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
dp -> {
onStashedInAppChanged(dp);
+ adjustHotseatForBubbleBar();
if (mControllers != null && mControllers.taskbarViewController != null) {
mControllers.taskbarViewController.onRotationChanged(dp);
}
@@ -152,8 +154,9 @@
@Override
protected boolean isTaskbarTouchable() {
- return !(mTaskbarLauncherStateController.isAnimatingToLauncher()
- && mTaskbarLauncherStateController.isTaskbarAlignedWithHotseat());
+ // Touching down during animation to Hotseat will end the transition and allow the touch to
+ // go through to the Hotseat directly.
+ return !isAnimatingToHotseat();
}
public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) {
@@ -210,8 +213,12 @@
}
private int getTaskbarAnimationDuration(boolean isVisible) {
- if (isVisible && !mLauncher.getPredictiveBackToHomeInProgress()) {
- return getTaskbarToHomeDuration();
+ // fast animation duration since we will not be playing workspace reveal animation.
+ boolean shouldOverrideToFastAnimation =
+ !isHotseatIconOnTopWhenAligned() || mLauncher.getPredictiveBackToHomeInProgress();
+ boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mLauncher);
+ if (isVisible || isPinnedTaskbar) {
+ return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinnedTaskbar);
} else {
return DisplayController.isTransientTaskbar(mLauncher)
? TRANSIENT_TASKBAR_TRANSITION_DURATION
@@ -263,6 +270,14 @@
}
}
+ private void adjustHotseatForBubbleBar() {
+ Hotseat hotseat = mLauncher.getHotseat();
+ if (mControllers.bubbleControllers.isEmpty() || hotseat == null) return;
+ boolean hiddenForBubbles =
+ mControllers.bubbleControllers.get().bubbleBarViewController.isHiddenForNoBubbles();
+ hotseat.post(() -> adjustHotseatForBubbleBar(!hiddenForBubbles));
+ }
+
/**
* Create Taskbar animation when going from an app to Launcher as part of recents transition.
* @param toState If known, the state we will end up in when reaching Launcher.
@@ -426,6 +441,17 @@
}
@Override
+ public boolean isAnimatingToHotseat() {
+ return mTaskbarLauncherStateController.isAnimatingToLauncher()
+ && isIconAlignedWithHotseat();
+ }
+
+ @Override
+ public void endAnimationToHotseat() {
+ mTaskbarLauncherStateController.resetIconAlignment();
+ }
+
+ @Override
protected boolean isInOverviewUi() {
return mTaskbarLauncherStateController.isInOverviewUi();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
new file mode 100644
index 0000000..c0c2a02
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.view.MotionEvent
+import android.view.View
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.popup.SystemShortcut
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext
+import com.android.launcher3.util.Themes
+import com.android.launcher3.util.TouchController
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer
+import java.util.Collections
+import java.util.function.Predicate
+
+/**
+ * A single menu item shortcut to execute displaying open instances of an app. Default interaction
+ * for [onClick] is to open the menu in a floating window. Touching one of the displayed tasks
+ * launches it.
+ */
+class ManageWindowsTaskbarShortcut<T>(
+ private val target: T,
+ private val itemInfo: ItemInfo?,
+ private val originalView: View?,
+ private val controllers: TaskbarControllers,
+) :
+ SystemShortcut<T>(
+ R.drawable.desktop_mode_ic_taskbar_menu_manage_windows,
+ R.string.manage_windows_option_taskbar,
+ target,
+ itemInfo,
+ originalView,
+ ) where T : Context?, T : ActivityContext? {
+ private lateinit var taskbarShortcutAllWindowsView: TaskbarShortcutManageWindowsView
+ private val recentsModel = RecentsModel.INSTANCE[controllers.taskbarActivityContext]
+
+ override fun onClick(v: View?) {
+ val filter =
+ Predicate<GroupTask> { task: GroupTask? ->
+ task != null && task.task1.key.packageName == itemInfo?.getTargetPackage()
+ }
+ recentsModel.getTasks(
+ { tasks: List<GroupTask> ->
+ // Since fetching thumbnails is asynchronous, use this set to gate until the tasks
+ // are ready to display
+ val pendingTaskIds =
+ Collections.synchronizedSet(tasks.map { it.task1.key.id }.toMutableSet())
+ createAndShowTaskShortcutView(tasks, pendingTaskIds)
+ },
+ filter,
+ )
+ }
+
+ /**
+ * Processes a list of tasks to generate thumbnails and create a taskbar shortcut view.
+ *
+ * Iterates through the tasks, retrieves thumbnails, and adds them to a list. When all
+ * thumbnails are processed, it creates a [TaskbarShortcutManageWindowsView] with the collected
+ * thumbnails and positions it appropriately.
+ */
+ private fun createAndShowTaskShortcutView(
+ tasks: List<GroupTask?>,
+ pendingTaskIds: MutableSet<Int>,
+ ) {
+ val taskList = arrayListOf<Pair<Int, Bitmap?>>()
+ tasks.forEach { groupTask ->
+ groupTask?.task1?.let { task ->
+ recentsModel.thumbnailCache.getThumbnailInBackground(task) {
+ thumbnailData: ThumbnailData ->
+ pendingTaskIds.remove(task.key.id)
+ // Add the current pair of task id and ThumbnailData to the list of all tasks
+ if (thumbnailData.thumbnail != null) {
+ taskList.add(task.key.id to thumbnailData.thumbnail)
+ }
+
+ // If the set is empty, all thumbnails have been fetched
+ if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) {
+ createAndPositionTaskbarShortcut(taskList)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates and positions the [TaskbarShortcutManageWindowsView] with the provided thumbnails.
+ */
+ private fun createAndPositionTaskbarShortcut(taskList: ArrayList<Pair<Int, Bitmap?>>) {
+ val onIconClickListener =
+ ({ taskId: Int? ->
+ taskbarShortcutAllWindowsView.removeFromContainer()
+ if (taskId != null) {
+ SystemUiProxy.INSTANCE.get(target).showDesktopApp(taskId, null)
+ }
+ })
+
+ val onOutsideClickListener = { taskbarShortcutAllWindowsView.removeFromContainer() }
+
+ taskbarShortcutAllWindowsView =
+ TaskbarShortcutManageWindowsView(
+ originalView!!,
+ controllers.taskbarOverlayController.requestWindow(),
+ taskList,
+ onIconClickListener,
+ onOutsideClickListener,
+ controllers,
+ )
+ }
+
+ /**
+ * A view container for displaying the window of open instances of an app
+ *
+ * Handles showing the window snapshots, adding the carousel to the overlay, and closing it.
+ * Also acts as a touch controller to intercept touch events outside the carousel to close it.
+ */
+ class TaskbarShortcutManageWindowsView(
+ private val originalView: View,
+ private val taskbarOverlayContext: TaskbarOverlayContext,
+ snapshotList: ArrayList<Pair<Int, Bitmap?>>,
+ onIconClickListener: (Int) -> Unit,
+ onOutsideClickListener: () -> Unit,
+ private val controllers: TaskbarControllers,
+ ) :
+ ManageWindowsViewContainer(
+ originalView.context,
+ Themes.getAttrColor(originalView.context, R.attr.materialColorSurfaceBright),
+ ),
+ TouchController {
+ private val taskbarActivityContext = controllers.taskbarActivityContext
+
+ init {
+ createAndShowMenuView(snapshotList, onIconClickListener, onOutsideClickListener)
+ taskbarOverlayContext.dragLayer.addTouchController(this)
+ }
+
+ /** Adds the carousel menu to the taskbar overlay drag layer */
+ override fun addToContainer(menuView: ManageWindowsView) {
+ taskbarOverlayContext.dragLayer.post { positionCarouselMenu() }
+
+ controllers.taskbarAutohideSuspendController.updateFlag(
+ FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+ true,
+ )
+ AbstractFloatingView.closeAllOpenViewsExcept(
+ taskbarActivityContext,
+ AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY,
+ )
+ menuView.rootView.minimumHeight = menuView.menuHeight
+ menuView.rootView.minimumWidth = menuView.menuWidth
+
+ taskbarOverlayContext.dragLayer?.addView(menuView.rootView)
+ menuView.rootView.requestFocus()
+ }
+
+ /**
+ * Positions the carousel menu relative to the taskbar and the calling app's icon.
+ *
+ * Calculates the Y position to place the carousel above the taskbar, and the X position to
+ * align with the calling app while ensuring it doesn't go beyond the screen edge.
+ */
+ private fun positionCarouselMenu() {
+ val margin =
+ context.resources.getDimension(
+ R.dimen.taskbar_multi_instance_menu_min_padding_from_screen_edge
+ )
+
+ // Calculate the Y position to place the carousel above the taskbar
+ val availableHeight = taskbarOverlayContext.dragLayer.height
+ menuView.rootView.y =
+ availableHeight -
+ menuView.menuHeight -
+ controllers.taskbarStashController.touchableHeight -
+ margin
+
+ // Calculate the X position to align with the calling app,
+ // but avoid clashing with the screen edge
+ val availableWidth = taskbarOverlayContext.dragLayer.width
+ if (Utilities.isRtl(context.resources)) {
+ menuView.rootView.translationX = -(availableWidth - menuView.menuWidth) / 2f
+ } else {
+ val maxX = availableWidth - menuView.menuWidth - margin
+ menuView.rootView.translationX = minOf(originalView.x, maxX)
+ }
+ }
+
+ /** Closes the carousel menu and removes it from the taskbar overlay drag layer */
+ override fun removeFromContainer() {
+ controllers.taskbarAutohideSuspendController.updateFlag(
+ FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+ false,
+ )
+ controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
+ taskbarOverlayContext.dragLayer?.removeView(menuView.rootView)
+ taskbarOverlayContext.dragLayer.removeTouchController(this)
+ }
+
+ /** TouchController implementations for closing the carousel when touched outside */
+ override fun onControllerTouchEvent(ev: MotionEvent?): Boolean {
+ return false
+ }
+
+ override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ ev?.let {
+ if (
+ ev.action == MotionEvent.ACTION_DOWN &&
+ !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, ev)
+ ) {
+ removeFromContainer()
+ }
+ }
+ return false
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index fbd1b6e..e05178d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -448,14 +448,16 @@
mPropertyHolders.add(new StatePropertyHolder(mBackButton,
flags -> (flags & FLAG_IME_VISIBLE) != 0,
ROTATION_DRAWABLE_PERCENT, 1f, 0f));
- // Translate back button to be at end/start of other buttons for keyguard
+ // Translate back button to be at end/start of other buttons for keyguard (only after SUW
+ // since it is laid to align with SUW actions while in that state)
int navButtonSize = mContext.getResources().getDimensionPixelSize(
R.dimen.taskbar_nav_buttons_size);
boolean isRtl = Utilities.isRtl(mContext.getResources());
if (!mContext.isPhoneMode()) {
mPropertyHolders.add(new StatePropertyHolder(
- mBackButton, flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0
- || (flags & FLAG_KEYGUARD_VISIBLE) != 0,
+ mBackButton, flags -> mContext.isUserSetupComplete()
+ && ((flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0
+ || (flags & FLAG_KEYGUARD_VISIBLE) != 0),
VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0));
}
@@ -1293,7 +1295,10 @@
boolean isNavbarOnRight = location.isOnLeft(mNavButtonsView.isLayoutRtl());
DeviceProfile dp = mContext.getDeviceProfile();
float navBarTargetStartX;
- if (mContext.shouldStartAlignTaskbar()) {
+ if (!mContext.isUserSetupComplete()) {
+ // Skip additional translations on the nav bar container while in SUW layout
+ return 0;
+ } else if (mContext.shouldStartAlignTaskbar()) {
int navBarSpacing = dp.inlineNavButtonsEndSpacingPx;
// If the taskbar is start aligned the navigation bar is aligned to the start or end of
// the container, depending on the bubble bar location
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 82acc0c..8149f81 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -418,7 +418,7 @@
/** Called when the visibility of the bubble bar changed. */
public void bubbleBarVisibilityChanged(boolean isVisible) {
mControllers.uiController.adjustHotseatForBubbleBar(isVisible);
- mControllers.taskbarViewController.resetIconAlignmentController();
+ mControllers.taskbarViewController.adjustTaskbarForBubbleBar();
}
public void init(@NonNull TaskbarSharedState sharedState) {
@@ -582,7 +582,9 @@
int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SLIPPERY
| WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
- if (DisplayController.isTransientTaskbar(this) && !isRunningInTestHarness()) {
+ boolean watchOutside = DisplayController.isTransientTaskbar(this)
+ || isThreeButtonNav();
+ if (watchOutside && !isRunningInTestHarness()) {
windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index 8ab2ffa..bdc7f92 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -47,6 +47,8 @@
public static final int FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR = 1 << 5;
// User has hovered the taskbar.
public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6;
+ // User has multi instance window open.
+ public static final int FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN = 1 << 7;
@IntDef(flag = true, value = {
FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
@@ -56,6 +58,7 @@
FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER,
FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+ FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AutohideSuspendFlag {}
@@ -133,6 +136,8 @@
"FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER");
appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
"FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR");
+ appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+ "FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN");
return str.toString();
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 5a63ca6..db70724 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -221,10 +221,13 @@
uiController = newUiController;
uiController.init(this);
uiController.updateStateForSysuiFlags(mSharedState.sysuiStateFlags);
- // if bubble controllers are present take bubble bar location, else set it to null
+ // if bubble controllers are present configure the UI controller
bubbleControllers.ifPresentOrElse(bubbleControllers -> {
BubbleBarLocation location =
bubbleControllers.bubbleBarViewController.getBubbleBarLocation();
+ boolean hiddenForBubbles =
+ bubbleControllers.bubbleBarViewController.isHiddenForNoBubbles();
+ uiController.adjustHotseatForBubbleBar(!hiddenForBubbles);
uiController.onBubbleBarLocationUpdated(location);
}, () -> uiController.onBubbleBarLocationUpdated(null));
// Notify that the ui controller has changed
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index e16c76d..8b52112 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -262,6 +262,7 @@
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
+ mControllerCallbacks.onDispatchTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 2845cee..925e10b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -23,6 +23,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemProperties;
+import android.view.MotionEvent;
import android.view.ViewTreeObserver;
import com.android.launcher3.DeviceProfile;
@@ -325,5 +326,15 @@
}
mControllers.taskbarInsetsController.drawDebugTouchableRegionBounds(canvas);
}
+
+ /** Handles any touch event before it is dispatched to the rest of TaskbarDragLayer. */
+ public void onDispatchTouchEvent(MotionEvent ev) {
+ if (mActivity.isThreeButtonNav() && ev.getAction() == MotionEvent.ACTION_OUTSIDE
+ && mControllers.uiController.isAnimatingToHotseat()) {
+ // When touching during animation to home, jump to the end so Hotseat can handle
+ // the touch. (Gesture Navigation handles this in AbsSwipeUpHandler.)
+ mControllers.uiController.endAnimationToHotseat();
+ }
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index fa04739..dce377d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -17,6 +17,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT;
import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH;
@@ -222,7 +223,9 @@
updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true);
if (!mShouldDelayLauncherStateAnim) {
if (toState == LauncherState.NORMAL) {
- applyState(QuickstepTransitionManager.getTaskbarToHomeDuration());
+ applyState(QuickstepTransitionManager.getTaskbarToHomeDuration(
+ DisplayController.isPinnedTaskbar(
+ mControllers.taskbarActivityContext)));
} else {
applyState();
}
@@ -459,9 +462,12 @@
private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
final boolean isInLauncher = isInLauncher();
+ final boolean isInOverview = mControllers.uiController.isInOverviewUi();
final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat();
final float toAlignment = isIconAlignedWithHotseat ? 1 : 0;
boolean handleOpenFloatingViews = false;
+ boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(
+ mControllers.taskbarActivityContext);
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher
+ ", mLauncherState: " + mLauncherState
@@ -573,10 +579,17 @@
}
float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1;
+ AnimatedFloat taskbarBgOffset =
+ mControllers.taskbarDragLayerController.getTaskbarBackgroundOffset();
+ boolean showTaskbar = !isInLauncher || isInOverview;
+ float taskbarBgOffsetEnd = showTaskbar ? 0f : 1f;
+ float taskbarBgOffsetStart = showTaskbar ? 1f : 0f;
// Don't animate if background has reached desired value.
if (mTaskbarBackgroundAlpha.isAnimating()
- || mTaskbarBackgroundAlpha.value != backgroundAlpha) {
+ || mTaskbarBackgroundAlpha.value != backgroundAlpha
+ || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)
+ || taskbarBgOffset.value != taskbarBgOffsetEnd) {
mTaskbarBackgroundAlpha.cancelAnimation();
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - "
@@ -587,25 +600,35 @@
boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat;
boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat;
boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat;
+ // When Hotseat icons are not on top don't change duration or add start delay.
+ // This will keep the duration in sync for icon alignment and background fade in/out.
+ // For example, launching app from launcher all apps.
+ boolean isHotseatIconOnTopWhenAligned =
+ mControllers.uiController.isHotseatIconOnTopWhenAligned();
float startDelay = 0;
// We want to delay the background from fading in so that the icons have time to move
// into the bounds of the background before it appears.
if (isInLauncherIconNotAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
- } else if (notInLauncherIconNotAligned) {
+ } else if (notInLauncherIconNotAligned && isHotseatIconOnTopWhenAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
}
float newDuration = duration - startDelay;
- if (isInLauncherIconIsAligned) {
+ if (isInLauncherIconIsAligned && isHotseatIconOnTopWhenAligned) {
// Make the background fade out faster so that it is gone by the time the
// icons move outside of the bounds of the background.
newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT;
}
- Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha
- .animateToValue(backgroundAlpha)
- .setDuration((long) newDuration);
- taskbarBackgroundAlpha.setStartDelay((long) startDelay);
+ Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha.animateToValue(
+ backgroundAlpha);
+ if (isPinnedTaskbar) {
+ setupPinnedTaskbarAnimation(animatorSet, showTaskbar, taskbarBgOffset,
+ taskbarBgOffsetStart, taskbarBgOffsetEnd, duration, taskbarBackgroundAlpha);
+ } else {
+ taskbarBackgroundAlpha.setDuration((long) newDuration);
+ taskbarBackgroundAlpha.setStartDelay((long) startDelay);
+ }
animatorSet.play(taskbarBackgroundAlpha);
}
@@ -671,15 +694,18 @@
+ mIconAlignment.value
+ " -> " + toAlignment + ": " + duration);
}
- if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
- iconAlignAnim.setInterpolator(FINAL_FRAME);
- } else {
- animatorSet.play(iconAlignAnim);
+ if (!isPinnedTaskbar) {
+ if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
+ iconAlignAnim.setInterpolator(FINAL_FRAME);
+ } else {
+ animatorSet.play(iconAlignAnim);
+ }
}
}
- Interpolator interpolator = enableScalingRevealHomeAnimation()
+ Interpolator interpolator = enableScalingRevealHomeAnimation() && !isPinnedTaskbar
? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED;
+
animatorSet.setInterpolator(interpolator);
if (start) {
@@ -688,6 +714,49 @@
return animatorSet;
}
+ private void setupPinnedTaskbarAnimation(AnimatorSet animatorSet, boolean showTaskbar,
+ AnimatedFloat taskbarBgOffset, float taskbarBgOffsetStart, float taskbarBgOffsetEnd,
+ long duration, Animator taskbarBackgroundAlpha) {
+ float targetAlpha = !showTaskbar ? 1 : 0;
+ mLauncher.getHotseat().setIconsAlpha(targetAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
+ if (mIsQsbInline) {
+ mLauncher.getHotseat().setQsbAlpha(targetAlpha,
+ ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
+ }
+
+ if ((taskbarBgOffset.value != taskbarBgOffsetEnd && !taskbarBgOffset.isAnimating())
+ || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)) {
+ taskbarBgOffset.cancelAnimation();
+ Animator taskbarIconAlpha = mTaskbarAlphaForHome.animateToValue(
+ showTaskbar ? 1f : 0f);
+ AnimatedFloat taskbarIconTranslationYForHome =
+ mControllers.taskbarViewController.mTaskbarIconTranslationYForHome;
+ ObjectAnimator taskbarBackgroundOffset = taskbarBgOffset.animateToValue(
+ taskbarBgOffsetStart,
+ taskbarBgOffsetEnd);
+ ObjectAnimator taskbarIconsYTranslation = null;
+ float taskbarHeight =
+ mControllers.taskbarActivityContext.getDeviceProfile().taskbarHeight;
+ if (showTaskbar) {
+ taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(
+ taskbarHeight, 0);
+ } else {
+ taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(0,
+ taskbarHeight);
+ }
+
+ taskbarIconAlpha.setDuration(duration);
+ taskbarIconsYTranslation.setDuration(duration);
+ taskbarBackgroundOffset.setDuration(duration);
+
+ animatorSet.play(taskbarIconAlpha);
+ animatorSet.play(taskbarIconsYTranslation);
+ animatorSet.play(taskbarBackgroundOffset);
+ }
+ taskbarBackgroundAlpha.setInterpolator(showTaskbar ? INSTANT : FINAL_FRAME);
+ taskbarBackgroundAlpha.setDuration(duration);
+ }
+
/**
* Whether the taskbar is aligned with the hotseat in the current/target launcher state.
*
@@ -940,7 +1009,12 @@
@Override
public void onRecentsAnimationFinished(RecentsAnimationController controller) {
- endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/);
+ endGestureStateOverride(!controller.getFinishTargetIsLauncher(),
+ controller.getLauncherIsVisibleAtFinish(), false /*canceled*/);
+ }
+
+ private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
+ endGestureStateOverride(finishedToApp, finishedToApp, canceled);
}
/**
@@ -950,10 +1024,13 @@
*
* @param finishedToApp {@code true} if the recents animation finished to showing an app and
* not workspace or overview
- * @param canceled {@code true} if the recents animation was canceled instead of finishing
- * to completion
+ * @param launcherIsVisible {code true} if launcher is visible at finish
+ * @param canceled {@code true} if the recents animation was canceled instead of
+ * finishing
+ * to completion
*/
- private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
+ private void endGestureStateOverride(boolean finishedToApp, boolean launcherIsVisible,
+ boolean canceled) {
mCallbacks.removeListener(this);
mTaskBarRecentsAnimationListener = null;
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null);
@@ -962,17 +1039,27 @@
mSkipNextRecentsAnimEnd = false;
return;
}
- updateStateForUserFinishedToApp(finishedToApp);
+ updateStateForUserFinishedToApp(finishedToApp, launcherIsVisible);
}
}
/**
- * Updates the visible state immediately to ensure a seamless handoff.
- * @param finishedToApp True iff user is in an app.
+ * @see #updateStateForUserFinishedToApp(boolean, boolean)
*/
private void updateStateForUserFinishedToApp(boolean finishedToApp) {
+ updateStateForUserFinishedToApp(finishedToApp, !finishedToApp);
+ }
+
+ /**
+ * Updates the visible state immediately to ensure a seamless handoff.
+ *
+ * @param finishedToApp True iff user is in an app.
+ * @param launcherIsVisible True iff launcher is still visible (ie. transparent app)
+ */
+ private void updateStateForUserFinishedToApp(boolean finishedToApp,
+ boolean launcherIsVisible) {
// Update the visible state immediately to ensure a seamless handoff
- boolean launcherVisible = !finishedToApp;
+ boolean launcherVisible = !finishedToApp || launcherIsVisible;
updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false);
updateStateForFlag(FLAG_VISIBLE, launcherVisible);
applyState();
@@ -981,7 +1068,7 @@
if (DEBUG) {
Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
}
- controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
+ controller.updateStateForFlag(FLAG_IN_APP, finishedToApp && !launcherIsVisible);
controller.applyState();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 0807ee9..7ab9ef3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -749,4 +749,9 @@
private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
+
+ @VisibleForTesting
+ public Context getWindowContext() {
+ return mContext;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 0f9ede9..d4764c7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -281,6 +281,10 @@
}
private void resetScreenUnpin() {
+ // if only back button was long pressed, navigate back like a single click back behavior.
+ if (mLongPressedButtons == BUTTON_BACK) {
+ executeBack(null);
+ }
mLongPressedButtons = 0;
mLastScreenPinLongPress = 0;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
index 126e9bb..8775766 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -16,20 +16,32 @@
package com.android.launcher3.taskbar;
+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.BlendMode;
+import android.graphics.BlendModeColorFilter;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.IntProperty;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
+import androidx.core.graphics.ColorUtils;
+import com.android.app.animation.Interpolators;
import com.android.launcher3.R;
import com.android.launcher3.Reorderable;
import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.util.MultiTranslateDelegate;
import com.android.launcher3.util.Themes;
import com.android.systemui.shared.recents.model.Task;
@@ -45,8 +57,111 @@
* each other in counter clockwise manner (icons of tasks partially overlapping with each other).
*/
public class TaskbarOverflowView extends FrameLayout implements Reorderable {
+ private static final int ALPHA_TRANSPARENT = 0;
+ private static final int ALPHA_OPAQUE = 255;
+ private static final long ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND = 300L;
+ private static final long ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS = 500L;
+ private static final long ANIMATION_SET_DURATION = 1000L;
+ private static final long ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION = 500L;
+ private static final long ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION = 600L;
+ private static final long ITEM_ICON_SIZE_ANIMATION_DURATION = 500L;
+ private static final long ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION = 500L;
+ private static final long LEAVE_BEHIND_ANIMATIONS_DELAY = 500L;
+ private static final long LEAVE_BEHIND_OPACITY_ANIMATION_DURATION = 100L;
+ private static final long LEAVE_BEHIND_SIZE_ANIMATION_DURATION = 500L;
+ private static final float LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER = 0.83f;
private static final int MAX_ITEMS_IN_PREVIEW = 4;
+ // The height divided by the width of the horizontal box containing two overlapping app icons.
+ // According to the spec, this ratio is constant for different sizes of taskbar app icons.
+ // Assuming the width of this box = taskbar app icon size - 2 paddings - 2 stroke widths, and
+ // the height = width * 0.61, which is also equal to the height of a single item in the preview.
+ private static final float TWO_ITEM_ICONS_BOX_ASPECT_RATIO = 0.61f;
+
+ private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_CENTER_OFFSET =
+ new FloatProperty<>("itemIconCenterOffset") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mItemIconCenterOffset;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mItemIconCenterOffset = value;
+ view.invalidate();
+ }
+ };
+
+ private static final IntProperty<TaskbarOverflowView> ITEM_ICON_COLOR_FILTER_OPACITY =
+ new IntProperty<>("itemIconColorFilterOpacity") {
+ @Override
+ public Integer get(TaskbarOverflowView view) {
+ return view.mItemIconColorFilterOpacity;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, int value) {
+ view.mItemIconColorFilterOpacity = value;
+ view.invalidate();
+ }
+ };
+
+ private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_SIZE =
+ new FloatProperty<>("itemIconSize") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mItemIconSize;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mItemIconSize = value;
+ view.invalidate();
+ }
+ };
+
+ private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_STROKE_WIDTH =
+ new FloatProperty<>("itemIconStrokeWidth") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mItemIconStrokeWidth;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mItemIconStrokeWidth = value;
+ view.invalidate();
+ }
+ };
+
+ private static final IntProperty<TaskbarOverflowView> LEAVE_BEHIND_OPACITY =
+ new IntProperty<>("leaveBehindOpacity") {
+ @Override
+ public Integer get(TaskbarOverflowView view) {
+ return view.mLeaveBehindOpacity;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, int value) {
+ view.mLeaveBehindOpacity = value;
+ view.invalidate();
+ }
+ };
+
+ private static final FloatProperty<TaskbarOverflowView> LEAVE_BEHIND_SIZE =
+ new FloatProperty<>("leaveBehindSize") {
+ @Override
+ public Float get(TaskbarOverflowView view) {
+ return view.mLeaveBehindSize;
+ }
+
+ @Override
+ public void setValue(TaskbarOverflowView view, float value) {
+ view.mLeaveBehindSize = value;
+ view.invalidate();
+ }
+ };
+
private boolean mIsRtlLayout;
private final List<Task> mItems = new ArrayList<Task>();
private int mIconSize;
@@ -56,11 +171,24 @@
private float mScaleForReorderBounce = 1f;
private int mItemBackgroundColor;
private int mLeaveBehindColor;
- private float mItemPreviewStrokeWidth;
// Active means the overflow icon has been pressed, which replaces the app icons with the
// leave-behind circle and shows the KQS UI.
private boolean mIsActive = false;
+ private ValueAnimator mStateTransitionAnimationWrapper;
+
+ private float mItemIconCenterOffsetDefault;
+ private float mItemIconCenterOffset; // [0..mItemIconCenterOffsetDefault]
+ private int mItemIconColorFilterOpacity; // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
+ private float mItemIconSizeDefault;
+ private float mItemIconSizeScaledDown;
+ private float mItemIconSize; // [mItemIconSizeScaledDown..mItemIconSizeDefault]
+ private float mItemIconStrokeWidthDefault;
+ private float mItemIconStrokeWidth; // [0..mItemIconStrokeWidthDefault]
+ private int mLeaveBehindOpacity; // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
+ private float mLeaveBehindSizeScaledDown;
+ private float mLeaveBehindSizeDefault;
+ private float mLeaveBehindSize; // [mLeaveBehindSizeScaledDown..mLeaveBehindSizeDefault]
public TaskbarOverflowView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -87,6 +215,27 @@
icon.mIconSize = iconSize;
icon.mPadding = padding;
+
+ final float taskbarIconRadius =
+ iconSize * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f - padding;
+
+ icon.mLeaveBehindSizeDefault = taskbarIconRadius; // 1/2 of taskbar app icon size
+ icon.mLeaveBehindSizeScaledDown =
+ icon.mLeaveBehindSizeDefault * LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER;
+ icon.mLeaveBehindSize = icon.mLeaveBehindSizeScaledDown;
+
+ icon.mItemIconStrokeWidthDefault = taskbarIconRadius / 5f; // 1/10 of taskbar app icon size
+ icon.mItemIconStrokeWidth = icon.mItemIconStrokeWidthDefault;
+
+ icon.mItemIconSizeDefault = 2 * (taskbarIconRadius - icon.mItemIconStrokeWidthDefault)
+ * TWO_ITEM_ICONS_BOX_ASPECT_RATIO;
+ icon.mItemIconSizeScaledDown = icon.mLeaveBehindSizeScaledDown;
+ icon.mItemIconSize = icon.mItemIconSizeDefault;
+
+ icon.mItemIconCenterOffsetDefault = taskbarIconRadius - icon.mItemIconSizeDefault / 2f
+ - icon.mItemIconStrokeWidthDefault;
+ icon.mItemIconCenterOffset = icon.mItemIconCenterOffsetDefault;
+
return icon;
}
@@ -95,8 +244,6 @@
mItemBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mItemBackgroundColor = getContext().getColor(R.color.taskbar_background);
mLeaveBehindColor = Themes.getAttrColor(getContext(), android.R.attr.textColorTertiary);
- mItemPreviewStrokeWidth = getResources().getDimension(
- R.dimen.taskbar_overflow_button_preview_stroke);
setWillNotDraw(false);
}
@@ -105,16 +252,14 @@
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
- if (mIsActive) {
- drawLeaveBehindCircle(canvas);
- } else {
- drawAppIcons(canvas);
- }
+ drawAppIcons(canvas);
+ drawLeaveBehindCircle(canvas);
}
private void drawAppIcons(@NonNull Canvas canvas) {
mItemBackgroundPaint.setColor(mItemBackgroundColor);
float radius = mIconSize / 2f - mPadding;
+ int adjustedItemIconSize = Math.round(mItemIconSize);
int itemsToShow = Math.min(mItems.size(), MAX_ITEMS_IN_PREVIEW);
for (int i = itemsToShow - 1; i >= 0; --i) {
@@ -123,36 +268,33 @@
continue;
}
- // Set the item icon size so two items fit within the overflow icon with stroke width
- // included, and overlap of 4 stroke width sizes between base item preview items.
- // 2 * strokeWidth + 2 * itemIconSize - 4 * strokeWidth = iconSize = 2 * radius.
- float itemIconSize = radius + mItemPreviewStrokeWidth;
- // Offset item icon from center so item icon stroke edge matches the parent icon edge.
- float itemCenterOffset = radius - itemIconSize / 2 - mItemPreviewStrokeWidth;
-
- float itemCenterX = getItemXOffset(itemCenterOffset, mIsRtlLayout, i, itemsToShow);
- float itemCenterY = getItemYOffset(itemCenterOffset, i, itemsToShow);
+ float itemCenterX = getItemXOffset(mItemIconCenterOffset, mIsRtlLayout, i, itemsToShow);
+ float itemCenterY = getItemYOffset(mItemIconCenterOffset, i, itemsToShow);
Drawable iconCopy = icon.getConstantState().newDrawable().mutate();
- iconCopy.setBounds(0, 0, (int) itemIconSize, (int) itemIconSize);
+ iconCopy.setBounds(0, 0, adjustedItemIconSize, adjustedItemIconSize);
+ iconCopy.setColorFilter(new BlendModeColorFilter(
+ ColorUtils.setAlphaComponent(mLeaveBehindColor, mItemIconColorFilterOpacity),
+ BlendMode.SRC_ATOP));
canvas.save();
- float itemIconRadius = itemIconSize / 2;
+ float itemIconRadius = adjustedItemIconSize / 2f;
canvas.translate(
mPadding + itemCenterX + radius - itemIconRadius,
mPadding + itemCenterY + radius - itemIconRadius);
canvas.drawCircle(itemIconRadius, itemIconRadius,
- itemIconRadius + mItemPreviewStrokeWidth, mItemBackgroundPaint);
+ itemIconRadius + mItemIconStrokeWidth, mItemBackgroundPaint);
iconCopy.draw(canvas);
canvas.restore();
}
}
private void drawLeaveBehindCircle(@NonNull Canvas canvas) {
- mItemBackgroundPaint.setColor(mLeaveBehindColor);
+ mItemBackgroundPaint.setColor(
+ ColorUtils.setAlphaComponent(mLeaveBehindColor, mLeaveBehindOpacity));
- final var xyCenter = mIconSize / 2f;
- canvas.drawCircle(xyCenter, xyCenter, mIconSize / 4f, mItemBackgroundPaint);
+ final float xyCenter = mIconSize / 2f;
+ canvas.drawCircle(xyCenter, xyCenter, mLeaveBehindSize / 2f, mItemBackgroundPaint);
}
/**
@@ -203,10 +345,98 @@
* @param isActive The next state of the view.
*/
public void setIsActive(boolean isActive) {
- if (mIsActive != isActive) {
- mIsActive = isActive;
- invalidate();
+ if (mIsActive == isActive) {
+ return;
}
+ mIsActive = isActive;
+
+ if (mStateTransitionAnimationWrapper != null
+ && mStateTransitionAnimationWrapper.isRunning()) {
+ mStateTransitionAnimationWrapper.reverse();
+ return;
+ }
+
+ final AnimatorSet stateTransitionAnimation = getStateTransitionAnimation();
+ mStateTransitionAnimationWrapper = ValueAnimator.ofFloat(0, 1f);
+ mStateTransitionAnimationWrapper.setDuration(mIsActive
+ ? ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND
+ : ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS);
+ mStateTransitionAnimationWrapper.setInterpolator(
+ mIsActive ? Interpolators.STANDARD : Interpolators.EMPHASIZED);
+ mStateTransitionAnimationWrapper.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mStateTransitionAnimationWrapper = null;
+ }
+ });
+ mStateTransitionAnimationWrapper.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ stateTransitionAnimation.setCurrentPlayTime(
+ (long) (ANIMATION_SET_DURATION * animator.getAnimatedFraction()));
+ }
+ });
+ mStateTransitionAnimationWrapper.start();
+ }
+
+ private AnimatorSet getStateTransitionAnimation() {
+ final AnimatorSet animation = new AnimatorSet();
+ animation.setInterpolator(Interpolators.LINEAR);
+ animation.playTogether(
+ buildAnimator(ITEM_ICON_CENTER_OFFSET, 0f, mItemIconCenterOffsetDefault,
+ ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION, 0L,
+ ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION),
+ buildAnimator(ITEM_ICON_COLOR_FILTER_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
+ ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION, 0L,
+ ANIMATION_SET_DURATION - ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION),
+ buildAnimator(ITEM_ICON_SIZE, mItemIconSizeScaledDown, mItemIconSizeDefault,
+ ITEM_ICON_SIZE_ANIMATION_DURATION, 0L,
+ ITEM_ICON_SIZE_ANIMATION_DURATION),
+ buildAnimator(ITEM_ICON_STROKE_WIDTH, 0f, mItemIconStrokeWidthDefault,
+ ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION, 0L,
+ ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION),
+ buildAnimator(LEAVE_BEHIND_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
+ LEAVE_BEHIND_OPACITY_ANIMATION_DURATION, LEAVE_BEHIND_ANIMATIONS_DELAY,
+ ANIMATION_SET_DURATION - LEAVE_BEHIND_ANIMATIONS_DELAY
+ - LEAVE_BEHIND_OPACITY_ANIMATION_DURATION),
+ buildAnimator(LEAVE_BEHIND_SIZE, mLeaveBehindSizeDefault,
+ mLeaveBehindSizeScaledDown, LEAVE_BEHIND_SIZE_ANIMATION_DURATION,
+ LEAVE_BEHIND_ANIMATIONS_DELAY, 0L)
+ );
+ return animation;
+ }
+
+ private ObjectAnimator buildAnimator(IntProperty<TaskbarOverflowView> property,
+ int finalValueWhenAnimatingToLeaveBehind, int finalValueWhenAnimatingToAppIcons,
+ long duration, long delayWhenAnimatingToLeaveBehind,
+ long delayWhenAnimatingToAppIcons) {
+ final ObjectAnimator animator = ObjectAnimator.ofInt(this, property,
+ mIsActive ? finalValueWhenAnimatingToLeaveBehind
+ : finalValueWhenAnimatingToAppIcons);
+ applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
+ delayWhenAnimatingToAppIcons);
+ return animator;
+ }
+
+ private ObjectAnimator buildAnimator(FloatProperty<TaskbarOverflowView> property,
+ float finalValueWhenAnimatingToLeaveBehind, float finalValueWhenAnimatingToAppIcons,
+ long duration, long delayWhenAnimatingToLeaveBehind,
+ long delayWhenAnimatingToAppIcons) {
+ final ObjectAnimator animator = ObjectAnimator.ofFloat(this, property,
+ mIsActive ? finalValueWhenAnimatingToLeaveBehind
+ : finalValueWhenAnimatingToAppIcons);
+ applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
+ delayWhenAnimatingToAppIcons);
+ return animator;
+ }
+
+ private void applyTiming(ObjectAnimator animator, long duration,
+ long delayWhenAnimatingToLeaveBehind,
+ long delayWhenAnimatingToAppIcons) {
+ animator.setDuration(duration);
+ animator.setStartDelay(
+ mIsActive ? delayWhenAnimatingToLeaveBehind : delayWhenAnimatingToAppIcons);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 70d4bb1..2e0bae5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -201,8 +201,10 @@
if (com.android.wm.shell.Flags.enableBubbleAnything()) {
shortcuts.add(BUBBLE);
}
+
if (Flags.enableMultiInstanceMenuTaskbar()
- && DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ && DesktopModeStatus.canEnterDesktopMode(mContext)
+ && !mControllers.taskbarStashController.isInOverview()) {
shortcuts.addAll(getMultiInstanceMenuOptions().toList());
}
return shortcuts.stream();
@@ -295,9 +297,9 @@
* Returns a stream of Multi Instance menu options if an app supports it.
*/
Stream<SystemShortcut.Factory<BaseTaskbarContext>> getMultiInstanceMenuOptions() {
- SystemShortcut.Factory<BaseTaskbarContext> factory = createNewWindowShortcutFactory();
- return factory != null ? Stream.of(factory) : Stream.empty();
-
+ SystemShortcut.Factory<BaseTaskbarContext> f1 = createNewWindowShortcutFactory();
+ SystemShortcut.Factory<BaseTaskbarContext> f2 = createManageWindowsShortcutFactory();
+ return f1 != null ? Stream.of(f1, f2) : Stream.empty();
}
/**
@@ -317,6 +319,23 @@
}
/**
+ * Creates a factory function representing a "Manage Windows" menu item only if the calling app
+ * supports multi-instance. This menu item shows the open instances of the calling app.
+ * @return A factory function to be used in populating the long-press menu.
+ */
+ public SystemShortcut.Factory<BaseTaskbarContext> createManageWindowsShortcutFactory() {
+ return (context, itemInfo, originalView) -> {
+ ComponentKey key = itemInfo.getComponentKey();
+ AppInfo app = getApp(key);
+ if (app != null && app.supportsMultiInstance()) {
+ return new ManageWindowsTaskbarShortcut<>(context, itemInfo, originalView,
+ mControllers);
+ }
+ return null;
+ };
+ }
+
+ /**
* A single menu item ("Split left," "Split right," or "Split top") that executes a split
* from the taskbar, as if the user performed a drag and drop split.
* Includes an onClick method that initiates the actual split.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index c1dd216..67be8da 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -23,6 +23,7 @@
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.internal.jank.InteractionJankMonitor.Configuration;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
+import static com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
@@ -398,6 +399,9 @@
* Returns how long the stash/unstash animation should play.
*/
public long getStashDuration() {
+ if (DisplayController.isPinnedTaskbar(mActivity)) {
+ return PINNED_TASKBAR_TRANSITION_DURATION;
+ }
return DisplayController.isTransientTaskbar(mActivity)
? TRANSIENT_TASKBAR_STASH_DURATION
: TASKBAR_STASH_DURATION;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index f7f5cf6..8b636dd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -195,6 +195,16 @@
return true;
}
+ public boolean isAnimatingToHotseat() {
+ return false;
+ }
+
+ /**
+ * Skips to the end of the animation to Hotseat - should only be used if
+ * {@link #isAnimatingToHotseat()} returns true.
+ */
+ public void endAnimationToHotseat() {}
+
/** Returns {@code true} if Taskbar is currently within overview. */
protected boolean isInOverviewUi() {
return false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 55bcb23..8816a6d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -120,7 +120,8 @@
private boolean mShouldTryStartAlign;
- private final int mMaxNumIcons;
+ private int mMaxNumIcons = 0;
+ private int mIdealNumIcons = 0;
private final int mAllAppsButtonTranslationOffset;
@@ -188,8 +189,6 @@
// TODO: Disable touch events on QSB otherwise it can crash.
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
-
- mMaxNumIcons = calculateMaxNumIcons();
}
/**
@@ -200,11 +199,15 @@
int availableWidth = deviceProfile.widthPx;
int defaultEdgeMargin =
(int) getResources().getDimension(deviceProfile.inv.inlineNavButtonsEndSpacing);
+ int spaceForBubbleBar =
+ Math.round(mControllerCallbacks.getBubbleBarMaxCollapsedWidthIfVisible());
// Reserve space required for edge margins, or for navbar if shown. If task bar needs to be
// center aligned with nav bar shown, reserve space on both sides.
- availableWidth -= Math.max(defaultEdgeMargin, deviceProfile.hotseatBarEndOffset);
- availableWidth -= Math.max(defaultEdgeMargin,
+ availableWidth -=
+ Math.max(defaultEdgeMargin + spaceForBubbleBar, deviceProfile.hotseatBarEndOffset);
+ availableWidth -= Math.max(
+ defaultEdgeMargin + (mShouldTryStartAlign ? 0 : spaceForBubbleBar),
mShouldTryStartAlign ? 0 : deviceProfile.hotseatBarEndOffset);
// The space taken by an item icon used during layout.
@@ -231,6 +234,21 @@
return Math.floorDiv(availableWidth, iconSize) + additionalIcons;
}
+ /**
+ * Recalculates the max number of icons the taskbar view can show without entering overflow.
+ * Returns whether the max number of icons changed and the change affects the number of icons
+ * that should be shown in the taskbar.
+ */
+ boolean updateMaxNumIcons() {
+ if (!Flags.taskbarOverflow()) {
+ return false;
+ }
+ int oldMaxNumIcons = mMaxNumIcons;
+ mMaxNumIcons = calculateMaxNumIcons();
+ return oldMaxNumIcons != mMaxNumIcons
+ && (mIdealNumIcons > oldMaxNumIcons || mIdealNumIcons > mMaxNumIcons);
+ }
+
@Override
public void setVisibility(int visibility) {
boolean changed = getVisibility() != visibility;
@@ -328,6 +346,10 @@
&& mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup()) {
setOnTouchListener(mControllerCallbacks.getTaskbarTouchListener());
}
+
+ if (Flags.taskbarOverflow()) {
+ mMaxNumIcons = calculateMaxNumIcons();
+ }
}
private void removeAndRecycle(View view) {
@@ -460,8 +482,9 @@
}
}
- overflowSize =
- nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded - mMaxNumIcons;
+ mIdealNumIcons = nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded;
+ overflowSize = mIdealNumIcons - mMaxNumIcons;
+
if (overflowSize > 0 && mTaskbarOverflowView != null) {
addView(mTaskbarOverflowView, nextViewIndex++);
} else if (mTaskbarOverflowView != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index 834f92e..f65f307 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -137,6 +137,17 @@
return null;
}
+ /**
+ * Get the max bubble bar collapsed width for the current bubble bar visibility state. Used to
+ * reserve space for the bubble bar when transitioning taskbar view into overflow.
+ */
+ public float getBubbleBarMaxCollapsedWidthIfVisible() {
+ return mControllers.bubbleControllers
+ .filter(c -> !c.bubbleBarViewController.isHiddenForNoBubbles())
+ .map(c -> c.bubbleBarViewController.getCollapsedWidthWithMaxVisibleBubbles())
+ .orElse(0f);
+ }
+
/** Returns true if bubble bar controllers present and enabled in persistent taskbar. */
public boolean isBubbleBarEnabledInPersistentTaskbar() {
return Flags.enableBubbleBarInPersistentTaskBar()
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 494c472..bb4f07a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -120,7 +120,7 @@
private final TaskbarView mTaskbarView;
private final MultiValueAlpha mTaskbarIconAlpha;
private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
- private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
+ public final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
this::updateTranslationY);
private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat(
this::updateTranslationY);
@@ -779,9 +779,16 @@
}
}
- /** Resets the icon alignment controller so that it can be recreated again later. */
- void resetIconAlignmentController() {
+ /**
+ * Resets the icon alignment controller so that it can be recreated again later, and updates
+ * the list of icons shown in the taskbar if the bubble bar visibility changes the taskbar
+ * overflow state.
+ */
+ void adjustTaskbarForBubbleBar() {
mIconAlignControllerLazy = null;
+ if (mTaskbarView.updateMaxNumIcons()) {
+ commitRunningAppsToUI();
+ }
}
/**
@@ -789,6 +796,8 @@
*/
private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
PendingAnimation setter = new PendingAnimation(100);
+ // icon alignment not needed for pinned taskbar.
+ if (DisplayController.isPinnedTaskbar(mActivity)) return setter.createPlaybackController();
mOnControllerPreCreateCallback.run();
DeviceProfile taskbarDp = mActivity.getDeviceProfile();
Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 350f56f..c5c2d69 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -1290,10 +1290,14 @@
// If there are more than 2 bubbles, the first 2 should be visible when collapsed,
// excluding the overflow.
return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED
- ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding
+ ? getCollapsedWidthWithMaxVisibleBubbles()
: getScaledIconSize() + horizontalPadding;
}
+ float getCollapsedWidthWithMaxVisibleBubbles() {
+ return getScaledIconSize() + mIconOverlapAmount + 2 * mBubbleBarPadding;
+ }
+
/** Returns the child count excluding the overflow if it's present. */
int getBubbleChildCount() {
return hasOverflow() ? getChildCount() - 1 : getChildCount();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 2dddb16..d13e0c4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -473,6 +473,13 @@
}
/**
+ * @return the max collapsed width for the bubble bar.
+ */
+ public float getCollapsedWidthWithMaxVisibleBubbles() {
+ return mBarView.getCollapsedWidthWithMaxVisibleBubbles();
+ }
+
+ /**
* @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on
* the right
*/
@@ -990,10 +997,9 @@
}
}
- /** Marks as should show education and shows the bubble bar in a collapsed state */
+ /** Marks as should show education. */
public void prepareToShowEducation() {
mShouldShowEducation = true;
- mBubbleStashController.showBubbleBar(false /* expand the bubbles */);
}
/**
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 7b20eea..908e97c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -21,6 +21,7 @@
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.animation.ValueAnimator
+import com.android.app.animation.InterpolatorsAndroidX
import com.android.launcher3.R
import com.android.systemui.util.addListener
@@ -35,7 +36,8 @@
) {
private companion object {
- const val ANIMATION_DURATION_MS = 250L
+ const val EXPAND_ANIMATION_DURATION_MS = 400L
+ const val COLLAPSE_ANIMATION_DURATION_MS = 350L
}
private var flyout: BubbleBarFlyoutView? = null
@@ -86,9 +88,10 @@
private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
val flyout = this.flyout ?: return
val startValue = getCurrentAnimatedValueIfRunning() ?: 0f
- val duration = (ANIMATION_DURATION_MS * (1f - startValue)).toLong()
+ val duration = (EXPAND_ANIMATION_DURATION_MS * (1f - startValue)).toLong()
animator?.cancel()
val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration)
+ animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
this.animator = animator
when (animationType) {
AnimationType.FADE ->
@@ -111,6 +114,7 @@
fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
val flyout = flyout ?: return
hideFlyout(AnimationType.FADE) {
+ callbacks.resetTopBoundary()
flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
}
}
@@ -152,9 +156,10 @@
private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
val flyout = this.flyout ?: return
val startValue = getCurrentAnimatedValueIfRunning() ?: 1f
- val duration = (ANIMATION_DURATION_MS * startValue).toLong()
+ val duration = (COLLAPSE_ANIMATION_DURATION_MS * startValue).toLong()
animator?.cancel()
val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration)
+ animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
this.animator = animator
when (animationType) {
AnimationType.FADE ->
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 418675c..f9f5a15 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -35,6 +35,7 @@
import androidx.core.animation.ArgbEvaluator
import com.android.launcher3.R
import com.android.launcher3.popup.RoundedArrowDrawable
+import kotlin.math.min
/** The flyout view used to notify the user of a new bubble notification. */
class BubbleBarFlyoutView(
@@ -46,6 +47,8 @@
private companion object {
// the minimum progress of the expansion animation before the content starts fading in.
const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f
+ // the rate multiple for the background color animation relative to the morph animation.
+ const val BACKGROUND_COLOR_CHANGE_RATE = 5
}
private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this)
@@ -204,6 +207,8 @@
minExpansionProgressForTriangle =
positioner.distanceToRevealTriangle / translationToCollapsedPosition.y
+ backgroundPaint.color = collapsedColor
+
// post the request to start the expand animation to the looper so the view can measure
// itself
scheduler.runAfterLayout(expandAnimation)
@@ -307,8 +312,16 @@
height.toFloat() - triangleHeight + triangleOverlap,
)
+ // transform the flyout color between the collapsed and expanded states. the color
+ // transformation completes at a faster rate (BACKGROUND_COLOR_CHANGE_RATE) than the
+ // expansion animation. this helps make the color change smooth.
backgroundPaint.color =
- ArgbEvaluator.getInstance().evaluate(expansionProgress, collapsedColor, backgroundColor)
+ ArgbEvaluator.getInstance()
+ .evaluate(
+ min(expansionProgress * BACKGROUND_COLOR_CHANGE_RATE, 1f),
+ collapsedColor,
+ backgroundColor,
+ )
canvas.save()
canvas.translate(backgroundRectTx, backgroundRectTy)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 6632721..45f5568 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -54,7 +54,9 @@
if (field == state) return
val transitionFromHome = field == BubbleLauncherState.HOME
field = state
- if (!bubbleBarViewController.hasBubbles()) {
+ val hasBubbles = bubbleBarViewController.hasBubbles()
+ bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
+ if (!hasBubbles) {
// if there are no bubbles, there's nothing to show, so just return.
return
}
@@ -65,7 +67,6 @@
// on home but in persistent taskbar elsewhere so the position is different.
animateBubbleBarY()
}
- bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
}
override var isSysuiLocked: Boolean = false
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 71303f8..e62c0d4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -87,7 +87,9 @@
set(state) {
if (field == state) return
field = state
- if (!bubbleBarViewController.hasBubbles()) {
+ val hasBubbles = bubbleBarViewController.hasBubbles()
+ bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
+ if (!hasBubbles) {
// if there are no bubbles, there's nothing to show, so just return.
return
}
@@ -103,7 +105,6 @@
// Only stash if we're in an app, otherwise we're in home or overview where we should
// be un-stashed
updateStashedAndExpandedState(field == BubbleLauncherState.IN_APP, expand = false)
- bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
}
override var isSysuiLocked: Boolean = false
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
index c5f8aa0..7e3b362 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -21,7 +21,6 @@
import android.content.res.ColorStateList
import android.graphics.Color.TRANSPARENT
import android.util.AttributeSet
-import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
@@ -57,7 +56,7 @@
}
init {
- LayoutInflater.from(context).inflate(R.layout.taskbar_all_apps_button, null, false)
+ contentDescription = context.getString(R.string.all_apps_button_label)
setUpIcon()
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
index 1fb835a..344f163 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
@@ -21,7 +21,6 @@
import android.content.res.ColorStateList
import android.graphics.Color.TRANSPARENT
import android.util.AttributeSet
-import android.view.LayoutInflater
import androidx.core.view.setPadding
import com.android.launcher3.R
import com.android.launcher3.Utilities.dpToPx
@@ -33,11 +32,8 @@
/** Taskbar divider view container for customizable taskbar. */
class TaskbarDividerContainer
@JvmOverloads
-constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
-) : IconButtonView(context, attrs), TaskbarContainer {
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+ IconButtonView(context, attrs), TaskbarContainer {
private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
override val spaceNeeded: Int
@@ -46,7 +42,7 @@
}
init {
- LayoutInflater.from(context).inflate(R.layout.taskbar_divider, null, false)
+ contentDescription = context.getString(R.string.taskbar_divider_a11y_title)
setUpIcon()
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 34d9a68..1baba74 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -304,7 +304,7 @@
mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
mDepthController = new DepthController(this);
- if (DesktopModeStatus.canEnterDesktopMode(this)) {
+ if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(this)) {
mSplitSelectStateController.initSplitFromDesktopController(this,
overviewComponentObserver);
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 3a39cf2..8ad00bf 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -95,6 +95,7 @@
public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
StateAnimationConfig config) {
RecentsView overview = mContainer.getOverviewPanel();
+ boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContainer);
if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) {
overview.switchToScreenshot(() ->
overview.finishRecentsAnimation(true /* toRecents */, null));
@@ -109,7 +110,8 @@
// We sync the scrim fade with the taskbar animation duration to avoid any flickers for
// taskbar icons disappearing before hotseat icons show up.
float scrimUpperBoundFromSplit =
- QuickstepTransitionManager.getTaskbarToHomeDuration() / (float) config.duration;
+ QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
+ / (float) config.duration;
scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f);
config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
config.setInterpolator(ANIM_SCRIM_FADE,
@@ -139,7 +141,8 @@
// Sync scroll so that it ends before or at the same time as the taskbar animation.
if (mContainer.getDeviceProfile().isTaskbarPresent) {
config.duration = Math.min(
- config.duration, QuickstepTransitionManager.getTaskbarToHomeDuration());
+ config.duration,
+ QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar));
}
overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration));
} else {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 97d7179..95e7737 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -100,6 +100,7 @@
import com.android.internal.util.LatencyTracker;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
@@ -667,7 +668,7 @@
TopTaskTracker.CachedTaskInfo cachedTaskInfo = mGestureState.getRunningTask();
if (mIsSwipeForSplit) {
int[] splitTaskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds();
- runningTasks = cachedTaskInfo.getPlaceholderTasks(splitTaskIds);
+ runningTasks = cachedTaskInfo.getSplitPlaceholderTasks(splitTaskIds);
} else {
runningTasks = cachedTaskInfo.getPlaceholderTasks();
}
@@ -1373,8 +1374,9 @@
mInputConsumerProxy.enable();
}
if (endTarget == HOME) {
+ boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContext);
duration = mContainer != null && mContainer.getDeviceProfile().isTaskbarPresent
- ? StaggeredWorkspaceAnim.DURATION_TASKBAR_MS
+ ? QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
: StaggeredWorkspaceAnim.DURATION_MS;
ContextualEduStatsManager.INSTANCE.get(mContext).updateEduStats(
mGestureState.isTrackpadGesture(), GestureType.HOME);
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index a3953ca..2164bc2 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -378,6 +378,9 @@
public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) {
out.x = dp.widthPx;
out.y = dp.heightPx;
+ if (dp.isTablet && !DisplayController.isTransientTaskbar(context)) {
+ out.y -= dp.taskbarHeight;
+ }
}
/**
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index cff352c..cfbcf0a 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -28,6 +28,7 @@
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_HOME;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_NEW_TASK;
+import android.app.TaskInfo;
import android.content.Intent;
import android.os.SystemClock;
import android.view.MotionEvent;
@@ -45,6 +46,7 @@
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.views.RecentsViewContainer;
import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.wm.shell.shared.GroupedTaskInfo;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -272,7 +274,7 @@
* @return the interface to the activity handing the UI updates for this gesture.
*/
public <S extends BaseState<S>, T extends RecentsViewContainer & StatefulContainer<S>>
- BaseContainerInterface getContainerInterface() {
+ BaseContainerInterface<S, T> getContainerInterface() {
return mContainerInterface;
}
@@ -330,13 +332,23 @@
if (mRunningTask == null) {
return new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
} else {
- int cachedTasksSize = mRunningTask.mAllCachedTasks.size();
- int count = Math.min(cachedTasksSize, getMultipleTasks ? 2 : 1);
- int[] runningTaskIds = new int[count];
- for (int i = 0; i < count; i++) {
- runningTaskIds[i] = mRunningTask.mAllCachedTasks.get(i).taskId;
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (mRunningTask.getVisibleTasks().isEmpty()) {
+ return new int[0];
+ }
+ GroupedTaskInfo topRunningTask = mRunningTask.getVisibleTasks().getFirst();
+ List<TaskInfo> groupedTasks = topRunningTask.getTaskInfoList();
+ return groupedTasks.stream().mapToInt(
+ groupedTask -> groupedTask.taskId).toArray();
+ } else {
+ int cachedTasksSize = mRunningTask.mAllCachedTasks.size();
+ int count = Math.min(cachedTasksSize, getMultipleTasks ? 2 : 1);
+ int[] runningTaskIds = new int[count];
+ for (int i = 0; i < count; i++) {
+ runningTaskIds[i] = mRunningTask.mAllCachedTasks.get(i).taskId;
+ }
+ return runningTaskIds;
}
- return runningTaskIds;
}
}
diff --git a/quickstep/src/com/android/quickstep/InputConsumerUtils.kt b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
new file mode 100644
index 0000000..bea3150
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
@@ -0,0 +1,746 @@
+/*
+ * 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
+
+import android.content.Context
+import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.statemanager.BaseState
+import com.android.launcher3.statemanager.StatefulContainer
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.util.LockedUserState.Companion.get
+import com.android.quickstep.inputconsumers.AccessibilityInputConsumer
+import com.android.quickstep.inputconsumers.AssistantInputConsumer
+import com.android.quickstep.inputconsumers.BubbleBarInputConsumer
+import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer
+import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer
+import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer
+import com.android.quickstep.inputconsumers.OtherActivityInputConsumer
+import com.android.quickstep.inputconsumers.OverviewInputConsumer
+import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer
+import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer
+import com.android.quickstep.inputconsumers.ResetGestureInputConsumer
+import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer
+import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer
+import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer
+import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer
+import com.android.quickstep.util.ActiveGestureErrorDetector
+import com.android.quickstep.util.ActiveGestureLog
+import com.android.quickstep.util.ActiveGestureLog.CompoundString
+import com.android.quickstep.util.ActiveGestureProtoLogProxy
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.systemui.shared.system.InputChannelCompat
+import com.android.systemui.shared.system.InputMonitorCompat
+import com.android.wm.shell.Flags
+import java.util.function.Consumer
+import java.util.function.Function
+
+/** Utility class for creating input consumers. */
+object InputConsumerUtils {
+ private const val SUBSTRING_PREFIX = "; "
+ private const val NEWLINE_PREFIX = "\n\t\t\t-> "
+
+ @JvmStatic
+ fun <S : BaseState<S>, T> newConsumer(
+ baseContext: Context,
+ tisContext: Context,
+ resetGestureInputConsumer: ResetGestureInputConsumer?,
+ overviewComponentObserver: OverviewComponentObserver,
+ deviceState: RecentsAnimationDeviceState,
+ previousGestureState: GestureState,
+ gestureState: GestureState,
+ taskAnimationManager: TaskAnimationManager,
+ inputMonitorCompat: InputMonitorCompat,
+ swipeUpHandlerFactory: AbsSwipeUpHandler.Factory,
+ onCompleteCallback: Consumer<OtherActivityInputConsumer>,
+ inputEventReceiver: InputChannelCompat.InputEventReceiver,
+ taskbarManager: TaskbarManager,
+ swipeUpProxyProvider: Function<GestureState?, AnimatedFloat?>,
+ overviewCommandHelper: OverviewCommandHelper,
+ event: MotionEvent,
+ ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+ val tac = taskbarManager.currentActivityContext
+ val bubbleControllers = tac?.bubbleControllers
+ if (bubbleControllers != null && BubbleBarInputConsumer.isEventOnBubbles(tac, event)) {
+ val consumer: InputConsumer =
+ BubbleBarInputConsumer(tisContext, bubbleControllers, inputMonitorCompat)
+ logInputConsumerSelectionReason(
+ consumer,
+ newCompoundString("event is on bubbles, creating new input consumer"),
+ )
+ return consumer
+ }
+ val progressProxy = swipeUpProxyProvider.apply(gestureState)
+ if (progressProxy != null) {
+ val consumer: InputConsumer =
+ ProgressDelegateInputConsumer(
+ tisContext,
+ taskAnimationManager,
+ gestureState,
+ inputMonitorCompat,
+ progressProxy,
+ )
+
+ logInputConsumerSelectionReason(
+ consumer,
+ newCompoundString(
+ "mSwipeUpProxyProvider has been set, using ProgressDelegateInputConsumer"
+ ),
+ )
+
+ return consumer
+ }
+
+ val canStartSystemGesture =
+ if (gestureState.isTrackpadGesture) deviceState.canStartTrackpadGesture()
+ else deviceState.canStartSystemGesture()
+
+ if (!get(tisContext).isUserUnlocked) {
+ val reasonString = newCompoundString("device locked")
+ val consumer =
+ if (canStartSystemGesture) {
+ // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
+ // launched while device is locked even after exiting direct boot mode (e.g.
+ // camera).
+ createDeviceLockedInputConsumer(
+ tisContext,
+ resetGestureInputConsumer,
+ deviceState,
+ gestureState,
+ taskAnimationManager,
+ inputMonitorCompat,
+ reasonString.append("%scan start system gesture", SUBSTRING_PREFIX),
+ )
+ } else {
+ getDefaultInputConsumer(
+ resetGestureInputConsumer,
+ reasonString.append("%scannot start system gesture", SUBSTRING_PREFIX),
+ )
+ }
+ logInputConsumerSelectionReason(consumer, reasonString)
+ return consumer
+ }
+
+ var reasonString: CompoundString
+ var base: InputConsumer
+ // When there is an existing recents animation running, bypass systemState check as this is
+ // a followup gesture and the first gesture started in a valid system state.
+ if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning) {
+ reasonString =
+ newCompoundString(
+ if (canStartSystemGesture)
+ "can start system gesture, trying to use base consumer"
+ else "recents animation was running, trying to use base consumer"
+ )
+ base =
+ newBaseConsumer<S, T>(
+ tisContext,
+ resetGestureInputConsumer,
+ overviewComponentObserver,
+ deviceState,
+ previousGestureState,
+ gestureState,
+ taskAnimationManager,
+ inputMonitorCompat,
+ swipeUpHandlerFactory,
+ onCompleteCallback,
+ inputEventReceiver,
+ event,
+ reasonString,
+ )
+ } else {
+ reasonString =
+ newCompoundString(
+ "cannot start system gesture and recents " +
+ "animation was not running, trying to use default input consumer"
+ )
+ base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+ }
+ if (deviceState.isGesturalNavMode || gestureState.isTrackpadGesture) {
+ handleOrientationSetup(base)
+ }
+ if (deviceState.isFullyGesturalNavMode || gestureState.isTrackpadGesture) {
+ val reasonPrefix =
+ "device is in gesture navigation mode or 3-button mode with a trackpad gesture"
+ if (deviceState.canTriggerAssistantAction(event)) {
+ reasonString.append(
+ "%s%s%sgesture can trigger the assistant, " +
+ "trying to use assistant input consumer",
+ NEWLINE_PREFIX,
+ reasonPrefix,
+ SUBSTRING_PREFIX,
+ )
+ base =
+ tryCreateAssistantInputConsumer(
+ tisContext,
+ deviceState,
+ inputMonitorCompat,
+ base,
+ gestureState,
+ event,
+ reasonString,
+ )
+ }
+
+ // If Taskbar is present, we listen for swipe or cursor hover events to unstash it.
+ if (tac != null && base !is AssistantInputConsumer) {
+ // Present always on large screen or on small screen w/ flag
+ val useTaskbarConsumer =
+ (tac.deviceProfile.isTaskbarPresent &&
+ !tac.isPhoneMode &&
+ !tac.isInStashedLauncherState)
+ if (canStartSystemGesture && useTaskbarConsumer) {
+ reasonString.append(
+ "%s%s%sTaskbarActivityContext != null, " +
+ "using TaskbarUnstashInputConsumer",
+ NEWLINE_PREFIX,
+ reasonPrefix,
+ SUBSTRING_PREFIX,
+ )
+ base =
+ TaskbarUnstashInputConsumer(
+ tisContext,
+ base,
+ inputMonitorCompat,
+ tac,
+ overviewCommandHelper,
+ gestureState,
+ )
+ }
+ }
+ if (Flags.enableBubblesLongPressNavHandle()) {
+ // Create bubbles input consumer before NavHandleLongPressInputConsumer.
+ // This allows for nav handle to fall back to bubbles.
+ if (deviceState.isBubblesExpanded) {
+ reasonString =
+ newCompoundString(reasonPrefix)
+ .append(
+ "%sbubbles expanded, trying to use default input consumer",
+ SUBSTRING_PREFIX,
+ )
+ // Bubbles can handle home gesture itself.
+ base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+ }
+ }
+
+ val navHandle = tac?.navHandle ?: SystemUiProxy.INSTANCE[tisContext]
+ if (
+ canStartSystemGesture &&
+ !previousGestureState.isRecentsAnimationRunning &&
+ navHandle.canNavHandleBeLongPressed() &&
+ !ignoreThreeFingerTrackpadForNavHandleLongPress(gestureState)
+ ) {
+ reasonString.append(
+ "%s%s%sNot running recents animation, ",
+ NEWLINE_PREFIX,
+ reasonPrefix,
+ SUBSTRING_PREFIX,
+ )
+ if (tac != null && tac.navHandle.canNavHandleBeLongPressed()) {
+ reasonString.append("stashed handle is long-pressable, ")
+ }
+ reasonString.append("using NavHandleLongPressInputConsumer")
+ base =
+ NavHandleLongPressInputConsumer(
+ tisContext,
+ base,
+ inputMonitorCompat,
+ deviceState,
+ navHandle,
+ gestureState,
+ )
+ }
+
+ if (!Flags.enableBubblesLongPressNavHandle()) {
+ // Continue overriding nav handle input consumer with bubbles
+ if (deviceState.isBubblesExpanded) {
+ reasonString =
+ newCompoundString(reasonPrefix)
+ .append(
+ "%sbubbles expanded, trying to use default input consumer",
+ SUBSTRING_PREFIX,
+ )
+ // Bubbles can handle home gesture itself.
+ base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+ }
+ }
+
+ if (deviceState.isSystemUiDialogShowing) {
+ reasonString =
+ newCompoundString(reasonPrefix)
+ .append(
+ "%ssystem dialog is showing, using SysUiOverlayInputConsumer",
+ SUBSTRING_PREFIX,
+ )
+ base = SysUiOverlayInputConsumer(baseContext, deviceState, inputMonitorCompat)
+ }
+
+ if (
+ gestureState.isTrackpadGesture &&
+ canStartSystemGesture &&
+ !previousGestureState.isRecentsAnimationRunning
+ ) {
+ reasonString =
+ newCompoundString(reasonPrefix)
+ .append(
+ "%sTrackpad 3-finger gesture, using TrackpadStatusBarInputConsumer",
+ SUBSTRING_PREFIX,
+ )
+ base = TrackpadStatusBarInputConsumer(baseContext, base, inputMonitorCompat)
+ }
+
+ if (deviceState.isScreenPinningActive) {
+ reasonString =
+ newCompoundString(reasonPrefix)
+ .append(
+ "%sscreen pinning is active, using ScreenPinnedInputConsumer",
+ SUBSTRING_PREFIX,
+ )
+ // Note: we only allow accessibility to wrap this, and it replaces the previous
+ // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
+ base = ScreenPinnedInputConsumer(tisContext, gestureState)
+ }
+
+ if (deviceState.canTriggerOneHandedAction(event)) {
+ reasonString.append(
+ "%s%s%sgesture can trigger one handed mode, " +
+ "using OneHandedModeInputConsumer",
+ NEWLINE_PREFIX,
+ reasonPrefix,
+ SUBSTRING_PREFIX,
+ )
+ base = OneHandedModeInputConsumer(tisContext, deviceState, base, inputMonitorCompat)
+ }
+
+ if (deviceState.isAccessibilityMenuAvailable) {
+ reasonString.append(
+ "%s%s%saccessibility menu is available, using AccessibilityInputConsumer",
+ NEWLINE_PREFIX,
+ reasonPrefix,
+ SUBSTRING_PREFIX,
+ )
+ base =
+ AccessibilityInputConsumer(
+ tisContext,
+ deviceState,
+ gestureState,
+ base,
+ inputMonitorCompat,
+ )
+ }
+ } else {
+ val reasonPrefix = "device is not in gesture navigation mode"
+ if (deviceState.isScreenPinningActive) {
+ reasonString =
+ newCompoundString(reasonPrefix)
+ .append(
+ "%sscreen pinning is active, trying to use default input consumer",
+ SUBSTRING_PREFIX,
+ )
+ base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+ }
+
+ if (deviceState.canTriggerOneHandedAction(event)) {
+ reasonString.append(
+ "%s%s%sgesture can trigger one handed mode, " +
+ "using OneHandedModeInputConsumer",
+ NEWLINE_PREFIX,
+ reasonPrefix,
+ SUBSTRING_PREFIX,
+ )
+ base = OneHandedModeInputConsumer(tisContext, deviceState, base, inputMonitorCompat)
+ }
+ }
+ logInputConsumerSelectionReason(base, reasonString)
+ return base
+ }
+
+ @JvmStatic
+ fun tryCreateAssistantInputConsumer(
+ context: Context,
+ deviceState: RecentsAnimationDeviceState,
+ inputMonitorCompat: InputMonitorCompat,
+ gestureState: GestureState,
+ motionEvent: MotionEvent,
+ ): InputConsumer {
+ return tryCreateAssistantInputConsumer(
+ context,
+ deviceState,
+ inputMonitorCompat,
+ InputConsumer.NO_OP,
+ gestureState,
+ motionEvent,
+ CompoundString.NO_OP,
+ )
+ }
+
+ private fun tryCreateAssistantInputConsumer(
+ context: Context,
+ deviceState: RecentsAnimationDeviceState,
+ inputMonitorCompat: InputMonitorCompat,
+ base: InputConsumer,
+ gestureState: GestureState,
+ motionEvent: MotionEvent,
+ reasonString: CompoundString,
+ ): InputConsumer {
+ return if (deviceState.isGestureBlockedTask(gestureState.runningTask)) {
+ reasonString.append(
+ "%sis gesture-blocked task, using base input consumer",
+ SUBSTRING_PREFIX,
+ )
+ base
+ } else {
+ reasonString.append("%susing AssistantInputConsumer", SUBSTRING_PREFIX)
+ AssistantInputConsumer(
+ context,
+ gestureState,
+ base,
+ inputMonitorCompat,
+ deviceState,
+ motionEvent,
+ )
+ }
+ }
+
+ @VisibleForTesting
+ @JvmStatic
+ fun <S : BaseState<S>, T> newBaseConsumer(
+ context: Context,
+ resetGestureInputConsumer: ResetGestureInputConsumer?,
+ overviewComponentObserver: OverviewComponentObserver,
+ deviceState: RecentsAnimationDeviceState,
+ previousGestureState: GestureState,
+ gestureState: GestureState,
+ taskAnimationManager: TaskAnimationManager,
+ inputMonitorCompat: InputMonitorCompat,
+ swipeUpHandlerFactory: AbsSwipeUpHandler.Factory,
+ onCompleteCallback: Consumer<OtherActivityInputConsumer>,
+ inputEventReceiver: InputChannelCompat.InputEventReceiver,
+ event: MotionEvent,
+ reasonString: CompoundString,
+ ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+ if (deviceState.isKeyguardShowingOccluded) {
+ // This handles apps showing over the lockscreen (e.g. camera)
+ return createDeviceLockedInputConsumer(
+ context,
+ resetGestureInputConsumer,
+ deviceState,
+ gestureState,
+ taskAnimationManager,
+ inputMonitorCompat,
+ reasonString.append(
+ "%skeyguard is showing occluded, " +
+ "trying to use device locked input consumer",
+ SUBSTRING_PREFIX,
+ ),
+ )
+ }
+
+ reasonString.append("%skeyguard is not showing occluded", SUBSTRING_PREFIX)
+
+ val runningTask = gestureState.runningTask
+ // Use overview input consumer for sharesheets on top of home.
+ val forceOverviewInputConsumer =
+ gestureState.getContainerInterface<S, T>().isStarted() &&
+ runningTask != null &&
+ runningTask.isRootChooseActivity
+
+ if (!Flags.enableShellTopTaskTracking()) {
+ // In the case where we are in an excluded, translucent overlay, ignore it and treat the
+ // running activity as the task behind the overlay.
+ val otherVisibleTask = runningTask?.visibleNonExcludedTask
+ if (otherVisibleTask != null) {
+ ActiveGestureProtoLogProxy.logUpdateGestureStateRunningTask(
+ otherVisibleTask.packageName ?: "MISSING",
+ runningTask.packageName ?: "MISSING",
+ )
+ gestureState.updateRunningTask(otherVisibleTask)
+ }
+ }
+
+ val previousGestureAnimatedToLauncher =
+ (previousGestureState.isRunningAnimationToLauncher ||
+ deviceState.isPredictiveBackToHomeInProgress)
+ // with shell-transitions, home is resumed during recents animation, so
+ // explicitly check against recents animation too.
+ val launcherResumedThroughShellTransition =
+ (gestureState.getContainerInterface<S, T>().isResumed() &&
+ !previousGestureState.isRecentsAnimationRunning)
+ // If a task fragment within Launcher is resumed
+ val launcherChildActivityResumed =
+ (com.android.launcher3.Flags.useActivityOverlay() &&
+ runningTask != null &&
+ runningTask.isHomeTask &&
+ overviewComponentObserver.isHomeAndOverviewSame &&
+ !launcherResumedThroughShellTransition &&
+ !previousGestureState.isRecentsAnimationRunning)
+
+ return if (gestureState.getContainerInterface<S, T>().isInLiveTileMode()) {
+ createOverviewInputConsumer<S, T>(
+ resetGestureInputConsumer,
+ deviceState,
+ inputMonitorCompat,
+ previousGestureState,
+ gestureState,
+ event,
+ reasonString.append(
+ "%sis in live tile mode, trying to use overview input consumer",
+ SUBSTRING_PREFIX,
+ ),
+ )
+ } else if (runningTask == null) {
+ getDefaultInputConsumer(
+ resetGestureInputConsumer,
+ reasonString.append("%srunning task == null", SUBSTRING_PREFIX),
+ )
+ } else if (
+ previousGestureAnimatedToLauncher ||
+ launcherResumedThroughShellTransition ||
+ forceOverviewInputConsumer
+ ) {
+ createOverviewInputConsumer<S, T>(
+ resetGestureInputConsumer,
+ deviceState,
+ inputMonitorCompat,
+ previousGestureState,
+ gestureState,
+ event,
+ reasonString.append(
+ if (previousGestureAnimatedToLauncher)
+ ("%sprevious gesture animated to launcher, " +
+ "trying to use overview input consumer")
+ else
+ (if (launcherResumedThroughShellTransition)
+ ("%slauncher resumed through a shell transition, " +
+ "trying to use overview input consumer")
+ else
+ ("%sforceOverviewInputConsumer == true, " +
+ "trying to use overview input consumer")),
+ SUBSTRING_PREFIX,
+ ),
+ )
+ } else if (deviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) {
+ getDefaultInputConsumer(
+ resetGestureInputConsumer,
+ reasonString.append(
+ if (launcherChildActivityResumed)
+ "%sis launcher child-task, trying to use default input consumer"
+ else "%sis gesture-blocked task, trying to use default input consumer",
+ SUBSTRING_PREFIX,
+ ),
+ )
+ } else {
+ reasonString.append("%susing OtherActivityInputConsumer", SUBSTRING_PREFIX)
+ createOtherActivityInputConsumer<S, T>(
+ context,
+ swipeUpHandlerFactory,
+ overviewComponentObserver,
+ deviceState,
+ taskAnimationManager,
+ inputMonitorCompat,
+ onCompleteCallback,
+ inputEventReceiver,
+ gestureState,
+ event,
+ )
+ }
+ }
+
+ private fun createDeviceLockedInputConsumer(
+ context: Context,
+ resetGestureInputConsumer: ResetGestureInputConsumer?,
+ deviceState: RecentsAnimationDeviceState,
+ gestureState: GestureState,
+ taskAnimationManager: TaskAnimationManager,
+ inputMonitorCompat: InputMonitorCompat,
+ reasonString: CompoundString,
+ ): InputConsumer {
+ return if (
+ (deviceState.isFullyGesturalNavMode || gestureState.isTrackpadGesture) &&
+ gestureState.runningTask != null
+ ) {
+ reasonString.append(
+ "%sdevice is in gesture nav mode or 3-button mode with a trackpad " +
+ "gesture and running task != null, using DeviceLockedInputConsumer",
+ SUBSTRING_PREFIX,
+ )
+ DeviceLockedInputConsumer(
+ context,
+ deviceState,
+ taskAnimationManager,
+ gestureState,
+ inputMonitorCompat,
+ )
+ } else {
+ getDefaultInputConsumer(
+ resetGestureInputConsumer,
+ reasonString.append(
+ if (deviceState.isFullyGesturalNavMode || gestureState.isTrackpadGesture)
+ "%srunning task == null, trying to use default input consumer"
+ else
+ ("%sdevice is not in gesture nav mode and it's not a trackpad gesture," +
+ " trying to use default input consumer"),
+ SUBSTRING_PREFIX,
+ ),
+ )
+ }
+ }
+
+ private fun <S : BaseState<S>, T> createOverviewInputConsumer(
+ resetGestureInputConsumer: ResetGestureInputConsumer?,
+ deviceState: RecentsAnimationDeviceState,
+ inputMonitorCompat: InputMonitorCompat,
+ previousGestureState: GestureState,
+ gestureState: GestureState,
+ event: MotionEvent,
+ reasonString: CompoundString,
+ ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+ val container: T =
+ gestureState.getContainerInterface<S, T>().getCreatedContainer()
+ ?: return getDefaultInputConsumer(
+ resetGestureInputConsumer,
+ reasonString.append(
+ "%sactivity == null, trying to use default input consumer",
+ SUBSTRING_PREFIX,
+ ),
+ )
+
+ val rootView = container.rootView
+ val hasWindowFocus = rootView?.hasWindowFocus() ?: false
+ val isPreviousGestureAnimatingToLauncher =
+ (previousGestureState.isRunningAnimationToLauncher ||
+ deviceState.isPredictiveBackToHomeInProgress)
+ val isInLiveTileMode: Boolean =
+ gestureState.getContainerInterface<S, T>().isInLiveTileMode()
+
+ reasonString.append(
+ if (hasWindowFocus) "%sactivity has window focus"
+ else
+ (if (isPreviousGestureAnimatingToLauncher)
+ "%sprevious gesture is still animating to launcher"
+ else if (isInLiveTileMode) "%sdevice is in live mode"
+ else "%sall overview focus conditions failed"),
+ SUBSTRING_PREFIX,
+ )
+ return if (hasWindowFocus || isPreviousGestureAnimatingToLauncher || isInLiveTileMode) {
+ reasonString.append(
+ "%soverview should have focus, using OverviewInputConsumer",
+ SUBSTRING_PREFIX,
+ )
+ OverviewInputConsumer(
+ gestureState,
+ container,
+ inputMonitorCompat,
+ /* startingInActivityBounds= */ false,
+ )
+ } else {
+ reasonString.append(
+ "%soverview shouldn't have focus, using OverviewWithoutFocusInputConsumer",
+ SUBSTRING_PREFIX,
+ )
+ val disableHorizontalSwipe = deviceState.isInExclusionRegion(event)
+ OverviewWithoutFocusInputConsumer(
+ container.asContext(),
+ deviceState,
+ gestureState,
+ inputMonitorCompat,
+ disableHorizontalSwipe,
+ )
+ }
+ }
+
+ /** Returns the [ResetGestureInputConsumer] if user is unlocked, else NO_OP. */
+ private fun getDefaultInputConsumer(
+ resetGestureInputConsumer: ResetGestureInputConsumer?,
+ reasonString: CompoundString,
+ ): InputConsumer {
+ return if (resetGestureInputConsumer != null) {
+ reasonString.append(
+ "%smResetGestureInputConsumer initialized, using ResetGestureInputConsumer",
+ SUBSTRING_PREFIX,
+ )
+ resetGestureInputConsumer
+ } else {
+ reasonString.append(
+ "%smResetGestureInputConsumer not initialized, using no-op input consumer",
+ SUBSTRING_PREFIX,
+ )
+ // mResetGestureInputConsumer isn't initialized until onUserUnlocked(), so reset to
+ // NO_OP until then (we never want these to be null).
+ InputConsumer.NO_OP
+ }
+ }
+
+ private fun <S : BaseState<S>, T> createOtherActivityInputConsumer(
+ context: Context,
+ swipeUpHandlerFactory: AbsSwipeUpHandler.Factory,
+ overviewComponentObserver: OverviewComponentObserver,
+ deviceState: RecentsAnimationDeviceState,
+ taskAnimationManager: TaskAnimationManager,
+ inputMonitorCompat: InputMonitorCompat,
+ onCompleteCallback: Consumer<OtherActivityInputConsumer>,
+ inputEventReceiver: InputChannelCompat.InputEventReceiver,
+ gestureState: GestureState,
+ event: MotionEvent,
+ ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+ val shouldDefer =
+ (!overviewComponentObserver.isHomeAndOverviewSame ||
+ gestureState
+ .getContainerInterface<S, T>()
+ .deferStartingActivity(deviceState, event))
+ val disableHorizontalSwipe = deviceState.isInExclusionRegion(event)
+ return OtherActivityInputConsumer(
+ /* base= */ context,
+ deviceState,
+ taskAnimationManager,
+ gestureState,
+ /* isDeferredDownTarget= */ shouldDefer,
+ onCompleteCallback,
+ inputMonitorCompat,
+ inputEventReceiver,
+ disableHorizontalSwipe,
+ swipeUpHandlerFactory,
+ )
+ }
+
+ private fun newCompoundString(substring: String): CompoundString {
+ return CompoundString("%s%s", NEWLINE_PREFIX, substring)
+ }
+
+ private fun logInputConsumerSelectionReason(
+ consumer: InputConsumer,
+ reasonString: CompoundString,
+ ) {
+ ActiveGestureProtoLogProxy.logSetInputConsumer(consumer.name, reasonString.toString())
+ if ((consumer.type and InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
+ ActiveGestureLog.INSTANCE.trackEvent(
+ ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER
+ )
+ }
+ }
+
+ private fun ignoreThreeFingerTrackpadForNavHandleLongPress(
+ gestureState: GestureState
+ ): Boolean {
+ return (com.android.launcher3.Flags.ignoreThreeFingerTrackpadForNavHandleLongPress() &&
+ gestureState.isThreeFingerTrackpadGesture)
+ }
+
+ private fun handleOrientationSetup(baseInputConsumer: InputConsumer) {
+ baseInputConsumer.notifyOrientationSetup()
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 1124aac..6719ab7 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -24,6 +24,7 @@
import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -60,6 +61,8 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.taskbar.LauncherTaskbarUIController;
import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.NavigationMode;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.quickstep.util.BackAnimState;
import com.android.systemui.shared.system.QuickStepContract;
@@ -295,8 +298,11 @@
mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
- // inset bottom in case of pinned taskbar being present
- mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+ // inset bottom in case of taskbar being present
+ if (!predictiveBackThreeButtonNav() || mLauncher.getDeviceProfile().isTaskbarPresent
+ || DisplayController.getNavigationMode(mLauncher) == NavigationMode.NO_BUTTON) {
+ mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+ }
mLauncherTargetView = mQuickstepTransitionManager.findLauncherView(
new RemoteAnimationTarget[]{ mBackTarget });
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 714838a..85e2b6e 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -114,12 +114,9 @@
}
@Override
- public void onTaskMovedToFront(GroupedTaskInfo[] visibleTasks) {
+ public void onTaskMovedToFront(GroupedTaskInfo taskToFront) {
mMainThreadExecutor.execute(() -> {
- // TODO(b/346588978): We currently are only sending a single task, but this will
- // be updated once we send the full set of visible tasks
- final TaskInfo info = visibleTasks[0].getTaskInfo1();
- topTaskTracker.handleTaskMovedToFront(info);
+ topTaskTracker.handleTaskMovedToFront(taskToFront.getTaskInfo1());
});
}
@@ -127,6 +124,13 @@
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
mMainThreadExecutor.execute(() -> topTaskTracker.onTaskChanged(taskInfo));
}
+
+ @Override
+ public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
+ mMainThreadExecutor.execute(() -> {
+ topTaskTracker.onVisibleTasksChanged(visibleTasks);
+ });
+ }
});
// 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/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index dcb0108..60fcff8 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -53,6 +53,8 @@
private boolean mFinishRequested = false;
// Only valid when mFinishRequested == true.
private boolean mFinishTargetIsLauncher;
+ // Only valid when mFinishRequested == true
+ private boolean mLauncherIsVisibleAtFinish;
private RunnableList mPendingFinishCallbacks = new RunnableList();
public RecentsAnimationController(RecentsAnimationControllerCompat controller,
@@ -117,13 +119,27 @@
}
@UiThread
+ public void finish(boolean toRecents, boolean launcherIsVisibleAtFinish,
+ Runnable onFinishComplete, boolean sendUserLeaveHint) {
+ Preconditions.assertUIThread();
+ finishController(toRecents, launcherIsVisibleAtFinish, onFinishComplete, sendUserLeaveHint,
+ false);
+ }
+
+ @UiThread
public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
- finishController(toRecents, callback, sendUserLeaveHint, false /* forceFinish */);
+ finishController(toRecents, false, callback, sendUserLeaveHint, false /* forceFinish */);
}
@UiThread
public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint,
boolean forceFinish) {
+ finishController(toRecents, toRecents, callback, sendUserLeaveHint, forceFinish);
+ }
+
+ @UiThread
+ public void finishController(boolean toRecents, boolean launcherIsVisibleAtFinish,
+ Runnable callback, boolean sendUserLeaveHint, boolean forceFinish) {
mPendingFinishCallbacks.add(callback);
if (!forceFinish && mFinishRequested) {
// If finish has already been requested, then add the callback to the pending list.
@@ -135,6 +151,7 @@
// Finish not yet requested
mFinishRequested = true;
mFinishTargetIsLauncher = toRecents;
+ mLauncherIsVisibleAtFinish = launcherIsVisibleAtFinish;
mOnFinishedListener.accept(this);
Runnable finishCb = () -> {
mController.finish(toRecents, sendUserLeaveHint, new IResultReceiver.Stub() {
@@ -211,6 +228,14 @@
return mFinishTargetIsLauncher;
}
+ /**
+ * RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether
+ * the animation was finished to launcher vs an app.
+ */
+ public boolean getLauncherIsVisibleAtFinish() {
+ return mLauncherIsVisibleAtFinish;
+ }
+
public void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "RecentsAnimationController:");
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index de8be50..e296449 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -354,7 +354,11 @@
* @return whether the given running task info matches the gesture-blocked task.
*/
public boolean isGestureBlockedTask(CachedTaskInfo taskInfo) {
- return taskInfo != null && taskInfo.getTaskId() == mGestureBlockingTaskId;
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return taskInfo != null && taskInfo.topGroupedTaskContainsTask(mGestureBlockingTaskId);
+ } else {
+ return taskInfo != null && taskInfo.getTaskId() == mGestureBlockingTaskId;
+ }
}
/**
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index c9dfe6d..7065f37 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -18,17 +18,20 @@
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.content.Intent.ACTION_CHOOSER;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_A;
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
-import android.annotation.UserIdInt;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
import android.content.Context;
+import android.util.ArrayMap;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -46,8 +49,10 @@
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.wm.shell.shared.GroupedTaskInfo;
import com.android.wm.shell.splitscreen.ISplitScreenListener;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -61,37 +66,58 @@
*/
public class TopTaskTracker extends ISplitScreenListener.Stub
implements TaskStackChangeListener, SafeCloseable {
-
+ private static final String TAG = "TopTaskTracker";
public static MainThreadInitializedObject<TopTaskTracker> INSTANCE =
new MainThreadInitializedObject<>(TopTaskTracker::new);
private static final int HISTORY_SIZE = 5;
+ private final Context mContext;
+
+ // Only used when Flags.enableShellTopTaskTracking() is disabled
// Ordered list with first item being the most recent task.
private final LinkedList<TaskInfo> mOrderedTaskList = new LinkedList<>();
-
- private final Context mContext;
private final SplitStageInfo mMainStagePosition = new SplitStageInfo();
private final SplitStageInfo mSideStagePosition = new SplitStageInfo();
private int mPinnedTaskId = INVALID_TASK_ID;
+ // Only used when Flags.enableShellTopTaskTracking() is enabled
+ // Mapping of display id to running tasks. Running tasks are ordered from top most to
+ // bottom most.
+ private ArrayMap<Integer, ArrayList<GroupedTaskInfo>> mVisibleTasks = new ArrayMap<>();
+
private TopTaskTracker(Context context) {
mContext = context;
- mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN;
- mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
- TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
- SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this);
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // Just prepopulate a list for the default display tasks so we don't need to add null
+ // checks everywhere
+ mVisibleTasks.put(DEFAULT_DISPLAY, new ArrayList<>());
+ } else {
+ mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN;
+ mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
+
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+ SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this);
+ }
}
@Override
public void close() {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this);
SystemUiProxy.INSTANCE.get(mContext).unregisterSplitScreenListener(this);
}
@Override
public void onTaskRemoved(int taskId) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
mOrderedTaskList.removeIf(rto -> rto.taskId == taskId);
}
@@ -100,7 +126,11 @@
handleTaskMovedToFront(taskInfo);
}
- public void handleTaskMovedToFront(TaskInfo taskInfo) {
+ void handleTaskMovedToFront(TaskInfo taskInfo) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId);
mOrderedTaskList.addFirst(taskInfo);
@@ -131,8 +161,39 @@
}
}
+ /**
+ * Called when the set of visible tasks have changed.
+ */
+ public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
+ if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
+ // TODO(346588978): Per-display info, just have everything in order by display
+
+ // Clear existing tasks for each display
+ mVisibleTasks.forEach((displayId, visibleTasksOnDisplay) -> visibleTasksOnDisplay.clear());
+
+ // Update the visible tasks on each display
+ for (int i = 0; i < visibleTasks.length; i++) {
+ final int displayId = visibleTasks[i].getTaskInfo1().getDisplayId();
+ final ArrayList<GroupedTaskInfo> displayTasks;
+ if (mVisibleTasks.containsKey(displayId)) {
+ displayTasks = mVisibleTasks.get(displayId);
+ } else {
+ displayTasks = new ArrayList<>();
+ mVisibleTasks.put(displayId, displayTasks);
+ }
+ displayTasks.add(visibleTasks[i]);
+ }
+ }
+
@Override
public void onStagePositionChanged(@StageType int stage, @StagePosition int position) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
mMainStagePosition.stagePosition = position;
} else {
@@ -141,6 +202,10 @@
}
public void onTaskChanged(RunningTaskInfo taskInfo) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
for (int i = 0; i < mOrderedTaskList.size(); i++) {
if (mOrderedTaskList.get(i).taskId == taskInfo.taskId) {
mOrderedTaskList.set(i, taskInfo);
@@ -151,6 +216,10 @@
@Override
public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
// If a task is not visible anymore or has been moved to undefined, stop tracking it.
if (!visible || stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) {
if (mMainStagePosition.taskId == taskId) {
@@ -161,7 +230,8 @@
return;
}
- if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
+ if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN
+ || (enableFlexibleSplit() && stage == STAGE_TYPE_A)) {
mMainStagePosition.taskId = taskId;
} else {
mSideStagePosition.taskId = taskId;
@@ -170,11 +240,19 @@
@Override
public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
mPinnedTaskId = taskId;
}
@Override
public void onActivityUnpinned() {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
mPinnedTaskId = INVALID_TASK_ID;
}
@@ -183,21 +261,59 @@
* Will return empty array if device is not in staged split
*/
public int[] getRunningSplitTaskIds() {
- if (mMainStagePosition.taskId == INVALID_TASK_ID
- || mSideStagePosition.taskId == INVALID_TASK_ID) {
- return new int[]{};
- }
- int[] out = new int[2];
- if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
- out[0] = mMainStagePosition.taskId;
- out[1] = mSideStagePosition.taskId;
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // TODO(346588978): This assumes default display for now
+ final ArrayList<GroupedTaskInfo> visibleTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
+ final GroupedTaskInfo splitTaskInfo = visibleTasks.stream()
+ .filter(taskInfo -> taskInfo.getType() == TYPE_SPLIT)
+ .findFirst().orElse(null);
+ if (splitTaskInfo != null && splitTaskInfo.getSplitBounds() != null) {
+ return new int[] {
+ splitTaskInfo.getSplitBounds().leftTopTaskId,
+ splitTaskInfo.getSplitBounds().rightBottomTaskId
+ };
+ }
+ return new int[0];
} else {
- out[1] = mMainStagePosition.taskId;
- out[0] = mSideStagePosition.taskId;
+ if (mMainStagePosition.taskId == INVALID_TASK_ID
+ || mSideStagePosition.taskId == INVALID_TASK_ID) {
+ return new int[]{};
+ }
+ int[] out = new int[2];
+ if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+ out[0] = mMainStagePosition.taskId;
+ out[1] = mSideStagePosition.taskId;
+ } else {
+ out[1] = mMainStagePosition.taskId;
+ out[0] = mSideStagePosition.taskId;
+ }
+ return out;
}
- return out;
}
+ /**
+ * Dumps the list of tasks in top task tracker.
+ */
+ public void dump(PrintWriter pw) {
+ if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return;
+ }
+
+ // TODO(346588978): This assumes default display for now
+ final ArrayList<GroupedTaskInfo> displayTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
+ pw.println("TopTaskTracker:");
+ pw.println(" tasks: [");
+ for (GroupedTaskInfo taskInfo : displayTasks) {
+ final TaskInfo info = taskInfo.getTaskInfo1();
+ final boolean isExcluded = (info.baseIntent.getFlags()
+ & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+ pw.println(" " + info.taskId + ": excluded=" + isExcluded
+ + " visibleRequested=" + info.isVisibleRequested
+ + " visible=" + info.isVisible
+ + " " + info.baseIntent.getComponent());
+ }
+ pw.println(" ]");
+ }
/**
* Returns the CachedTaskInfo for the top most task
@@ -205,25 +321,35 @@
@NonNull
@UiThread
public CachedTaskInfo getCachedTopTask(boolean filterOnlyVisibleRecents) {
- if (filterOnlyVisibleRecents) {
- // Since we only know about the top most task, any filtering may not be applied on the
- // cache. The second to top task may change while the top task is still the same.
- RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () ->
- ActivityManagerWrapper.getInstance().getRunningTasks(true));
- return new CachedTaskInfo(Arrays.asList(tasks));
- }
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // TODO(346588978): Currently ignore filterOnlyVisibleRecents, but perhaps make this an
+ // explicit filter For things to ignore (ie. PIP/Bubbles/Assistant/etc/so that this is
+ // explicit)
+ // TODO(346588978): This assumes default display for now (as does all of Launcher)
+ final ArrayList<GroupedTaskInfo> displayTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
+ return new CachedTaskInfo(new ArrayList<>(displayTasks));
+ } else {
+ if (filterOnlyVisibleRecents) {
+ // Since we only know about the top most task, any filtering may not be applied on
+ // the cache. The second to top task may change while the top task is still the
+ // same.
+ RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () ->
+ ActivityManagerWrapper.getInstance().getRunningTasks(true));
+ return new CachedTaskInfo(Arrays.asList(tasks));
+ }
- if (mOrderedTaskList.isEmpty()) {
- RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () ->
- ActivityManagerWrapper.getInstance().getRunningTasks(
- false /* filterOnlyVisibleRecents */));
- Collections.addAll(mOrderedTaskList, tasks);
- }
+ if (mOrderedTaskList.isEmpty()) {
+ RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () ->
+ ActivityManagerWrapper.getInstance().getRunningTasks(
+ false /* filterOnlyVisibleRecents */));
+ Collections.addAll(mOrderedTaskList, tasks);
+ }
- ArrayList<TaskInfo> tasks = new ArrayList<>(mOrderedTaskList);
- // Strip the pinned task and recents task
- tasks.removeIf(t -> t.taskId == mPinnedTaskId || isRecentsTask(t));
- return new CachedTaskInfo(tasks);
+ ArrayList<TaskInfo> tasks = new ArrayList<>(mOrderedTaskList);
+ // Strip the pinned task and recents task
+ tasks.removeIf(t -> t.taskId == mPinnedTaskId || isRecentsTask(t));
+ return new CachedTaskInfo(tasks);
+ }
}
private static boolean isRecentsTask(TaskInfo task) {
@@ -237,24 +363,79 @@
*/
public static class CachedTaskInfo {
+ // Only used when enableShellTopTaskTracking() is disabled
@Nullable
private final TaskInfo mTopTask;
+ @Nullable
public final List<TaskInfo> mAllCachedTasks;
- CachedTaskInfo(List<TaskInfo> allCachedTasks) {
+ // Only used when enableShellTopTaskTracking() is enabled
+ @Nullable
+ private final GroupedTaskInfo mTopGroupedTask;
+ @Nullable
+ private final ArrayList<GroupedTaskInfo> mVisibleTasks;
+
+
+ // Only used when enableShellTopTaskTracking() is enabled
+ CachedTaskInfo(@NonNull ArrayList<GroupedTaskInfo> visibleTasks) {
+ mAllCachedTasks = null;
+ mTopTask = null;
+ mVisibleTasks = visibleTasks;
+ mTopGroupedTask = !mVisibleTasks.isEmpty() ? mVisibleTasks.getFirst() : null;
+
+ }
+
+ // Only used when enableShellTopTaskTracking() is disabled
+ CachedTaskInfo(@NonNull List<TaskInfo> allCachedTasks) {
+ mVisibleTasks = null;
+ mTopGroupedTask = null;
mAllCachedTasks = allCachedTasks;
mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0);
}
+ /**
+ * @return The list of visible tasks
+ */
+ public ArrayList<GroupedTaskInfo> getVisibleTasks() {
+ return mVisibleTasks;
+ }
+
+ /**
+ * @return The top task id
+ */
public int getTaskId() {
- return mTopTask == null ? INVALID_TASK_ID : mTopTask.taskId;
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // Callers should use topGroupedTaskContainsTask() instead
+ return INVALID_TASK_ID;
+ } else {
+ return mTopTask != null ? mTopTask.taskId : INVALID_TASK_ID;
+ }
+ }
+
+ /**
+ * @return Whether the top grouped task contains the given {@param taskId} if
+ * Flags.enableShellTopTaskTracking() is true, otherwise it checks the top
+ * task as reported from TaskStackListener.
+ */
+ public boolean topGroupedTaskContainsTask(int taskId) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ return mTopGroupedTask != null && mTopGroupedTask.containsTask(taskId);
+ } else {
+ return mTopTask != null && mTopTask.taskId == taskId;
+ }
}
/**
* Returns true if the root of the task chooser activity
*/
public boolean isRootChooseActivity() {
- return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction());
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // TODO(346588978): Update this to not make an assumption on a specific task info
+ return mTopGroupedTask != null && ACTION_CHOOSER.equals(
+ mTopGroupedTask.getTaskInfo1().baseIntent.getAction());
+ } else {
+ return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction());
+ }
}
/**
@@ -262,6 +443,10 @@
* is another running task that is not excluded from recents, returns that underlying task.
*/
public @Nullable CachedTaskInfo getVisibleNonExcludedTask() {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // Callers should not need this when the full set of visible tasks are provided
+ return null;
+ }
if (mTopTask == null
|| (mTopTask.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == 0) {
// Not an excluded task.
@@ -278,24 +463,30 @@
}
/**
- * Returns true if this represents the HOME task
+ * Returns true if this represents the HOME activity type task
*/
public boolean isHomeTask() {
- return mTopTask != null && mTopTask.configuration.windowConfiguration
- .getActivityType() == ACTIVITY_TYPE_HOME;
- }
-
- public boolean isRecentsTask() {
- return TopTaskTracker.isRecentsTask(mTopTask);
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // TODO(346588978): Update this to not make an assumption on a specific task info
+ return mTopGroupedTask != null
+ && mTopGroupedTask.getTaskInfo1().getActivityType() == ACTIVITY_TYPE_HOME;
+ } else {
+ return mTopTask != null && mTopTask.configuration.windowConfiguration
+ .getActivityType() == ACTIVITY_TYPE_HOME;
+ }
}
/**
- * Returns {@code true} if this task windowing mode is set to {@link
- * android.app.WindowConfiguration#WINDOWING_MODE_FREEFORM}
+ * Returns true if this represents the RECENTS activity type task
*/
- public boolean isFreeformTask() {
- return mTopTask != null && mTopTask.configuration.windowConfiguration.getWindowingMode()
- == WINDOWING_MODE_FREEFORM;
+ public boolean isRecentsTask() {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // TODO(346588978): Update this to not make an assumption on a specific task info
+ return mTopGroupedTask != null
+ && TopTaskTracker.isRecentsTask(mTopGroupedTask.getTaskInfo1());
+ } else {
+ return TopTaskTracker.isRecentsTask(mTopTask);
+ }
}
/**
@@ -303,43 +494,78 @@
* is loaded by the model
*/
public Task[] getPlaceholderTasks() {
- return mTopTask == null ? new Task[0]
- : new Task[]{Task.from(new TaskKey(mTopTask), mTopTask, false)};
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // TODO(346588978): Update this to return more than a single task once the callers
+ // are refactored
+ if (mVisibleTasks.isEmpty()) {
+ return new Task[0];
+ }
+ final TaskInfo info = mVisibleTasks.getFirst().getTaskInfo1();
+ return new Task[]{Task.from(new TaskKey(info), info, false)};
+ } else {
+ return mTopTask == null ? new Task[0]
+ : new Task[]{Task.from(new TaskKey(mTopTask), mTopTask, false)};
+ }
}
/**
* Returns {@link Task} array corresponding to the provided task ids which can be used as a
* placeholder until the true object is loaded by the model
*/
- public Task[] getPlaceholderTasks(int[] taskIds) {
- if (mTopTask == null) {
- return new Task[0];
- }
- Task[] result = new Task[taskIds.length];
- for (int i = 0; i < taskIds.length; i++) {
- final int index = i;
- int taskId = taskIds[i];
- mAllCachedTasks.forEach(rti -> {
- if (rti.taskId == taskId) {
- result[index] = Task.from(new TaskKey(rti), rti, false);
- }
- });
- }
- return result;
- }
+ public Task[] getSplitPlaceholderTasks(int[] taskIds) {
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ if (mVisibleTasks.isEmpty()
+ || mVisibleTasks.getFirst().getType() != TYPE_SPLIT) {
+ return new Task[0];
+ }
- @UserIdInt
- @Nullable
- public Integer getUserId() {
- return mTopTask == null ? null : mTopTask.userId;
+ GroupedTaskInfo splitTask = mVisibleTasks.getFirst();
+ Task[] result = new Task[taskIds.length];
+ for (int i = 0; i < taskIds.length; i++) {
+ TaskInfo info = splitTask.getTaskById(taskIds[i]);
+ if (info == null) {
+ Log.w(TAG, "Requested task (" + taskIds[i] + ") not found");
+ return new Task[0];
+ }
+ result[i] = Task.from(new TaskKey(info), info, false);
+ }
+ return result;
+ } else {
+ if (mTopTask == null) {
+ return new Task[0];
+ }
+ Task[] result = new Task[taskIds.length];
+ for (int i = 0; i < taskIds.length; i++) {
+ final int index = i;
+ int taskId = taskIds[i];
+ mAllCachedTasks.forEach(rti -> {
+ if (rti.taskId == taskId) {
+ result[index] = Task.from(new TaskKey(rti), rti, false);
+ }
+ });
+ }
+ return result;
+ }
}
@Nullable
public String getPackageName() {
- if (mTopTask == null || mTopTask.baseActivity == null) {
- return null;
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ // TODO(346588978): Update this to not make an assumption on a specific task info
+ if (mTopGroupedTask == null) {
+ return null;
+ }
+ final TaskInfo info = mTopGroupedTask.getTaskInfo1();
+ if (info.baseActivity == null) {
+ return null;
+ }
+ return info.baseActivity.getPackageName();
+ } else {
+ if (mTopTask == null || mTopTask.baseActivity == null) {
+ return null;
+ }
+ return mTopTask.baseActivity.getPackageName();
}
- return mTopTask.baseActivity.getPackageName();
}
}
}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index ad5720f..d38eaf3 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,7 +24,6 @@
import static com.android.launcher3.Flags.enableCursorHoverStates;
import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
-import static com.android.launcher3.Flags.useActivityOverlay;
import static com.android.launcher3.LauncherPrefs.backedUpItem;
import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
@@ -35,12 +34,12 @@
import static com.android.quickstep.GestureState.DEFAULT_STATE;
import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
+import static com.android.quickstep.InputConsumerUtils.newConsumer;
+import static com.android.quickstep.InputConsumerUtils.tryCreateAssistantInputConsumer;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
-import static com.android.wm.shell.Flags.enableBubblesLongPressNavHandle;
import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
@@ -71,7 +70,6 @@
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.MotionEvent;
-import android.view.View;
import androidx.annotation.BinderThread;
import androidx.annotation.MainThread;
@@ -106,21 +104,9 @@
import com.android.quickstep.OverviewCommandHelper.CommandType;
import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
-import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
-import com.android.quickstep.inputconsumers.AssistantInputConsumer;
import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
-import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
-import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer;
import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
-import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
-import com.android.quickstep.inputconsumers.OverviewInputConsumer;
-import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
-import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer;
import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
-import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
-import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
-import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer;
-import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.ActiveGestureLog.CompoundString;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
@@ -959,7 +945,8 @@
+ "consuming gesture for assistant action");
mGestureState =
createGestureState(mGestureState, getTrackpadGestureType(event));
- mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
+ mUncheckedConsumer = tryCreateAssistantInputConsumer(
+ this, mDeviceState, mInputMonitorCompat, mGestureState, event);
} else {
reasonString.append(" but event cannot trigger Assistant, "
+ "consuming gesture as no-op");
@@ -978,7 +965,23 @@
getTrackpadGestureType(event));
mConsumer.onConsumerAboutToBeSwitched();
mGestureState = newGestureState;
- mConsumer = newConsumer(prevGestureState, mGestureState, event);
+ mConsumer = newConsumer(
+ getBaseContext(),
+ this,
+ mResetGestureInputConsumer,
+ mOverviewComponentObserver,
+ mDeviceState,
+ prevGestureState,
+ mGestureState,
+ mTaskAnimationManager,
+ mInputMonitorCompat,
+ getSwipeUpHandlerFactory(),
+ this::onConsumerInactive,
+ mInputEventReceiver,
+ mTaskbarManager,
+ mSwipeUpProxyProvider,
+ mOverviewCommandHelper,
+ event);
mUncheckedConsumer = mConsumer;
} else if ((mDeviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
&& mDeviceState.canTriggerAssistantAction(event)) {
@@ -991,7 +994,8 @@
// Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
// should not interrupt it. QuickSwitch assumes that interruption can only
// happen if the next gesture is also quick switch.
- mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
+ mUncheckedConsumer = tryCreateAssistantInputConsumer(
+ this, mDeviceState, mInputMonitorCompat, mGestureState, event);
} else if (mDeviceState.canTriggerOneHandedAction(event)) {
reasonString.append("event can trigger one-handed action, "
+ "consuming gesture for one-handed action");
@@ -1073,28 +1077,6 @@
return event.isHoverEvent() && event.getSource() == InputDevice.SOURCE_MOUSE;
}
- private InputConsumer tryCreateAssistantInputConsumer(
- GestureState gestureState, MotionEvent motionEvent) {
- return tryCreateAssistantInputConsumer(
- InputConsumer.NO_OP, gestureState, motionEvent, CompoundString.NO_OP);
- }
-
- private InputConsumer tryCreateAssistantInputConsumer(
- InputConsumer base,
- GestureState gestureState,
- MotionEvent motionEvent,
- CompoundString reasonString) {
- if (mDeviceState.isGestureBlockedTask(gestureState.getRunningTask())) {
- reasonString.append(
- "%sis gesture-blocked task, using base input consumer", SUBSTRING_PREFIX);
- return base;
- } else {
- reasonString.append("%susing AssistantInputConsumer", SUBSTRING_PREFIX);
- return new AssistantInputConsumer(
- this, gestureState, base, mInputMonitorCompat, mDeviceState, motionEvent);
- }
- }
-
public GestureState createGestureState(GestureState previousGestureState,
GestureState.TrackpadGestureType trackpadGestureType) {
final GestureState gestureState;
@@ -1125,310 +1107,6 @@
return gestureState;
}
- private InputConsumer newConsumer(
- GestureState previousGestureState, GestureState newGestureState, MotionEvent event) {
- TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
- BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null;
- if (bubbleControllers != null && BubbleBarInputConsumer.isEventOnBubbles(tac, event)) {
- InputConsumer consumer = new BubbleBarInputConsumer(this, bubbleControllers,
- mInputMonitorCompat);
- logInputConsumerSelectionReason(consumer, newCompoundString(
- "event is on bubbles, creating new input consumer"));
- return consumer;
- }
- AnimatedFloat progressProxy = mSwipeUpProxyProvider.apply(mGestureState);
- if (progressProxy != null) {
- InputConsumer consumer = new ProgressDelegateInputConsumer(
- this, mTaskAnimationManager, mGestureState, mInputMonitorCompat, progressProxy);
-
- logInputConsumerSelectionReason(consumer, newCompoundString(
- "mSwipeUpProxyProvider has been set, using ProgressDelegateInputConsumer"));
-
- return consumer;
- }
-
- boolean canStartSystemGesture =
- mGestureState.isTrackpadGesture() ? mDeviceState.canStartTrackpadGesture()
- : mDeviceState.canStartSystemGesture();
-
- if (!LockedUserState.get(this).isUserUnlocked()) {
- CompoundString reasonString = newCompoundString("device locked");
- InputConsumer consumer;
- if (canStartSystemGesture) {
- // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
- // launched while device is locked even after exiting direct boot mode (e.g. camera).
- consumer = createDeviceLockedInputConsumer(
- newGestureState,
- reasonString.append("%scan start system gesture", SUBSTRING_PREFIX));
- } else {
- consumer = getDefaultInputConsumer(
- reasonString.append("%scannot start system gesture", SUBSTRING_PREFIX));
- }
- logInputConsumerSelectionReason(consumer, reasonString);
- return consumer;
- }
-
- CompoundString reasonString;
- InputConsumer base;
- // When there is an existing recents animation running, bypass systemState check as this is
- // a followup gesture and the first gesture started in a valid system state.
- if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning()) {
- reasonString = newCompoundString(canStartSystemGesture
- ? "can start system gesture, trying to use base consumer"
- : "recents animation was running, trying to use base consumer");
- base = newBaseConsumer(previousGestureState, newGestureState, event, reasonString);
- } else {
- reasonString = newCompoundString("cannot start system gesture and recents "
- + "animation was not running, trying to use default input consumer");
- base = getDefaultInputConsumer(reasonString);
- }
- if (mDeviceState.isGesturalNavMode() || newGestureState.isTrackpadGesture()) {
- handleOrientationSetup(base);
- }
- if (mDeviceState.isFullyGesturalNavMode() || newGestureState.isTrackpadGesture()) {
- String reasonPrefix =
- "device is in gesture navigation mode or 3-button mode with a trackpad gesture";
- if (mDeviceState.canTriggerAssistantAction(event)) {
- reasonString.append("%s%s%sgesture can trigger the assistant, "
- + "trying to use assistant input consumer",
- NEWLINE_PREFIX,
- reasonPrefix,
- SUBSTRING_PREFIX);
- base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString);
- }
-
- // If Taskbar is present, we listen for swipe or cursor hover events to unstash it.
- if (tac != null && !(base instanceof AssistantInputConsumer)) {
- // Present always on large screen or on small screen w/ flag
- boolean useTaskbarConsumer = tac.getDeviceProfile().isTaskbarPresent
- && !tac.isPhoneMode()
- && !tac.isInStashedLauncherState();
- if (canStartSystemGesture && useTaskbarConsumer) {
- reasonString.append("%s%s%sTaskbarActivityContext != null, "
- + "using TaskbarUnstashInputConsumer",
- NEWLINE_PREFIX,
- reasonPrefix,
- SUBSTRING_PREFIX);
- base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
- mOverviewCommandHelper, mGestureState);
- }
- }
- if (enableBubblesLongPressNavHandle()) {
- // Create bubbles input consumer before NavHandleLongPressInputConsumer.
- // This allows for nav handle to fall back to bubbles.
- if (mDeviceState.isBubblesExpanded()) {
- reasonString = newCompoundString(reasonPrefix).append(
- "%sbubbles expanded, trying to use default input consumer",
- SUBSTRING_PREFIX);
- // Bubbles can handle home gesture itself.
- base = getDefaultInputConsumer(reasonString);
- }
- }
-
- NavHandle navHandle = tac != null ? tac.getNavHandle()
- : SystemUiProxy.INSTANCE.get(this);
- if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
- && navHandle.canNavHandleBeLongPressed()
- && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
- reasonString.append("%s%s%sNot running recents animation, ",
- NEWLINE_PREFIX,
- reasonPrefix,
- SUBSTRING_PREFIX);
- if (tac != null && tac.getNavHandle().canNavHandleBeLongPressed()) {
- reasonString.append("stashed handle is long-pressable, ");
- }
- reasonString.append("using NavHandleLongPressInputConsumer");
- base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat,
- mDeviceState, navHandle, mGestureState);
- }
-
- if (!enableBubblesLongPressNavHandle()) {
- // Continue overriding nav handle input consumer with bubbles
- if (mDeviceState.isBubblesExpanded()) {
- reasonString = newCompoundString(reasonPrefix).append(
- "%sbubbles expanded, trying to use default input consumer",
- SUBSTRING_PREFIX);
- // Bubbles can handle home gesture itself.
- base = getDefaultInputConsumer(reasonString);
- }
- }
-
- if (mDeviceState.isSystemUiDialogShowing()) {
- reasonString = newCompoundString(reasonPrefix).append(
- "%ssystem dialog is showing, using SysUiOverlayInputConsumer",
- SUBSTRING_PREFIX);
- base = new SysUiOverlayInputConsumer(
- getBaseContext(), mDeviceState, mInputMonitorCompat);
- }
-
- if (mGestureState.isTrackpadGesture()
- && canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()) {
- reasonString = newCompoundString(reasonPrefix).append(
- "%sTrackpad 3-finger gesture, using TrackpadStatusBarInputConsumer",
- SUBSTRING_PREFIX);
- base = new TrackpadStatusBarInputConsumer(getBaseContext(), base,
- mInputMonitorCompat);
- }
-
- if (mDeviceState.isScreenPinningActive()) {
- reasonString = newCompoundString(reasonPrefix).append(
- "%sscreen pinning is active, using ScreenPinnedInputConsumer",
- SUBSTRING_PREFIX);
- // Note: we only allow accessibility to wrap this, and it replaces the previous
- // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
- base = new ScreenPinnedInputConsumer(this, newGestureState);
- }
-
- if (mDeviceState.canTriggerOneHandedAction(event)) {
- reasonString.append("%s%s%sgesture can trigger one handed mode, "
- + "using OneHandedModeInputConsumer",
- NEWLINE_PREFIX,
- reasonPrefix,
- SUBSTRING_PREFIX);
- base = new OneHandedModeInputConsumer(
- this, mDeviceState, base, mInputMonitorCompat);
- }
-
- if (mDeviceState.isAccessibilityMenuAvailable()) {
- reasonString.append(
- "%s%s%saccessibility menu is available, using AccessibilityInputConsumer",
- NEWLINE_PREFIX,
- reasonPrefix,
- SUBSTRING_PREFIX);
- base = new AccessibilityInputConsumer(
- this, mDeviceState, mGestureState, base, mInputMonitorCompat);
- }
- } else {
- String reasonPrefix = "device is not in gesture navigation mode";
- if (mDeviceState.isScreenPinningActive()) {
- reasonString = newCompoundString(reasonPrefix).append(
- "%sscreen pinning is active, trying to use default input consumer",
- SUBSTRING_PREFIX);
- base = getDefaultInputConsumer(reasonString);
- }
-
- if (mDeviceState.canTriggerOneHandedAction(event)) {
- reasonString.append("%s%s%sgesture can trigger one handed mode, "
- + "using OneHandedModeInputConsumer",
- NEWLINE_PREFIX,
- reasonPrefix,
- SUBSTRING_PREFIX);
- base = new OneHandedModeInputConsumer(
- this, mDeviceState, base, mInputMonitorCompat);
- }
- }
- logInputConsumerSelectionReason(base, reasonString);
- return base;
- }
-
- private CompoundString newCompoundString(String substring) {
- return new CompoundString("%s%s", NEWLINE_PREFIX, substring);
- }
-
- private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
- return Flags.ignoreThreeFingerTrackpadForNavHandleLongPress()
- && gestureState.isThreeFingerTrackpadGesture();
- }
-
- private void logInputConsumerSelectionReason(
- InputConsumer consumer, CompoundString reasonString) {
- ActiveGestureProtoLogProxy.logSetInputConsumer(consumer.getName(), reasonString.toString());
- if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
- ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER);
- }
- }
-
- private void handleOrientationSetup(InputConsumer baseInputConsumer) {
- baseInputConsumer.notifyOrientationSetup();
- }
-
- private InputConsumer newBaseConsumer(
- GestureState previousGestureState,
- GestureState gestureState,
- MotionEvent event,
- CompoundString reasonString) {
- if (mDeviceState.isKeyguardShowingOccluded()) {
- // This handles apps showing over the lockscreen (e.g. camera)
- return createDeviceLockedInputConsumer(gestureState, reasonString.append(
- "%skeyguard is showing occluded, trying to use device locked input consumer",
- SUBSTRING_PREFIX));
- }
-
- reasonString.append("%skeyguard is not showing occluded", SUBSTRING_PREFIX);
-
- TopTaskTracker.CachedTaskInfo runningTask = gestureState.getRunningTask();
- // Use overview input consumer for sharesheets on top of home.
- boolean forceOverviewInputConsumer = gestureState.getContainerInterface().isStarted()
- && runningTask != null
- && runningTask.isRootChooseActivity();
-
- // In the case where we are in an excluded, translucent overlay, ignore it and treat the
- // running activity as the task behind the overlay.
- TopTaskTracker.CachedTaskInfo otherVisibleTask = runningTask == null
- ? null
- : runningTask.getVisibleNonExcludedTask();
- if (otherVisibleTask != null) {
- ActiveGestureProtoLogProxy.logUpdateGestureStateRunningTask(
- otherVisibleTask.getPackageName(), runningTask.getPackageName());
- gestureState.updateRunningTask(otherVisibleTask);
- }
-
- boolean previousGestureAnimatedToLauncher =
- previousGestureState.isRunningAnimationToLauncher()
- || mDeviceState.isPredictiveBackToHomeInProgress();
- // with shell-transitions, home is resumed during recents animation, so
- // explicitly check against recents animation too.
- boolean launcherResumedThroughShellTransition =
- gestureState.getContainerInterface().isResumed()
- && !previousGestureState.isRecentsAnimationRunning();
- // If a task fragment within Launcher is resumed
- boolean launcherChildActivityResumed = useActivityOverlay()
- && runningTask != null
- && runningTask.isHomeTask()
- && mOverviewComponentObserver.isHomeAndOverviewSame()
- && !launcherResumedThroughShellTransition
- && !previousGestureState.isRecentsAnimationRunning();
-
- if (gestureState.getContainerInterface().isInLiveTileMode()) {
- return createOverviewInputConsumer(
- previousGestureState,
- gestureState,
- event,
- forceOverviewInputConsumer,
- reasonString.append(
- "%sis in live tile mode, trying to use overview input consumer",
- SUBSTRING_PREFIX));
- } else if (runningTask == null) {
- return getDefaultInputConsumer(reasonString.append(
- "%srunning task == null", SUBSTRING_PREFIX));
- } else if (previousGestureAnimatedToLauncher
- || launcherResumedThroughShellTransition
- || forceOverviewInputConsumer) {
- return createOverviewInputConsumer(
- previousGestureState,
- gestureState,
- event,
- forceOverviewInputConsumer,
- reasonString.append(previousGestureAnimatedToLauncher
- ? "%sprevious gesture animated to launcher, "
- + "trying to use overview input consumer"
- : (launcherResumedThroughShellTransition
- ? "%slauncher resumed through a shell transition, "
- + "trying to use overview input consumer"
- : "%sforceOverviewInputConsumer == true, "
- + "trying to use overview input consumer"),
- SUBSTRING_PREFIX));
- } else if (mDeviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) {
- return getDefaultInputConsumer(reasonString.append(launcherChildActivityResumed
- ? "%sis launcher child-task, trying to use default input consumer"
- : "%sis gesture-blocked task, trying to use default input consumer",
- SUBSTRING_PREFIX));
- } else {
- reasonString.append("%susing OtherActivityInputConsumer", SUBSTRING_PREFIX);
- return createOtherActivityInputConsumer(gestureState, event);
- }
- }
-
public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
boolean recentsInWindow =
Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
@@ -1437,80 +1115,6 @@
? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
}
- private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
- MotionEvent event) {
-
- final AbsSwipeUpHandler.Factory factory = getSwipeUpHandlerFactory();
- final boolean shouldDefer = !mOverviewComponentObserver.isHomeAndOverviewSame()
- || gestureState.getContainerInterface().deferStartingActivity(mDeviceState, event);
- final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
- return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager,
- gestureState, shouldDefer, this::onConsumerInactive,
- mInputMonitorCompat, mInputEventReceiver, disableHorizontalSwipe, factory);
- }
-
- private InputConsumer createDeviceLockedInputConsumer(
- GestureState gestureState, CompoundString reasonString) {
- if ((mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture())
- && gestureState.getRunningTask() != null) {
- reasonString.append("%sdevice is in gesture nav mode or 3-button mode with a trackpad "
- + "gesture and running task != null, using DeviceLockedInputConsumer",
- SUBSTRING_PREFIX);
- return new DeviceLockedInputConsumer(
- this, mDeviceState, mTaskAnimationManager, gestureState, mInputMonitorCompat);
- } else {
- return getDefaultInputConsumer(reasonString.append(
- mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture()
- ? "%srunning task == null, trying to use default input consumer"
- : "%sdevice is not in gesture nav mode and it's not a trackpad gesture,"
- + " trying to use default input consumer",
- SUBSTRING_PREFIX));
- }
- }
-
- public InputConsumer createOverviewInputConsumer(
- GestureState previousGestureState,
- GestureState gestureState,
- MotionEvent event,
- boolean forceOverviewInputConsumer,
- CompoundString reasonString) {
- RecentsViewContainer container = gestureState.getContainerInterface().getCreatedContainer();
- if (container == null) {
- return getDefaultInputConsumer(reasonString.append(
- "%sactivity == null, trying to use default input consumer", SUBSTRING_PREFIX));
- }
-
- View rootview = container.getRootView();
- boolean hasWindowFocus = rootview != null && rootview.hasWindowFocus();
- boolean isPreviousGestureAnimatingToLauncher =
- previousGestureState.isRunningAnimationToLauncher()
- || mDeviceState.isPredictiveBackToHomeInProgress();
- boolean isInLiveTileMode = gestureState.getContainerInterface().isInLiveTileMode();
-
- reasonString.append(hasWindowFocus
- ? "%sactivity has window focus"
- : (isPreviousGestureAnimatingToLauncher
- ? "%sprevious gesture is still animating to launcher"
- : isInLiveTileMode
- ? "%sdevice is in live mode"
- : "%sall overview focus conditions failed"), SUBSTRING_PREFIX);
- if (hasWindowFocus
- || isPreviousGestureAnimatingToLauncher
- || isInLiveTileMode) {
- reasonString.append(
- "%soverview should have focus, using OverviewInputConsumer", SUBSTRING_PREFIX);
- return new OverviewInputConsumer(gestureState, container, mInputMonitorCompat,
- false /* startingInActivityBounds */);
- } else {
- reasonString.append(
- "%soverview shouldn't have focus, using OverviewWithoutFocusInputConsumer",
- SUBSTRING_PREFIX);
- final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
- return new OverviewWithoutFocusInputConsumer(container.asContext(), mDeviceState,
- gestureState, mInputMonitorCompat, disableHorizontalSwipe);
- }
- }
-
/**
* To be called by the consumer when it's no longer active. This can be called by any consumer
* in the hierarchy at any point during the gesture (ie. if a delegate consumer starts
@@ -1672,6 +1276,7 @@
ContextualSearchStateManager.INSTANCE.get(this).dump("\t", pw);
SystemUiProxy.INSTANCE.get(this).dump(pw);
DeviceConfigWrapper.get().dump(" ", pw);
+ TopTaskTracker.INSTANCE.get(this).dump(pw);
}
private AbsSwipeUpHandler createLauncherSwipeHandler(
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
index 4f9d837..c1d3f6e 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
@@ -149,7 +149,8 @@
if (mActiveAnimationFactory != null) {
return;
}
- setHomeScaleAndAlpha(builder, app, mCurrentShift.value, 0);
+ setHomeScaleAndAlpha(builder, app, mCurrentShift.value,
+ Utilities.boundToRange(1 - mCurrentShift.value, 0, 1));
}
private void setHomeScaleAndAlpha(SurfaceProperties builder,
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 7388d59..1312aa4 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -409,8 +409,8 @@
);
} else {
// Tapped an app pair while in a single app
- int runningTaskId = topTaskTracker
- .getCachedTopTask(false /* filterOnlyVisibleRecents */).getTaskId();
+ final TopTaskTracker.CachedTaskInfo runningTask = topTaskTracker
+ .getCachedTopTask(false /* filterOnlyVisibleRecents */);
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
componentKeys,
@@ -418,10 +418,21 @@
foundTasks -> {
Task foundTask1 = foundTasks[0];
Task foundTask2 = foundTasks[1];
- boolean task1IsOnScreen =
- foundTask1 != null && foundTask1.getKey().getId() == runningTaskId;
- boolean task2IsOnScreen =
- foundTask2 != null && foundTask2.getKey().getId() == runningTaskId;
+ boolean task1IsOnScreen;
+ boolean task2IsOnScreen;
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ task1IsOnScreen = foundTask1 != null
+ && runningTask.topGroupedTaskContainsTask(
+ foundTask1.getKey().getId());
+ task2IsOnScreen = foundTask2 != null
+ && runningTask.topGroupedTaskContainsTask(
+ foundTask2.getKey().getId());
+ } else {
+ task1IsOnScreen = foundTask1 != null && foundTask1.getKey().getId()
+ == runningTask.getTaskId();
+ task2IsOnScreen = foundTask2 != null && foundTask2.getKey().getId()
+ == runningTask.getTaskId();
+ }
if (!task1IsOnScreen && !task2IsOnScreen) {
// Neither App A nor App B are on-screen, launch the app pair normally.
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index ea582c4..d35a36a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -163,6 +163,8 @@
*/
private Pair<InstanceId, com.android.launcher3.logging.InstanceId> mSessionInstanceIds;
+ private boolean mIsDestroyed = false;
+
private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
@Override
public boolean canHandleBack() {
@@ -199,6 +201,7 @@
public void onDestroy() {
mContainer = null;
+ mIsDestroyed = true;
mActivityBackCallback = null;
mAppPairsController.onDestroy();
mSplitSelectDataHolder.onDestroy();
@@ -744,7 +747,9 @@
*/
public void resetState() {
mSplitSelectDataHolder.resetState();
- mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
+ if (!mIsDestroyed) {
+ mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
+ }
dispatchOnSplitSelectionExit();
mRecentsAnimationRunning = false;
mLaunchingTaskView = null;
diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 997a842..12ca257 100644
--- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -49,6 +49,7 @@
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DynamicResource;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.plugins.ResourceProvider;
@@ -63,8 +64,7 @@
private static final int APP_CLOSE_ROW_START_DELAY_MS = 10;
// Should be used for animations running alongside this StaggeredWorkspaceAnim.
public static final int DURATION_MS = 250;
- public static final int DURATION_TASKBAR_MS =
- QuickstepTransitionManager.getTaskbarToHomeDuration();
+ private final int mTaskbarDurationInMs;
private static final float MAX_VELOCITY_PX_PER_S = 22f;
@@ -81,6 +81,8 @@
public StaggeredWorkspaceAnim(QuickstepLauncher launcher, float velocity,
boolean animateOverviewScrim, @Nullable View ignoredView, boolean staggerWorkspace) {
+ mTaskbarDurationInMs = QuickstepTransitionManager.getTaskbarToHomeDuration(
+ DisplayController.isPinnedTaskbar(launcher));
prepareToAnimate(launcher, animateOverviewScrim);
mIgnoredView = ignoredView;
@@ -93,7 +95,7 @@
.getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y);
DeviceProfile grid = launcher.getDeviceProfile();
- long duration = grid.isTaskbarPresent ? DURATION_TASKBAR_MS : DURATION_MS;
+ long duration = grid.isTaskbarPresent ? mTaskbarDurationInMs : DURATION_MS;
if (staggerWorkspace) {
Workspace<?> workspace = launcher.getWorkspace();
Hotseat hotseat = launcher.getHotseat();
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 3a4e328..6ab3e28 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -610,6 +610,8 @@
private int mKeyboardTaskFocusSnapAnimationDuration;
private int mKeyboardTaskFocusIndex = INVALID_PAGE;
+ private int[] mDismissPrimaryTranslations;
+
/**
* TODO: Call reloadIdNeeded in onTaskStackChanged.
*/
@@ -847,6 +849,8 @@
private final RecentsViewModelHelper mHelper;
private final RecentsViewUtils mUtils = new RecentsViewUtils();
+ private final Matrix mTmpMatrix = new Matrix();
+
public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
BaseContainerInterface sizeStrategy) {
super(context, attrs, defStyleAttr);
@@ -1416,7 +1420,7 @@
if (showAsGrid()) {
int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
- return isTaskViewWithinBounds(tv, screenStart, screenEnd);
+ return isTaskViewWithinBounds(tv, screenStart, screenEnd, /*taskViewTranslation=*/ 0);
} else {
// For now, just check if it's the active task or an adjacent task
return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
@@ -1463,14 +1467,28 @@
return clearAllScroll + (mIsRtl ? distance : -distance);
}
- private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
- int taskStart = getPagedOrientationHandler().getChildStart(tv)
- + (int) tv.getOffsetAdjustment(showAsGrid());
- int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(tv)
- * tv.getSizeAdjustment(showAsFullscreen()));
+ /*
+ * Returns if TaskView is within screen bounds defined in [screenStart, screenEnd].
+ *
+ * @param taskViewTranslation taskView is considered within bounds if either translated or
+ * original position of taskView is within screen bounds.
+ */
+ private boolean isTaskViewWithinBounds(TaskView taskView, int screenStart, int screenEnd,
+ int taskViewTranslation) {
+ int taskStart = getPagedOrientationHandler().getChildStart(taskView)
+ + (int) taskView.getOffsetAdjustment(showAsGrid());
+ int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(taskView)
+ * taskView.getSizeAdjustment(showAsFullscreen()));
int taskEnd = taskStart + taskSize;
- return (taskStart >= start && taskStart <= end) || (taskEnd >= start
- && taskEnd <= end);
+
+ int translatedTaskStart = taskStart + taskViewTranslation;
+ int translatedTaskEnd = taskEnd + taskViewTranslation;
+
+ taskStart = Math.min(taskStart, translatedTaskStart);
+ taskEnd = Math.max(taskEnd, translatedTaskEnd);
+
+ return (taskStart >= screenStart && taskStart <= screenEnd) || (taskEnd >= screenStart
+ && taskEnd <= screenEnd);
}
private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
@@ -1966,7 +1984,7 @@
// We try to avoid this because it can cause a scroll jump, but it is needed
// for cases where the running task isn't included in this load plan (e.g. if
// the current running task is excludedFromRecents.)
- showCurrentTask(mActiveGestureRunningTasks);
+ showCurrentTask(mActiveGestureRunningTasks, "applyLoadPlan");
} else {
setRunningTaskViewId(INVALID_TASK_ID);
}
@@ -2468,7 +2486,8 @@
}
boolean visible;
if (showAsGrid()) {
- visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
+ visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd,
+ mDismissPrimaryTranslations != null ? mDismissPrimaryTranslations[i] : 0);
} else {
visible = lower <= i && i <= upper;
}
@@ -2749,7 +2768,7 @@
updateSizeAndPadding();
}
- showCurrentTask(mActiveGestureRunningTasks);
+ showCurrentTask(mActiveGestureRunningTasks, "onGestureAnimationStart");
setEnableFreeScroll(false);
setEnableDrawingLiveTile(false);
setRunningTaskHidden(true);
@@ -2930,8 +2949,9 @@
* All subsequent calls to reload will keep the task as the first item until {@link #reset()}
* is called. Also scrolls the view to this task.
*/
- private void showCurrentTask(Task[] runningTasks) {
- Log.d(TAG, "showCurrentTask - runningTasks: " + Arrays.toString(runningTasks));
+ private void showCurrentTask(Task[] runningTasks, String caller) {
+ Log.d(TAG, "showCurrentTask(" + caller + ") - runningTasks: "
+ + Arrays.toString(runningTasks));
if (runningTasks.length == 0) {
return;
}
@@ -3834,6 +3854,7 @@
stagingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
: -newClearAllShortTotalWidthTranslation;
}
+ mDismissPrimaryTranslations = new int[taskCount];
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child == dismissedTaskView) {
@@ -3851,7 +3872,7 @@
Math.abs(i - dismissedIndex),
scrollDiff,
anim,
- splitTimings);
+ splitTimings, i);
needsCurveUpdates = true;
}
} else if (child instanceof TaskView taskView) {
@@ -3912,10 +3933,12 @@
primaryTranslation += mIsRtl ? stagingTranslation : -stagingTranslation;
if (primaryTranslation != 0) {
+ float finalTranslation = mIsRtl ? primaryTranslation : -primaryTranslation;
anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
- mIsRtl ? primaryTranslation : -primaryTranslation,
+ finalTranslation,
clampToProgress(dismissInterpolator, animationStartProgress,
animationEndProgress));
+ mDismissPrimaryTranslations[i] = (int) finalTranslation;
distanceFromDismissedTask++;
}
}
@@ -3934,7 +3957,7 @@
if (animateTaskView && dismissedTaskView != null) {
dismissedTaskView.setTranslationZ(0.1f);
}
-
+ loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
mPendingAnimation = anim;
final TaskView finalNextFocusedTaskView = nextFocusedTaskView;
final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll;
@@ -4152,6 +4175,7 @@
updateCurrentTaskActionsVisibility();
onDismissAnimationEnds();
mPendingAnimation = null;
+ mDismissPrimaryTranslations = null;
}
});
}
@@ -4190,7 +4214,8 @@
int indexDiff,
int scrollDiffPerPage,
PendingAnimation pendingAnimation,
- SplitAnimationTimings splitTimings) {
+ SplitAnimationTimings splitTimings,
+ int index) {
FloatProperty translationProperty = view instanceof TaskView
? ((TaskView) view).getPrimaryDismissTranslationProperty()
: getPagedOrientationHandler().getPrimaryViewTranslate();
@@ -4224,6 +4249,9 @@
)
);
+ if (view instanceof TaskView) {
+ mDismissPrimaryTranslations[index] = scrollDiffPerPage;
+ }
if (mEnableDrawingLiveTile && view instanceof TaskView
&& ((TaskView) view).isRunningTask()) {
pendingAnimation.addOnFrameCallback(() -> {
@@ -5807,6 +5835,14 @@
// mSyncTransactionApplier doesn't get transferred over
runActionOnRemoteHandles(remoteTargetHandle -> {
final TransformParams params = remoteTargetHandle.getTransformParams();
+ if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
+ params.setHomeBuilderProxy((builder, app, transformParams) -> {
+ mTmpMatrix.setScale(
+ 1f, 1f, app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
+ builder.setMatrix(mTmpMatrix).setAlpha(1f).setShow();
+ });
+ }
+
if (mSyncTransactionApplier != null) {
params.setSyncTransactionApplier(mSyncTransactionApplier);
params.getTargetSet().addReleaseCheck(mSyncTransactionApplier);
@@ -5843,15 +5879,22 @@
* Finish recents animation.
*/
public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete) {
- finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete);
+ finishRecentsAnimation(toRecents, false, true /* shouldPip */, onFinishComplete);
}
/**
+ * Finish recents animation.
+ */
+ public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
+ @Nullable Runnable onFinishComplete) {
+ finishRecentsAnimation(toRecents, shouldPip, false, onFinishComplete);
+ }
+ /**
* NOTE: Whatever value gets passed through to the toRecents param may need to also be set on
* {@link #mRecentsAnimationController#setWillFinishToHome}.
*/
public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
- @Nullable Runnable onFinishComplete) {
+ boolean allAppTargetsAreTranslucent, @Nullable Runnable onFinishComplete) {
Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: "
+ mRecentsAnimationController);
// TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
@@ -5883,7 +5926,7 @@
tx, null /* overlay */);
}
}
- mRecentsAnimationController.finish(toRecents, () -> {
+ mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> {
if (onFinishComplete != null) {
onFinishComplete.run();
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
index 71f4ef4..5e438bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
@@ -21,6 +21,7 @@
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION
import com.android.launcher3.R
import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED
import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
@@ -158,7 +159,7 @@
@Test
@TaskbarMode(PINNED)
fun testGetStashDuration_pinnedMode() {
- assertThat(stashController.stashDuration).isEqualTo(TASKBAR_STASH_DURATION)
+ assertThat(stashController.stashDuration).isEqualTo(PINNED_TASKBAR_TRANSITION_DURATION)
}
@Test
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 48f3fc2..582ea54 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
@@ -1123,7 +1123,7 @@
animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
// the flyout should now reverse and expand
- animatorTestRule.advanceTimeBy(100)
+ animatorTestRule.advanceTimeBy(400)
}
assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
@@ -1362,21 +1362,21 @@
private fun waitForFlyoutToShow() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(400)
}
assertThat(flyoutView).isNotNull()
}
private fun waitForFlyoutToHide() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(350)
}
assertThat(flyoutView).isNull()
}
private fun waitForFlyoutToFadeOutAndBackIn() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(500)
+ animatorTestRule.advanceTimeBy(750)
}
assertThat(flyoutView).isNotNull()
}
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 2997ac9..103c769 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
@@ -50,6 +50,9 @@
private var onLeft = true
private var flyoutTy = 50f
+ private val showAnimationDuration = 400L
+ private val hideAnimationDuration = 350L
+
@Before
fun setUp() {
flyoutContainer = FrameLayout(context)
@@ -118,7 +121,7 @@
assertThat(flyoutController.hasFlyout()).isTrue()
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.collapseFlyout {}
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
}
assertThat(flyoutContainer.childCount).isEqualTo(0)
assertThat(flyoutController.hasFlyout()).isFalse()
@@ -135,7 +138,7 @@
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
}
@@ -148,7 +151,7 @@
}
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(0)
}
@@ -159,7 +162,7 @@
setupAndShowFlyout()
assertThat(flyoutContainer.childCount).isEqualTo(1)
flyoutController.collapseFlyout {}
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
}
assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
}
@@ -172,7 +175,7 @@
val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
assertThat(flyoutView.alpha).isEqualTo(1f)
flyoutController.cancelFlyout {}
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
assertThat(flyoutView.alpha).isEqualTo(0f)
}
assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
@@ -185,7 +188,7 @@
assertThat(flyoutContainer.childCount).isEqualTo(1)
val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
assertThat(flyoutView.alpha).isEqualTo(1f)
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
flyoutView.performClick()
}
assertThat(flyoutCallbacks.flyoutClicked).isTrue()
@@ -221,7 +224,7 @@
fun updateFlyoutFullyExpanded() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
setupAndShowFlyout()
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutController.hasFlyout()).isTrue()
@@ -234,13 +237,13 @@
flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {}
// advance the timer so that the fade out animation plays
- animatorTestRule.advanceTimeBy(250)
+ animatorTestRule.advanceTimeBy(hideAnimationDuration)
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)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
assertThat(flyout.alpha).isEqualTo(1)
}
assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
@@ -250,7 +253,7 @@
fun updateFlyoutWhileCollapsing() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
setupAndShowFlyout()
- animatorTestRule.advanceTimeBy(300)
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
}
assertThat(flyoutController.hasFlyout()).isTrue()
@@ -265,9 +268,10 @@
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)
+ // the collapse and expand animations use an emphasized interpolator, so the reverse
+ // path does not take the same time. advance the timer the by full duration of the show
+ // animation to ensure it completes
+ animatorTestRule.advanceTimeBy(showAnimationDuration)
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)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
index b7ee6c4..f795ab1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -86,6 +86,20 @@
}
@Test
+ fun updateLauncherState_noBubbles_controllerNotified() {
+ // Given bubble bar has no bubbles
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+
+ // When switch to home screen
+ getInstrumentation().runOnMainSync {
+ persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+ }
+
+ // Then bubble bar view controller is notified
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false)
+ }
+
+ @Test
fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() {
// Given bubble bar is on home and has bubbles
whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index 64416dd..1bbd12a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -119,6 +119,20 @@
}
@Test
+ fun updateLauncherState_noBubbles_controllerNotified() {
+ // Given bubble bar has no bubbles
+ whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+
+ // When switch to home screen
+ getInstrumentation().runOnMainSync {
+ mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME
+ }
+
+ // Then bubble bar view controller is notified
+ verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false)
+ }
+
+ @Test
fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() {
// Given bubble bar is on home and has bubbles
whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
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 096f879..cd4e78b 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
@@ -37,6 +37,7 @@
import com.android.quickstep.AllAppsActionManager
import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType
+import java.util.Locale
import java.util.Optional
import org.junit.Assume.assumeTrue
import org.junit.rules.TestRule
@@ -119,6 +120,15 @@
}
}
+ if (description.getAnnotation(ForceRtl::class.java) != null) {
+ // Needs to be set on window context instead of sandbox context, because it does
+ // does not propagate between them. However, this change will impact created
+ // TaskbarActivityContext instances, since they wrap the window context.
+ taskbarManager.windowContext.resources.configuration.setLayoutDirection(
+ RTL_LOCALE
+ )
+ }
+
try {
TaskbarViewController.enableModelLoadingForTests(false)
@@ -191,4 +201,11 @@
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class NavBarKidsMode
+
+ /** Forces RTL UI for tests. */
+ @Retention(AnnotationRetention.RUNTIME)
+ @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+ annotation class ForceRtl
}
+
+private val RTL_LOCALE = Locale.of("ar", "XB")
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
index 7daa142..b8b0b5d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -19,11 +19,13 @@
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Utilities
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarKeyguardController
import com.android.launcher3.taskbar.TaskbarManager
import com.android.launcher3.taskbar.TaskbarStashController
import com.android.launcher3.taskbar.bubbles.BubbleBarController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
@@ -197,6 +199,14 @@
}
}
+ @Test
+ fun testForceRtlAnnotation_setsActivityContextLayoutDirection() {
+ @ForceRtl class Rtl
+ onSetup(description = Description.createSuiteDescription(Rtl::class.java)) {
+ assertThat(Utilities.isRtl(activityContext.resources)).isTrue()
+ }
+ }
+
/**
* Executes [runTest] after the [testRule] setup phase completes.
*
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index dc5223c..6b95f8d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -80,8 +80,6 @@
protected final Context mContext =
InstrumentationRegistry.getInstrumentation().getTargetContext();
- protected final RecentsAnimationDeviceState mRecentsAnimationDeviceState =
- new RecentsAnimationDeviceState(mContext, true);
protected final InputConsumerController mInputConsumerController =
InputConsumerController.getRecentsAnimationInputConsumer();
protected final ActivityManager.RunningTaskInfo mRunningTaskInfo =
@@ -114,6 +112,7 @@
new Bundle());
protected TaskAnimationManager mTaskAnimationManager;
+ protected RecentsAnimationDeviceState mRecentsAnimationDeviceState;
@Mock protected CONTAINER_INTERFACE mActivityInterface;
@Mock protected ContextInitListener<?> mContextInitListener;
@@ -176,6 +175,12 @@
}).when(recentsContainer).runOnBindToTouchInteractionService(any());
}
+ @Before
+ public void setUpRecentsAnimationDeviceState() {
+ runOnMainSync(() ->
+ mRecentsAnimationDeviceState = new RecentsAnimationDeviceState(mContext, true));
+ }
+
@Test
public void testInitWhenReady_registersActivityInitListener() {
String reasonString = "because i said so";
@@ -306,8 +311,6 @@
}
private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) {
- when(mActivityInterface.getOverviewWindowBounds(any(), any())).thenReturn(new Rect());
-
runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart(
mRecentsAnimationController, mRecentsAnimationTargets));
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index 541a48d..ee70e0a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
@@ -202,7 +202,7 @@
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -226,7 +226,7 @@
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -250,7 +250,7 @@
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -274,7 +274,7 @@
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -298,7 +298,7 @@
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -322,7 +322,7 @@
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -341,12 +341,16 @@
whenever(mockTaskKey1.getId()).thenReturn(1)
whenever(mockTaskKey2.getId()).thenReturn(2)
// ... with app 1 already on screen
- whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true)
+ } else {
+ whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+ }
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -365,12 +369,16 @@
whenever(mockTaskKey1.getId()).thenReturn(1)
whenever(mockTaskKey2.getId()).thenReturn(2)
// ... with app 2 already on screen
- whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true)
+ } else {
+ whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+ }
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -389,12 +397,16 @@
whenever(mockTaskKey1.getId()).thenReturn(1)
whenever(mockTaskKey2.getId()).thenReturn(2)
// ... with app 3 already on screen
- whenever(mockCachedTaskInfo.taskId).thenReturn(3)
+ if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+ whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(3))).thenReturn(true)
+ } else {
+ whenever(mockCachedTaskInfo.taskId).thenReturn(3)
+ }
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
spyAppPairsController.handleAppPairLaunchInApp(
mockAppPairIcon,
- listOf(mockItemInfo1, mockItemInfo2)
+ listOf(mockItemInfo1, mockItemInfo2),
)
verify(splitSelectStateController)
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index a8f39af..2fb08dd 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
@@ -16,6 +16,8 @@
package com.android.quickstep;
+import android.util.Log;
+
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
@@ -29,6 +31,8 @@
@RunWith(AndroidJUnit4.class)
public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest {
+ public static final String TAG = "TaplStartLauncherViaGestureTests";
+
static final int STRESS_REPEAT_COUNT = 10;
private enum TestCase {
@@ -69,7 +73,9 @@
}
private void runTest(TestCase testCase) {
+ long testStartTime = System.currentTimeMillis();
for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
+ long loopStartTime = System.currentTimeMillis();
// Destroy Launcher activity.
closeLauncherActivity();
@@ -84,7 +90,10 @@
default:
throw new IllegalStateException("Cannot run test case: " + testCase);
}
+ Log.d(TAG, "Loop " + (i + 1) + " runtime="
+ + (System.currentTimeMillis() - loopStartTime) + "ms");
}
+ Log.d(TAG, "Test runtime=" + (System.currentTimeMillis() - testStartTime) + "ms");
switch (testCase) {
case TO_OVERVIEW:
closeLauncherActivity();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 43ebb17..3c4f1d9 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -49,6 +49,7 @@
DISMISS(0),
LAUNCH_LAST_APP(0),
LAUNCH_SELECTED_APP(1),
+ DISMISS_WHEN_GOING_HOME(1),
LAUNCH_OVERVIEW(KeyboardQuickSwitchController.MAX_TASKS - 1);
private final int mNumAdditionalRunningTasks;
@@ -156,6 +157,11 @@
mLauncher.goHome().showQuickSwitchView().launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
}
+ @Test
+ public void testDismissedWhenGoingHome() {
+ runTest(TestSurface.LAUNCHED_APP, TestCase.DISMISS_WHEN_GOING_HOME);
+ }
+
private void runTest(@NonNull TestSurface testSurface, @NonNull TestCase testCase) {
for (int i = 0; i < testCase.mNumAdditionalRunningTasks; i++) {
startTestActivity(3 + i);
@@ -197,6 +203,9 @@
}
kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
break;
+ case DISMISS_WHEN_GOING_HOME:
+ kqs.dismissByGoingHome();
+ break;
case LAUNCH_OVERVIEW:
kqs.moveFocusBackward();
if (!testSurface.mInitialFocusAtZero) {
diff --git a/res/layout/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml
index d113a38..2bb2eb3 100644
--- a/res/layout/add_item_confirmation_activity.xml
+++ b/res/layout/add_item_confirmation_activity.xml
@@ -71,7 +71,8 @@
android:id="@+id/widget_preview_scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_marginVertical="16dp"
+ android:layout_margin="16dp"
+ android:background="@drawable/widgets_surface_background"
android:layout_weight="1">
<include
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
index 622f0d6..7c57726 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -81,6 +81,7 @@
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/widgets_surface_background"
+ android:clipToOutline="true"
android:orientation="vertical"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
android:visibility="gone">
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index 5427732..1ce1c55 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -64,6 +64,7 @@
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/widgets_surface_background"
+ android:clipToOutline="true"
android:orientation="vertical"
android:visibility="gone">
<include layout="@layout/widget_recommendations" />
diff --git a/res/layout/widgets_list_expand_button.xml b/res/layout/widgets_list_expand_button.xml
index 17c19ac..ff2d777 100644
--- a/res/layout/widgets_list_expand_button.xml
+++ b/res/layout/widgets_list_expand_button.xml
@@ -15,6 +15,7 @@
-->
<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widget_list_expand_button"
style="@style/Button.Rounded.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index 5dc1b47..cf090ad 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -133,6 +133,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widgets_surface_background"
+ android:clipToOutline="true"
android:orientation="vertical"
android:visibility="gone">
<include layout="@layout/widget_recommendations" />
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index 46f2d8a..e2f0e09 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -17,6 +17,7 @@
android:id="@+id/work_mode_toggle"
android:layout_height="@dimen/work_fab_height"
android:layout_width="wrap_content"
+ android:elevation="@dimen/work_fab_elevation"
android:minHeight="@dimen/work_fab_height"
android:gravity="center_vertical"
android:background="@drawable/work_mode_fab_background"
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 06f0eee..a891e39 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -38,16 +38,16 @@
<item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
<item name="materialColorOnErrorContainer">@color/system_on_error_container_dark</item>
<item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
- <item name="materialColorOnSurfaceInverse">@color/system_on_surface_light</item>
+ <item name="materialColorInverseOnSurface">@color/system_on_surface_light</item>
<item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
<item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
<item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
<item name="materialColorSecondaryContainer">@color/system_secondary_container_dark</item>
<item name="materialColorErrorContainer">@color/system_error_container_dark</item>
<item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
- <item name="materialColorPrimaryInverse">@color/system_primary_light</item>
+ <item name="materialColorInversePrimary">@color/system_primary_light</item>
<item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
- <item name="materialColorSurfaceInverse">@color/system_surface_light</item>
+ <item name="materialColorInverseSurface">@color/system_surface_light</item>
<item name="materialColorSurfaceVariant">@color/system_surface_variant_dark</item>
<item name="materialColorTertiaryContainer">@color/system_tertiary_container_dark</item>
<item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 77d789f..535d61f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -58,16 +58,16 @@
<attr name="materialColorSecondaryFixedDim" format="color" />
<attr name="materialColorOnErrorContainer" format="color" />
<attr name="materialColorOnSecondaryFixed" format="color" />
- <attr name="materialColorOnSurfaceInverse" format="color" />
+ <attr name="materialColorInverseOnSurface" format="color" />
<attr name="materialColorTertiaryFixedDim" format="color" />
<attr name="materialColorOnTertiaryFixed" format="color" />
<attr name="materialColorPrimaryFixedDim" format="color" />
<attr name="materialColorSecondaryContainer" format="color" />
<attr name="materialColorErrorContainer" format="color" />
<attr name="materialColorOnPrimaryFixed" format="color" />
- <attr name="materialColorPrimaryInverse" format="color" />
+ <attr name="materialColorInversePrimary" format="color" />
<attr name="materialColorSecondaryFixed" format="color" />
- <attr name="materialColorSurfaceInverse" format="color" />
+ <attr name="materialColorInverseSurface" format="color" />
<attr name="materialColorSurfaceVariant" format="color" />
<attr name="materialColorTertiaryContainer" format="color" />
<attr name="materialColorTertiaryFixed" format="color" />
@@ -210,10 +210,10 @@
<attr name="layout_sticky" format="boolean" />
</declare-styleable>
- <declare-styleable name="NumRows">
+ <declare-styleable name="GridDimension">
<attr name="minDeviceWidthPx" format="float"/>
<attr name="minDeviceHeightPx" format="float"/>
- <attr name="numRowsNew" format="integer"/>
+ <attr name="numGridDimension" format="integer"/>
<attr name="dbFile" />
<attr name="defaultLayoutId"/>
<attr name="demoModeLayoutId"/>
@@ -312,9 +312,10 @@
<attr name="rowCountSpecsId" format="reference" />
<!-- defaults to allAppsCellSpecsId, if not specified -->
<attr name="allAppsCellSpecsTwoPanelId" format="reference" />
-
<!-- defaults to false, if not specified -->
<attr name="isFixedLandscape" format="boolean" />
+ <!-- defaults to false, if not specified -->
+ <attr name="isOldGrid" format="boolean" />
<!-- By default all categories are enabled -->
<attr name="deviceCategory" format="integer">
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c31b4ee..0d4e79b 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -154,6 +154,7 @@
<!-- Floating action button inside work tab to toggle work profile -->
<dimen name="work_fab_height">56dp</dimen>
<dimen name="work_fab_radius">16dp</dimen>
+ <dimen name="work_fab_elevation">6dp</dimen>
<dimen name="work_fab_icon_size">24dp</dimen>
<dimen name="work_fab_icon_vertical_margin">16dp</dimen>
<dimen name="work_fab_icon_start_margin_expanded">4dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 123e2b8..c280307 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -47,6 +47,8 @@
<!-- Title for an option to open a new window for a given app -->
<string name="new_window_option_taskbar">New Window</string>
+ <!-- Title for an option to manage open windows for a given app -->
+ <string name="manage_windows_option_taskbar">Manage Windows</string>
<!-- App pairs -->
<string name="save_app_pair">Save app pair</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6d3579b..1c70d6c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -41,16 +41,16 @@
<item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
<item name="materialColorOnErrorContainer">@color/system_on_error_container_light</item>
<item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
- <item name="materialColorOnSurfaceInverse">@color/system_on_surface_dark</item>
+ <item name="materialColorInverseOnSurface">@color/system_on_surface_dark</item>
<item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
<item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
<item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
<item name="materialColorSecondaryContainer">@color/system_secondary_container_light</item>
<item name="materialColorErrorContainer">@color/system_error_container_light</item>
<item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
- <item name="materialColorPrimaryInverse">@color/system_primary_dark</item>
+ <item name="materialColorInversePrimary">@color/system_primary_dark</item>
<item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
- <item name="materialColorSurfaceInverse">@color/system_surface_dark</item>
+ <item name="materialColorInverseSurface">@color/system_surface_dark</item>
<item name="materialColorSurfaceVariant">@color/system_surface_variant_light</item>
<item name="materialColorTertiaryContainer">@color/system_tertiary_container_light</item>
<item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 34cf56b..817cc40 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -20,6 +20,7 @@
import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
import static android.text.Layout.Alignment.ALIGN_NORMAL;
+import static com.android.launcher3.Flags.enableContrastTiles;
import static com.android.launcher3.Flags.enableCursorHoverStates;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
@@ -39,6 +40,7 @@
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.icu.text.MessageFormat;
@@ -125,6 +127,8 @@
private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
+ private static final int APP_PILL_TITLE_PADDING = 8;
+
private float mScaleForReorderBounce = 1f;
private IntArray mBreakPointsIntArray;
@@ -722,6 +726,34 @@
}
}
+ /** Draws a background behind the App Title label when required. **/
+ public void drawAppContrastTile(Canvas canvas) {
+ RectF appTitleBounds;
+ Paint.FontMetrics fm = getPaint().getFontMetrics();
+ Rect tmpRect = new Rect();
+ getDrawingRect(tmpRect);
+ CharSequence text = getText();
+
+ float titleLength = (getPaint().measureText(text, 0, text.length())
+ + APP_PILL_TITLE_PADDING * 2);
+ titleLength = Math.min(titleLength, tmpRect.width());
+ appTitleBounds = new RectF((tmpRect.width() - titleLength) / 2.f - getCompoundPaddingLeft(),
+ 0, (tmpRect.width() + titleLength) / 2.f + getCompoundPaddingRight(),
+ (int) Math.ceil(fm.bottom - fm.top));
+
+
+ if (mIcon != null) {
+ Rect iconBounds = new Rect();
+ getIconBounds(iconBounds);
+ int textStart = iconBounds.bottom + getCompoundDrawablePadding();
+ appTitleBounds.offset(0, textStart);
+ }
+
+ canvas.drawRoundRect(appTitleBounds, appTitleBounds.height() / 2,
+ appTitleBounds.height() / 2,
+ PillColorProvider.getInstance(getContext()).getAppTitlePillPaint());
+ }
+
/** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
@@ -826,6 +858,11 @@
setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
getPaddingBottom());
}
+ if (shouldDrawAppContrastTile()) {
+ setPadding(getPaddingLeft() + APP_PILL_TITLE_PADDING, getPaddingTop(),
+ getPaddingRight() + APP_PILL_TITLE_PADDING,
+ getPaddingBottom());
+ }
// Only apply two line for all_apps and device search only if necessary.
if (shouldUseTwoLine() && (mLastOriginalText != null)) {
int allowedVerticalSpace = height - getPaddingTop() - getPaddingBottom()
@@ -909,7 +946,9 @@
@Override
public void setTextColor(ColorStateList colors) {
- mTextColor = colors.getDefaultColor();
+ mTextColor = shouldDrawAppContrastTile() ? PillColorProvider.getInstance(
+ getContext()).getAppTitleTextPaint().getColor()
+ : colors.getDefaultColor();
mTextColorStateList = colors;
if (Float.compare(mTextAlpha, 1) == 0) {
super.setTextColor(colors);
@@ -926,6 +965,15 @@
&& info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
}
+ /**
+ * Whether or not an App title contrast tile should be drawn for this element.
+ **/
+ public boolean shouldDrawAppContrastTile() {
+ return mDisplay == DISPLAY_WORKSPACE && shouldTextBeVisible()
+ && PillColorProvider.getInstance(getContext()).isMatchaEnabled()
+ && enableContrastTiles();
+ }
+
public void setTextVisibility(boolean visible) {
setTextAlpha(visible ? 1 : 0);
}
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 3d715e5..867bf98 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -41,8 +41,11 @@
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.MSDLPlayerWrapper;
import com.android.launcher3.views.ActivityContext;
+import com.google.android.msdl.data.model.MSDLToken;
+
/**
* Implements a DropTarget.
*/
@@ -62,6 +65,7 @@
protected final ActivityContext mActivityContext;
protected final DropTargetHandler mDropTargetHandler;
protected DropTargetBar mDropTargetBar;
+ private final MSDLPlayerWrapper mMSDLPlayerWrapper;
/** Whether this drop target is active for the current drag */
protected boolean mActive;
@@ -94,6 +98,7 @@
super(context, attrs, defStyle);
mActivityContext = ActivityContext.lookupContext(context);
mDropTargetHandler = mActivityContext.getDropTargetHandler();
+ mMSDLPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context);
Resources resources = getResources();
mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold);
@@ -142,6 +147,10 @@
@Override
public final void onDragEnter(DragObject d) {
+ // Perform Haptic feedback
+ if (Flags.msdlFeedback()) {
+ mMSDLPlayerWrapper.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR);
+ }
if (!mAccessibleDrag && !mTextVisible) {
// Show tooltip
hideTooltip();
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 78535a1..09225e7 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -1833,7 +1833,8 @@
workspacePageIndicatorHeight - mWorkspacePageIndicatorOverlapWorkspace;
}
int paddingTop = workspaceTopPadding + (mIsScalableGrid ? 0 : edgeMarginPx);
- int paddingSide = desiredWorkspaceHorizontalMarginPx;
+ // On isFixedLandscapeMode on phones we already have padding because of the camera hole
+ int paddingSide = inv.isFixedLandscapeMode ? 0 : desiredWorkspaceHorizontalMarginPx;
padding.set(paddingSide, paddingTop, paddingSide, paddingBottom);
}
@@ -1941,10 +1942,8 @@
startSpacing += getAdditionalQsbSpace();
if (inv.isFixedLandscapeMode) {
- endSpacing += workspacePadding.right + cellLayoutPaddingPx.right
- + mInsets.right;
- startSpacing += workspacePadding.left + cellLayoutPaddingPx.left
- + mInsets.left;
+ endSpacing += mInsets.right;
+ startSpacing += mInsets.left;
}
hotseatBarPadding.top = hotseatBarTopPadding;
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 6468f74..b2ccba4 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -187,22 +187,20 @@
public void adjustForBubbleBar(boolean isBubbleBarVisible) {
DeviceProfile dp = mActivity.getDeviceProfile();
float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
- boolean adjustmentRequired = Float.compare(adjustedBorderSpace, 0f) != 0;
-
+ boolean shouldAdjustHotseat = isBubbleBarVisible
+ && Float.compare(adjustedBorderSpace, 0f) != 0;
ShortcutAndWidgetContainer icons = getShortcutsAndWidgets();
// update the translation provider for future layout passes of hotseat icons.
- if (adjustmentRequired && isBubbleBarVisible) {
+ if (shouldAdjustHotseat) {
icons.setTranslationProvider(
cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX));
} else {
icons.setTranslationProvider(null);
}
- if (!adjustmentRequired) return;
-
AnimatorSet animatorSet = new AnimatorSet();
for (int i = 0; i < icons.getChildCount(); i++) {
View child = icons.getChildAt(i);
- float tx = isBubbleBarVisible ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0;
+ float tx = shouldAdjustHotseat ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0;
if (child instanceof Reorderable) {
MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate();
animatorSet.play(
@@ -213,8 +211,8 @@
}
if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) {
final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets();
- final float targetInsetFraction =
- isBubbleBarVisible ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0;
+ final float targetInsetFraction = shouldAdjustHotseat
+ ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0;
ValueAnimator qsbAnimator =
ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction);
qsbAnimator.addUpdateListener(animation -> {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 04e4b57..e1d84be 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -651,14 +651,14 @@
}
/**
- * Parses through the xml to find NumRows specs. Then calls findBestRowCount to get the correct
- * row count for this GridOption.
+ * Parses through the xml to find GridDimension specs. Then calls findBestRowCount to get the
+ * correct row count for this GridOption.
*
* @return the result of {@link #findBestRowCount(List, Info)}.
*/
- public static NumRows getRowCount(ResourceHelper resourceHelper, Context context,
+ public static GridDimension getRowCount(ResourceHelper resourceHelper, Context context,
Info displayInfo) {
- ArrayList<NumRows> rowCounts = new ArrayList<>();
+ ArrayList<GridDimension> rowCounts = new ArrayList<>();
try (XmlResourceParser parser = resourceHelper.getXml()) {
final int depth = parser.getDepth();
@@ -666,8 +666,8 @@
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)));
+ && "GridDimension".equals(parser.getName())) {
+ rowCounts.add(new GridDimension(context, Xml.asAttributeSet(parser)));
}
}
} catch (IOException | XmlPullParserException e) {
@@ -678,10 +678,10 @@
}
/**
- * @return the biggest row count that fits the display dimensions spec using NumRows to
+ * @return the biggest row count that fits the display dimensions spec using GridDimension to
* determine that. If no best row count is found, return -1.
*/
- public static NumRows findBestRowCount(List<NumRows> list, Info displayInfo) {
+ public static GridDimension findBestRowCount(List<GridDimension> list, Info displayInfo) {
int minWidthPx = Integer.MAX_VALUE;
int minHeightPx = Integer.MAX_VALUE;
for (WindowBounds bounds : displayInfo.supportedBounds) {
@@ -700,10 +700,10 @@
}
}
- NumRows selectedRow = null;
- for (NumRows item: list) {
+ GridDimension selectedRow = null;
+ for (GridDimension item: list) {
if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
- if (selectedRow == null || selectedRow.mNumRowsNew < item.mNumRowsNew) {
+ if (selectedRow == null || selectedRow.mNumGridDimension < item.mNumGridDimension) {
selectedRow = item;
}
}
@@ -1031,6 +1031,7 @@
private final int mAllAppsCellSpecsTwoPanelId;
private final int mRowCountSpecsId;
private final boolean mIsFixedLandscape;
+ private final boolean mIsOldGrid;
public GridOption(Context context, AttributeSet attrs, Info displayInfo) {
TypedArray a = context.obtainStyledAttributes(
@@ -1044,8 +1045,8 @@
mIsDualGrid = a.getBoolean(R.styleable.GridDisplayOption_isDualGrid, false);
if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
ResourceHelper resourceHelper = new ResourceHelper(context, mRowCountSpecsId);
- NumRows numR = getRowCount(resourceHelper, context, displayInfo);
- numRows = numR.mNumRowsNew;
+ GridDimension numR = getRowCount(resourceHelper, context, displayInfo);
+ numRows = numR.mNumGridDimension;
dbFile = numR.mDbFile;
defaultLayoutId = numR.mDefaultLayoutId;
demoModeLayoutId = numR.mDemoModeLayoutId;
@@ -1175,6 +1176,7 @@
}
mIsFixedLandscape = a.getBoolean(R.styleable.GridDisplayOption_isFixedLandscape, false);
+ mIsOldGrid = a.getBoolean(R.styleable.GridDisplayOption_isOldGrid, false);
int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
DONT_INLINE_QSB);
@@ -1206,25 +1208,35 @@
}
}
- public boolean isNewGridOption() {
- return mRowCountSpecsId != INVALID_RESOURCE_HANDLE;
- }
-
+ /**
+ * Returns true if the grid option should be used given the flags that are toggled on/off.
+ */
public boolean filterByFlag(int deviceType, boolean isFixedLandscape) {
if (deviceType == TYPE_TABLET) {
return Flags.oneGridRotationHandling() == mIsDualGrid;
}
- if (isFixedLandscape) {
- return Flags.oneGridSpecs() && mIsFixedLandscape;
+ // Here we return true if fixed landscape mode should be on.
+ if (mIsFixedLandscape || isFixedLandscape) {
+ return mIsFixedLandscape && isFixedLandscape && Flags.oneGridSpecs();
}
- return ((Flags.oneGridSpecs() == isNewGridOption()) && !mIsFixedLandscape);
+ // Here we return true if we want to show the new grids.
+ if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
+ return Flags.oneGridSpecs();
+ }
+
+ // Here we return true if we want to show the old grids.
+ if (mIsOldGrid) {
+ return !Flags.oneGridSpecs();
+ }
+
+ return true;
}
}
- public static final class NumRows {
- final int mNumRowsNew;
+ public static final class GridDimension {
+ final int mNumGridDimension;
final float mMinDeviceWidthPx;
final float mMinDeviceHeightPx;
final String mDbFile;
@@ -1232,17 +1244,17 @@
final int mDemoModeLayoutId;
- NumRows(Context context, AttributeSet attrs) {
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumRows);
+ GridDimension(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridDimension);
- 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);
+ mNumGridDimension = (int) a.getFloat(R.styleable.GridDimension_numGridDimension, 0);
+ mMinDeviceWidthPx = a.getFloat(R.styleable.GridDimension_minDeviceWidthPx, 0);
+ mMinDeviceHeightPx = a.getFloat(R.styleable.GridDimension_minDeviceHeightPx, 0);
+ mDbFile = a.getString(R.styleable.GridDimension_dbFile);
mDefaultLayoutId = a.getResourceId(
- R.styleable.NumRows_defaultLayoutId, 0);
+ R.styleable.GridDimension_defaultLayoutId, 0);
mDemoModeLayoutId = a.getResourceId(
- R.styleable.NumRows_demoModeLayoutId, mDefaultLayoutId);
+ R.styleable.GridDimension_demoModeLayoutId, mDefaultLayoutId);
a.recycle();
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 6145077..74dd971 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -537,6 +537,7 @@
mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
mWidgetPickerDataProvider = new WidgetPickerDataProvider();
+ PillColorProvider.getInstance(mWorkspace.getContext()).registerObserver();
boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
if (internalStateHandled) {
@@ -781,6 +782,11 @@
if (!com.android.launcher3.Flags.oneGridSpecs()) {
return;
}
+ // When the flag oneGridSpecs is on we want to disable ALLOW_ROTATION which is replaced
+ // by FIXED_LANDSCAPE_MODE, ALLOW_ROTATION will only be used on Tablets afterwards.
+ if (!getDeviceProfile().isTablet) {
+ LauncherPrefs.get(this).put(LauncherPrefs.ALLOW_ROTATION, false);
+ }
getRotationHelper().setFixedLandscape(
Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscapeMode
);
@@ -1813,6 +1819,7 @@
// changes while launcher is still loading.
getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener);
mOverlayManager.onActivityDestroyed();
+ PillColorProvider.getInstance(mWorkspace.getContext()).unregisterObserver();
}
public LauncherAccessibilityDelegate getAccessibilityDelegate() {
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index df75470..a5b8168 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -24,7 +24,7 @@
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";
- public static final String LAUNCHER_7_BY_3_DB = "launcher_7_by_3.db";
+ public static final String LAUNCHER_8_BY_3_DB = "launcher_8_by_3.db";
public static final String BACKUP_DB = "backup.db";
public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs";
public static final String MANAGED_USER_PREFERENCES_KEY =
@@ -45,7 +45,7 @@
LAUNCHER_4_BY_4_DB,
LAUNCHER_3_BY_3_DB,
LAUNCHER_2_BY_2_DB,
- LAUNCHER_7_BY_3_DB));
+ LAUNCHER_8_BY_3_DB));
public static final List<String> OTHER_FILES = Collections.unmodifiableList(Arrays.asList(
BACKUP_DB,
diff --git a/src/com/android/launcher3/PillColorPorovider.kt b/src/com/android/launcher3/PillColorPorovider.kt
new file mode 100644
index 0000000..347c5d6
--- /dev/null
+++ b/src/com/android/launcher3/PillColorPorovider.kt
@@ -0,0 +1,85 @@
+/*
+ * 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
+
+import android.content.Context
+import android.database.ContentObserver
+import android.graphics.Paint
+import android.net.Uri
+import android.provider.Settings
+import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
+
+class PillColorProvider private constructor(c: Context) {
+ private val context = c.applicationContext
+
+ private val matchaUri by lazy { Settings.Secure.getUriFor(MATCHA_SETTING) }
+ var appTitlePillPaint = Paint()
+ private set
+
+ var appTitleTextPaint = Paint()
+ private set
+
+ private var isMatchaEnabledInternal = 0
+
+ var isMatchaEnabled = isMatchaEnabledInternal != 0
+
+ private val pillColorObserver =
+ object : ContentObserver(ORDERED_BG_EXECUTOR.handler) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ if (uri == matchaUri) {
+ isMatchaEnabledInternal =
+ Settings.Secure.getInt(context.contentResolver, MATCHA_SETTING, 0)
+ isMatchaEnabled = isMatchaEnabledInternal != 0
+ }
+ }
+ }
+
+ fun registerObserver() {
+ context.contentResolver.registerContentObserver(matchaUri, false, pillColorObserver)
+ setup()
+ }
+
+ fun unregisterObserver() {
+ context.contentResolver.unregisterContentObserver(pillColorObserver)
+ }
+
+ fun setup() {
+ appTitlePillPaint.color =
+ context.resources.getColor(
+ R.color.material_color_surface_container_lowest,
+ context.theme,
+ )
+ appTitleTextPaint.color =
+ context.resources.getColor(R.color.material_color_on_surface, context.theme)
+ isMatchaEnabledInternal = Settings.Secure.getInt(context.contentResolver, MATCHA_SETTING, 0)
+ isMatchaEnabled = isMatchaEnabledInternal != 0
+ }
+
+ companion object {
+ private var INSTANCE: PillColorProvider? = null
+ private const val MATCHA_SETTING = "matcha_enable"
+
+ // TODO: Replace with a Dagger injection that is a singleton.
+ @JvmStatic
+ fun getInstance(context: Context): PillColorProvider {
+ if (INSTANCE == null) {
+ INSTANCE = PillColorProvider(context)
+ }
+ return INSTANCE!!
+ }
+ }
+}
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index b3cb948..f4d3146 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -303,10 +303,11 @@
.setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName()))
.putExtra(Intent.EXTRA_USER, info.user);
context.startActivity(i);
- FileLog.d(TAG, "start uninstall activity " + cn.getPackageName());
+ FileLog.d(TAG, "start uninstall activity from drop target " + cn.getPackageName());
return cn;
} catch (URISyntaxException e) {
- Log.e(TAG, "Failed to parse intent to start uninstall activity for item=" + info);
+ Log.e(TAG, "Failed to parse intent to start drop target uninstall activity for"
+ + " item=" + info);
return null;
}
}
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index 6168e41..ea5eb8f 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -73,8 +73,9 @@
|| alreadyAddedPromiseIcon) {
FileLog.d(LOG,
String.format(Locale.ENGLISH,
- "Removing PromiseIcon for package: %s, install reason: %d,"
- + " alreadyAddedPromiseIcon: %s",
+ "Removing unneeded PromiseIcon for package: %s"
+ + ", install reason: %d,"
+ + " alreadyAddedPromiseIcon: %s",
info.getAppPackageName(),
info.getInstallReason(),
alreadyAddedPromiseIcon
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 0e9c861..95dbf5f 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -16,6 +16,7 @@
package com.android.launcher3;
+import static com.android.launcher3.AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME;
import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
@@ -1222,6 +1223,10 @@
}
protected void onPageBeginTransition() {
+ // Widget resize frame doesn't receive events to close when talkback is enabled. For that
+ // case, close it here.
+ AbstractFloatingView.closeOpenViews(mLauncher, false, TYPE_WIDGET_RESIZE_FRAME);
+
super.onPageBeginTransition();
updateChildrenLayersEnabled();
}
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 1b58987..c938482 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -28,6 +28,7 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.ALL_APPS_SCROLLER;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -1173,8 +1174,10 @@
super.dispatchDraw(canvas);
if (mNavBarScrimHeight > 0) {
- canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
- mNavBarScrimPaint);
+ float left = (getWidth() - getWidth() / getScaleX()) / 2;
+ float top = getHeight() / 2f + (getHeight() / 2f - mNavBarScrimHeight) / getScaleY();
+ canvas.drawRect(left, top, getWidth() / getScaleX(),
+ top + mNavBarScrimHeight / getScaleY(), mNavBarScrimPaint);
}
}
@@ -1340,6 +1343,17 @@
invalidateHeader();
}
+ @Override
+ public void setScaleY(float scaleY) {
+ super.setScaleY(scaleY);
+ if (predictiveBackThreeButtonNav() && mNavBarScrimHeight > 0) {
+ // Call invalidate to prevent navbar scrim from scaling. The navbar scrim is drawn
+ // directly onto the canvas. To prevent it from being scaled with the canvas, there's a
+ // counter scale applied in dispatchDraw.
+ invalidate(20, getHeight() - mNavBarScrimHeight, getWidth(), getHeight());
+ }
+ }
+
/**
* Set {@link Animator.AnimatorListener} on {@link mAllAppsTransitionController} to observe
* animation of backing out of all apps search view to all apps view.
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 21dce14..609edd2 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -45,6 +45,7 @@
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
+import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
@@ -220,6 +221,7 @@
* when animation is not running.
*/
public void reset() {
+ Trace.beginSection("PrivateProfileManager#reset");
// Ensure the state of the header view is what it should be before animating.
updateView();
getMainRecyclerView().setChildAttachedConsumer(null);
@@ -239,6 +241,7 @@
executeLock();
}
addPrivateSpaceDecorator(updatedState);
+ Trace.endSection();
}
/** Returns whether or not Private Space Settings Page is available. */
@@ -293,31 +296,12 @@
}
}
- @Override
public void setQuietMode(boolean enable) {
- UI_HELPER_EXECUTOR.post(() ->
- mUserCache.getUserProfiles()
- .stream()
- .filter(getUserMatcher())
- .findFirst()
- .ifPresent(userHandle -> setQuietModeSafely(enable, userHandle)));
+ setQuietMode(enable, mAllApps.mActivityContext);
mReadyToAnimate = true;
}
/**
- * Sets Quiet Mode for Private Profile.
- * If {@link SecurityException} is thrown, prompts the user to set this launcher as HOME app.
- */
- private void setQuietModeSafely(boolean enable, UserHandle userHandle) {
- try {
- mUserManager.requestQuietModeEnabled(enable, userHandle);
- } catch (SecurityException ex) {
- ApiWrapper.INSTANCE.get(mAllApps.mActivityContext)
- .assignDefaultHomeRole(mAllApps.mActivityContext);
- }
- }
-
- /**
* Expand the private space after the app list has been added and updated from
* {@link AlphabeticalAppsList#onAppsUpdated()}
*/
@@ -331,7 +315,9 @@
/** Collapses the private space before the app list has been updated. */
void executeLock() {
+ Trace.beginSection("PrivateProfileManager#executeLock");
MAIN_EXECUTOR.execute(() -> updatePrivateStateAnimator(false));
+ Trace.endSection();
}
void setAnimationRunning(boolean isAnimationRunning) {
@@ -378,6 +364,7 @@
if (mPSHeader == null) {
return;
}
+ Trace.beginSection("PrivateProfileManager#updateView");
Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: "
+ getCurrentState());
mPSHeader.setAlpha(1);
@@ -436,6 +423,7 @@
}
}
mPSHeader.invalidate();
+ Trace.endSection();
}
/** Sets the enablement of the profile when header or button is clicked. */
@@ -840,6 +828,7 @@
ActivityAllAppsContainerView<?>.AdapterHolder mainAdapterHolder = mAllApps.mAH.get(MAIN);
List<BaseAllAppsAdapter.AdapterItem> adapterItems =
mainAdapterHolder.mAppsList.getAdapterItems();
+ Trace.beginSection("PrivateProfileManager#expandPrivateSpace");
if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()
&& mAllApps.isPersonalTab()) {
// Animate the text and settings icon.
@@ -849,6 +838,7 @@
getPsHeaderHeight(), deviceProfile.allAppsCellHeightPx);
updatePrivateStateAnimator(true);
}
+ Trace.endSection();
}
private void exitSearchAndExpand() {
diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java
index 93b6b29..765c29c 100644
--- a/src/com/android/launcher3/allapps/UserProfileManager.java
+++ b/src/com/android/launcher3/allapps/UserProfileManager.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import android.content.Context;
import android.os.UserHandle;
import android.os.UserManager;
@@ -26,6 +27,7 @@
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ApiWrapper;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -69,14 +71,26 @@
}
/** Sets quiet mode as enabled/disabled for the profile type. */
- protected void setQuietMode(boolean enabled) {
+ protected void setQuietMode(boolean enabled, Context context) {
UI_HELPER_EXECUTOR.post(() ->
mUserCache.getUserProfiles()
.stream()
.filter(getUserMatcher())
.findFirst()
.ifPresent(userHandle ->
- mUserManager.requestQuietModeEnabled(enabled, userHandle)));
+ setQuietModeSafely(enabled, userHandle, context)));
+ }
+
+ /**
+ * Sets Quiet Mode for Private Profile.
+ * If {@link SecurityException} is thrown, prompts the user to set this launcher as HOME app.
+ */
+ private void setQuietModeSafely(boolean enable, UserHandle userHandle, Context context) {
+ try {
+ mUserManager.requestQuietModeEnabled(enable, userHandle);
+ } catch (SecurityException ex) {
+ ApiWrapper.INSTANCE.get(context).assignDefaultHomeRole(context);
+ }
}
/** Sets current state for the profile type. */
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 3d0c1d0..6ebab5a 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -74,7 +74,7 @@
*/
public void setWorkProfileEnabled(boolean enabled) {
updateCurrentState(STATE_TRANSITION);
- setQuietMode(!enabled);
+ setQuietMode(!enabled, mAllApps.mActivityContext);
}
@Override
diff --git a/src/com/android/launcher3/dagger/LauncherComponentProvider.kt b/src/com/android/launcher3/dagger/LauncherComponentProvider.kt
new file mode 100644
index 0000000..5015e54
--- /dev/null
+++ b/src/com/android/launcher3/dagger/LauncherComponentProvider.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.dagger
+
+import android.content.Context
+import android.view.LayoutInflater
+import com.android.launcher3.LauncherApplication
+
+/**
+ * Utility class to extract LauncherAppComponent from a context.
+ *
+ * If the context doesn't provide LauncherAppComponent by default, it creates a new one and
+ * associate it with that context
+ */
+object LauncherComponentProvider {
+
+ @JvmStatic
+ fun get(c: Context): LauncherAppComponent {
+ val app = c.applicationContext
+ if (app is LauncherApplication) return app.appComponent
+
+ val inflater = LayoutInflater.from(app)
+ val existingFilter = inflater.filter
+ if (existingFilter is Holder) return existingFilter.component
+
+ // Create a new component
+ return Holder(
+ DaggerLauncherAppComponent.builder().appContext(app).build()
+ as LauncherAppComponent,
+ existingFilter,
+ )
+ .apply { inflater.filter = this }
+ .component
+ }
+
+ private data class Holder(
+ val component: LauncherAppComponent,
+ private val filter: LayoutInflater.Filter?,
+ ) : LayoutInflater.Filter {
+
+ override fun onLoadClass(clazz: Class<*>?) = filter?.onLoadClass(clazz) ?: true
+ }
+}
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
index 29fc613..4aa3673 100644
--- a/src/com/android/launcher3/dragndrop/LauncherDragController.java
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -119,6 +119,9 @@
initialDragViewScale,
dragViewScaleOnDrop,
scalePx);
+ // During a drag, we don't want to expose the descendendants of drag view to a11y users,
+ // since those decendents are not a valid position in the workspace.
+ dragView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
dragView.setItemInfo(dragInfo);
mDragObject.dragComplete = false;
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 617cac7..bfa00bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -17,12 +17,14 @@
package com.android.launcher3.model;
import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.Flags.oneGridSpecs;
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.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells;
import android.content.ComponentName;
import android.content.ContentValues;
@@ -130,6 +132,20 @@
// Only use this strategy when comparing the previous grid to the new grid and the
// columns are the same and the destination has more rows
copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
+
+ if (oneGridSpecs()) {
+ DbReader destReader = new DbReader(
+ target.getWritableDatabase(), TABLE_NAME, context);
+ boolean shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.getRows());
+ if (shouldShiftCells) {
+ shiftTableByXCells(
+ target.getWritableDatabase(),
+ (destDeviceState.getRows() - srcDeviceState.getRows()),
+ TABLE_NAME);
+ }
+ }
+
+ // Save current configuration, so that the migration does not run again.
destDeviceState.writeToPrefs(context);
return true;
}
@@ -427,17 +443,22 @@
}
}
- static void copyCurrentGridToNewGrid(
- @NonNull Context context,
- @NonNull DeviceGridState destDeviceState,
- @NonNull DatabaseHelper target,
- @NonNull SQLiteDatabase source) {
- // Only use this strategy when comparing the previous grid to the new grid and the
- // columns are the same and the destination has more rows
- copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
- destDeviceState.writeToPrefs(context);
+ private static boolean shouldShiftCells(final DbReader destReader, final int srcGridRowCount) {
+ List<DbEntry> workspaceItems = destReader.loadAllWorkspaceEntries();
+ int firstPageItemsRowPosSum = workspaceItems.stream()
+ .filter(entry -> entry.screenId == 0)
+ .mapToInt(entry -> entry.cellY).sum();
+ int firstPageWorkspaceItemsCount = (int) workspaceItems.stream()
+ .filter(entry -> entry.screenId == 0).count();
+ if (firstPageWorkspaceItemsCount == 0) {
+ return false;
+ }
+ float srcGridMidPoint = srcGridRowCount / 2f;
+ float firstPageItemPosAvg = (float) firstPageItemsRowPosSum / firstPageWorkspaceItemsCount;
+ return (firstPageItemPosAvg >= srcGridMidPoint);
}
+
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static class DbReader {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index c856d4b..3f52d8a 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -21,15 +21,20 @@
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags
+import com.android.launcher3.Flags.oneGridSpecs
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.LauncherSettings.Favorites.TABLE_NAME
+import com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE
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.provider.LauncherDbUtils.copyTable
+import com.android.launcher3.provider.LauncherDbUtils.dropTable
+import com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells
import com.android.launcher3.util.CellAndSpan
import com.android.launcher3.util.GridOccupancy
import com.android.launcher3.util.IntArray
@@ -59,27 +64,30 @@
// amount of rows we simply copy over the source grid to the destination grid, rather
// than undergoing the general grid migration.
if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
- GridSizeMigrationDBController.copyCurrentGridToNewGrid(
- context,
- destDeviceState,
- target,
- source,
- )
+ copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
+ if (oneGridSpecs()) {
+ val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
+ val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
+ if (shouldShiftCells) {
+ shiftTableByXCells(
+ target.writableDatabase,
+ (destDeviceState.rows - srcDeviceState.rows),
+ TABLE_NAME,
+ )
+ }
+ }
+ // Save current configuration, so that the migration does not run again.
+ destDeviceState.writeToPrefs(context)
return
}
- LauncherDbUtils.copyTable(
- source,
- LauncherSettings.Favorites.TABLE_NAME,
- target.writableDatabase,
- LauncherSettings.Favorites.TMP_TABLE,
- context,
- )
+
+ copyTable(source, TABLE_NAME, target.writableDatabase, 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 srcReader = DbReader(t.db, TMP_TABLE, context)
+ val destReader = DbReader(t.db, TABLE_NAME, context)
val targetSize = Point(destDeviceState.columns, destDeviceState.rows)
@@ -95,7 +103,7 @@
// Migrate workspace.
migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
- LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE)
+ dropTable(t.db, TMP_TABLE)
t.commit()
}
} catch (e: Exception) {
@@ -112,6 +120,19 @@
}
}
+ private fun shouldShiftCells(destReader: DbReader, srcGridRowCount: Int): Boolean {
+ val workspaceItems = destReader.loadAllWorkspaceEntries()
+ val firstPageItemsRowPosSum =
+ workspaceItems.sumOf { entry -> if (entry.screenId == 0) entry.cellY else 0 }
+ val firstPageWorkspaceItemsCount = workspaceItems.count { entry -> entry.screenId == 0 }
+ if (firstPageWorkspaceItemsCount == 0) {
+ return false
+ }
+ val srcGridMidPoint = srcGridRowCount / 2f
+ val firstPageItemPosAvg = firstPageItemsRowPosSum / firstPageWorkspaceItemsCount.toFloat()
+ return (firstPageItemPosAvg >= srcGridMidPoint)
+ }
+
/** Handles hotseat migration. */
@VisibleForTesting
fun migrateHotseat(
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index a830c96..83eace8 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -246,7 +246,7 @@
TraceHelper.INSTANCE.beginSection(TAG);
LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
mIsRestoreFromBackup =
- (Boolean) LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
+ LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
LauncherRestoreEventLogger restoreEventLogger = null;
if (enableLauncherBrMetricsFixed()) {
restoreEventLogger = LauncherRestoreEventLogger.Companion
@@ -266,21 +266,21 @@
sanitizeFolders(mItemsDeleted);
sanitizeAppPairs();
sanitizeWidgetsShortcutsAndPackages();
- logASplit("sanitizeData");
+ logASplit("sanitizeData finished");
}
verifyNotStopped();
mLauncherBinder.bindWorkspace(true /* incrementBindId */, /* isBindSync= */ false);
- logASplit("bindWorkspace");
+ logASplit("bindWorkspace finished");
mModelDelegate.workspaceLoadComplete();
// Notify the installer packages of packages with active installs on the first screen.
sendFirstScreenActiveInstallsBroadcast();
- logASplit("sendFirstScreenBroadcast");
+ logASplit("sendFirstScreenBroadcast finished");
// Take a break
waitForIdle();
- logASplit("step 1 complete");
+ logASplit("step 1 loading workspace complete");
verifyNotStopped();
// second step
@@ -291,11 +291,11 @@
} finally {
Trace.endSection();
}
- logASplit("loadAllApps");
+ logASplit("loadAllApps finished");
verifyNotStopped();
mLauncherBinder.bindAllApps();
- logASplit("bindAllApps");
+ logASplit("bindAllApps finished");
verifyNotStopped();
IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
@@ -303,28 +303,28 @@
updateHandler.updateIcons(allActivityList,
LauncherActivityCachingLogic.INSTANCE,
mApp.getModel()::onPackageIconsUpdated);
- logASplit("update icon cache");
+ logASplit("update AllApps icon cache finished");
verifyNotStopped();
- logASplit("save shortcuts in icon cache");
+ logASplit("saving all shortcuts in icon cache");
updateHandler.updateIcons(allShortcuts, CacheableShortcutCachingLogic.INSTANCE,
mApp.getModel()::onPackageIconsUpdated);
// Take a break
waitForIdle();
- logASplit("step 2 complete");
+ logASplit("step 2 loading AllApps complete");
verifyNotStopped();
// third step
List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
- logASplit("loadDeepShortcuts");
+ logASplit("loadDeepShortcuts finished");
verifyNotStopped();
mLauncherBinder.bindDeepShortcuts();
- logASplit("bindDeepShortcuts");
+ logASplit("bindDeepShortcuts finished");
verifyNotStopped();
- logASplit("save deep shortcuts in icon cache");
+ logASplit("saving deep shortcuts in icon cache");
updateHandler.updateIcons(
convertShortcutsToCacheableShortcuts(allDeepShortcuts, allActivityList),
CacheableShortcutCachingLogic.INSTANCE,
@@ -332,7 +332,7 @@
// Take a break
waitForIdle();
- logASplit("step 3 complete");
+ logASplit("step 3 loading all shortcuts complete");
verifyNotStopped();
// fourth step
@@ -345,11 +345,11 @@
widgetsModel.updateWidgetFilters(mWidgetsFilterDataProvider);
}
List<CachedObject> allWidgetsList = widgetsModel.update(mApp, /*packageUser=*/null);
- logASplit("load widgets");
+ logASplit("load widgets finished");
verifyNotStopped();
mLauncherBinder.bindWidgets();
- logASplit("bindWidgets");
+ logASplit("bindWidgets finished");
verifyNotStopped();
LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext());
@@ -357,7 +357,7 @@
mLauncherBinder.bindSmartspaceWidget();
// Turn off pref.
prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(false));
- logASplit("bindSmartspaceWidget");
+ logASplit("bindSmartspaceWidget finished");
verifyNotStopped();
} else if (!enableSmartspaceAsAWidget() && WIDGET_ON_FIRST_SCREEN
&& !prefs.get(LauncherPrefs.SHOULD_SHOW_SMARTSPACE)) {
@@ -365,10 +365,10 @@
prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(true));
}
+ logASplit("saving all widgets in icon cache");
updateHandler.updateIcons(allWidgetsList,
CachedObjectCachingLogic.INSTANCE,
mApp.getModel()::onWidgetLabelsUpdated);
- logASplit("save widgets in icon cache");
// fifth step
loadFolderNames();
@@ -414,7 +414,7 @@
} finally {
Trace.endSection();
}
- logASplit("loadWorkspace");
+ logASplit("loadWorkspace finished");
mBgDataModel.isFirstPagePinnedItemEnabled = FeatureFlags.QSB_ON_FIRST_SCREEN
&& (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(
@@ -440,7 +440,7 @@
} else {
dbController.tryMigrateDB(restoreEventLogger);
}
- Log.d(TAG, "loadWorkspace: loading default favorites");
+ Log.d(TAG, "loadWorkspace: loading default favorites if necessary");
dbController.loadDefaultFavoritesIfNecessary();
synchronized (mBgDataModel) {
@@ -453,7 +453,7 @@
mInstallingPkgsCached = installingPkgs;
}
installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
- FileLog.d(TAG, "loadWorkspace: Packages with active install sessions: "
+ FileLog.d(TAG, "loadWorkspace: Packages with active install/update sessions: "
+ installingPkgs.keySet().stream().map(info -> info.mPackageName).toList());
mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
@@ -478,8 +478,12 @@
widgetInflater, mPmHelper, iconRequestInfos, unlockedUsers,
allDeepShortcuts);
- while (!mStopped && c.moveToNext()) {
- itemProcessor.processItem();
+ if (mStopped) {
+ Log.w(TAG, "loadWorkspaceImpl: Loader stopped, skipping item processing");
+ } else {
+ while (!mStopped && c.moveToNext()) {
+ itemProcessor.processItem();
+ }
}
tryLoadWorkspaceIconsInBulk(iconRequestInfos);
} finally {
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index 856c294..b9c928c 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -33,7 +33,6 @@
import androidx.annotation.WorkerThread;
import com.android.launcher3.Flags;
-import com.android.launcher3.logging.FileLog;
import com.android.launcher3.util.PackageUserKey;
import java.lang.ref.WeakReference;
@@ -79,7 +78,7 @@
}
SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback);
if (sessionInfo != null) {
- FileLog.d(TAG, "onCreated: Install session created for"
+ Log.d(TAG, "onCreated: Install session created for"
+ " appPackageName=" + sessionInfo.getAppPackageName()
+ ", sessionId=" + sessionInfo.getSessionId()
+ ", appIcon=" + sessionInfo.getAppIcon()
@@ -111,7 +110,7 @@
activeSessions.remove(sessionId);
if (key != null && key.mPackageName != null) {
- FileLog.d(TAG, "onFinished: active install session finished for"
+ Log.d(TAG, "onFinished: active install session finished for"
+ " appPackageName=" + key.mPackageName
+ ", sessionId=" + sessionId
+ ", success=" + success);
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt
index 3c68e46..6f1d0dd 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.kt
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -131,6 +131,11 @@
}
}
+ @JvmStatic
+ fun shiftTableByXCells(db: SQLiteDatabase, x: Int, toTable: String) {
+ db.run { execSQL("UPDATE $toTable SET cellY = cellY + $x") }
+ }
+
/**
* Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
* shortcut or any shortcut which requires some permission to launch
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 82229f8..e4c50f0 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -18,18 +18,23 @@
import android.content.Context
import android.util.Log
+import android.view.InflateException
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.android.launcher3.BubbleTextView
import com.android.launcher3.BuildConfig
import com.android.launcher3.allapps.BaseAllAppsAdapter
+import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.util.CancellableTask
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
import com.android.launcher3.util.Themes
import com.android.launcher3.views.ActivityContext
import com.android.launcher3.views.ActivityContext.ActivityContextDelegate
+import java.lang.IllegalStateException
const val PREINFLATE_ICONS_ROW_COUNT = 4
const val EXTRA_ICONS_COUNT = 2
@@ -39,10 +44,11 @@
* [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
* will be added to [RecycledViewPool] on main thread.
*/
-class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
var hasWorkProfile = false
- private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+ @VisibleForTesting(otherwise = PROTECTED)
+ var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
companion object {
private const val TAG = "AllAppsRecyclerViewPool"
@@ -53,7 +59,7 @@
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
*/
- fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
+ fun preInflateAllAppsViewHolders(context: T) {
val appsView = context.appsView ?: return
val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
val preInflateCount = getPreinflateCount(context)
@@ -97,36 +103,65 @@
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
+ preInflateAllAppsViewHolders(
+ adapter,
+ BaseAllAppsAdapter.VIEW_TYPE_ICON,
+ activeRv,
+ preInflateCount,
+ ) {
+ getPreinflateCount(context)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PROTECTED)
+ fun preInflateAllAppsViewHolders(
+ adapter: RecyclerView.Adapter<*>,
+ viewType: Int,
+ activeRv: RecyclerView,
+ preInflationCount: Int,
+ preInflationCountProvider: () -> Int,
+ ) {
+ if (preInflationCount <= 0) {
+ return
+ }
mCancellableTask?.cancel()
var task: CancellableTask<List<ViewHolder>>? = null
task =
CancellableTask(
{
val list: ArrayList<ViewHolder> = ArrayList()
- for (i in 0 until preInflateCount) {
+ for (i in 0 until preInflationCount) {
if (task?.canceled == true) {
break
}
// If activeRv's layout manager has been reset to null on main thread, skip
// the preinflation as we cannot generate correct LayoutParams
if (activeRv.layoutManager == null) {
+ list.clear()
break
}
- list.add(
- adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
- )
+ try {
+ list.add(adapter.createViewHolder(activeRv, viewType))
+ } catch (e: InflateException) {
+ list.clear()
+ // It's still possible for UI thread to set activeRv's layout manager to
+ // null and we should break the loop and cancel the preinflation.
+ break
+ }
}
list
},
MAIN_EXECUTOR,
{ viewHolders ->
- for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
+ // Run preInflationCountProvider again as the needed VH might have changed
+ val newPreInflationCount = preInflationCountProvider.invoke()
+ for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) {
putRecycledView(viewHolders[i])
}
},
)
mCancellableTask = task
- VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
+ VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
}
/**
@@ -143,10 +178,11 @@
* app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
* suffice fast scrolling.
*
- * Note that we need to preinfate extra app icons in size of one all apps pages, so that opening
- * all apps don't need to inflate app icons.
+ * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
+ * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
+ * icons.
*/
- fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
+ fun getPreinflateCount(context: T): Int {
var targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 5851f62..5068b48 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -22,6 +22,7 @@
import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE;
import static com.android.launcher3.BuildConfig.IS_STUDIO_BUILD;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
import android.app.Activity;
@@ -52,6 +53,7 @@
import com.android.launcher3.BuildConfig;
import com.android.launcher3.Flags;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherFiles;
import com.android.launcher3.R;
import com.android.launcher3.states.RotationHelper;
@@ -310,7 +312,10 @@
}
return mDeveloperOptionsEnabled;
case FIXED_LANDSCAPE_MODE:
- if (!Flags.oneGridSpecs()) {
+ if (!Flags.oneGridSpecs()
+ // adding this condition until fixing b/378972567
+ || InvariantDeviceProfile.INSTANCE.get(getContext()).deviceType
+ == TYPE_MULTI_DISPLAY) {
return false;
}
// When the setting changes rotate the screen accordingly to showcase the result
diff --git a/src/com/android/launcher3/util/DaggerSingletonObject.java b/src/com/android/launcher3/util/DaggerSingletonObject.java
index febe6af..a245761 100644
--- a/src/com/android/launcher3/util/DaggerSingletonObject.java
+++ b/src/com/android/launcher3/util/DaggerSingletonObject.java
@@ -18,8 +18,8 @@
import android.content.Context;
-import com.android.launcher3.LauncherApplication;
import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherComponentProvider;
import java.util.function.Function;
@@ -37,8 +37,6 @@
}
public T get(Context context) {
- LauncherAppComponent component =
- ((LauncherApplication) context.getApplicationContext()).getAppComponent();
- return mFunction.apply(component);
+ return mFunction.apply(LauncherComponentProvider.get(context));
}
}
diff --git a/src/com/android/launcher3/util/MSDLPlayerWrapper.java b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
index 1e53ac1..aab7009 100644
--- a/src/com/android/launcher3/util/MSDLPlayerWrapper.java
+++ b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
@@ -28,6 +28,9 @@
import com.google.android.msdl.data.model.MSDLToken;
import com.google.android.msdl.domain.InteractionProperties;
import com.google.android.msdl.domain.MSDLPlayer;
+import com.google.android.msdl.logging.MSDLEvent;
+
+import java.util.List;
import javax.inject.Inject;
@@ -58,4 +61,8 @@
public void playToken(MSDLToken token) {
mMSDLPlayer.playToken(token, null);
}
+
+ public List<MSDLEvent> getHistory() {
+ return mMSDLPlayer.getHistory();
+ }
}
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index f457e4e..44a7c6f 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -73,7 +73,27 @@
*/
public static final int STAGE_TYPE_SIDE = 1;
- @IntDef({STAGE_TYPE_UNDEFINED, STAGE_TYPE_MAIN, STAGE_TYPE_SIDE})
+ /**
+ * Position independent stage identifier for a given Stage
+ */
+ public static final int STAGE_TYPE_A = 2;
+ /**
+ * Position independent stage identifier for a given Stage
+ */
+ public static final int STAGE_TYPE_B = 3;
+ /**
+ * Position independent stage identifier for a given Stage
+ */
+ public static final int STAGE_TYPE_C = 4;
+
+ @IntDef({
+ STAGE_TYPE_UNDEFINED,
+ STAGE_TYPE_MAIN,
+ STAGE_TYPE_SIDE,
+ STAGE_TYPE_A,
+ STAGE_TYPE_B,
+ STAGE_TYPE_C
+ })
public @interface StageType {}
///////////////////////////////////
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index ef66ffe..392d9a7 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -102,6 +102,9 @@
@Override
public void onDraw(Canvas canvas) {
+ if (shouldDrawAppContrastTile()) {
+ drawAppContrastTile(canvas);
+ }
// If text is transparent or shadow alpha is 0, don't draw any shadow
if (skipDoubleShadow()) {
super.onDraw(canvas);
diff --git a/src/com/android/launcher3/views/StickyHeaderLayout.java b/src/com/android/launcher3/views/StickyHeaderLayout.java
index 090251f..4142e1f 100644
--- a/src/com/android/launcher3/views/StickyHeaderLayout.java
+++ b/src/com/android/launcher3/views/StickyHeaderLayout.java
@@ -120,7 +120,19 @@
}
private float getCurrentScroll() {
- return mScrollOffset + (mCurrentEmptySpaceView == null ? 0 : mCurrentEmptySpaceView.getY());
+ float scroll;
+ if (mCurrentRecyclerView.getVisibility() != VISIBLE) {
+ // When no list is displayed, assume no scroll.
+ scroll = 0f;
+ } else if (mCurrentEmptySpaceView != null) {
+ // Otherwise use empty space view as reference to position.
+ scroll = mCurrentEmptySpaceView.getY();
+ } else {
+ // If there is no empty space view, but the list is visible, we are scrolled away
+ // completely, so assume all non-sticky children should also be scrolled away.
+ scroll = -mHeaderHeight;
+ }
+ return mScrollOffset + scroll;
}
@Override
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 1c0d94c..fda5175 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
import android.content.Context;
import android.graphics.Canvas;
@@ -128,6 +129,17 @@
}
@Override
+ public void setScaleY(float scaleY) {
+ super.setScaleY(scaleY);
+ if (predictiveBackThreeButtonNav() && mNavBarScrimHeight > 0) {
+ // Call invalidate to prevent navbar scrim from scaling. The navbar scrim is drawn
+ // directly onto the canvas. To prevent it from being scaled with the canvas, there's a
+ // counter scale applied in dispatchDraw.
+ invalidate();
+ }
+ }
+
+ @Override
public final void onClick(View v) {
WidgetCell wc;
if (v instanceof WidgetCell view) {
@@ -318,8 +330,10 @@
super.dispatchDraw(canvas);
if (mNavBarScrimHeight > 0) {
- canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
- mNavBarScrimPaint);
+ float left = (getWidth() - getWidth() / getScaleX()) / 2;
+ float top = getHeight() / 2f + (getHeight() / 2f - mNavBarScrimHeight) / getScaleY();
+ canvas.drawRect(left, top, getWidth() / getScaleX(),
+ top + mNavBarScrimHeight / getScaleY(), mNavBarScrimPaint);
}
}
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index e190dc3..2c07fd9 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -16,7 +16,7 @@
package com.android.launcher3.widget;
-import static com.android.launcher3.Flags.enforceSystemRadiusForAppWidgets;
+import static com.android.launcher3.Flags.useSystemRadiusForAppWidgets;
import android.appwidget.AppWidgetHostView;
import android.content.Context;
@@ -99,7 +99,7 @@
public static float computeEnforcedRadius(@NonNull Context context) {
Resources res = context.getResources();
float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius);
- if (enforceSystemRadiusForAppWidgets()) {
+ if (useSystemRadiusForAppWidgets()) {
return systemRadius;
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 2f64ab1..150806a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -295,8 +295,11 @@
}
private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
- recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
if (mCurrentWidgetsRecyclerView != recyclerView) {
+ // Bind scrollbar if changing the recycler view. If widgets list updates, since
+ // scrollbar is already attached to the recycler view, it will automatically adjust as
+ // needed with recycler view's onScrollListener.
+ recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
// Only reset the scroll position & expanded apps if the currently shown recycler view
// has been updated.
reset();
@@ -605,9 +608,12 @@
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
mAdapters.get(getCurrentAdapterHolderType()).mWidgetsRecyclerView.setVisibility(
VISIBLE);
- // Visibility of recommended widgets, recycler views and headers are handled in methods
- // below.
- post(this::onRecommendedWidgetsBound);
+ if (mRecommendedWidgetsCount > 0) {
+ // Display recommendations immediately, if present, so that other parts of sticky
+ // header (e.g. personal / work tabs) don't flash in interim.
+ mWidgetRecommendationsContainer.setVisibility(VISIBLE);
+ }
+ // Visibility of recycler views and headers are handled in methods below.
onWidgetsBound();
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 3c67538..74a9a5c 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -103,7 +103,7 @@
.equals(mWidgetsContentVisiblePackageUserKey);
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
@Nullable private RecyclerView mRecyclerView;
- @Nullable private PackageUserKey mPendingClickHeader;
+ @Nullable private PackageUserKey mHeaderPositionToMaintain;
@Px private int mMaxHorizontalSpan;
private boolean mShowOnlyDefaultList = true;
@@ -215,7 +215,7 @@
// Get the current top of the header with the matching key before adjusting the visible
// entries.
OptionalInt previousPositionForPackageUserKey =
- getPositionForPackageUserKey(mPendingClickHeader);
+ getPositionForPackageUserKey(mHeaderPositionToMaintain);
OptionalInt topForPackageUserKey =
getOffsetForPosition(previousPositionForPackageUserKey);
@@ -247,13 +247,15 @@
mVisibleEntries.addAll(newVisibleEntries);
diffResult.dispatchUpdatesTo(this);
- if (mPendingClickHeader != null) {
+ if (mHeaderPositionToMaintain != null && mRecyclerView != null) {
// Get the position for the clicked header after adjusting the visible entries. The
// position may have changed if another header had previously been expanded.
OptionalInt positionForPackageUserKey =
- getPositionForPackageUserKey(mPendingClickHeader);
- scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
- mPendingClickHeader = null;
+ getPositionForPackageUserKey(mHeaderPositionToMaintain);
+ // Post scroll updates to be applied after diff updates.
+ mRecyclerView.post(() -> scrollToPositionAndMaintainOffset(positionForPackageUserKey,
+ topForPackageUserKey));
+ mHeaderPositionToMaintain = null;
}
}
@@ -384,7 +386,7 @@
// Store the header that was clicked so that its position will be maintained the next time
// we update the entries.
- mPendingClickHeader = packageUserKey;
+ mHeaderPositionToMaintain = packageUserKey;
updateVisibleEntries();
@@ -470,6 +472,16 @@
*/
public void useExpandedList() {
mShowOnlyDefaultList = false;
+ if (mWidgetsContentVisiblePackageUserKey != null) {
+ // Maintain selected header for the next update that expands the list.
+ mHeaderPositionToMaintain = mWidgetsContentVisiblePackageUserKey;
+ } else if (mVisibleEntries.size() > 2) {
+ // Maintain last visible header shown above expand button since there was no selected
+ // header.
+ mHeaderPositionToMaintain = PackageUserKey.fromPackageItemInfo(
+ mVisibleEntries.get(mVisibleEntries.size() - 2).mPkgItem);
+ }
+
}
/** Comparator for sorting WidgetListRowEntry based on package title. */
diff --git a/tests/Android.bp b/tests/Android.bp
index 35a2275..e4fecc5 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -63,7 +63,6 @@
"src/com/android/launcher3/dragging/TaplDragTest.java",
"src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java",
"src/com/android/launcher3/ui/TaplTestsLauncher3Test.java",
- "src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java",
"src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java",
],
}
@@ -173,6 +172,7 @@
"multivalentTests/src/**/*.java",
"multivalentTests/src/**/*.kt",
"src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
+ "src/com/android/launcher3/ui/BaseLauncherTaplTest.java",
"tapl/com/android/launcher3/tapl/*.java",
"tapl/com/android/launcher3/tapl/*.kt",
],
diff --git a/tests/assets/ReorderWidgets/push_reorder_case b/tests/assets/ReorderWidgets/push_reorder_case
index 1eacfae..73b67d0 100644
--- a/tests/assets/ReorderWidgets/push_reorder_case
+++ b/tests/assets/ReorderWidgets/push_reorder_case
@@ -39,6 +39,6 @@
board: 6x5
xxxxxx
bbbb--
---m---
---aaa-
---ddd-
\ No newline at end of file
+--maaa
+--ddd-
+------
\ No newline at end of file
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index f8794f8..c4519eb 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -168,7 +168,6 @@
isLandscape: Boolean = false,
isGestureMode: Boolean = true,
isFolded: Boolean = false,
- isFixedLandscape: Boolean = false,
) {
val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
val unfoldedWindowsBounds =
@@ -195,7 +194,6 @@
rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
isGestureMode = isGestureMode,
densityDpi = deviceSpecFolded.densityDpi,
- isFixedLandscape = isFixedLandscape,
)
} else {
initializeCommonVars(
@@ -204,7 +202,6 @@
rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
isGestureMode = isGestureMode,
densityDpi = deviceSpecUnfolded.densityDpi,
- isFixedLandscape = isFixedLandscape,
)
}
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt b/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
index 46e66e4..42374a5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
@@ -1,20 +1,29 @@
package com.android.launcher3
import android.content.Context
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.Utilities.*
+import com.android.launcher3.dragndrop.DragView
import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.MSDLPlayerWrapper
+import com.google.android.msdl.data.model.MSDLToken
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeleteDropTargetTest {
+ @get:Rule val mSetFlagsRule = SetFlagsRule()
+
private var mContext: Context = ActivityContextWrapper(getApplicationContext())
// Use a non-abstract class implementation
@@ -37,4 +46,17 @@
// A lot of space for text so the text should not be clipped
assertThat(buttonDropTarget.isTextClippedVertically(1000)).isFalse()
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+ fun onDragEnter_performsMSDLSwipeThresholdFeedback() {
+ val target = DropTarget.DragObject(mContext)
+ target.dragView = mock<DragView<*>>()
+ buttonDropTarget.onDragEnter(target)
+ val wrapper = MSDLPlayerWrapper.INSTANCE.get(mContext)
+
+ val history = wrapper.history
+ assertThat(history.size).isEqualTo(1)
+ assertThat(history[0].tokenName).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR.name)
+ }
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/dagger/LauncherComponentProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/dagger/LauncherComponentProviderTest.kt
new file mode 100644
index 0000000..9255877
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/dagger/LauncherComponentProviderTest.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.dagger
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.view.ContextThemeWrapper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.R
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertSame
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class LauncherComponentProviderTest {
+
+ val app: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun `returns same component as Launcher application`() {
+ val c = SandboxModelContext()
+ assertSame(c.appComponent, LauncherComponentProvider.get(c))
+ assertNotSame(LauncherComponentProvider.get(c), LauncherComponentProvider.get(app))
+ }
+
+ @Test
+ fun `returns same component for isolated context`() {
+ val c = IsolatedContext()
+
+ // Same component is returned for multiple calls, irrespective of the wrappers
+ assertNotNull(LauncherComponentProvider.get(c))
+ assertSame(
+ LauncherComponentProvider.get(c),
+ LauncherComponentProvider.get(ContextThemeWrapper(c, R.style.LauncherTheme)),
+ )
+
+ // Different than main application
+ assertNotSame(LauncherComponentProvider.get(c), LauncherComponentProvider.get(app))
+ }
+
+ @Test
+ fun `different components for different isolated context`() {
+ val c1 = IsolatedContext()
+ val c2 = IsolatedContext()
+
+ assertNotNull(LauncherComponentProvider.get(c1))
+ assertNotNull(LauncherComponentProvider.get(c2))
+ assertNotSame(LauncherComponentProvider.get(c1), LauncherComponentProvider.get(c2))
+ }
+
+ inner class IsolatedContext : ContextWrapper(app.createPackageContext(TEST_PACKAGE, 0)) {
+
+ override fun getApplicationContext(): Context = this
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
index ce00b28..9b4bd71 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
@@ -71,6 +71,7 @@
import com.google.common.truth.Truth;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -99,6 +100,11 @@
new LauncherIconProvider(mContext));
}
+ @After
+ public void tearDown() {
+ mIconCache.close();
+ }
+
@Test
public void getShortcutInfoBadge_nullComponent_overrideAllowed() throws Exception {
String overridePackage = "com.android.settings";
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/DatabaseHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/DatabaseHelperTest.kt
index c9ea421..09752b8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/DatabaseHelperTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/DatabaseHelperTest.kt
@@ -1,5 +1,6 @@
package com.android.launcher3.model
+import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -11,8 +12,10 @@
import com.android.launcher3.pm.UserCache
import com.android.launcher3.provider.LauncherDbUtils
import java.util.function.ToLongFunction
+import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -24,6 +27,19 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class DatabaseHelperTest {
+ val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
+ // v30 - 21 columns
+ lateinit var db: SQLiteDatabase
+
+ @Before
+ fun setUp() {
+ db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb
+ }
+
+ @After
+ fun tearDown() {
+ db.close()
+ }
/**
* b/304687723 occurred when a return was accidentally added to a case statement in
@@ -33,13 +49,11 @@
*/
@Test
fun onUpgrade_to_version_32_from_30() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
val userSerialProvider =
ToLongFunction<UserHandle> {
UserCache.INSTANCE.get(context).getSerialNumberForUser(it)
}
val dbHelper = DatabaseHelper(context, null, userSerialProvider) {}
- val db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb
dbHelper.onUpgrade(db, 30, 32)
@@ -54,9 +68,6 @@
*/
@Test
fun after_migrating_from_db_v30_to_v32_copy_table() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb // v30 - 21 columns
-
addTableToDb(db, 1, true, TMP_TABLE)
LauncherDbUtils.copyTable(db, TABLE_NAME, db, TMP_TABLE, context)
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index 7933331..eee6191 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -82,6 +82,7 @@
@After
fun tearDown() {
+ db.close()
modelHelper.destroy()
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
index b4945d7..63359ec 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -109,6 +109,7 @@
@After
public void tearDown() {
+ mCursor.close();
mModelHelper.destroy();
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index d0c168a..c30b730 100644
--- a/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -92,6 +92,7 @@
private Cursor mMockCursor;
private LauncherPrefs mPrefs;
private LauncherRestoreEventLogger mMockRestoreEventLogger;
+ private SQLiteDatabase mDb;
@Before
public void setup() {
@@ -107,57 +108,60 @@
@After
public void teardown() {
+ if (mDb != null) {
+ mDb.close();
+ }
mModelHelper.destroy();
LauncherPrefs.get(mContext).removeSync(RESTORE_DEVICE);
}
@Test
public void testGetProfileId() throws Exception {
- SQLiteDatabase db = new MyModelDbController(23).getDb();
- assertEquals(23, new RestoreDbTask().getDefaultProfileId(db));
+ mDb = new MyModelDbController(23).getDb();
+ assertEquals(23, new RestoreDbTask().getDefaultProfileId(mDb));
}
@Test
public void testMigrateProfileId() throws Exception {
- SQLiteDatabase db = new MyModelDbController(42).getDb();
+ mDb = new MyModelDbController(42).getDb();
// Add some mock data
for (int i = 0; i < 5; i++) {
ContentValues values = new ContentValues();
values.put(Favorites._ID, i);
values.put(Favorites.TITLE, "item " + i);
- db.insert(Favorites.TABLE_NAME, null, values);
+ mDb.insert(Favorites.TABLE_NAME, null, values);
}
// Verify item add
- assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+ assertEquals(5, getCount(mDb, "select * from favorites where profileId = 42"));
- new RestoreDbTask().migrateProfileId(db, 42, 33);
+ new RestoreDbTask().migrateProfileId(mDb, 42, 33);
// verify data migrated
- assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
- assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
+ assertEquals(0, getCount(mDb, "select * from favorites where profileId = 42"));
+ assertEquals(5, getCount(mDb, "select * from favorites where profileId = 33"));
}
@Test
public void testChangeDefaultColumn() throws Exception {
- SQLiteDatabase db = new MyModelDbController(42).getDb();
+ mDb = new MyModelDbController(42).getDb();
// Add some mock data
for (int i = 0; i < 5; i++) {
ContentValues values = new ContentValues();
values.put(Favorites._ID, i);
values.put(Favorites.TITLE, "item " + i);
- db.insert(Favorites.TABLE_NAME, null, values);
+ mDb.insert(Favorites.TABLE_NAME, null, values);
}
// Verify default column is 42
- assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+ assertEquals(5, getCount(mDb, "select * from favorites where profileId = 42"));
- new RestoreDbTask().changeDefaultColumn(db, 33);
+ new RestoreDbTask().changeDefaultColumn(mDb, 33);
// Verify default value changed
ContentValues values = new ContentValues();
values.put(Favorites._ID, 100);
values.put(Favorites.TITLE, "item 100");
- db.insert(Favorites.TABLE_NAME, null, values);
- assertEquals(1, getCount(db, "select * from favorites where profileId = 33"));
+ mDb.insert(Favorites.TABLE_NAME, null, values);
+ assertEquals(1, getCount(mDb, "select * from favorites where profileId = 33"));
}
@Test
@@ -170,7 +174,7 @@
long workProfileId_old = myProfileId + 3;
MyModelDbController controller = new MyModelDbController(myProfileId);
- SQLiteDatabase db = controller.getDb();
+ mDb = controller.getDb();
BackupManager bm = spy(new BackupManager(mContext));
doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
doReturn(mWorkUser).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old));
@@ -178,16 +182,16 @@
addIconsBulk(controller, 10, 1, myProfileId_old);
addIconsBulk(controller, 6, 2, workProfileId_old);
- assertEquals(10, getItemCountForProfile(db, myProfileId_old));
- assertEquals(6, getItemCountForProfile(db, workProfileId_old));
+ assertEquals(10, getItemCountForProfile(mDb, myProfileId_old));
+ assertEquals(6, getItemCountForProfile(mDb, workProfileId_old));
mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
// All the data has been migrated to the new user ids
- assertEquals(0, getItemCountForProfile(db, myProfileId_old));
- assertEquals(0, getItemCountForProfile(db, workProfileId_old));
- assertEquals(10, getItemCountForProfile(db, myProfileId));
- assertEquals(6, getItemCountForProfile(db, workProfileId));
+ assertEquals(0, getItemCountForProfile(mDb, myProfileId_old));
+ assertEquals(0, getItemCountForProfile(mDb, workProfileId_old));
+ assertEquals(10, getItemCountForProfile(mDb, myProfileId));
+ assertEquals(6, getItemCountForProfile(mDb, workProfileId));
}
@Test
@@ -199,7 +203,7 @@
long workProfileId_old = myProfileId + 3;
MyModelDbController controller = new MyModelDbController(myProfileId);
- SQLiteDatabase db = controller.getDb();
+ mDb = controller.getDb();
BackupManager bm = spy(new BackupManager(mContext));
doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
// Work profile is not migrated
@@ -207,16 +211,16 @@
addIconsBulk(controller, 10, 1, myProfileId_old);
addIconsBulk(controller, 6, 2, workProfileId_old);
- assertEquals(10, getItemCountForProfile(db, myProfileId_old));
- assertEquals(6, getItemCountForProfile(db, workProfileId_old));
+ assertEquals(10, getItemCountForProfile(mDb, myProfileId_old));
+ assertEquals(6, getItemCountForProfile(mDb, workProfileId_old));
mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
// All the data has been migrated to the new user ids
- assertEquals(0, getItemCountForProfile(db, myProfileId_old));
- assertEquals(0, getItemCountForProfile(db, workProfileId_old));
- assertEquals(10, getItemCountForProfile(db, myProfileId));
- assertEquals(10, getCount(db, "select * from favorites"));
+ assertEquals(0, getItemCountForProfile(mDb, myProfileId_old));
+ assertEquals(0, getItemCountForProfile(mDb, workProfileId_old));
+ assertEquals(10, getItemCountForProfile(mDb, myProfileId));
+ assertEquals(10, getCount(mDb, "select * from favorites"));
}
@Test
@@ -342,24 +346,24 @@
}
private void runRemoveScreenIdGapsTest(int[] screenIds, int[] expectedScreenIds) {
- SQLiteDatabase db = new MyModelDbController(42).getDb();
+ mDb = new MyModelDbController(42).getDb();
// Add some mock data
for (int i = 0; i < screenIds.length; i++) {
ContentValues values = new ContentValues();
values.put(Favorites._ID, i);
values.put(Favorites.SCREEN, screenIds[i]);
values.put(Favorites.CONTAINER, CONTAINER_DESKTOP);
- db.insert(Favorites.TABLE_NAME, null, values);
+ mDb.insert(Favorites.TABLE_NAME, null, values);
}
// Verify items are added
assertEquals(screenIds.length,
- getCount(db, "select * from favorites where container = -100"));
+ getCount(mDb, "select * from favorites where container = -100"));
- new RestoreDbTask().removeScreenIdGaps(db);
+ new RestoreDbTask().removeScreenIdGaps(mDb);
// verify screenId gaps removed
int[] resultScreenIds = new int[screenIds.length];
- try (Cursor c = db.rawQuery(
+ try (Cursor c = mDb.rawQuery(
"select screen from favorites where container = -100 order by screen", null)) {
int i = 0;
while (c.moveToNext()) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
new file mode 100644
index 0000000..3afb0b5
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.recyclerview
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.LayoutManager
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.launcher3.views.ActivityContext
+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.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
+
+ private lateinit var underTest: AllAppsRecyclerViewPool<T>
+ private lateinit var adapter: RecyclerView.Adapter<*>
+
+ @Mock private lateinit var parent: RecyclerView
+ @Mock private lateinit var itemView: View
+ @Mock private lateinit var layoutManager: LayoutManager
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = spy(AllAppsRecyclerViewPool())
+ adapter =
+ object : RecyclerView.Adapter<ViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+ object : ViewHolder(itemView) {}
+
+ override fun getItemCount() = 0
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
+ }
+ underTest.setMaxRecycledViews(VIEW_TYPE, 20)
+ `when`(parent.layoutManager).thenReturn(layoutManager)
+ }
+
+ @Test
+ fun preinflate_success() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
+ }
+
+ @Test
+ fun preinflate_not_triggered() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_before_runOnMainThread() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+
+ underTest.clear()
+
+ awaitTasksCompleted()
+ verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_after_run() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+ awaitTasksCompleted()
+
+ underTest.clear()
+
+ verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ private fun awaitTasksCompleted() {
+ Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
+ Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+ }
+
+ companion object {
+ private const val VIEW_TYPE: Int = 4
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
index 7484bce..ac5fda2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -26,6 +26,7 @@
import com.android.launcher3.util.ActivityContextWrapper
import com.android.launcher3.util.Executors
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -38,7 +39,7 @@
@get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
private val providerName =
ComponentName(
- getInstrumentation().getContext().getPackageName(),
+ getInstrumentation().context.packageName,
"com.android.launcher3.testcomponent.AppWidgetNoConfig",
)
private val generatedPreviewLayout =
@@ -51,6 +52,7 @@
private lateinit var helper: WidgetManagerHelper
private lateinit var appWidgetProviderInfo: LauncherAppWidgetProviderInfo
private lateinit var widgetItem: WidgetItem
+ private lateinit var iconCache: IconCache
@Before
fun setup() {
@@ -88,17 +90,17 @@
createWidgetItem()
}
+ @After
+ fun tearDown() {
+ iconCache.close()
+ }
+
private fun createWidgetItem() {
Executors.MODEL_EXECUTOR.submit {
val idp = InvariantDeviceProfile()
- widgetItem =
- WidgetItem(
- appWidgetProviderInfo,
- idp,
- IconCache(context, idp, null, IconProvider(context)),
- context,
- helper,
- )
+ if (::iconCache.isInitialized) iconCache.close()
+ iconCache = IconCache(context, idp, null, IconProvider(context))
+ widgetItem = WidgetItem(appWidgetProviderInfo, idp, iconCache, context, helper)
}
.get()
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
index c82e84c..13e23c9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -86,7 +86,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+ @DisableFlags(Flags.FLAG_USE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
fun `Compute system radius when smaller`() {
val mockContext = mock(Context::class.java)
val mockRes = mock(Resources::class.java)
@@ -103,7 +103,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+ @DisableFlags(Flags.FLAG_USE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
fun `Compute launcher radius when smaller`() {
val mockContext = mock(Context::class.java)
val mockRes = mock(Resources::class.java)
@@ -120,7 +120,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+ @EnableFlags(Flags.FLAG_USE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
fun `Compute system radius ignoring launcher radius`() {
val mockContext = mock(Context::class.java)
val mockRes = mock(Resources::class.java)
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 59e1f99..e2f9feb9a 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -64,7 +64,6 @@
@Test
@PortraitLandscape
@PlatinumTest(focusArea = "launcher")
- @ScreenRecordRule.ScreenRecord // b/353600888
public void testDragToFolder() {
// TODO: add the use case to drag an icon to an existing folder. Currently it either fails
// on tablets or phones due to difference in resolution.
@@ -97,7 +96,6 @@
* icon left.
*/
@Test
- @ScreenRecordRule.ScreenRecord // b/353600888
public void testDragOutOfFolder() {
final HomeAppIcon playStoreIcon = createShortcutIfNotExist(STORE_APP_NAME, 0, 1);
final HomeAppIcon photosIcon = createShortcutInCenterIfNotExist(PHOTOS_APP_NAME);
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index a273648..8e4db5c 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -15,75 +15,41 @@
*/
package com.android.launcher3.ui;
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
import static androidx.test.InstrumentationRegistry.getInstrumentation;
-import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.TestUtil.resolveSystemAppInfo;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
-import android.app.ActivityManager;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.graphics.Point;
-import android.os.Debug;
import android.os.Process;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.platform.test.flag.junit.SetFlagsRule;
-import android.platform.test.rule.LimitDevicesRule;
import android.system.OsConstants;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
-import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.tapl.HomeAllApps;
-import com.android.launcher3.tapl.HomeAppIcon;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.tapl.TestHelpers;
import com.android.launcher3.testcomponent.TestCommandReceiver;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule;
import com.android.launcher3.util.rule.FailureWatcher;
-import com.android.launcher3.util.rule.SamplerRule;
-import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.ShellCommandRule;
import com.android.launcher3.util.rule.TestIsolationRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.launcher3.util.rule.ViewCaptureRule;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
-import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
@@ -93,169 +59,51 @@
/**
* Base class for all instrumentation tests providing various utility methods.
*/
-public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> {
-
- public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 10;
+public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher>
+ extends BaseLauncherTaplTest {
private static final String TAG = "AbstractLauncherUiTest";
- private static final long BYTES_PER_MEGABYTE = 1 << 20;
-
- private static boolean sDumpWasGenerated = false;
- private static boolean sActivityLeakReported = false;
- private static boolean sSeenKeyguard = false;
- private static boolean sFirstTimeWaitingForWizard = true;
-
- private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
-
protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
- protected final UiDevice mDevice = getUiDevice();
- protected final LauncherInstrumentation mLauncher = createLauncherInstrumentation();
-
- @NonNull
- public static LauncherInstrumentation createLauncherInstrumentation() {
- waitForSetupWizardDismissal(); // precondition for creating LauncherInstrumentation
- return new LauncherInstrumentation(true);
- }
-
- protected Context mTargetContext;
- protected String mTargetPackage;
- private int mLauncherPid;
-
- private final ActivityManager.MemoryInfo mMemoryInfo = new ActivityManager.MemoryInfo();
- private final ActivityManager mActivityManager;
- private long mMemoryBefore;
-
- /** Detects activity leaks and throws an exception if a leak is found. */
- public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
- checkDetectedLeaks(launcher, false);
- }
-
- /** Detects activity leaks and throws an exception if a leak is found. */
- public static void checkDetectedLeaks(LauncherInstrumentation launcher,
- boolean requireOneActiveActivityUnused) {
- if (TestStabilityRule.isPresubmit()) return; // b/313501215
-
- final boolean requireOneActiveActivity =
- false; // workaround for leaks when there is an unexpected Recents activity
-
- if (sActivityLeakReported) return;
-
- // Check whether activity leak detector has found leaked activities.
- Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity),
- () -> {
- launcher.forceGc();
- return MAIN_EXECUTOR.submit(
- () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
- }, launcher);
- }
-
- public static String getAppPackageName() {
- return getInstrumentation().getContext().getPackageName();
- }
-
- private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher,
- boolean requireOneActiveActivity) {
- sActivityLeakReported = true;
- return "Activity leak detector has found leaked activities, requirining 1 activity: "
- + requireOneActiveActivity + "; "
- + dumpHprofData(launcher, false, requireOneActiveActivity) + ".";
- }
-
- private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak,
- boolean requireOneActiveActivity) {
- if (intentionalLeak) return "intentional leak; not generating dump";
-
- String result;
- if (sDumpWasGenerated) {
- result = "dump has already been generated by another test";
- } else {
- try {
- final String fileName =
- getInstrumentation().getTargetContext().getFilesDir().getPath()
- + "/ActivityLeakHeapDump.hprof";
- if (TestHelpers.isInLauncherProcess()) {
- Debug.dumpHprofData(fileName);
- } else {
- final UiDevice device = getUiDevice();
- device.executeShellCommand(
- "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
- }
- Log.d(TAG, "Saved leak dump, the leak is still present: "
- + !launcher.noLeakedActivities(requireOneActiveActivity));
- sDumpWasGenerated = true;
- result = "saved memory dump as an artifact";
- } catch (Throwable e) {
- Log.e(TAG, "dumpHprofData failed", e);
- result = "failed to save memory dump";
- }
- }
- return result + ". Full list of activities: " + launcher.getRootedActivitiesList();
- }
protected AbstractLauncherUiTest() {
- mActivityManager = InstrumentationRegistry.getContext()
- .getSystemService(ActivityManager.class);
- mLauncher.enableCheckEventsForSuccessfulGestures();
- mLauncher.setAnomalyChecker(AbstractLauncherUiTest::verifyKeyguardInvisible);
- try {
- mDevice.setOrientationNatural();
- } catch (RemoteException e) {
- throw new RuntimeException(e);
- }
if (TestHelpers.isInLauncherProcess()) {
Utilities.enableRunningInTestHarnessForTests();
mLauncher.setSystemHealthSupplier(startTime -> TestCommandReceiver.callCommand(
TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString())
.getString("result"));
}
- mLauncher.enableDebugTracing();
- // Avoid double-reporting of Launcher crashes.
- mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
}
- @Rule
- public ShellCommandRule mDisableHeadsUpNotification =
- ShellCommandRule.disableHeadsUpNotification();
-
- @Rule
- public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
-
- @Rule
- public SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
- @Rule
- public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
-
- @Rule
- public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
-
+ /**
+ * @deprecated call {@link #performInitialization} instead
+ */
+ @Deprecated
public static void initialize(AbstractLauncherUiTest test) throws Exception {
- test.reinitializeLauncherData();
- test.mDevice.pressHome();
- test.waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
- test.waitForState("Launcher internal state didn't switch to Home",
- () -> LauncherState.NORMAL);
- test.waitForResumed("Launcher internal state is still Background");
+ test.performInitialization();
+ }
+
+ @Override
+ protected void performInitialization() {
+ reinitializeLauncherData();
+ mDevice.pressHome();
// Check that we switched to home.
- test.mLauncher.getWorkspace();
- AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher, true);
+ mLauncher.getWorkspace();
+
+ waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+ waitForState("Launcher internal state didn't switch to Home",
+ () -> LauncherState.NORMAL);
+ waitForResumed("Launcher internal state is still Background");
+
+ checkDetectedLeaks(mLauncher, true);
}
- protected void clearPackageData(String pkg) throws IOException, InterruptedException {
- assertTrue("pm clear command failed",
- mDevice.executeShellCommand("pm clear " + pkg)
- .contains("Success"));
- assertTrue("pm wait-for-handler command failed",
- mDevice.executeShellCommand("pm wait-for-handler")
- .contains("Success"));
- }
-
+ @Override
protected TestRule getRulesInsideActivityMonitor() {
final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
Launcher.ACTIVITY_TRACKER::getCreatedContext);
final RuleChain inner = RuleChain
- .outerRule(new PortraitLandscapeRunner<LAUNCHER_TYPE>(this))
+ .outerRule(new PortraitLandscapeRunner<>(this))
.around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
// .around(viewCaptureRule) // b/315482167
.around(new TestIsolationRule(mLauncher, true));
@@ -265,175 +113,6 @@
: inner;
}
- @Rule
- public TestRule mOrderSensitiveRules = RuleChain
- .outerRule(new SamplerRule())
- .around(new TestStabilityRule())
- .around(getRulesInsideActivityMonitor());
-
- public UiDevice getDevice() {
- return mDevice;
- }
-
- @Before
- public void setUp() throws Exception {
- mLauncher.onTestStart();
-
- final String launcherPackageName = mDevice.getLauncherPackageName();
- try {
- final Context context = InstrumentationRegistry.getContext();
- final PackageManager pm = context.getPackageManager();
- final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0);
-
- if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) {
- Assert.assertEquals("Launcher version doesn't match tests version",
- pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(),
- launcherPackage.getLongVersionCode());
- }
- } catch (PackageManager.NameNotFoundException e) {
- throw new RuntimeException(e);
- }
-
- mLauncherPid = 0;
-
- mTargetContext = InstrumentationRegistry.getTargetContext();
- mTargetPackage = mTargetContext.getPackageName();
- mLauncherPid = mLauncher.getPid();
-
- UserManager userManager = mTargetContext.getSystemService(UserManager.class);
- if (userManager != null) {
- for (UserHandle userHandle : userManager.getUserProfiles()) {
- if (!userHandle.isSystem()) {
- mDevice.executeShellCommand(
- "pm remove-user --wait " + userHandle.getIdentifier());
- }
- }
- }
-
- onTestStart();
-
- initialize(this);
- }
-
- private long getAvailableMemory() {
- mActivityManager.getMemoryInfo(mMemoryInfo);
-
- return Math.divideExact(mMemoryInfo.availMem, BYTES_PER_MEGABYTE);
- }
-
- @Before
- public void saveMemoryBefore() {
- mMemoryBefore = getAvailableMemory();
- }
-
- @After
- public void logMemoryAfter() {
- long memoryAfter = getAvailableMemory();
-
- Log.d(TAG, "Available memory: before=" + mMemoryBefore
- + "MB, after=" + memoryAfter
- + "MB, delta=" + (memoryAfter - mMemoryBefore) + "MB");
- }
-
- /** Method that should be called when a test starts. */
- public static void onTestStart() {
- waitForSetupWizardDismissal();
-
- if (TestStabilityRule.isPresubmit()) {
- aggressivelyUnlockSysUi();
- } else {
- verifyKeyguardInvisible();
- }
- }
-
- private static boolean hasSystemUiObject(String resId) {
- return getUiDevice().hasObject(
- By.res(SYSTEMUI_PACKAGE, resId));
- }
-
- @NonNull
- private static UiDevice getUiDevice() {
- return UiDevice.getInstance(getInstrumentation());
- }
-
- private static void aggressivelyUnlockSysUi() {
- final UiDevice device = getUiDevice();
- for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
- Log.d(TAG, "Before attempting to unlock the phone");
- try {
- device.executeShellCommand("input keyevent 82");
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- device.waitForIdle();
- }
- Assert.assertTrue("Keyguard still visible",
- TestHelpers.wait(
- Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
- Log.d(TAG, "Keyguard is not visible");
- }
-
- /** Waits for setup wizard to go away. */
- private static void waitForSetupWizardDismissal() {
- if (sFirstTimeWaitingForWizard) {
- try {
- getUiDevice().executeShellCommand(
- "am force-stop com.google.android.setupwizard");
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- final boolean wizardDismissed = TestHelpers.wait(
- Until.gone(By.pkg("com.google.android.setupwizard").depth(0)),
- sFirstTimeWaitingForWizard ? 120000 : 0);
- sFirstTimeWaitingForWizard = false;
- Assert.assertTrue("Setup wizard is still visible", wizardDismissed);
- }
-
- /** Asserts that keyguard is not visible */
- public static void verifyKeyguardInvisible() {
- final boolean keyguardAlreadyVisible = sSeenKeyguard;
-
- sSeenKeyguard = sSeenKeyguard
- || !TestHelpers.wait(
- Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
-
- Assert.assertFalse(
- "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
- + " for the first time = "
- + !keyguardAlreadyVisible,
- sSeenKeyguard);
- }
-
- @After
- public void verifyLauncherState() {
- try {
- // Limits UI tests affecting tests running after them.
- mDevice.pressHome();
- mLauncher.waitForLauncherInitialized();
- if (mLauncherPid != 0) {
- assertEquals("Launcher crashed, pid mismatch:",
- mLauncherPid, mLauncher.getPid().intValue());
- }
- } finally {
- mLauncher.onTestFinish();
- }
- }
-
- protected void reinitializeLauncherData() {
- reinitializeLauncherData(false);
- }
-
- protected void reinitializeLauncherData(boolean clearWorkspace) {
- if (clearWorkspace) {
- mLauncher.clearLauncherData();
- } else {
- mLauncher.reinitializeLauncherData();
- }
- mLauncher.waitForLauncherInitialized();
- }
-
/**
* Runs the callback on the UI thread and returns the result.
*/
@@ -533,38 +212,6 @@
}, mLauncher, timeout);
}
- /**
- * Broadcast receiver which blocks until the result is received.
- */
- public class BlockingBroadcastReceiver extends BroadcastReceiver {
-
- private final CountDownLatch latch = new CountDownLatch(1);
- private Intent mIntent;
-
- public BlockingBroadcastReceiver(String action) {
- mTargetContext.registerReceiver(this, new IntentFilter(action),
- Context.RECEIVER_EXPORTED/*UNAUDITED*/);
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- mIntent = intent;
- latch.countDown();
- }
-
- public Intent blockingGetIntent() throws InterruptedException {
- assertTrue("Timed Out", latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS));
- mTargetContext.unregisterReceiver(this);
- return mIntent;
- }
-
- public Intent blockingGetExtraIntent() throws InterruptedException {
- Intent intent = blockingGetIntent();
- return intent == null ? null : (Intent) intent.getParcelableExtra(
- Intent.EXTRA_INTENT);
- }
- }
-
public static void startAppFast(String packageName) {
startIntent(
getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
@@ -659,45 +306,4 @@
protected void onLauncherActivityClose(LAUNCHER_TYPE launcher) {
}
-
- protected HomeAppIcon createShortcutInCenterIfNotExist(String name) {
- Point dimension = mLauncher.getWorkspace().getIconGridDimensions();
- return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2);
- }
-
- protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) {
- return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y);
- }
-
- protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
- HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
- Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name +
- " cell: " + cellX + ", " + cellY);
- if (homeAppIcon == null) {
- HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
- allApps.freeze();
- try {
- allApps.getAppIcon(name).dragToWorkspace(cellX, cellY);
- } finally {
- allApps.unfreeze();
- }
- homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name);
- }
- return homeAppIcon;
- }
-
- protected void commitTransactionAndLoadHome(FavoriteItemsTransaction transaction) {
- transaction.commit();
-
- // Launch the home activity
- UiDevice.getInstance(getInstrumentation()).pressHome();
- mLauncher.waitForLauncherInitialized();
- }
-
- /** Clears all recent tasks */
- protected void clearAllRecentTasks() {
- if (!mLauncher.getRecentTasks().isEmpty()) {
- mLauncher.goHome().switchToOverview().dismissAllTasks();
- }
- }
}
diff --git a/tests/src/com/android/launcher3/ui/BaseLauncherTaplTest.java b/tests/src/com/android/launcher3/ui/BaseLauncherTaplTest.java
new file mode 100644
index 0000000..8449853
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/BaseLauncherTaplTest.java
@@ -0,0 +1,529 @@
+/*
+ * 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.ui;
+
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.os.Debug;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.rule.LimitDevicesRule;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.tapl.HomeAllApps;
+import com.android.launcher3.tapl.HomeAppIcon;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule;
+import com.android.launcher3.util.rule.FailureWatcher;
+import com.android.launcher3.util.rule.SamplerRule;
+import com.android.launcher3.util.rule.ScreenRecordRule;
+import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.util.rule.TestIsolationRule;
+import com.android.launcher3.util.rule.TestStabilityRule;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for all TAPL tests in Launcher providing various utility methods.
+ */
+public abstract class BaseLauncherTaplTest {
+
+ public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
+ public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 10;
+
+ public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT;
+ private static final String TAG = "BaseLauncherTaplTest";
+
+ private static final long BYTES_PER_MEGABYTE = 1 << 20;
+
+ private static boolean sDumpWasGenerated = false;
+ private static boolean sActivityLeakReported = false;
+ private static boolean sSeenKeyguard = false;
+ private static boolean sFirstTimeWaitingForWizard = true;
+
+ private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+
+ protected final UiDevice mDevice = getUiDevice();
+ protected final LauncherInstrumentation mLauncher = createLauncherInstrumentation();
+
+ @NonNull
+ public static LauncherInstrumentation createLauncherInstrumentation() {
+ waitForSetupWizardDismissal(); // precondition for creating LauncherInstrumentation
+ return new LauncherInstrumentation(true);
+ }
+
+ protected Context mTargetContext;
+ protected String mTargetPackage;
+ private int mLauncherPid;
+
+ private final ActivityManager.MemoryInfo mMemoryInfo = new ActivityManager.MemoryInfo();
+ private final ActivityManager mActivityManager;
+ private long mMemoryBefore;
+
+ /** Detects activity leaks and throws an exception if a leak is found. */
+ public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
+ checkDetectedLeaks(launcher, false);
+ }
+
+ /** Detects activity leaks and throws an exception if a leak is found. */
+ public static void checkDetectedLeaks(LauncherInstrumentation launcher,
+ boolean requireOneActiveActivityUnused) {
+ if (TestStabilityRule.isPresubmit()) return; // b/313501215
+
+ final boolean requireOneActiveActivity =
+ false; // workaround for leaks when there is an unexpected Recents activity
+
+ if (sActivityLeakReported) return;
+
+ // Check whether activity leak detector has found leaked activities.
+ Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity),
+ () -> {
+ launcher.forceGc();
+ return MAIN_EXECUTOR.submit(
+ () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
+ }, launcher, DEFAULT_UI_TIMEOUT);
+ }
+
+ public static String getAppPackageName() {
+ return getInstrumentation().getContext().getPackageName();
+ }
+
+ private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher,
+ boolean requireOneActiveActivity) {
+ sActivityLeakReported = true;
+ return "Activity leak detector has found leaked activities, requirining 1 activity: "
+ + requireOneActiveActivity + "; "
+ + dumpHprofData(launcher, false, requireOneActiveActivity) + ".";
+ }
+
+ private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak,
+ boolean requireOneActiveActivity) {
+ if (intentionalLeak) return "intentional leak; not generating dump";
+
+ String result;
+ if (sDumpWasGenerated) {
+ result = "dump has already been generated by another test";
+ } else {
+ try {
+ final String fileName =
+ getInstrumentation().getTargetContext().getFilesDir().getPath()
+ + "/ActivityLeakHeapDump.hprof";
+ if (TestHelpers.isInLauncherProcess()) {
+ Debug.dumpHprofData(fileName);
+ } else {
+ final UiDevice device = getUiDevice();
+ device.executeShellCommand(
+ "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
+ }
+ Log.d(TAG, "Saved leak dump, the leak is still present: "
+ + !launcher.noLeakedActivities(requireOneActiveActivity));
+ sDumpWasGenerated = true;
+ result = "saved memory dump as an artifact";
+ } catch (Throwable e) {
+ Log.e(TAG, "dumpHprofData failed", e);
+ result = "failed to save memory dump";
+ }
+ }
+ return result + ". Full list of activities: " + launcher.getRootedActivitiesList();
+ }
+
+ protected BaseLauncherTaplTest() {
+ mActivityManager = InstrumentationRegistry.getContext()
+ .getSystemService(ActivityManager.class);
+ mLauncher.enableCheckEventsForSuccessfulGestures();
+ mLauncher.setAnomalyChecker(BaseLauncherTaplTest::verifyKeyguardInvisible);
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ mLauncher.enableDebugTracing();
+ // Avoid double-reporting of Launcher crashes.
+ mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
+ }
+
+ @Rule
+ public ShellCommandRule mDisableHeadsUpNotification =
+ ShellCommandRule.disableHeadsUpNotification();
+
+ @Rule
+ public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
+
+ @Rule
+ public SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
+ @Rule
+ public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
+
+ @Rule
+ public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
+
+ protected void performInitialization() {
+ reinitializeLauncherData();
+ mDevice.pressHome();
+ // Check that we switched to home.
+ mLauncher.getWorkspace();
+ checkDetectedLeaks(mLauncher, true);
+ }
+
+ protected void clearPackageData(String pkg) throws IOException, InterruptedException {
+ assertTrue("pm clear command failed",
+ mDevice.executeShellCommand("pm clear " + pkg)
+ .contains("Success"));
+ assertTrue("pm wait-for-handler command failed",
+ mDevice.executeShellCommand("pm wait-for-handler")
+ .contains("Success"));
+ }
+
+ protected TestRule getRulesInsideActivityMonitor() {
+ final RuleChain inner = RuleChain
+ .outerRule(new FailureWatcher(mLauncher, null))
+ .around(new TestIsolationRule(mLauncher, true));
+ return TestHelpers.isInLauncherProcess()
+ ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
+ : inner;
+ }
+
+ @Rule
+ public TestRule mOrderSensitiveRules = RuleChain
+ .outerRule(new SamplerRule())
+ .around(new TestStabilityRule())
+ .around(getRulesInsideActivityMonitor());
+
+ public UiDevice getDevice() {
+ return mDevice;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mLauncher.onTestStart();
+
+ final String launcherPackageName = mDevice.getLauncherPackageName();
+ try {
+ final Context context = InstrumentationRegistry.getContext();
+ final PackageManager pm = context.getPackageManager();
+ final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0);
+
+ if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) {
+ Assert.assertEquals("Launcher version doesn't match tests version",
+ pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(),
+ launcherPackage.getLongVersionCode());
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+
+ mLauncherPid = 0;
+
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ mTargetPackage = mTargetContext.getPackageName();
+ mLauncherPid = mLauncher.getPid();
+
+ UserManager userManager = mTargetContext.getSystemService(UserManager.class);
+ if (userManager != null) {
+ for (UserHandle userHandle : userManager.getUserProfiles()) {
+ if (!userHandle.isSystem()) {
+ mDevice.executeShellCommand(
+ "pm remove-user --wait " + userHandle.getIdentifier());
+ }
+ }
+ }
+
+ onTestStart();
+ performInitialization();
+ }
+
+ private long getAvailableMemory() {
+ mActivityManager.getMemoryInfo(mMemoryInfo);
+
+ return Math.divideExact(mMemoryInfo.availMem, BYTES_PER_MEGABYTE);
+ }
+
+ @Before
+ public void saveMemoryBefore() {
+ mMemoryBefore = getAvailableMemory();
+ }
+
+ @After
+ public void logMemoryAfter() {
+ long memoryAfter = getAvailableMemory();
+
+ Log.d(TAG, "Available memory: before=" + mMemoryBefore
+ + "MB, after=" + memoryAfter
+ + "MB, delta=" + (memoryAfter - mMemoryBefore) + "MB");
+ }
+
+ /** Method that should be called when a test starts. */
+ public static void onTestStart() {
+ waitForSetupWizardDismissal();
+
+ if (TestStabilityRule.isPresubmit()) {
+ aggressivelyUnlockSysUi();
+ } else {
+ verifyKeyguardInvisible();
+ }
+ }
+
+ private static boolean hasSystemUiObject(String resId) {
+ return getUiDevice().hasObject(
+ By.res(SYSTEMUI_PACKAGE, resId));
+ }
+
+ @NonNull
+ private static UiDevice getUiDevice() {
+ return UiDevice.getInstance(getInstrumentation());
+ }
+
+ private static void aggressivelyUnlockSysUi() {
+ final UiDevice device = getUiDevice();
+ for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
+ Log.d(TAG, "Before attempting to unlock the phone");
+ try {
+ device.executeShellCommand("input keyevent 82");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ device.waitForIdle();
+ }
+ Assert.assertTrue("Keyguard still visible",
+ TestHelpers.wait(
+ Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
+ Log.d(TAG, "Keyguard is not visible");
+ }
+
+ /** Waits for setup wizard to go away. */
+ private static void waitForSetupWizardDismissal() {
+ if (sFirstTimeWaitingForWizard) {
+ try {
+ getUiDevice().executeShellCommand(
+ "am force-stop com.google.android.setupwizard");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ final boolean wizardDismissed = TestHelpers.wait(
+ Until.gone(By.pkg("com.google.android.setupwizard").depth(0)),
+ sFirstTimeWaitingForWizard ? 120000 : 0);
+ sFirstTimeWaitingForWizard = false;
+ Assert.assertTrue("Setup wizard is still visible", wizardDismissed);
+ }
+
+ /** Asserts that keyguard is not visible */
+ public static void verifyKeyguardInvisible() {
+ final boolean keyguardAlreadyVisible = sSeenKeyguard;
+
+ sSeenKeyguard = sSeenKeyguard
+ || !TestHelpers.wait(
+ Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
+
+ Assert.assertFalse(
+ "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
+ + " for the first time = "
+ + !keyguardAlreadyVisible,
+ sSeenKeyguard);
+ }
+
+ @After
+ public void resetFreezeRecentTaskList() {
+ try {
+ mDevice.executeShellCommand("wm reset-freeze-recent-tasks");
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to reset fozen recent tasks list", e);
+ }
+ }
+
+ @After
+ public void verifyLauncherState() {
+ try {
+ // Limits UI tests affecting tests running after them.
+ mDevice.pressHome();
+ mLauncher.waitForLauncherInitialized();
+ if (mLauncherPid != 0) {
+ assertEquals("Launcher crashed, pid mismatch:",
+ mLauncherPid, mLauncher.getPid().intValue());
+ }
+ } finally {
+ mLauncher.onTestFinish();
+ }
+ }
+
+ protected void reinitializeLauncherData() {
+ reinitializeLauncherData(false);
+ }
+
+ protected void reinitializeLauncherData(boolean clearWorkspace) {
+ if (clearWorkspace) {
+ mLauncher.clearLauncherData();
+ } else {
+ mLauncher.reinitializeLauncherData();
+ }
+ mLauncher.waitForLauncherInitialized();
+ }
+
+ public static void startAppFast(String packageName) {
+ startIntent(
+ getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
+ packageName),
+ By.pkg(packageName).depth(0),
+ true /* newTask */);
+ }
+
+ public static void startTestActivity(String activityName, String activityLabel) {
+ final String packageName = getAppPackageName();
+ final Intent intent = getInstrumentation().getContext().getPackageManager()
+ .getLaunchIntentForPackage(packageName);
+ intent.setComponent(new ComponentName(packageName,
+ "com.android.launcher3.tests." + activityName));
+ startIntent(intent, By.pkg(packageName).text(activityLabel),
+ false /* newTask */);
+ }
+
+ public static void startTestActivity(int activityNumber) {
+ startTestActivity("Activity" + activityNumber, "TestActivity" + activityNumber);
+ }
+
+ public static void startImeTestActivity() {
+ final String packageName = getAppPackageName();
+ final Intent intent = getInstrumentation().getContext().getPackageManager()
+ .getLaunchIntentForPackage(packageName);
+ intent.setComponent(new ComponentName(packageName,
+ "com.android.launcher3.testcomponent.ImeTestActivity"));
+ startIntent(intent, By.pkg(packageName).text("ImeTestActivity"),
+ false /* newTask */);
+ }
+
+ /** Starts ExcludeFromRecentsTestActivity, which has excludeFromRecents="true". */
+ public static void startExcludeFromRecentsTestActivity() {
+ final String packageName = getAppPackageName();
+ final Intent intent = getInstrumentation().getContext().getPackageManager()
+ .getLaunchIntentForPackage(packageName);
+ intent.setComponent(new ComponentName(packageName,
+ "com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity"));
+ startIntent(intent, By.pkg(packageName).text("ExcludeFromRecentsTestActivity"),
+ false /* newTask */);
+ }
+
+ private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ if (newTask) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ } else {
+ intent.addFlags(
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ }
+ getInstrumentation().getTargetContext().startActivity(intent);
+ assertTrue("App didn't start: " + selector,
+ TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
+
+ // Wait for the Launcher to stop.
+ final LauncherInstrumentation launcherInstrumentation = new LauncherInstrumentation();
+ Wait.atMost("Launcher activity didn't stop",
+ () -> !launcherInstrumentation.isLauncherActivityStarted(),
+ launcherInstrumentation, DEFAULT_ACTIVITY_TIMEOUT);
+ }
+
+ public static ActivityInfo resolveSystemAppInfo(String category) {
+ return getInstrumentation().getContext().getPackageManager().resolveActivity(
+ new Intent(Intent.ACTION_MAIN).addCategory(category),
+ PackageManager.MATCH_SYSTEM_ONLY)
+ .activityInfo;
+ }
+
+
+ public static String resolveSystemApp(String category) {
+ return resolveSystemAppInfo(category).packageName;
+ }
+
+ protected HomeAppIcon createShortcutInCenterIfNotExist(String name) {
+ Point dimension = mLauncher.getWorkspace().getIconGridDimensions();
+ return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2);
+ }
+
+ protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) {
+ return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y);
+ }
+
+ protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
+ HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
+ Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name
+ + " cell: " + cellX + ", " + cellY);
+ if (homeAppIcon == null) {
+ HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+ allApps.freeze();
+ try {
+ allApps.getAppIcon(name).dragToWorkspace(cellX, cellY);
+ } finally {
+ allApps.unfreeze();
+ }
+ homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name);
+ }
+ return homeAppIcon;
+ }
+
+ protected void commitTransactionAndLoadHome(FavoriteItemsTransaction transaction) {
+ transaction.commit();
+
+ // Launch the home activity
+ UiDevice.getInstance(getInstrumentation()).pressHome();
+ mLauncher.waitForLauncherInitialized();
+ }
+
+ /** Clears all recent tasks */
+ protected void clearAllRecentTasks() {
+ if (!mLauncher.getRecentTasks().isEmpty()) {
+ mLauncher.goHome().switchToOverview().dismissAllTasks();
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
new file mode 100644
index 0000000..bb645d7
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 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.ui.widget;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Process;
+import android.view.View;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.testcomponent.WidgetConfigActivity;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.BlockingBroadcastReceiver;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.views.OptionsPopupView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsListAdapter;
+import com.android.launcher3.widget.picker.WidgetsRecyclerView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to verify widget configuration is properly shown.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class AddConfigWidgetTest extends BaseLauncherActivityTest<Launcher> {
+
+ @Rule
+ public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+
+ private LauncherAppWidgetProviderInfo mWidgetInfo;
+ private AppWidgetManager mAppWidgetManager;
+
+ private int mWidgetId;
+
+ @Before
+ public void setUp() throws Exception {
+ mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */);
+ mAppWidgetManager = AppWidgetManager.getInstance(targetContext());
+ }
+
+ @Test
+ @PortraitLandscape
+ public void testWidgetConfig() throws Throwable {
+ runTest(true);
+ }
+
+ @Test
+ @PortraitLandscape
+ public void testConfigCancelled() throws Throwable {
+ runTest(false);
+ }
+
+ /**
+ * @param acceptConfig accept the config activity
+ */
+ private void runTest(boolean acceptConfig) throws Throwable {
+ new FavoriteItemsTransaction(targetContext()).commit();
+ loadLauncherSync();
+
+ // Add widget to homescreen
+ WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
+ executeOnLauncher(OptionsPopupView::openWidgets);
+ uiDevice.waitForIdle();
+
+ // Select the widget header
+ Context testContext = getInstrumentation().getContext();
+ String packageName = testContext.getPackageName();
+ executeOnLauncher(l -> {
+ WidgetsRecyclerView wrv = WidgetsFullSheet.getWidgetsView(l);
+ WidgetsListAdapter adapter = (WidgetsListAdapter) wrv.getAdapter();
+ int pos = adapter.getItems().indexOf(
+ adapter.getItems().stream()
+ .filter(entry -> packageName.equals(entry.mPkgItem.packageName))
+ .findFirst()
+ .get());
+ wrv.getLayoutManager().scrollToPosition(pos);
+ adapter.onHeaderClicked(true, new PackageUserKey(packageName, Process.myUserHandle()));
+ });
+ uiDevice.waitForIdle();
+
+ View widgetView = getOnceNotNull("Widget not found", l -> searchView(l.getDragLayer(), v ->
+ v instanceof WidgetCell
+ && v.getTag() instanceof PendingAddWidgetInfo pawi
+ && mWidgetInfo.provider.equals(pawi.componentName)));
+ addToWorkspace(widgetView);
+
+ // Widget id for which the config activity was opened
+ mWidgetId = monitor.getWidgetId();
+
+ // Verify that the widget id is valid and bound
+ assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+ setResult(acceptConfig);
+
+ if (acceptConfig) {
+ getOnceNotNull("Widget was not added", l -> {
+ // Close the resize frame before searching for widget
+ AbstractFloatingView.closeAllOpenViews(l);
+ return l.getWorkspace().getFirstMatch(new WidgetSearchCondition());
+ });
+ assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+ } else {
+ // Verify that the widget id is deleted.
+ Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null);
+ }
+ }
+
+ private void setResult(boolean success) {
+ getInstrumentation().getTargetContext().sendBroadcast(
+ WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
+ success ? "clickOK" : "clickCancel"));
+ uiDevice.waitForIdle();
+ }
+
+ /**
+ * Condition for searching widget id
+ */
+ private class WidgetSearchCondition implements ItemOperator {
+
+ @Override
+ public boolean evaluate(ItemInfo info, View view) {
+ return info instanceof LauncherAppWidgetInfo lawi
+ && lawi.providerName.equals(mWidgetInfo.provider)
+ && lawi.appWidgetId == mWidgetId;
+ }
+ }
+
+ /**
+ * Broadcast receiver for receiving widget config activity status.
+ */
+ private static class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
+
+ WidgetConfigStartupMonitor() {
+ super(WidgetConfigActivity.class.getName());
+ }
+
+ public int getWidgetId() throws InterruptedException {
+ Intent intent = blockingGetExtraIntent();
+ assertNotNull("Null EXTRA_INTENT", intent);
+ assertEquals("Intent action is not ACTION_APPWIDGET_CONFIGURE",
+ AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
+ int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ LauncherAppWidgetInfo.NO_ID);
+ assertNotSame("Widget id is NO_ID", widgetId, LauncherAppWidgetInfo.NO_ID);
+ return widgetId;
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
similarity index 77%
rename from tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index 4cdbd96..8846d65 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -1,17 +1,17 @@
/*
* Copyright (C) 2017 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
+ * 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
+ * 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.
+ * 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.ui.widget;
@@ -22,12 +22,12 @@
import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.TestUtil.getOnUiThread;
+import static com.android.launcher3.util.Wait.atMost;
import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
@@ -36,6 +36,7 @@
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
+import android.text.TextUtils;
import android.widget.RemoteViews;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -49,13 +50,12 @@
import com.android.launcher3.celllayout.FavoriteItemsTransaction;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.tapl.Widget;
-import com.android.launcher3.tapl.Workspace;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TestViewHelpers;
-import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.BaseLauncherActivityTest;
import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.PendingAppWidgetHostView;
import com.android.launcher3.widget.WidgetManagerHelper;
import org.junit.After;
@@ -67,6 +67,7 @@
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.function.Function;
/**
* Tests for bind widget flow.
@@ -75,7 +76,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
+public class BindWidgetTest extends BaseLauncherActivityTest<Launcher> {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
@@ -87,11 +88,9 @@
private LauncherModel mModel;
- @Override
@Before
public void setUp() throws Exception {
- super.setUp();
- mModel = LauncherAppState.getInstance(mTargetContext).getModel();
+ mModel = LauncherAppState.getInstance(targetContext()).getModel();
}
@After
@@ -101,7 +100,7 @@
}
if (mSessionId > -1) {
- mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+ targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
}
}
@@ -122,13 +121,12 @@
LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false,
item -> item.appWidgetId = -33);
- final Workspace workspace = mLauncher.getWorkspace();
// Item deleted from db
mCursor = queryItem();
assertEquals(0, mCursor.getCount());
// The view does not exist
- assertTrue("Widget exists", workspace.tryGetWidget(info.label, 0) == null);
+ verifyItemEventuallyNull("Widget exists", widgetProvider(info));
}
@Test
@@ -154,18 +152,19 @@
// Widget has a valid Id now.
assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
& FLAG_ID_NOT_VALID);
- assertNotNull(AppWidgetManager.getInstance(mTargetContext)
+ assertNotNull(AppWidgetManager.getInstance(targetContext())
.getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
LauncherSettings.Favorites.APPWIDGET_ID))));
// send OPTION_APPWIDGET_RESTORE_COMPLETED
int appWidgetId = mCursor.getInt(
mCursor.getColumnIndex(LauncherSettings.Favorites.APPWIDGET_ID));
- AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mTargetContext);
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(targetContext());
Bundle b = new Bundle();
b.putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true);
- RemoteViews remoteViews = new RemoteViews(mTargetPackage, R.layout.appwidget_not_ready);
+ RemoteViews remoteViews = new RemoteViews(
+ targetContext().getPackageName(), R.layout.appwidget_not_ready);
appWidgetManager.updateAppWidgetOptions(appWidgetId, b);
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
@@ -175,15 +174,14 @@
WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
executeOnLauncher(l -> l.getAppWidgetHolder().startListening());
verifyWidgetPresent(info);
- assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100));
+ verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
}
@Test
public void testPendingWidget_notRestored_removed() {
addPendingItemToScreen(getInvalidWidgetInfo(), FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
- assertTrue("Pending widget exists",
- mLauncher.getWorkspace().tryGetPendingWidget(0) == null);
+ verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
// Item deleted from db
mCursor = queryItem();
assertEquals(0, mCursor.getCount());
@@ -216,7 +214,7 @@
// Create an active installer session
SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
params.setAppPackageName(item.providerName.getPackageName());
- PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
+ PackageInstaller installer = targetContext().getPackageManager().getPackageInstaller();
mSessionId = installer.createSession(params);
addPendingItemToScreen(item, FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
@@ -234,36 +232,47 @@
}
private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
- final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label,
- TestUtil.DEFAULT_UI_TIMEOUT);
- assertTrue("Widget is not present",
- widget != null);
+ getOnceNotNull("Widget is not present", widgetProvider(info));
}
private void verifyPendingWidgetPresent() {
- final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(
- TestUtil.DEFAULT_UI_TIMEOUT);
- assertTrue("Pending widget is not present",
- widget != null);
+ getOnceNotNull("Widget is not present", pendingWidgetProvider());
+ }
+
+ private Function<Launcher, Object> pendingWidgetProvider() {
+ return l -> l.getWorkspace().getFirstMatch(
+ (item, view) -> view instanceof PendingAppWidgetHostView);
+ }
+
+ private Function<Launcher, Object> widgetProvider(LauncherAppWidgetProviderInfo info) {
+ return l -> l.getWorkspace().getFirstMatch((item, view) ->
+ view instanceof LauncherAppWidgetHostView
+ && TextUtils.equals(info.label, view.getContentDescription()));
+ }
+
+ private void verifyItemEventuallyNull(String message, Function<Launcher, Object> provider) {
+ atMost(message, () -> getFromLauncher(provider) == null);
}
private void addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus) {
item.restoreStatus = restoreStatus;
item.screenId = FIRST_SCREEN_ID;
- commitTransactionAndLoadHome(
- new FavoriteItemsTransaction(mTargetContext).addItem(() -> item));
+ new FavoriteItemsTransaction(targetContext()).addItem(() -> item).commit();
+ loadLauncherSync();
}
private LauncherAppWidgetProviderInfo addWidgetToScreen(boolean hasConfigureScreen,
boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride) {
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(hasConfigureScreen);
- commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext)
+ new FavoriteItemsTransaction(targetContext())
.addItem(() -> {
- LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, bindWidget);
+ LauncherAppWidgetInfo item =
+ createWidgetInfo(info, targetContext(), bindWidget);
item.screenId = FIRST_SCREEN_ID;
itemOverride.accept(item);
return item;
- }));
+ }).commit();
+ loadLauncherSync();
return info;
}
@@ -277,13 +286,13 @@
Set<String> activePackage = getOnUiThread(() -> {
Set<String> packages = new HashSet<>();
- InstallSessionHelper.INSTANCE.get(mTargetContext).getActiveSessions()
+ InstallSessionHelper.INSTANCE.get(targetContext()).getActiveSessions()
.keySet().forEach(packageUserKey -> packages.add(packageUserKey.mPackageName));
return packages;
});
while (true) {
try {
- mTargetContext.getPackageManager().getPackageInfo(
+ targetContext().getPackageManager().getPackageInfo(
pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (Exception e) {
if (!activePackage.contains(pkg)) {
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
similarity index 62%
rename from tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
rename to tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index fe3b2ee..2fb7987 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -1,34 +1,41 @@
/*
* Copyright (C) 2017 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
+ * 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
+ * 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.
+ * 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.ui.widget;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
+import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.view.View;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings.Favorites;
@@ -37,14 +44,13 @@
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.tapl.AddToHomeScreenPrompt;
import com.android.launcher3.testcomponent.AppWidgetNoConfig;
import com.android.launcher3.testcomponent.AppWidgetWithConfig;
import com.android.launcher3.testcomponent.RequestPinItemActivity;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.BlockingBroadcastReceiver;
import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
-import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.Wait.Condition;
+import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.rule.ShellCommandRule;
import org.junit.Before;
@@ -53,25 +59,27 @@
import org.junit.runner.RunWith;
import java.util.UUID;
+import java.util.regex.Pattern;
/**
* Test to verify pin item request flow.
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> {
+public class RequestPinItemTest extends BaseLauncherActivityTest<Launcher> {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+ @Rule
+ public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher();
+
private String mCallbackAction;
private String mShortcutId;
private int mAppWidgetId;
- @Override
@Before
public void setUp() throws Exception {
- super.setUp();
mCallbackAction = UUID.randomUUID().toString();
mShortcutId = UUID.randomUUID().toString();
}
@@ -81,9 +89,9 @@
@Test
public void testPinWidgetNoConfig() throws Throwable {
- runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
- ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
- ((LauncherAppWidgetInfo) info).providerName.getClassName()
+ runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
+ && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
+ && ((LauncherAppWidgetInfo) info).providerName.getClassName()
.equals(AppWidgetNoConfig.class.getName()));
}
@@ -94,18 +102,18 @@
RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
- runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
- ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
- ((LauncherAppWidgetInfo) info).providerName.getClassName()
+ runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
+ && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
+ && ((LauncherAppWidgetInfo) info).providerName.getClassName()
.equals(AppWidgetNoConfig.class.getName()), command);
}
@Test
public void testPinWidgetWithConfig() throws Throwable {
runTest("pinWidgetWithConfig", true,
- (info, view) -> info instanceof LauncherAppWidgetInfo &&
- ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
- ((LauncherAppWidgetInfo) info).providerName.getClassName()
+ (info, view) -> info instanceof LauncherAppWidgetInfo
+ && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
+ && ((LauncherAppWidgetInfo) info).providerName.getClassName()
.equals(AppWidgetWithConfig.class.getName()));
}
@@ -119,47 +127,48 @@
runTest("pinShortcut", false, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View view) {
- return info instanceof WorkspaceItemInfo &&
- info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
- ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
+ return info instanceof WorkspaceItemInfo
+ && info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT
+ && ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
}
}, command);
}
private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
Intent... commandIntents) throws Throwable {
- commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
+ new FavoriteItemsTransaction(targetContext()).commit();
+ loadLauncherSync();
// Open Pin item activity
BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
RequestPinItemActivity.class.getName());
- mLauncher.
- getWorkspace().
- switchToAllApps().
- getAppIcon("Test Pin Item").
- launch(getAppPackageName());
+ Context testContext = getInstrumentation().getContext();
+ startAppFast(
+ testContext.getPackageName(),
+ new Intent(testContext, RequestPinItemActivity.class));
assertNotNull(openMonitor.blockingGetExtraIntent());
// Set callback
- PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0,
- new Intent(mCallbackAction).setPackage(mTargetContext.getPackageName()),
+ PendingIntent callback = PendingIntent.getBroadcast(targetContext(), 0,
+ new Intent(mCallbackAction).setPackage(targetContext().getPackageName()),
FLAG_ONE_SHOT | FLAG_MUTABLE);
- mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+ targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent(
RequestPinItemActivity.class, "setCallback").putExtra(
RequestPinItemActivity.EXTRA_PARAM + "0", callback));
for (Intent command : commandIntents) {
- mTargetContext.sendBroadcast(command);
+ targetContext().sendBroadcast(command);
}
// call the requested method to start the flow
- mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+ targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent(
RequestPinItemActivity.class, activityMethod));
- final AddToHomeScreenPrompt addToHomeScreenPrompt = mLauncher.getAddToHomeScreenPrompt();
// Accept confirmation:
BlockingBroadcastReceiver resultReceiver = new BlockingBroadcastReceiver(mCallbackAction);
- addToHomeScreenPrompt.addAutomatically();
+ BySelector selector = By.text(Pattern.compile("^Add to home screen$", CASE_INSENSITIVE))
+ .pkg(targetContext().getPackageName());
+ uiDevice.wait(device -> device.findObject(selector), TestUtil.DEFAULT_UI_TIMEOUT).click();
Intent result = resultReceiver.blockingGetIntent();
assertNotNull(result);
mAppWidgetId = result.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
@@ -167,28 +176,9 @@
assertNotSame(-1, mAppWidgetId);
}
- // Go back to home
- mLauncher.goHome();
- Wait.atMost("", new ItemSearchCondition(itemMatcher), mLauncher);
- }
-
- /**
- * Condition for for an item
- */
- private class ItemSearchCondition implements Condition {
-
- private final ItemOperator mOp;
-
- ItemSearchCondition(ItemOperator op) {
- mOp = op;
- }
-
- @Override
- public boolean isTrue() throws Throwable {
- return mMainThreadExecutor.submit(() -> {
- Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
- return l != null && l.getWorkspace().getFirstMatch(mOp) != null;
- }).get();
- }
+ // Reload activity, so that the activity is focused
+ closeCurrentActivity();
+ loadLauncherSync();
+ getOnceNotNull("", l -> l.getWorkspace().getFirstMatch(itemMatcher));
}
}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
deleted file mode 100644
index 7845222..0000000
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2017 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.ui.widget;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-
-import android.appwidget.AppWidgetManager;
-import android.content.Intent;
-import android.view.View;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.testcomponent.WidgetConfigActivity;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
-import com.android.launcher3.ui.TestViewHelpers;
-import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test to verify widget configuration is properly shown.
- */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> {
-
- @Rule
- public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
-
- private LauncherAppWidgetProviderInfo mWidgetInfo;
- private AppWidgetManager mAppWidgetManager;
-
- private int mWidgetId;
-
- @Override
- @Before
- public void setUp() throws Exception {
- super.setUp();
- mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */);
- mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
- }
-
- @Test
- @PortraitLandscape
- public void testWidgetConfig() throws Throwable {
- runTest(true);
- }
-
- @Test
- @PortraitLandscape
- public void testConfigCancelled() throws Throwable {
- runTest(false);
- }
-
-
- /**
- * @param acceptConfig accept the config activity
- */
- private void runTest(boolean acceptConfig) throws Throwable {
- commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
-
- // Drag widget to homescreen
- WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
- mLauncher.getWorkspace()
- .openAllWidgets()
- .getWidget(mWidgetInfo.getLabel())
- .dragToWorkspace(true, false);
- // Widget id for which the config activity was opened
- mWidgetId = monitor.getWidgetId();
-
- // Verify that the widget id is valid and bound
- assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
-
- setResultAndWaitForAnimation(acceptConfig);
- if (acceptConfig) {
- Wait.atMost("", new WidgetSearchCondition(), mLauncher);
- assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
- } else {
- // Verify that the widget id is deleted.
- Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
- mLauncher);
- }
- }
-
- private static void setResult(boolean success) {
- getInstrumentation().getTargetContext().sendBroadcast(
- WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
- success ? "clickOK" : "clickCancel"));
- }
-
- private void setResultAndWaitForAnimation(boolean success) {
- if (mLauncher.isLauncher3()) {
- setResult(success);
- } else {
- mLauncher.executeAndWaitForWallpaperAnimation(
- () -> setResult(success),
- "setting widget coinfig result");
- }
- }
-
- /**
- * Condition for searching widget id
- */
- private class WidgetSearchCondition implements Wait.Condition, ItemOperator {
-
- @Override
- public boolean isTrue() throws Throwable {
- return mMainThreadExecutor.submit(() -> {
- Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
- return l != null && l.getWorkspace().getFirstMatch(this) != null;
- }).get();
- }
-
- @Override
- public boolean evaluate(ItemInfo info, View view) {
- return info instanceof LauncherAppWidgetInfo
- && ((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
- mWidgetInfo.provider.getClassName())
- && ((LauncherAppWidgetInfo) info).appWidgetId == mWidgetId;
- }
- }
-
- /**
- * Broadcast receiver for receiving widget config activity status.
- */
- private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
-
- public WidgetConfigStartupMonitor() {
- super(WidgetConfigActivity.class.getName());
- }
-
- public int getWidgetId() throws InterruptedException {
- Intent intent = blockingGetExtraIntent();
- assertNotNull("Null EXTRA_INTENT", intent);
- assertEquals("Intent action is not ACTION_APPWIDGET_CONFIGURE",
- AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
- int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
- LauncherAppWidgetInfo.NO_ID);
- assertNotSame("Widget id is NO_ID", widgetId, LauncherAppWidgetInfo.NO_ID);
- return widgetId;
- }
- }
-}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java
similarity index 68%
rename from tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
rename to tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java
index 19c5850..caad1d9 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java
@@ -22,24 +22,30 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
+import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
-import com.android.launcher3.tapl.Widgets;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.launcher3.widget.picker.WidgetsRecyclerView;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
/**
- * This test run in both Out of process (Oop) and in-process (Ipc).
* Make sure the basic interactions with the WidgetPicker works.
*/
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class TaplWidgetPickerTest extends AbstractLauncherUiTest<Launcher> {
+public class WidgetPickerTest extends BaseLauncherActivityTest<Launcher> {
+
+ @Rule
+ public TestRule screenRecordRule = new ScreenRecordRule();
private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
return WidgetsFullSheet.getWidgetsView(launcher);
@@ -56,30 +62,21 @@
@ScreenRecord
@PortraitLandscape
public void testWidgets() {
- mLauncher.goHome();
+ loadLauncherSync();
// Test opening widgets.
executeOnLauncher(launcher ->
assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
- Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
- assertNotNull("openAllWidgets() returned null", widgets);
- widgets = mLauncher.getAllWidgets();
+ assertNotNull("openAllWidgets() returned null",
+ getFromLauncher(OptionsPopupView::openWidgets));
+ WidgetsRecyclerView widgets = getFromLauncher(this::getWidgetsView);
assertNotNull("getAllWidgets() returned null", widgets);
- executeOnLauncher(launcher ->
- assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
+ executeOnLauncher(launcher -> assertTrue("Widgets is not shown", widgets.isShown()));
executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
0, getWidgetsScroll(launcher)));
- // Test flinging widgets.
- widgets.flingForward();
- Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
- executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
- flingForwardY > 0));
+ executeOnLauncher(AbstractFloatingView::closeAllOpenViews);
+ uiDevice.waitForIdle();
- widgets.flingBackward();
- executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
- getWidgetsScroll(launcher) < flingForwardY));
-
- mLauncher.goHome();
waitForLauncherCondition("Widgets were not closed",
launcher -> getWidgetsView(launcher) == null);
}
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index cfc0a6b..c623513 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -15,8 +15,6 @@
*/
package com.android.launcher3.ui.workspace;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
@@ -29,11 +27,9 @@
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
-import android.view.View;
import android.view.ViewGroup;
import androidx.test.filters.LargeTest;
-import androidx.test.uiautomator.UiDevice;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
@@ -49,9 +45,6 @@
import org.junit.Test;
-import java.util.ArrayDeque;
-import java.util.Queue;
-
/**
* Tests for theme icon support in Launcher
*
@@ -137,27 +130,10 @@
} catch (Exception e) {
throw new RuntimeException(e);
}
-
- // Find the app icon
- Queue<View> viewQueue = new ArrayDeque<>();
- viewQueue.add(parent);
- BubbleTextView icon = null;
- while (!viewQueue.isEmpty()) {
- View view = viewQueue.poll();
- if (view instanceof ViewGroup) {
- parent = (ViewGroup) view;
- for (int i = parent.getChildCount() - 1; i >= 0; i--) {
- viewQueue.add(parent.getChildAt(i));
- }
- } else if (view instanceof BubbleTextView btv) {
- if (btv.getContentDescription() != null
- && title.equals(btv.getContentDescription().toString())) {
- icon = btv;
- break;
- }
- }
- }
- return icon;
+ return (BubbleTextView) searchView(parent, v ->
+ v instanceof BubbleTextView btv
+ && btv.getContentDescription() != null
+ && title.equals(btv.getContentDescription().toString()));
}
private BubbleTextView verifyIconTheme(String title, ViewGroup parent, boolean isThemed) {
@@ -193,11 +169,4 @@
rv.getLayoutManager().scrollToPosition(pos);
});
}
-
- private void addToWorkspace(View btv) {
- TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () ->
- btv.getAccessibilityDelegate().performAccessibilityAction(
- btv, com.android.launcher3.R.id.action_add_to_workspace, null));
- UiDevice.getInstance(getInstrumentation()).waitForIdle();
- }
}
diff --git a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
index 476e497..61fa7d5 100644
--- a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
+++ b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
@@ -22,6 +22,9 @@
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.children
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.ActivityAction
@@ -30,11 +33,13 @@
import com.android.launcher3.Launcher
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherState
+import com.android.launcher3.R
import com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST
import com.android.launcher3.tapl.TestHelpers
import com.android.launcher3.util.ModelTestExtensions.loadModelSync
import com.android.launcher3.util.Wait.atMost
import java.util.function.Function
+import java.util.function.Predicate
import java.util.function.Supplier
import org.junit.After
@@ -56,6 +61,8 @@
)
.also { currentScenario = it }
+ @JvmField val uiDevice = UiDevice.getInstance(getInstrumentation())
+
@After
fun closeCurrentActivity() {
currentScenario?.close()
@@ -117,9 +124,10 @@
@JvmOverloads
protected fun injectKeyEvent(keyCode: Int, actionDown: Boolean, metaState: Int = 0) {
+ uiDevice.waitForIdle()
val eventTime = SystemClock.uptimeMillis()
val event =
- KeyEvent.obtain(
+ KeyEvent(
eventTime,
eventTime,
if (actionDown) KeyEvent.ACTION_DOWN else MotionEvent.ACTION_UP,
@@ -130,24 +138,47 @@
/* scancode= */ 0,
/* flags= */ 0,
InputDevice.SOURCE_KEYBOARD,
- /* characters =*/ null,
)
executeOnLauncher { it.dispatchKeyEvent(event) }
- event.recycle()
}
- fun startAppFast(packageName: String) {
- val intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!
+ @JvmOverloads
+ fun startAppFast(
+ packageName: String,
+ intent: Intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!,
+ ) {
intent.addCategory(Intent.CATEGORY_LAUNCHER)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
targetContext().startActivity(intent)
- UiDevice.getInstance(getInstrumentation()).waitForIdle()
+ uiDevice.waitForIdle()
}
fun freezeAllApps() = executeOnLauncher {
it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST)
}
- fun executeShellCommand(cmd: String) =
- UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd)
+ fun executeShellCommand(cmd: String) = uiDevice.executeShellCommand(cmd)
+
+ fun addToWorkspace(view: View) {
+ TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {
+ view.accessibilityDelegate.performAccessibilityAction(
+ view,
+ R.id.action_add_to_workspace,
+ null,
+ )
+ }
+ UiDevice.getInstance(getInstrumentation()).waitForIdle()
+ }
+
+ fun ViewGroup.searchView(filter: Predicate<View>): View? {
+ if (filter.test(this)) return this
+ for (child in children) {
+ if (filter.test(child)) return child
+ if (child is ViewGroup)
+ child.searchView(filter)?.let {
+ return it
+ }
+ }
+ return null
+ }
}
diff --git a/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt b/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt
new file mode 100644
index 0000000..20881d1
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Parcelable
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit.SECONDS
+
+private const val DEFAULT_BROADCAST_TIMEOUT_SECS: Long = 10
+
+/** Broadcast receiver which blocks until the result is received. */
+open class BlockingBroadcastReceiver(action: String) : BroadcastReceiver() {
+
+ val value = CompletableFuture<Intent>()
+
+ init {
+ getInstrumentation()
+ .targetContext
+ .registerReceiver(this, IntentFilter(action), Context.RECEIVER_EXPORTED)
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ value.complete(intent)
+ }
+
+ @Throws(InterruptedException::class)
+ fun blockingGetIntent(): Intent =
+ value.get(DEFAULT_BROADCAST_TIMEOUT_SECS, SECONDS).also {
+ getInstrumentation().targetContext.unregisterReceiver(this)
+ }
+
+ @Throws(InterruptedException::class)
+ fun blockingGetExtraIntent(): Intent? =
+ blockingGetIntent().getParcelableExtra<Parcelable>(Intent.EXTRA_INTENT) as Intent?
+}
diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
index 7bdc040..3b85309 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -12,7 +12,7 @@
import com.android.app.viewcapture.data.ExportedData;
import com.android.launcher3.tapl.LauncherInstrumentation;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.BaseLauncherTaplTest;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
@@ -57,7 +57,7 @@
@Override
protected void succeeded(Description description) {
super.succeeded(description);
- AbstractLauncherUiTest.checkDetectedLeaks(mLauncher);
+ BaseLauncherTaplTest.checkDetectedLeaks(mLauncher);
}
@Override
diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
index 7ff55fe..7cb2614 100644
--- a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
+++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
@@ -163,6 +163,32 @@
}
/**
+ * Dismisses the Keyboard Quick Switch view by going home. After the Keyboard Quick Switch view
+ * gets hidden, it unpresses ALT key, which is generally used to keep the view visible.
+ */
+ public Workspace dismissByGoingHome() {
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "verifying keyboard quick switch view is shown")) {
+ mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+ }
+
+ mLauncher.goHome();
+
+ try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+ "waiting for keyboard quick switch dismissal");
+ LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ mLauncher.waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
+ }
+
+ try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+ "get workspace after releasing ALT key")) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_HOME_ALT_LEFT_UP);
+ mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+ return mLauncher.getWorkspace();
+ }
+ }
+
+ /**
* Launches the currently-focused app task.
* <p>
* This method should only be used if the focused task is for a recent running app, otherwise
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 08c5552..fac73d3 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1728,6 +1728,27 @@
scrollDownByDistance(container, distance, appsListBottomPadding);
}
+ /** Scrolls up by given distance within the container. */
+ void scrollUpByDistance(UiObject2 container, int distance) {
+ scrollUpByDistance(container, distance, 0);
+ }
+
+ /** Scrolls up by given distance within the container considering the given bottom padding. */
+ void scrollUpByDistance(UiObject2 container, int distance, int bottomPadding) {
+ final Rect containerRect = getVisibleBounds(container);
+ final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
+ scroll(
+ container,
+ Direction.UP,
+ new Rect(
+ 0,
+ containerRect.height() - bottomGestureMarginInContainer - distance,
+ 0,
+ bottomGestureMarginInContainer + bottomPadding),
+ /* steps= */ 10,
+ /* slowDown= */ true);
+ }
+
void scrollDownByDistance(UiObject2 container, int distance) {
scrollDownByDistance(container, distance, 0);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 3097d9c..ac2748e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
import static com.android.launcher3.tapl.LauncherInstrumentation.log;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
@@ -31,6 +32,7 @@
import com.android.launcher3.testing.shared.TestProtocol;
import java.util.Collection;
+import java.util.List;
/**
* All widgets container.
@@ -128,8 +130,10 @@
final UiObject2 searchBar = findSearchBar();
final int searchBarHeight = searchBar.getVisibleBounds().height();
final UiObject2 fullWidgetsPicker = verifyActiveContainer();
- mLauncher.assertTrue("Widgets container didn't become scrollable",
- fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
+
+ // Widget picker may not be scrollable if there are few items. Instead of waiting on
+ // picker being scrollable, we wait on widget headers to be available.
+ waitForWidgetListItems(fullWidgetsPicker);
final UiObject2 widgetsContainer =
findTestAppWidgetsTableContainer(testAppWidgetPackage);
@@ -176,6 +180,13 @@
}
}
+ private void waitForWidgetListItems(UiObject2 fullWidgetsPicker) {
+ List<UiObject2> headers = fullWidgetsPicker.wait(Until.findObjects(
+ By.res(mLauncher.getLauncherPackageName(), "widgets_list_header")), WAIT_TIME_MS);
+ mLauncher.assertTrue("Widgets list is not available",
+ headers != null && !headers.isEmpty());
+ }
+
private UiObject2 findSearchBar() {
final BySelector searchBarContainerSelector = By.res(mLauncher.getLauncherPackageName(),
"search_and_recommendations_container");
@@ -199,19 +210,38 @@
"container");
String packageName = mLauncher.getContext().getPackageName();
+ String packageNameToFind =
+ (testAppWidgetPackage == null || testAppWidgetPackage.isEmpty()) ? packageName
+ : testAppWidgetPackage;
+
final BySelector targetAppSelector = By
.clazz("android.widget.TextView")
- .text((testAppWidgetPackage == null || testAppWidgetPackage.isEmpty())
- ? packageName
- : testAppWidgetPackage);
+ .text(packageNameToFind);
+ final BySelector expandListButtonSelector =
+ By.res(mLauncher.getLauncherPackageName(), "widget_list_expand_button");
final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(),
"widgets_table");
boolean hasHeaderExpanded = false;
+ // List was expanded by clicking "Show all" button.
+ boolean hasListExpanded = false;
+
int scrollDistance = 0;
for (int i = 0; i < SCROLL_ATTEMPTS; i++) {
UiObject2 widgetPicker = mLauncher.waitForLauncherObject(widgetPickerSelector);
UiObject2 widgetListView = verifyActiveContainer();
+
+ // Press "Show all" button if it exists. Otherwise, keep scrolling to
+ // find the header or show all button.
+ UiObject2 expandListButton =
+ mLauncher.findObjectInContainer(widgetListView, expandListButtonSelector);
+ if (expandListButton != null) {
+ expandListButton.click();
+ hasListExpanded = true;
+ i = -1;
+ continue;
+ }
+
UiObject2 header = mLauncher.waitForObjectInContainer(widgetListView,
headerSelector);
// If a header is barely visible in the bottom edge of the screen, its height could be
@@ -222,6 +252,17 @@
// Look for a header that has the test app name.
UiObject2 headerTitle = mLauncher.findObjectInContainer(widgetListView,
targetAppSelector);
+
+ final UiObject2 searchBar = findSearchBar();
+ // If header's title is under or above search bar, let's not process the header yet,
+ // scroll a bit more to bring the header into visible area.
+ if (headerTitle != null
+ && headerTitle.getVisibleCenter().y <= searchBar.getVisibleCenter().y) {
+ log("Test app's header is behind the searchbar, scrolling up");
+ mLauncher.scrollUpByDistance(widgetListView, scrollDistance);
+ continue;
+ }
+
if (headerTitle != null) {
// If we find the header and it has not been expanded, let's click it to see the
// widgets list. Note that we wait until the header is out of the gesture region at
@@ -258,11 +299,24 @@
widgetPicker,
widgetsContainerSelector);
- mLauncher.scrollDownByDistance(hasHeaderExpanded && rightPane != null
- ? rightPane
- : widgetListView, scrollDistance);
+ if (hasListExpanded && packageNameToFind.compareToIgnoreCase(
+ getFirstHeaderTitle(widgetListView)) < 0) {
+ mLauncher.scrollUpByDistance(hasHeaderExpanded && rightPane != null
+ ? rightPane
+ : widgetListView, scrollDistance);
+ } else {
+ mLauncher.scrollDownByDistance(hasHeaderExpanded && rightPane != null
+ ? rightPane
+ : widgetListView, scrollDistance);
+ }
}
return null;
}
+
+ @NonNull
+ private String getFirstHeaderTitle(UiObject2 widgetListView) {
+ UiObject2 firstHeader = mLauncher.getObjectsInContainer(widgetListView, "app_title").get(0);
+ return firstHeader != null ? firstHeader.getText() : "";
+ }
}